Repository: diasurgical/DevilutionX Branch: master Commit: 4f6a172aa37e Files: 1343 Total size: 14.3 MB Directory structure: gitextract_h4wrmfpe/ ├── .devcontainer/ │ ├── Dockerfile │ ├── devcontainer.json │ └── fluxbox/ │ ├── apps │ └── menu ├── .editorconfig ├── .gdbinit ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ └── feature_request.yml │ ├── dependabot.yml │ └── workflows/ │ ├── 3ds.yml │ ├── Android.yml │ ├── Linux_aarch64.yml │ ├── Linux_x86.yml │ ├── Linux_x86_64.yml │ ├── Linux_x86_64_SDL1.yml │ ├── Linux_x86_64_SDL3_test.yml │ ├── Linux_x86_64_test.yml │ ├── PS4.yml │ ├── PS5.yml │ ├── Windows9x_MinGW.yml │ ├── Windows_MSVC_x64.yml │ ├── Windows_MinGW_x64.yml │ ├── Windows_MinGW_x86.yml │ ├── Windows_XP_32bit.yml │ ├── amiga-m68k.yml │ ├── cache-cleanup.yml │ ├── clang-format-check.yml │ ├── clang-tidy-check.yml │ ├── iOS.yml │ ├── macOS_arm64.yml │ ├── macOS_x86_64.yml │ ├── miyoo_mini_release.yml │ ├── opendingux_release.yml │ ├── retrofw_release.yml │ ├── s390x_qemu_big_endian_tests.yml │ ├── src_dist_release.yml │ ├── switch.yml │ ├── translations.yml │ ├── vita.yml │ ├── xbox_nxdk.yml │ └── xbox_one.yml ├── .gitignore ├── .lldbinit ├── 3rdParty/ │ ├── Lua/ │ │ └── CMakeLists.txt │ ├── PKWare/ │ │ ├── CMakeLists.txt │ │ ├── Makefile │ │ ├── PKWare.vcxproj │ │ ├── PKWare.vcxproj.filters │ │ ├── Pkware.dsp │ │ ├── explode.cpp │ │ ├── implode.cpp │ │ └── pkware.h │ ├── PicoSHA2/ │ │ └── picosha2.h │ ├── SDL2/ │ │ ├── CMake/ │ │ │ └── FindSDL2.cmake │ │ └── CMakeLists.txt │ ├── SDL3/ │ │ └── CMakeLists.txt │ ├── SDL3_image/ │ │ └── CMakeLists.txt │ ├── SDL3_mixer/ │ │ └── CMakeLists.txt │ ├── SDL_audiolib/ │ │ └── CMakeLists.txt │ ├── SDL_image/ │ │ ├── CMakeLists.txt │ │ └── IMG.c │ ├── SheenBidi/ │ │ └── CMakeLists.txt │ ├── asio/ │ │ ├── CMakeLists.txt │ │ ├── asio_handle_exception.cpp │ │ └── asio_handle_exception.hpp │ ├── benchmark/ │ │ └── CMakeLists.txt │ ├── bzip2/ │ │ ├── CMakeLists.txt │ │ └── bz_internal_error.c │ ├── discord/ │ │ ├── .editorconfig │ │ ├── CMakeLists.txt │ │ └── fixes.patch │ ├── find_steam_game/ │ │ └── CMakeLists.txt │ ├── googletest/ │ │ └── CMakeLists.txt │ ├── libfmt/ │ │ └── CMakeLists.txt │ ├── libmpq/ │ │ ├── CMakeLists.txt │ │ └── config.h │ ├── libpng/ │ │ └── CMakeLists.txt │ ├── libsmackerdec/ │ │ └── CMakeLists.txt │ ├── libsodium/ │ │ └── CMakeLists.txt │ ├── libzt/ │ │ └── CMakeLists.txt │ ├── magic_enum/ │ │ └── CMakeLists.txt │ ├── sol2/ │ │ ├── CMakeLists.txt │ │ └── sol_config/ │ │ └── sol/ │ │ ├── config.hpp │ │ └── debug.hpp │ ├── tl/ │ │ ├── CMakeLists.txt │ │ ├── expected.hpp │ │ └── function_ref.hpp │ ├── tolk/ │ │ └── CMakeLists.txt │ ├── unordered_dense/ │ │ ├── 0001-Disable-PMR-support-for-mingw-std-threads-compat.patch │ │ └── CMakeLists.txt │ └── zlib/ │ ├── CMake/ │ │ └── FindZLIB.cmake │ └── CMakeLists.txt ├── Brewfile ├── CMake/ │ ├── Assets.cmake │ ├── Definitions.cmake │ ├── Dependencies.cmake │ ├── Mods.cmake │ ├── MoldLinker.cmake │ ├── Platforms.cmake │ ├── Tests.cmake │ ├── VcPkgManifestFeatures.cmake │ ├── finders/ │ │ ├── FindGperftools.cmake │ │ ├── FindSDL2_image.cmake │ │ ├── FindSDL_audiolib.cmake │ │ ├── FindSpeechd.cmake │ │ ├── Findsimpleini.cmake │ │ └── Findsodium.cmake │ ├── functions/ │ │ ├── FetchContent_ExcludeFromAll_backport.cmake │ │ ├── copy_files.cmake │ │ ├── dependency_options.cmake │ │ ├── devilutionx_library.cmake │ │ ├── emscripten_system_library.cmake │ │ ├── genex.cmake │ │ ├── git.cmake │ │ ├── object_libraries.cmake │ │ ├── set_relative_file_macro.cmake │ │ └── trim_retired_files.cmake │ └── platforms/ │ ├── .editorconfig │ ├── aarch64-linux-gnu-clang-static-libc++.toolchain.cmake │ ├── aarch64-linux-gnu.toolchain.cmake │ ├── amiga.cmake │ ├── android.cmake │ ├── cpigamesh.cmake │ ├── ctr/ │ │ ├── Tools3DS.cmake │ │ ├── asio_defs.cmake │ │ ├── bin2s_header.h.in │ │ └── modules/ │ │ ├── FindCITRO3D.cmake │ │ ├── FindCTRULIB.cmake │ │ ├── FindPNG.cmake │ │ ├── FindZLIB.cmake │ │ ├── LibFindMacros.cmake │ │ └── try_add_imported_target.cmake │ ├── debian-cross-pkg-config.sh │ ├── djcpp.toolchain.cmake │ ├── dos.cmake │ ├── emscripten.cmake │ ├── gkd350h.cmake │ ├── haiku.cmake │ ├── ios.cmake │ ├── ios.toolchain.cmake │ ├── lepus.cmake │ ├── linux_i386.toolchain.cmake │ ├── macos_tiger.cmake │ ├── macports/ │ │ └── finders/ │ │ └── FindMacportsLegacySupport.cmake │ ├── mingw/ │ │ └── zt_defs.cmake │ ├── mingw9x/ │ │ └── include/ │ │ └── windef.h │ ├── mingw9x.toolchain.cmake │ ├── mingwcc.toolchain.cmake │ ├── mingwcc64.toolchain.cmake │ ├── miyoo_mini.cmake │ ├── n3ds.cmake │ ├── ps4.cmake │ ├── retrofw.cmake │ ├── rg350.cmake │ ├── rg99.cmake │ ├── switch/ │ │ └── asio_defs.cmake │ ├── switch.cmake │ ├── threads-stub/ │ │ └── FindThreads.cmake │ ├── uwp_lib.cmake │ ├── vita.cmake │ ├── windows.cmake │ ├── windows9x.cmake │ ├── windowsXP.cmake │ ├── xbox_nxdk/ │ │ └── finders/ │ │ ├── FindPNG.cmake │ │ ├── FindSDL2.cmake │ │ └── FindZLIB.cmake │ └── xbox_nxdk.cmake ├── CMakeLists.txt ├── CMakeSettings.json ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── Packaging/ │ ├── OpenDingux/ │ │ ├── .editorconfig │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build-all.sh │ │ ├── build.sh │ │ ├── devilutionx-from-disk.sh │ │ ├── devilutionx-umount-opk-and-run.sh │ │ ├── gkd350h-manual.txt │ │ ├── gkd350h.desktop │ │ ├── lepus-manual.txt │ │ ├── lepus.desktop │ │ ├── package-opk.sh │ │ ├── profile-generate.sh │ │ ├── retrofw-manual.txt │ │ ├── retrofw.desktop │ │ ├── rg350-manual.txt │ │ ├── rg350.desktop │ │ ├── rg99-manual.txt │ │ ├── rg99-pgo.md │ │ ├── rg99.desktop │ │ └── targets.sh │ ├── amiga/ │ │ ├── Dockerfile │ │ ├── devilutionx.info │ │ └── prep.sh │ ├── apple/ │ │ ├── AppIcon.icns │ │ ├── AppIcon_128.icns │ │ ├── Info.plist │ │ ├── LaunchScreen.storyboard │ │ └── png2icns_tiger/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── png2icns_tiger.c │ │ └── run.sh │ ├── cpi-gamesh/ │ │ ├── __init__.py │ │ ├── build.sh │ │ └── readme.md │ ├── ctr/ │ │ └── template.rsf │ ├── emscripten/ │ │ └── index.html │ ├── haiku/ │ │ └── devilutionX.rdef.in │ ├── miyoo_mini/ │ │ ├── build.sh │ │ ├── setup_toolchain.sh │ │ ├── skeleton_MiniUI/ │ │ │ └── Diablo/ │ │ │ ├── Diablo.m3u │ │ │ ├── launch.sh │ │ │ └── readme.txt │ │ ├── skeleton_OnionOS/ │ │ │ ├── Roms/ │ │ │ │ └── PORTS/ │ │ │ │ ├── Games/ │ │ │ │ │ └── Diablo (devilutionX)/ │ │ │ │ │ └── _required_files.txt │ │ │ │ └── Shortcuts/ │ │ │ │ └── Action/ │ │ │ │ └── Diablo (DevilutionX).notfound │ │ │ └── readme.txt │ │ └── toolchainfile.cmake │ ├── nix/ │ │ ├── AppImage.sh │ │ ├── AppRun │ │ ├── LinuxReleasePackaging.sh │ │ ├── README.txt │ │ ├── debian-cross-aarch64-prep.sh │ │ ├── debian-cross-i386-prep.sh │ │ ├── debian-host-prep.sh │ │ ├── devilutionx-hellfire.desktop │ │ ├── devilutionx.6 │ │ ├── devilutionx.desktop │ │ └── devilutionx.metainfo.xml │ ├── pi/ │ │ └── README.txt │ ├── ps4/ │ │ ├── README.md │ │ └── build.sh │ ├── ps5/ │ │ ├── README.md │ │ ├── build.sh │ │ └── homebrew.js │ ├── resources/ │ │ ├── LICENSE.CC-BY.txt │ │ ├── LICENSE.OFL.txt │ │ ├── LICENSE.zlib.txt │ │ └── README-SDL.txt │ ├── switch/ │ │ ├── README.txt │ │ └── packages.txt │ ├── vita/ │ │ ├── README.txt │ │ └── sce_sys/ │ │ └── livearea/ │ │ └── contents/ │ │ └── template.xml │ ├── windows/ │ │ ├── CMakePresets.json │ │ ├── README.txt │ │ ├── devilutionx.exe.manifest │ │ ├── devilutionx.rc │ │ ├── dos-prep.sh │ │ ├── mingw-prep.sh │ │ └── mingw9x-prep.sh │ ├── xbox-one/ │ │ └── build.bat │ └── xbox_nxdk/ │ └── xbe_logo.pgm ├── README.md ├── Source/ │ ├── .clang-format │ ├── .clang-tidy │ ├── CMakeLists.txt │ ├── DiabloUI/ │ │ ├── button.cpp │ │ ├── button.h │ │ ├── credits.cpp │ │ ├── credits_lines.cpp │ │ ├── credits_lines.h │ │ ├── diabloui.cpp │ │ ├── diabloui.h │ │ ├── dialogs.cpp │ │ ├── dialogs.h │ │ ├── hero/ │ │ │ ├── selhero.cpp │ │ │ └── selhero.h │ │ ├── mainmenu.cpp │ │ ├── multi/ │ │ │ ├── selconn.cpp │ │ │ ├── selgame.cpp │ │ │ └── selgame.h │ │ ├── progress.cpp │ │ ├── scrollbar.cpp │ │ ├── scrollbar.h │ │ ├── selok.cpp │ │ ├── selok.h │ │ ├── selstart.cpp │ │ ├── selstart.h │ │ ├── selyesno.cpp │ │ ├── selyesno.h │ │ ├── settingsmenu.cpp │ │ ├── settingsmenu.h │ │ ├── support_lines.cpp │ │ ├── support_lines.h │ │ ├── text_input.cpp │ │ ├── text_input.hpp │ │ ├── title.cpp │ │ ├── ui_flags.hpp │ │ └── ui_item.h │ ├── appfat.cpp │ ├── appfat.h │ ├── automap.cpp │ ├── automap.h │ ├── capture.cpp │ ├── capture.h │ ├── codec.cpp │ ├── codec.h │ ├── control/ │ │ ├── control.hpp │ │ ├── control_chat.cpp │ │ ├── control_chat.hpp │ │ ├── control_chat_commands.cpp │ │ ├── control_chat_commands.hpp │ │ ├── control_flasks.cpp │ │ ├── control_flasks.hpp │ │ ├── control_gold.cpp │ │ ├── control_infobox.cpp │ │ ├── control_panel.cpp │ │ └── control_panel.hpp │ ├── controls/ │ │ ├── README.md │ │ ├── axis_direction.cpp │ │ ├── axis_direction.h │ │ ├── control_mode.cpp │ │ ├── control_mode.hpp │ │ ├── controller.cpp │ │ ├── controller.h │ │ ├── controller_buttons.cpp │ │ ├── controller_buttons.h │ │ ├── controller_motion.cpp │ │ ├── controller_motion.h │ │ ├── devices/ │ │ │ ├── game_controller.cpp │ │ │ ├── game_controller.h │ │ │ ├── joystick.cpp │ │ │ ├── joystick.h │ │ │ ├── kbcontroller.cpp │ │ │ └── kbcontroller.h │ │ ├── game_controls.cpp │ │ ├── game_controls.h │ │ ├── input.h │ │ ├── keymapper.cpp │ │ ├── keymapper.hpp │ │ ├── menu_controls.cpp │ │ ├── menu_controls.h │ │ ├── modifier_hints.cpp │ │ ├── modifier_hints.h │ │ ├── padmapper.cpp │ │ ├── padmapper.hpp │ │ ├── plrctrls.cpp │ │ ├── plrctrls.h │ │ ├── remap_keyboard.h │ │ └── touch/ │ │ ├── event_handlers.cpp │ │ ├── event_handlers.h │ │ ├── gamepad.cpp │ │ ├── gamepad.h │ │ ├── renderers.cpp │ │ └── renderers.h │ ├── cpp.hint │ ├── crawl.cpp │ ├── crawl.hpp │ ├── cursor.cpp │ ├── cursor.h │ ├── data/ │ │ ├── file.cpp │ │ ├── file.hpp │ │ ├── iterators.hpp │ │ ├── parser.cpp │ │ ├── parser.hpp │ │ ├── record_reader.cpp │ │ ├── record_reader.hpp │ │ ├── value_reader.cpp │ │ └── value_reader.hpp │ ├── dead.cpp │ ├── dead.h │ ├── debug.cpp │ ├── debug.h │ ├── diablo.cpp │ ├── diablo.h │ ├── diablo_msg.cpp │ ├── diablo_msg.hpp │ ├── discord/ │ │ ├── discord.cpp │ │ └── discord.h │ ├── doom.cpp │ ├── doom.h │ ├── dvlnet/ │ │ ├── abstract_net.cpp │ │ ├── abstract_net.h │ │ ├── base.cpp │ │ ├── base.h │ │ ├── base_protocol.h │ │ ├── cdwrap.cpp │ │ ├── cdwrap.h │ │ ├── frame_queue.cpp │ │ ├── frame_queue.h │ │ ├── leaveinfo.hpp │ │ ├── loopback.cpp │ │ ├── loopback.h │ │ ├── packet.cpp │ │ ├── packet.h │ │ ├── protocol_zt.cpp │ │ ├── protocol_zt.h │ │ ├── tcp_client.cpp │ │ ├── tcp_client.h │ │ ├── tcp_server.cpp │ │ ├── tcp_server.h │ │ ├── zerotier_lwip.cpp │ │ ├── zerotier_lwip.h │ │ ├── zerotier_native.cpp │ │ └── zerotier_native.h │ ├── effects.cpp │ ├── effects.h │ ├── effects_stubs.cpp │ ├── encrypt.cpp │ ├── encrypt.h │ ├── engine/ │ │ ├── actor_position.cpp │ │ ├── actor_position.hpp │ │ ├── animationinfo.cpp │ │ ├── animationinfo.h │ │ ├── assets.cpp │ │ ├── assets.hpp │ │ ├── backbuffer_state.cpp │ │ ├── backbuffer_state.hpp │ │ ├── circle.hpp │ │ ├── clx_sprite.hpp │ │ ├── demomode.cpp │ │ ├── demomode.h │ │ ├── direction.cpp │ │ ├── direction.hpp │ │ ├── displacement.hpp │ │ ├── dx.cpp │ │ ├── dx.h │ │ ├── events.cpp │ │ ├── events.hpp │ │ ├── lighting_defs.hpp │ │ ├── load_cel.cpp │ │ ├── load_cel.hpp │ │ ├── load_cl2.cpp │ │ ├── load_cl2.hpp │ │ ├── load_clx.cpp │ │ ├── load_clx.hpp │ │ ├── load_file.hpp │ │ ├── load_pcx.cpp │ │ ├── load_pcx.hpp │ │ ├── palette.cpp │ │ ├── palette.h │ │ ├── path.cpp │ │ ├── path.h │ │ ├── point.hpp │ │ ├── points_in_rectangle_range.hpp │ │ ├── random.cpp │ │ ├── random.hpp │ │ ├── rectangle.hpp │ │ ├── render/ │ │ │ ├── automap_render.cpp │ │ │ ├── automap_render.hpp │ │ │ ├── blit_impl.hpp │ │ │ ├── clx_render.cpp │ │ │ ├── clx_render.hpp │ │ │ ├── dun_render.cpp │ │ │ ├── dun_render.hpp │ │ │ ├── light_render.cpp │ │ │ ├── light_render.hpp │ │ │ ├── primitive_render.cpp │ │ │ ├── primitive_render.hpp │ │ │ ├── scrollrt.cpp │ │ │ ├── scrollrt.h │ │ │ ├── text_render.cpp │ │ │ └── text_render.hpp │ │ ├── size.hpp │ │ ├── sound.cpp │ │ ├── sound.h │ │ ├── sound_defs.hpp │ │ ├── sound_position.cpp │ │ ├── sound_position.hpp │ │ ├── sound_stubs.cpp │ │ ├── surface.cpp │ │ ├── surface.hpp │ │ ├── ticks.cpp │ │ ├── ticks.hpp │ │ ├── trn.cpp │ │ ├── trn.hpp │ │ └── world_tile.hpp │ ├── game_mode.cpp │ ├── game_mode.hpp │ ├── gamemenu.cpp │ ├── gamemenu.h │ ├── gmenu.cpp │ ├── gmenu.h │ ├── headless_mode.cpp │ ├── headless_mode.hpp │ ├── help.cpp │ ├── help.h │ ├── hwcursor.cpp │ ├── hwcursor.hpp │ ├── init.cpp │ ├── init.hpp │ ├── interfac.cpp │ ├── interfac.h │ ├── inv.cpp │ ├── inv.h │ ├── inv_iterators.hpp │ ├── items/ │ │ ├── validation.cpp │ │ └── validation.h │ ├── items.cpp │ ├── items.h │ ├── levels/ │ │ ├── crypt.cpp │ │ ├── crypt.h │ │ ├── drlg_l1.cpp │ │ ├── drlg_l1.h │ │ ├── drlg_l2.cpp │ │ ├── drlg_l2.h │ │ ├── drlg_l3.cpp │ │ ├── drlg_l3.h │ │ ├── drlg_l4.cpp │ │ ├── drlg_l4.h │ │ ├── dun_tile.hpp │ │ ├── gendung.cpp │ │ ├── gendung.h │ │ ├── gendung_defs.hpp │ │ ├── reencode_dun_cels.cpp │ │ ├── reencode_dun_cels.hpp │ │ ├── setmaps.cpp │ │ ├── setmaps.h │ │ ├── themes.cpp │ │ ├── themes.h │ │ ├── tile_properties.cpp │ │ ├── tile_properties.hpp │ │ ├── town.cpp │ │ ├── town.h │ │ ├── trigs.cpp │ │ └── trigs.h │ ├── lighting.cpp │ ├── lighting.h │ ├── loadsave.cpp │ ├── loadsave.h │ ├── lua/ │ │ ├── autocomplete.cpp │ │ ├── autocomplete.hpp │ │ ├── lua_event.cpp │ │ ├── lua_event.hpp │ │ ├── lua_global.cpp │ │ ├── lua_global.hpp │ │ ├── metadoc.hpp │ │ ├── modules/ │ │ │ ├── audio.cpp │ │ │ ├── audio.hpp │ │ │ ├── dev/ │ │ │ │ ├── display.cpp │ │ │ │ ├── display.hpp │ │ │ │ ├── items.cpp │ │ │ │ ├── items.hpp │ │ │ │ ├── level/ │ │ │ │ │ ├── map.cpp │ │ │ │ │ ├── map.hpp │ │ │ │ │ ├── warp.cpp │ │ │ │ │ └── warp.hpp │ │ │ │ ├── level.cpp │ │ │ │ ├── level.hpp │ │ │ │ ├── monsters.cpp │ │ │ │ ├── monsters.hpp │ │ │ │ ├── player/ │ │ │ │ │ ├── gold.cpp │ │ │ │ │ ├── gold.hpp │ │ │ │ │ ├── spells.cpp │ │ │ │ │ ├── spells.hpp │ │ │ │ │ ├── stats.cpp │ │ │ │ │ └── stats.hpp │ │ │ │ ├── player.cpp │ │ │ │ ├── player.hpp │ │ │ │ ├── quests.cpp │ │ │ │ ├── quests.hpp │ │ │ │ ├── search.cpp │ │ │ │ ├── search.hpp │ │ │ │ ├── towners.cpp │ │ │ │ └── towners.hpp │ │ │ ├── dev.cpp │ │ │ ├── dev.hpp │ │ │ ├── floatingnumbers.cpp │ │ │ ├── floatingnumbers.hpp │ │ │ ├── hellfire.cpp │ │ │ ├── hellfire.hpp │ │ │ ├── i18n.cpp │ │ │ ├── i18n.hpp │ │ │ ├── items.cpp │ │ │ ├── items.hpp │ │ │ ├── log.cpp │ │ │ ├── log.hpp │ │ │ ├── monsters.cpp │ │ │ ├── monsters.hpp │ │ │ ├── player.cpp │ │ │ ├── player.hpp │ │ │ ├── render.cpp │ │ │ ├── render.hpp │ │ │ ├── system.cpp │ │ │ ├── system.hpp │ │ │ ├── towners.cpp │ │ │ └── towners.hpp │ │ ├── repl.cpp │ │ └── repl.hpp │ ├── main.cpp │ ├── menu.cpp │ ├── menu.h │ ├── minitext.cpp │ ├── minitext.h │ ├── missiles.cpp │ ├── missiles.h │ ├── monster.cpp │ ├── monster.h │ ├── monsters/ │ │ ├── validation.cpp │ │ └── validation.hpp │ ├── movie.cpp │ ├── movie.h │ ├── mpq/ │ │ ├── mpq_common.cpp │ │ ├── mpq_common.hpp │ │ ├── mpq_reader.cpp │ │ ├── mpq_reader.hpp │ │ ├── mpq_sdl_rwops.cpp │ │ ├── mpq_sdl_rwops.hpp │ │ ├── mpq_writer.cpp │ │ └── mpq_writer.hpp │ ├── msg.cpp │ ├── msg.h │ ├── multi.cpp │ ├── multi.h │ ├── nthread.cpp │ ├── nthread.h │ ├── objects.cpp │ ├── objects.h │ ├── options.cpp │ ├── options.h │ ├── pack.cpp │ ├── pack.h │ ├── panels/ │ │ ├── charpanel.cpp │ │ ├── charpanel.hpp │ │ ├── console.cpp │ │ ├── console.hpp │ │ ├── info_box.cpp │ │ ├── info_box.hpp │ │ ├── mainpanel.cpp │ │ ├── mainpanel.hpp │ │ ├── partypanel.cpp │ │ ├── partypanel.hpp │ │ ├── spell_book.cpp │ │ ├── spell_book.hpp │ │ ├── spell_icons.cpp │ │ ├── spell_icons.hpp │ │ ├── spell_list.cpp │ │ ├── spell_list.hpp │ │ └── ui_panels.hpp │ ├── pfile.cpp │ ├── pfile.h │ ├── platform/ │ │ ├── android/ │ │ │ ├── CMakeLists.txt │ │ │ └── android.cpp │ │ ├── ctr/ │ │ │ ├── CMakeLists.txt │ │ │ ├── asio/ │ │ │ │ ├── include/ │ │ │ │ │ ├── errno.h │ │ │ │ │ ├── net/ │ │ │ │ │ │ └── if.h │ │ │ │ │ ├── netdb.h │ │ │ │ │ ├── netinet/ │ │ │ │ │ │ └── in.h │ │ │ │ │ └── sys/ │ │ │ │ │ ├── ioctl.h │ │ │ │ │ ├── poll.h │ │ │ │ │ ├── socket.h │ │ │ │ │ ├── uio.h │ │ │ │ │ └── un.h │ │ │ │ ├── net/ │ │ │ │ │ └── if.c │ │ │ │ └── sys/ │ │ │ │ ├── socket.c │ │ │ │ └── uio.c │ │ │ ├── cfgu_service.hpp │ │ │ ├── display.cpp │ │ │ ├── display.hpp │ │ │ ├── keyboard.cpp │ │ │ ├── keyboard.h │ │ │ ├── locale.cpp │ │ │ ├── locale.hpp │ │ │ ├── messagebox.cpp │ │ │ ├── random.cpp │ │ │ ├── random.hpp │ │ │ ├── sockets.cpp │ │ │ ├── sockets.hpp │ │ │ ├── system.cpp │ │ │ └── system.h │ │ ├── ios/ │ │ │ ├── ios_paths.h │ │ │ └── ios_paths.m │ │ ├── locale.cpp │ │ ├── locale.hpp │ │ ├── macos_sdl1/ │ │ │ └── SDL_filesystem.m │ │ ├── switch/ │ │ │ ├── CMakeLists.txt │ │ │ ├── asio/ │ │ │ │ ├── include/ │ │ │ │ │ ├── errno.h │ │ │ │ │ ├── net/ │ │ │ │ │ │ └── if.h │ │ │ │ │ ├── netinet/ │ │ │ │ │ │ └── in.h │ │ │ │ │ └── sys/ │ │ │ │ │ ├── uio.h │ │ │ │ │ └── un.h │ │ │ │ ├── net/ │ │ │ │ │ └── if.c │ │ │ │ ├── pause.c │ │ │ │ └── sys/ │ │ │ │ └── signal.c │ │ │ ├── docking.cpp │ │ │ ├── docking.h │ │ │ ├── keyboard.cpp │ │ │ ├── keyboard.h │ │ │ ├── network.cpp │ │ │ ├── network.h │ │ │ ├── random.cpp │ │ │ ├── random.hpp │ │ │ ├── romfs.cpp │ │ │ └── romfs.hpp │ │ └── vita/ │ │ ├── CMakeLists.txt │ │ ├── keyboard.cpp │ │ ├── keyboard.h │ │ ├── network.cpp │ │ ├── network.h │ │ ├── random.cpp │ │ ├── random.hpp │ │ ├── touch.cpp │ │ └── touch.h │ ├── player.cpp │ ├── player.h │ ├── plrmsg.cpp │ ├── plrmsg.h │ ├── portal.cpp │ ├── portal.h │ ├── portals/ │ │ ├── validation.cpp │ │ └── validation.hpp │ ├── qol/ │ │ ├── autopickup.cpp │ │ ├── autopickup.h │ │ ├── chatlog.cpp │ │ ├── chatlog.h │ │ ├── floatingnumbers.cpp │ │ ├── floatingnumbers.h │ │ ├── itemlabels.cpp │ │ ├── itemlabels.h │ │ ├── monhealthbar.cpp │ │ ├── monhealthbar.h │ │ ├── stash.cpp │ │ ├── stash.h │ │ ├── xpbar.cpp │ │ └── xpbar.h │ ├── quests/ │ │ ├── validation.cpp │ │ └── validation.hpp │ ├── quests.cpp │ ├── quests.h │ ├── quick_messages.cpp │ ├── quick_messages.hpp │ ├── restrict.cpp │ ├── restrict.h │ ├── sha.cpp │ ├── sha.h │ ├── sound_effect_enums.h │ ├── spells.cpp │ ├── spells.h │ ├── stores.cpp │ ├── stores.h │ ├── storm/ │ │ ├── storm_net.cpp │ │ ├── storm_net.hpp │ │ ├── storm_svid.cpp │ │ └── storm_svid.h │ ├── sync.cpp │ ├── sync.h │ ├── tables/ │ │ ├── itemdat.cpp │ │ ├── itemdat.h │ │ ├── misdat.cpp │ │ ├── misdat.h │ │ ├── monstdat.cpp │ │ ├── monstdat.h │ │ ├── objdat.cpp │ │ ├── objdat.h │ │ ├── playerdat.cpp │ │ ├── playerdat.hpp │ │ ├── spelldat.cpp │ │ ├── spelldat.h │ │ ├── textdat.cpp │ │ ├── textdat.h │ │ ├── townerdat.cpp │ │ └── townerdat.hpp │ ├── tmsg.cpp │ ├── tmsg.h │ ├── towners.cpp │ ├── towners.h │ ├── track.cpp │ ├── track.h │ ├── translation_dummy.cpp │ ├── utils/ │ │ ├── algorithm/ │ │ │ └── container.hpp │ │ ├── attributes.h │ │ ├── aulib.hpp │ │ ├── bitset2d.hpp │ │ ├── cel_to_clx.cpp │ │ ├── cel_to_clx.hpp │ │ ├── cl2_to_clx.cpp │ │ ├── cl2_to_clx.hpp │ │ ├── clx_decode.hpp │ │ ├── clx_encode.hpp │ │ ├── console.cpp │ │ ├── console.h │ │ ├── display.cpp │ │ ├── display.h │ │ ├── endian_read.hpp │ │ ├── endian_stream.hpp │ │ ├── endian_swap.hpp │ │ ├── endian_write.hpp │ │ ├── enum_traits.h │ │ ├── file_name_generator.hpp │ │ ├── file_util.cpp │ │ ├── file_util.h │ │ ├── format_int.cpp │ │ ├── format_int.hpp │ │ ├── hp_mana_units.hpp │ │ ├── ini.cpp │ │ ├── ini.hpp │ │ ├── intrusive_optional.hpp │ │ ├── is_of.hpp │ │ ├── language.cpp │ │ ├── language.h │ │ ├── log.hpp │ │ ├── logged_fstream.cpp │ │ ├── logged_fstream.hpp │ │ ├── math.h │ │ ├── palette_blending.cpp │ │ ├── palette_blending.hpp │ │ ├── palette_kd_tree.cpp │ │ ├── palette_kd_tree.hpp │ │ ├── parse_int.cpp │ │ ├── parse_int.hpp │ │ ├── paths.cpp │ │ ├── paths.h │ │ ├── pcx.hpp │ │ ├── pcx_to_clx.cpp │ │ ├── pcx_to_clx.hpp │ │ ├── png.h │ │ ├── pointer_value_union.hpp │ │ ├── push_aulib_decoder.cpp │ │ ├── push_aulib_decoder.h │ │ ├── screen_reader.cpp │ │ ├── screen_reader.hpp │ │ ├── sdl2_backports.h │ │ ├── sdl2_to_1_2_backports.cpp │ │ ├── sdl2_to_1_2_backports.h │ │ ├── sdl_bilinear_scale.cpp │ │ ├── sdl_bilinear_scale.hpp │ │ ├── sdl_compat.h │ │ ├── sdl_geometry.h │ │ ├── sdl_mutex.h │ │ ├── sdl_ptrs.h │ │ ├── sdl_thread.cpp │ │ ├── sdl_thread.h │ │ ├── sdl_wrap.h │ │ ├── soundsample.cpp │ │ ├── soundsample.h │ │ ├── static_vector.hpp │ │ ├── status_macros.hpp │ │ ├── stdcompat/ │ │ │ ├── filesystem.hpp │ │ │ └── shared_ptr_array.hpp │ │ ├── str_case.cpp │ │ ├── str_case.hpp │ │ ├── str_cat.cpp │ │ ├── str_cat.hpp │ │ ├── str_split.hpp │ │ ├── string_or_view.hpp │ │ ├── string_view_hash.hpp │ │ ├── stubs.h │ │ ├── surface_to_clx.cpp │ │ ├── surface_to_clx.hpp │ │ ├── surface_to_pcx.cpp │ │ ├── surface_to_pcx.hpp │ │ ├── surface_to_png.cpp │ │ ├── surface_to_png.hpp │ │ ├── timer.cpp │ │ ├── timer.hpp │ │ ├── ui_fwd.h │ │ ├── utf8.cpp │ │ └── utf8.hpp │ ├── vision.cpp │ └── vision.hpp ├── Translations/ │ ├── be.po │ ├── bg.po │ ├── cs.po │ ├── da.po │ ├── de.po │ ├── devilutionx.pot │ ├── el.po │ ├── es.po │ ├── et.po │ ├── fi.po │ ├── fr.po │ ├── glossary.md │ ├── hr.po │ ├── hu.po │ ├── it.po │ ├── ja.po │ ├── ko.po │ ├── pl.po │ ├── pt_BR.po │ ├── ro.po │ ├── ru.po │ ├── sv.po │ ├── tr.po │ ├── uk.po │ ├── zh_CN.po │ └── zh_TW.po ├── VERSION ├── android-project/ │ ├── .gitignore │ ├── app/ │ │ ├── build.gradle │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── org/ │ │ │ ├── diasurgical/ │ │ │ │ └── devilutionx/ │ │ │ │ ├── DataActivity.java │ │ │ │ ├── DevilutionXSDLActivity.java │ │ │ │ ├── ExternalFilesManager.java │ │ │ │ └── ImportActivity.java │ │ │ └── libsdl/ │ │ │ └── app/ │ │ │ ├── HIDDevice.java │ │ │ ├── HIDDeviceBLESteamController.java │ │ │ ├── HIDDeviceManager.java │ │ │ ├── HIDDeviceUSB.java │ │ │ ├── SDL.java │ │ │ ├── SDLActivity.java │ │ │ ├── SDLAudioManager.java │ │ │ ├── SDLControllerManager.java │ │ │ └── SDLSurface.java │ │ └── res/ │ │ ├── drawable/ │ │ │ └── gamepad.xml │ │ ├── layout/ │ │ │ └── activity_data.xml │ │ ├── mipmap-anydpi-v26/ │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── values-be/ │ │ │ └── strings.xml │ │ ├── values-bg/ │ │ │ └── strings.xml │ │ ├── values-cs/ │ │ │ └── strings.xml │ │ ├── values-da/ │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-es/ │ │ │ └── strings.xml │ │ ├── values-fi/ │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ └── strings.xml │ │ ├── values-hr/ │ │ │ └── strings.xml │ │ ├── values-hu/ │ │ │ └── strings.xml │ │ ├── values-it/ │ │ │ └── strings.xml │ │ ├── values-ko-rKR/ │ │ │ └── strings.xml │ │ ├── values-pl/ │ │ │ └── strings.xml │ │ ├── values-pt-rBR/ │ │ │ └── strings.xml │ │ ├── values-ro-rRO/ │ │ │ └── strings.xml │ │ ├── values-ru/ │ │ │ └── strings.xml │ │ ├── values-sv/ │ │ │ └── strings.xml │ │ ├── values-tr/ │ │ │ └── strings.xml │ │ ├── values-uk/ │ │ │ └── strings.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ ├── values-zh-rTW/ │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── backup_descriptor.xml │ │ ├── backup_rules.xml │ │ └── shortcuts.xml │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── assets/ │ ├── ASSETS_VERSION │ ├── arena/ │ │ ├── church.dun │ │ ├── circle_of_death.dun │ │ └── hell.dun │ ├── data/ │ │ ├── boxleftend.clx │ │ ├── boxmiddle.clx │ │ ├── boxrightend.clx │ │ ├── charbg.clx │ │ ├── dirtybuc.clx │ │ ├── dirtybucp.clx │ │ ├── health.clx │ │ ├── healthbox.clx │ │ ├── hintbox.clx │ │ ├── hintboxbackground.clx │ │ ├── hinticons.clx │ │ ├── inv/ │ │ │ └── objcurs-widths.txt │ │ ├── monstertags.clx │ │ ├── panel8buc.clx │ │ ├── panel8bucp.clx │ │ ├── resistance.clx │ │ ├── stash.clx │ │ ├── stashnavbtns.clx │ │ ├── talkbutton.clx │ │ └── xpbar.clx │ ├── fonts/ │ │ ├── 12-00.clx │ │ ├── 12-01.clx │ │ ├── 12-02.clx │ │ ├── 12-03.clx │ │ ├── 12-04.clx │ │ ├── 12-05.clx │ │ ├── 12-1f1.clx │ │ ├── 12-1f3.clx │ │ ├── 12-1f4.clx │ │ ├── 12-1f5.clx │ │ ├── 12-1f6.clx │ │ ├── 12-1f9.clx │ │ ├── 12-20.clx │ │ ├── 12-26.clx │ │ ├── 12-e0.clx │ │ ├── 22-00.clx │ │ ├── 22-01.clx │ │ ├── 22-02.clx │ │ ├── 22-03.clx │ │ ├── 22-04.clx │ │ ├── 22-05.clx │ │ ├── 22-20.clx │ │ ├── 24-00.clx │ │ ├── 24-01.clx │ │ ├── 24-02.clx │ │ ├── 24-03.clx │ │ ├── 24-04.clx │ │ ├── 24-05.clx │ │ ├── 24-1f1.clx │ │ ├── 24-1f3.clx │ │ ├── 24-1f4.clx │ │ ├── 24-1f5.clx │ │ ├── 24-1f6.clx │ │ ├── 24-1f9.clx │ │ ├── 24-20.clx │ │ ├── 24-26.clx │ │ ├── 24-e0.clx │ │ ├── 30-00.clx │ │ ├── 30-01.clx │ │ ├── 30-02.clx │ │ ├── 30-03.clx │ │ ├── 30-04.clx │ │ ├── 30-05.clx │ │ ├── 30-20.clx │ │ ├── 30-e0.clx │ │ ├── 42-00.clx │ │ ├── 42-01.clx │ │ ├── 42-02.clx │ │ ├── 42-03.clx │ │ ├── 42-04.clx │ │ ├── 42-05.clx │ │ ├── 42-20.clx │ │ ├── 46-00.clx │ │ ├── 46-01.clx │ │ ├── 46-02.clx │ │ ├── 46-03.clx │ │ ├── 46-04.clx │ │ ├── 46-05.clx │ │ ├── 46-20.clx │ │ ├── black.trn │ │ ├── blue.trn │ │ ├── buttonface.trn │ │ ├── buttonpushed.trn │ │ ├── gamedialogred.trn │ │ ├── gamedialogwhite.trn │ │ ├── gamedialogyellow.trn │ │ ├── goldui.trn │ │ ├── golduis.trn │ │ ├── grayui.trn │ │ ├── grayuis.trn │ │ ├── orange.trn │ │ ├── red.trn │ │ ├── tr/ │ │ │ ├── 12-00.clx │ │ │ ├── 24-00.clx │ │ │ ├── 30-00.clx │ │ │ ├── 42-00.clx │ │ │ └── 46-00.clx │ │ ├── white.trn │ │ ├── whitegold.trn │ │ └── yellow.trn │ ├── gendata/ │ │ ├── cut2w.clx │ │ ├── cut3w.clx │ │ ├── cut4w.clx │ │ ├── cutgatew.clx │ │ ├── cutl1dw.clx │ │ ├── cutportlw.clx │ │ ├── cutportrw.clx │ │ ├── cutstartw.clx │ │ ├── cutttw.clx │ │ └── pause.trn │ ├── levels/ │ │ ├── l1data/ │ │ │ └── sklkngt.dun │ │ ├── l2data/ │ │ │ └── bonechat.dun │ │ └── towndata/ │ │ ├── automap.amp │ │ └── automap.dun │ ├── lua/ │ │ ├── devilutionx/ │ │ │ └── events.lua │ │ ├── inspect.lua │ │ ├── mods/ │ │ │ ├── Floating Numbers - Damage/ │ │ │ │ └── init.lua │ │ │ ├── Floating Numbers - XP/ │ │ │ │ └── init.lua │ │ │ ├── adria_refills_mana/ │ │ │ │ └── init.lua │ │ │ └── clock/ │ │ │ └── init.lua │ │ └── repl_prelude.lua │ ├── lua_internal/ │ │ └── get_lua_function_signature.lua │ ├── plrgfx/ │ │ └── warrior/ │ │ └── whu/ │ │ ├── whufm.trn │ │ ├── whulm.trn │ │ └── whuqm.trn │ ├── txtdata/ │ │ ├── Experience.tsv │ │ ├── Readme.md │ │ ├── classes/ │ │ │ ├── README.md │ │ │ ├── barbarian/ │ │ │ │ ├── animations.tsv │ │ │ │ ├── attributes.tsv │ │ │ │ ├── sounds.tsv │ │ │ │ ├── sprites.tsv │ │ │ │ └── starting_loadout.tsv │ │ │ ├── bard/ │ │ │ │ ├── animations.tsv │ │ │ │ ├── attributes.tsv │ │ │ │ ├── sounds.tsv │ │ │ │ ├── sprites.tsv │ │ │ │ └── starting_loadout.tsv │ │ │ ├── classdat.tsv │ │ │ ├── monk/ │ │ │ │ ├── animations.tsv │ │ │ │ ├── attributes.tsv │ │ │ │ ├── sounds.tsv │ │ │ │ ├── sprites.tsv │ │ │ │ └── starting_loadout.tsv │ │ │ ├── rogue/ │ │ │ │ ├── animations.tsv │ │ │ │ ├── attributes.tsv │ │ │ │ ├── sounds.tsv │ │ │ │ ├── sprites.tsv │ │ │ │ └── starting_loadout.tsv │ │ │ ├── sorcerer/ │ │ │ │ ├── animations.tsv │ │ │ │ ├── attributes.tsv │ │ │ │ ├── sounds.tsv │ │ │ │ ├── sprites.tsv │ │ │ │ └── starting_loadout.tsv │ │ │ └── warrior/ │ │ │ ├── animations.tsv │ │ │ ├── attributes.tsv │ │ │ ├── sounds.tsv │ │ │ ├── sprites.tsv │ │ │ └── starting_loadout.tsv │ │ ├── items/ │ │ │ ├── item_prefixes.tsv │ │ │ ├── item_suffixes.tsv │ │ │ ├── itemdat.tsv │ │ │ └── unique_itemdat.tsv │ │ ├── missiles/ │ │ │ ├── misdat.tsv │ │ │ └── missile_sprites.tsv │ │ ├── monsters/ │ │ │ ├── monstdat.tsv │ │ │ └── unique_monstdat.tsv │ │ ├── objects/ │ │ │ └── objdat.tsv │ │ ├── quests/ │ │ │ └── questdat.tsv │ │ ├── sound/ │ │ │ ├── effects-unused.tsv │ │ │ └── effects.tsv │ │ ├── spells/ │ │ │ └── spelldat.tsv │ │ ├── text/ │ │ │ ├── textdat-unused.tsv │ │ │ └── textdat.tsv │ │ └── towners/ │ │ ├── quest_dialog.tsv │ │ └── towners.tsv │ └── ui_art/ │ ├── creditsw.clx │ ├── diablo.pal │ ├── dvl_but_sml.clx │ ├── dvl_lrpopup.clx │ ├── hellfire.pal │ └── mainmenuw.clx ├── codecov.yml ├── docs/ │ ├── BACKGROUND.md │ ├── CHANGELOG.md │ ├── CONTRIBUTING.md │ ├── TODO.md │ ├── benchmarking.md │ ├── building.md │ ├── debug.md │ ├── github-codespaces.md │ ├── installing.md │ ├── manual/ │ │ └── platforms/ │ │ ├── 3ds.md │ │ ├── gkd350h.md │ │ ├── retrofw.md │ │ ├── rg350.md │ │ ├── rg99.md │ │ ├── switch.md │ │ ├── vita.md │ │ ├── xbox-one.md │ │ └── xbox.md │ ├── privacy.md │ └── profiling-linux.md ├── doxygen.config ├── mods/ │ └── Hellfire/ │ ├── data/ │ │ └── inv/ │ │ └── objcurs2-widths.txt │ ├── lua/ │ │ └── mods/ │ │ └── Hellfire/ │ │ └── init.lua │ ├── nlevels/ │ │ ├── cutl5w.clx │ │ ├── cutl6w.clx │ │ └── l5data/ │ │ ├── cornerstone.dun │ │ └── uberroom.dun │ ├── txtdata/ │ │ ├── classes/ │ │ │ ├── classdat.tsv │ │ │ └── sorcerer/ │ │ │ └── starting_loadout.tsv │ │ ├── items/ │ │ │ ├── item_prefixes.tsv │ │ │ ├── item_suffixes.tsv │ │ │ └── unique_itemdat.tsv │ │ ├── missiles/ │ │ │ ├── misdat.tsv │ │ │ └── missile_sprites.tsv │ │ ├── monsters/ │ │ │ └── monstdat.tsv │ │ ├── sound/ │ │ │ ├── effects-unused.tsv │ │ │ └── effects.tsv │ │ ├── spells/ │ │ │ └── spelldat.tsv │ │ └── towners/ │ │ ├── quest_dialog.tsv │ │ └── towners.tsv │ └── ui_art/ │ ├── diablo.pal │ ├── hf_titlew.clx │ ├── mainmenuw.clx │ └── supportw.clx ├── test/ │ ├── .clang-format │ ├── Fixtures.cmake │ ├── animationinfo_test.cpp │ ├── app_fatal_for_testing.cpp │ ├── appfat_test.cpp │ ├── automap_test.cpp │ ├── clx_render_benchmark.cpp │ ├── codec_test.cpp │ ├── crawl_benchmark.cpp │ ├── crawl_test.cpp │ ├── cursor_test.cpp │ ├── data_file_test.cpp │ ├── dead_test.cpp │ ├── diablo_test.cpp │ ├── drlg_common_test.cpp │ ├── drlg_l1_test.cpp │ ├── drlg_l2_test.cpp │ ├── drlg_l3_test.cpp │ ├── drlg_l4_test.cpp │ ├── drlg_test.hpp │ ├── dun_render_benchmark.cpp │ ├── effects_test.cpp │ ├── file_util_test.cpp │ ├── fixtures/ │ │ ├── diablo/ │ │ │ ├── 1-2588.dun │ │ │ ├── 1-743271966.dun │ │ │ ├── 10-1630062353.dun │ │ │ ├── 10-879635115.dun │ │ │ ├── 11-384626536.dun │ │ │ ├── 12-2104541047.dun │ │ │ ├── 13-428074402.dun │ │ │ ├── 13-594689775.dun │ │ │ ├── 14-717625719.dun │ │ │ ├── 14-815743776.dun │ │ │ ├── 15-1256511996.dun │ │ │ ├── 15-1583642716-changed.dun │ │ │ ├── 15-1583642716.dun │ │ │ ├── 16-741281013.dun │ │ │ ├── 2-1383137027.dun │ │ │ ├── 3-844660068.dun │ │ │ ├── 4-609325643.dun │ │ │ ├── 4-902156014.dun │ │ │ ├── 5-1677631846.dun │ │ │ ├── 5-68685319.dun │ │ │ ├── 6-1824554527.dun │ │ │ ├── 6-2033265779.dun │ │ │ ├── 6-2034738122.dun │ │ │ ├── 7-1607627156.dun │ │ │ ├── 7-680552750.dun │ │ │ ├── 8-1999936419.dun │ │ │ └── 9-262005438.dun │ │ ├── hellfire/ │ │ │ ├── 1-401921334.dun │ │ │ ├── 1-536340718.dun │ │ │ ├── 17-19770182.dun │ │ │ ├── 18-1522546307.dun │ │ │ ├── 19-125121312.dun │ │ │ ├── 2-1180526547.dun │ │ │ ├── 2-128964898.dun │ │ │ ├── 20-1511478689.dun │ │ │ ├── 21-2122696790.dun │ │ │ ├── 22-1191662129.dun │ │ │ ├── 23-97055268.dun │ │ │ ├── 24-1324803725.dun │ │ │ ├── 3-1369955278.dun │ │ │ ├── 3-1512491184.dun │ │ │ ├── 3-1799396623.dun │ │ │ ├── 4-1190318991.dun │ │ │ └── 4-1924296259.dun │ │ ├── levels/ │ │ │ ├── l1data/ │ │ │ │ ├── banner1.dun │ │ │ │ ├── banner2.dun │ │ │ │ ├── rnd6.dun │ │ │ │ └── skngdo.dun │ │ │ ├── l2data/ │ │ │ │ ├── blind1.dun │ │ │ │ ├── blood1.dun │ │ │ │ ├── blood2.dun │ │ │ │ ├── bonestr1.dun │ │ │ │ └── bonestr2.dun │ │ │ ├── l3data/ │ │ │ │ └── anvil.dun │ │ │ └── l4data/ │ │ │ ├── diab1.dun │ │ │ ├── diab2a.dun │ │ │ ├── diab2b.dun │ │ │ ├── diab3a.dun │ │ │ ├── diab3b.dun │ │ │ ├── diab4a.dun │ │ │ ├── diab4b.dun │ │ │ ├── vile1.dun │ │ │ ├── warlord.dun │ │ │ └── warlord2.dun │ │ ├── light_render_benchmark/ │ │ │ └── dLight.dmp │ │ ├── memory_map/ │ │ │ ├── additionalMissiles.txt │ │ │ ├── game.txt │ │ │ ├── hero.txt │ │ │ ├── item.txt │ │ │ ├── itemPack.txt │ │ │ ├── level.txt │ │ │ ├── levelSeed.txt │ │ │ ├── lightning.txt │ │ │ ├── missile.txt │ │ │ ├── monster.txt │ │ │ ├── object.txt │ │ │ ├── player.txt │ │ │ ├── portal.txt │ │ │ └── quest.txt │ │ ├── timedemo/ │ │ │ └── WarriorLevel1to2/ │ │ │ ├── demo_0.dmo │ │ │ ├── demo_0_reference_spawn_0.sv │ │ │ └── spawn_0.sv │ │ └── txtdata/ │ │ ├── cr.tsv │ │ ├── crlf.tsv │ │ ├── empty.tsv │ │ ├── empty_with_utf8_bom.tsv │ │ ├── lf.tsv │ │ ├── lf_no_trail.tsv │ │ ├── sample.tsv │ │ └── utf8_bom.tsv │ ├── format_int_test.cpp │ ├── ini_test.cpp │ ├── inv_test.cpp │ ├── items_test.cpp │ ├── language_for_testing.cpp │ ├── light_render_benchmark.cpp │ ├── main.cpp │ ├── math_test.cpp │ ├── missiles_test.cpp │ ├── multi_logging_test.cpp │ ├── pack_test.cpp │ ├── palette_blending_benchmark.cpp │ ├── palette_blending_test.cpp │ ├── parse_int_test.cpp │ ├── path_benchmark.cpp │ ├── path_test.cpp │ ├── player_test.cpp │ ├── player_test.h │ ├── quests_test.cpp │ ├── random_test.cpp │ ├── rectangle_test.cpp │ ├── scrollrt_test.cpp │ ├── static_vector_test.cpp │ ├── stores_test.cpp │ ├── str_cat_test.cpp │ ├── text_render_integration_test.cpp │ ├── tile_properties_test.cpp │ ├── timedemo_test.cpp │ ├── townerdat_test.cpp │ ├── utf8_test.cpp │ ├── vendor_test.cpp │ ├── vision_test.cpp │ └── writehero_test.cpp ├── tools/ │ ├── Dockerfile.s390x │ ├── build_and_install_smpq.ps1 │ ├── build_and_install_smpq.sh │ ├── build_and_run_benchmark.py │ ├── build_pgo.sh │ ├── extract_translation_data.py │ ├── gdb/ │ │ ├── README.md │ │ └── devilution_gdb/ │ │ ├── __init__.py │ │ └── pretty_printers/ │ │ └── utils/ │ │ └── static_vector_pp.py │ ├── linux_reduced_cpu_variance_run.sh │ ├── lldb/ │ │ ├── README.md │ │ └── devilution_lldb/ │ │ ├── __init__.py │ │ └── pretty_printers/ │ │ └── utils/ │ │ └── static_vector_pp.py │ ├── make_src_dist.py │ ├── measure_timedemo_performance.py │ ├── run_big_endian_tests.sh │ ├── segmenter/ │ │ ├── README.md │ │ ├── requirements.txt │ │ ├── segment_all.py │ │ ├── segment_ja.py │ │ ├── segment_zh.py │ │ └── segmenter_lib.py │ ├── update_bundled_assets.sh │ ├── update_sdl_android_project.sh │ ├── update_translations_pot.py │ └── validate_translations.py ├── uwp-project/ │ ├── Package.appxmanifest │ ├── Package.appxmanifest.template │ ├── devilutionX_TemporaryKey.pfx │ ├── devilutionx.sln │ ├── devilutionx.vcxproj │ └── src/ │ └── SDL_winrt_main_NonXAML.cpp └── vcpkg.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .devcontainer/Dockerfile ================================================ ARG VARIANT=debian-12 FROM mcr.microsoft.com/devcontainers/base:${VARIANT} USER root # Install APT packages RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ && apt-get -y install tar curl zip unzip bash-completion build-essential ripgrep htop \ ninja-build ccache g++ mold gdb clang-format clang-tidy \ rpm pkg-config cmake git gettext libsdl2-dev libsdl2-image-dev libsodium-dev \ libpng-dev libbz2-dev libfmt-dev libgtest-dev libgmock-dev libbenchmark-dev zsh \ qtbase5-dev qt6-base-dev ristretto \ && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* RUN --mount=type=bind,source=tools/build_and_install_smpq.sh,target=/tmp/build_and_install_smpq.sh sh /tmp/build_and_install_smpq.sh # Install devilutionx-graphics-tools RUN git clone https://github.com/diasurgical/devilutionx-graphics-tools.git /tmp/devilutionx-graphics-tools && \ cd /tmp/devilutionx-graphics-tools && \ cmake -S. -Bbuild-rel -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF && \ cmake --build build-rel -j $(getconf _NPROCESSORS_ONLN) && \ cmake --install build-rel --component Binaries && \ rm -rf /tmp/devilutionx-graphics-tools # Install devilutionx-mpq-tools RUN git clone https://github.com/diasurgical/devilutionx-mpq-tools.git /tmp/devilutionx-mpq-tools && \ cd /tmp/devilutionx-mpq-tools && \ cmake -S. -Bbuild-rel -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF && \ cmake --build build-rel -j $(getconf _NPROCESSORS_ONLN) && \ cmake --install build-rel && \ rm -rf /tmp/devilutionx-mpq-tools # Install d1-graphics-tool RUN git clone https://github.com/diasurgical/d1-graphics-tool.git /tmp/d1-graphics-tool && \ cd /tmp/d1-graphics-tool && \ cmake -S. -Bbuild-rel -G Ninja -DCMAKE_BUILD_TYPE=Release && \ cmake --build build-rel -j $(getconf _NPROCESSORS_ONLN) && \ cmake --install build-rel && \ rm -rf /tmp/d1-graphics-tool # Download spawn.mpq and fonts.mpq RUN curl --create-dirs -O -L --output-dir /usr/local/share/diasurgical/devilutionx/ \ https://github.com/diasurgical/devilutionx-assets/releases/latest/download/spawn.mpq && \ curl --create-dirs -O -L --output-dir /usr/local/share/diasurgical/devilutionx/ \ https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq && \ chown -R vscode: /usr/local/share/diasurgical/ # Desktop environment configuration COPY .devcontainer/fluxbox /home/vscode/.fluxbox/ ================================================ FILE: .devcontainer/devcontainer.json ================================================ { "build": { "dockerfile": "Dockerfile", "context": ".." }, "customizations": { "vscode": { "extensions": [ "github.vscode-github-actions", "ms-vscode.cmake-tools" ] } }, "features": { // https://github.com/devcontainers/features/tree/main/src/desktop-lite "ghcr.io/devcontainers/features/desktop-lite:1": { "webPort": 6080, "password": "vscode" }, "ghcr.io/devcontainers-contrib/features/zsh-plugins:0": {}, "ghcr.io/stuartleeks/dev-container-features/shell-history:0": {} }, "forwardPorts": [ 6080 ], "portsAttributes": { "6080": { "label": "desktop" } } } ================================================ FILE: .devcontainer/fluxbox/apps ================================================ [transient] (role=GtkFileChooserDialog) [Dimensions] {70% 70%} [Position] (CENTER) {0 0} [end] [app] (name=AppRun) (class=tiled) [Fullscreen] {yes} [end] ================================================ FILE: .devcontainer/fluxbox/menu ================================================ [begin] ( Application Menu ) [exec] (File Manager) { nautilus /workspaces/devilutionX } [exec] (D1 Graphics Tool) { D1GraphicsTool } <> [exec] (Text Editor) { mousepad } <> [exec] (Terminal) { tilix -w ~ -e $(readlink -f /proc/$$/exe) -il } <> [exec] (Web Browser) { x-www-browser --disable-dev-shm-usage } <> [submenu] (System) {} [exec] (Set Resolution) { tilix -t "Set Resolution" -e bash /usr/local/bin/set-resolution } <> [exec] (Edit Application Menu) { mousepad ~/.fluxbox/menu } <> [exec] (Passwords and Keys) { seahorse } <> [exec] (Top Processes) { tilix -t "Top" -e htop } <> [exec] (Disk Utilization) { tilix -t "Disk Utilization" -e ncdu / } <> [exec] (Editres) {editres} <> [exec] (Xfontsel) {xfontsel} <> [exec] (Xkill) {xkill} <> [exec] (Xrefresh) {xrefresh} <> [end] [config] (Configuration) [workspaces] (Workspaces) [end] ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab end_of_line = crlf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{c++,cc,cpp,cppm,cxx,h,h++,hh,hpp,hxx,inl,ipp,ixx,tlh,tli}] # Visual C++ Code Style settings cpp_generate_documentation_comments = doxygen_slash_star [*.java] end_of_line = lf [*.pot] end_of_line = lf [*.po] end_of_line = lf [*.lua] indent_style = space indent_size = 2 end_of_line = lf [*.py] indent_style = space indent_size = 4 end_of_line = lf [*.rb] end_of_line = lf [*.yml] indent_style = space end_of_line = lf [*.sh] end_of_line = lf [*.xml] end_of_line = lf [.clang-format] end_of_line = lf [.gitignore] end_of_line = lf [*.cmake] indent_style = space indent_size = 2 [*.desktop] end_of_line = lf [*.md] indent_style = space indent_size = 2 end_of_line = lf [*.txt] end_of_line = crlf [*.tsv] trim_trailing_whitespace = false [*.plist] end_of_line = lf [AppRun] end_of_line = lf [{CMakeLists.txt,CMakeSettings.json}] indent_style = space indent_size = 2 end_of_line = crlf [control] end_of_line = lf [devilutionx.spec] end_of_line = lf [Dockerfile] end_of_line = lf [ASSETS_VERSION] end_of_line = lf ================================================ FILE: .gdbinit ================================================ source tools/gdb/devilution_gdb/__init__.py ================================================ FILE: .gitattributes ================================================ # Do not let git change line endings. * -text ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create a report to help us improve title: "[Issue Report]: " labels: ["issue report"] body: - type: dropdown id: operating-system attributes: label: Operating System options: - Windows x64 - Windows x86 - Linux x64 - Linux x86 - Mac - Android - iOS - Xbox One - PlayStation 4 - Nintendo Switch - PlayStation Vita - Nintendo 3DS - Other (please specify) validations: required: true - type: dropdown id: version attributes: label: DevilutionX version options: - 1.5.5 - 1.5.4 - 1.5.3 - 1.5.2 - 1.5.1 - 1.5.0 - 1.4.1 - 1.4.0 - 1.3.0 - 1.2.1 - 1.1.0 - Custom build (please specify commit ID) - Other (please specify version number) validations: required: true - type: textarea id: description attributes: label: Describe placeholder: A clear and concise description of what the bug is. validations: required: true - type: textarea id: steps-to-reproduce attributes: label: To Reproduce description: Steps to reproduce the behavior placeholder: | 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error validations: required: true - type: input id: expected-behavior attributes: label: Expected Behavior placeholder: A clear and concise description of what you expected to happen. - type: textarea id: additional-context attributes: label: Additional context placeholder: Any other context about the problem here (screenshots, videos, code blocks, etc.). ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: Feature Request description: Request a feature or improvement. title: "[Feature Request]: " labels: ["enhancement"] body: - type: dropdown id: feature-type attributes: label: Feature Type options: - Quality of Life - Touch Controls - Gamepad Controls - Other (please specify) validations: required: true - type: textarea id: description attributes: label: Describe placeholder: A clear and concise description of the desired feature/change. validations: required: true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: '/' schedule: interval: weekly ================================================ FILE: .github/workflows/3ds.yml ================================================ --- name: Nintendo 3DS on: # yamllint disable-line rule:truthy push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest container: devkitpro/devkitarm:latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | apt-get update && apt-get install -y --no-install-recommends --no-install-suggests \ ffmpeg - name: Get external dependencies run: | wget https://github.com/diasurgical/bannertool/releases/download/1.2.0/bannertool.zip unzip -j "bannertool.zip" "linux-x86_64/bannertool" -d "/opt/devkitpro/tools/bin" wget https://github.com/3DSGuy/Project_CTR/releases/download/makerom-v0.18/makerom-v0.18-ubuntu_x86_64.zip unzip "makerom-v0.18-ubuntu_x86_64.zip" "makerom" -d "/opt/devkitpro/tools/bin" chmod a+x /opt/devkitpro/tools/bin/makerom - name: Configure CMake run: | cmake \ -S . \ -B build \ -G Ninja \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/3DS.cmake - name: Build DevilutionX run: cmake --build build - name: Upload 3dsx Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx.3dsx path: ./build/devilutionx.3dsx - name: Upload cia Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx.cia path: ./build/devilutionx.cia - name: Update Release 3dsx if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-3ds.3dsx file: ./build/devilutionx.3dsx overwrite: true - name: Update Release cia if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-3ds.cia file: ./build/devilutionx.cia overwrite: true ================================================ FILE: .github/workflows/Android.yml ================================================ name: Android on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - name: Install gettext run: sudo apt-get update && sudo apt-get install -y gettext - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'adopt' cache: gradle - name: Install CMake run: | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager "cmake;3.31.0" - name: Cache CMake build folder uses: actions/cache@v5 with: path: android-project/app/.cxx key: ${{ github.workflow }}-v5-${{ github.sha }} restore-keys: ${{ github.workflow }}-v5- - name: Build working-directory: ${{github.workspace}} shell: bash run: cd android-project && ./gradlew assembleDebug - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-debug.apk path: android-project/app/build/outputs/apk/debug/app-debug.apk - name: Clean up artifacts run: rm -rf android-project/app/build/outputs ================================================ FILE: .github/workflows/Linux_aarch64.yml ================================================ name: Linux AArch64 (aarch64-linux-gnu) on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Installs dependencies, including arm64 libraries (runs `sudo apt-get update` as part of it) - name: Install dependencies run: Packaging/nix/debian-cross-aarch64-prep.sh - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v8-${{ github.sha }} restore-keys: ${{ github.workflow }}-v8- - name: Build working-directory: ${{github.workspace}} shell: bash env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. run: | cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/aarch64-linux-gnu.toolchain.cmake \ -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON -DDEVILUTIONX_SYSTEM_LIBFMT=OFF \ -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package run: Packaging/nix/LinuxReleasePackaging.sh && mv devilutionx.tar.xz devilutionx-aarch64-linux-gnu.tar.xz # AppImage cross-packaging is not implemented yet. # - name: Package AppImage # run: Packaging/nix/AppImage.sh && mv devilutionx.appimage devilutionx-aarch64-linux-gnu.appimage - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-aarch64-linux-gnu.tar.xz path: devilutionx-aarch64-linux-gnu.tar.xz # AppImage cross-packaging is not implemented yet. # - name: Upload AppImage # if: ${{ !env.ACT }} # uses: actions/upload-artifact@v7 # with: # name: devilutionx-aarch64-linux-gnu.appimage # path: devilutionx-aarch64-linux-gnu.appimage - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-aarch64-linux-gnu.* file_glob: true overwrite: true - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/package build/*.deb build/*.rpm build/*.appimage build/*.tar.xz ================================================ FILE: .github/workflows/Linux_x86.yml ================================================ name: Linux x86 (i386-linux-gnu) on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Installs dependencies, including x86 libraries (runs `sudo apt-get update` as part of it) - name: Install dependencies run: Packaging/nix/debian-cross-i386-prep.sh - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v7-${{ github.sha }} restore-keys: ${{ github.workflow }}-v7- - name: Build working-directory: ${{github.workspace}} shell: bash env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. run: | cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/linux_i386.toolchain.cmake \ -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON \ -DBUILD_TESTING=OFF -DDEVILUTIONX_SYSTEM_LIBFMT=OFF -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF \ -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package run: Packaging/nix/LinuxReleasePackaging.sh && mv devilutionx.tar.xz devilutionx-i386-linux-gnu.tar.xz # AppImage cross-packaging is not implemented yet. # - name: Package AppImage # run: Packaging/nix/AppImage.sh && mv devilutionx.appimage devilutionx-i386-linux-gnu.appimage - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-i386-linux-gnu.tar.xz path: devilutionx-i386-linux-gnu.tar.xz # AppImage cross-packaging is not implemented yet. # - name: Upload AppImage # if: ${{ !env.ACT }} # uses: actions/upload-artifact@v7 # with: # name: devilutionx-i386-linux-gnu.appimage # path: devilutionx-i386-linux-gnu.appimage - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-i386-linux-gnu.* file_glob: true overwrite: true - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/package build/*.deb build/*.rpm build/*.appimage build/*.tar.xz ================================================ FILE: .github/workflows/Linux_x86_64.yml ================================================ name: Linux x86_64 (x86_64-linux-gnu) on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Installs dependencies and runs `sudo apt-get update` as part of it. - name: Install dependencies run: Packaging/nix/debian-host-prep.sh - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v7-${{ github.sha }} restore-keys: ${{ github.workflow }}-v7- - name: Build working-directory: ${{github.workspace}} shell: bash env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} # We set DEVILUTIONX_SYSTEM_LIBFMT=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_LIBSODIUM=OFF because its soversion changes frequently. # We set DEVILUTIONX_SYSTEM_BZIP2=OFF because Fedora and Debian do not agree on how to link it. run: | cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_INSTALL_PREFIX=/usr -DCPACK=ON \ -DDISCORD_INTEGRATION=ON -DBUILD_TESTING=OFF -DDEVILUTIONX_SYSTEM_LIBFMT=OFF \ -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF -DDEVILUTIONX_SYSTEM_BZIP2=OFF && \ cmake --build build -j $(getconf _NPROCESSORS_ONLN) --target package - name: Package run: Packaging/nix/LinuxReleasePackaging.sh && mv devilutionx.tar.xz devilutionx-x86_64-linux-gnu.tar.xz - name: Package AppImage run: Packaging/nix/AppImage.sh && mv devilutionx.appimage devilutionx-x86_64-linux-gnu.appimage - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-x86_64-linux-gnu.tar.xz path: devilutionx-x86_64-linux-gnu.tar.xz - name: Upload AppImage if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-x86_64-linux-gnu.appimage path: devilutionx-x86_64-linux-gnu.appimage - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-x86_64-linux-gnu.* file_glob: true overwrite: true - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/package build/*.deb build/*.rpm build/*.appimage build/*.tar.xz ================================================ FILE: .github/workflows/Linux_x86_64_SDL1.yml ================================================ name: Linux x64 SDL1 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y cmake file g++ git libfmt-dev libsdl1.2-dev libsodium-dev libpng-dev libbz2-dev rpm smpq - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v3-${{ github.sha }} restore-keys: ${{ github.workflow }}-v3- - name: Configure CMake shell: bash working-directory: ${{github.workspace}} # Disable LTO to work around an ICE in gcc11 run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DUSE_SDL1=ON -DDISCORD_INTEGRATION=ON -DDISABLE_LTO=ON - name: Build working-directory: ${{github.workspace}} shell: bash run: cmake --build build -j $(nproc) --target package - name: Package run: Packaging/nix/LinuxReleasePackaging.sh - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx_linux_x86_64_SDL1.tar.xz path: devilutionx.tar.xz - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/package build/*.deb build/*.rpm build/*.appimage build/*.tar.xz ================================================ FILE: .github/workflows/Linux_x86_64_SDL3_test.yml ================================================ name: Linux x64 SDL3 Tests on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake curl g++ git libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsodium-dev libpng-dev libbz2-dev libasound2-dev libxcursor-dev libxi-dev libxrandr-dev libxss-dev libxtst-dev libxkbcommon-dev wget - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v4-${{ github.sha }} restore-keys: ${{ github.workflow }}-v4- # We specify `-DDEVILUTIONX_SYSTEM_BENCHMARK=OFF` to work around the following error: # lto1: fatal error: bytecode stream in file ‘/usr/lib/x86_64-linux-gnu/libbenchmark_main.a’ generated with LTO version 11.2 instead of the expected 11.3 - name: Build tests run: | cmake -S. -Bbuild -G Ninja -DUSE_SDL3=ON -DDEVILUTIONX_SYSTEM_SDL3=OFF -DDEVILUTIONX_STATIC_SDL3=ON -DDEVILUTIONX_SYSTEM_SDL_IMAGE=OFF -DDEVILUTIONX_SYSTEM_BENCHMARK=OFF wget -qnc https://github.com/diasurgical/devilutionx-assets/releases/download/v2/spawn.mpq -P build cmake --build build -j $(nproc) - name: Run tests run: cd build && ctest --output-on-failure ================================================ FILE: .github/workflows/Linux_x86_64_test.yml ================================================ name: Linux x64 Tests on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake curl g++ git lcov libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsdl2-dev libsodium-dev libpng-dev libbz2-dev wget - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v3-${{ github.sha }} restore-keys: ${{ github.workflow }}-v3- # We specify `-DDEVILUTIONX_SYSTEM_BENCHMARK=OFF` to work around the following error: # lto1: fatal error: bytecode stream in file ‘/usr/lib/x86_64-linux-gnu/libbenchmark_main.a’ generated with LTO version 11.2 instead of the expected 11.3 - name: Build tests run: | cmake -S. -Bbuild -G Ninja -DENABLE_CODECOVERAGE=ON -DDEVILUTIONX_SYSTEM_BENCHMARK=OFF wget -qnc https://github.com/diasurgical/devilutionx-assets/releases/download/v2/spawn.mpq -P build cmake --build build -j $(nproc) - name: Run tests run: cd build && ctest --output-on-failure - name: Upload results uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/PS4.yml ================================================ name: PS4 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: ps4: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: | sudo apt-get update && \ sudo apt-get install -y pacman-package-manager wget cmake git gettext smpq && \ sudo tee -a /etc/pacman.conf > /dev/null <" | sudo tee /opt/pacbrew/ps4/openorbis/include/sys/endian.h - name: Build working-directory: ${{github.workspace}} shell: bash run: Packaging/ps4/build.sh - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-ps4.pkg path: build-ps4/devilutionx.pkg - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-ps4.pkg file: build-ps4/devilutionx.pkg overwrite: true ================================================ FILE: .github/workflows/PS5.yml ================================================ name: PS5 (ps5-payload-dev) on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: ps5: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: | sudo apt update sudo apt install cmake pkg-config clang-15 lld-15 sudo apt install build-essential autoconf libtool yasm nasm sudo apt install smpq gperf pkgconf libarchive-tools autopoint po4a git curl doxygen wget wget https://github.com/ps5-payload-dev/pacbrew-repo/releases/download/v0.27/ps5-payload-dev.tar.gz sudo tar -xf ps5-payload-dev.tar.gz -C / - name: Build working-directory: ${{github.workspace}} shell: bash run: Packaging/ps5/build.sh - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-ps5.zip path: build-ps5/devilutionx-ps5.zip - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-ps5.zip file: build-ps5/devilutionx-ps5.zip overwrite: true ================================================ FILE: .github/workflows/Windows9x_MinGW.yml ================================================ name: Windows 9x MinGW on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y cmake gcc-mingw-w64-i686 g++-mingw-w64-i686 mingw-w64-tools libz-mingw-w64-dev gettext dpkg-dev wget git sudo smpq && sudo rm /usr/i686-w64-mingw32/lib/libz.dll.a && sudo Packaging/windows/mingw9x-prep.sh - name: Configure CMake shell: bash working-directory: ${{github.workspace}} run: cmake -S. -Bbuild-windows9x -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingw9x.toolchain.cmake -DTARGET_PLATFORM=windows9x - name: Build working-directory: ${{github.workspace}} shell: bash run: | cmake --build build-windows9x -j $(nproc) --target package mv build-windows9x/devilutionx.zip devilutionx-win9x.zip - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: path: devilutionx-win9x.zip - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-win9x.zip overwrite: true ================================================ FILE: .github/workflows/Windows_MSVC_x64.yml ================================================ name: Windows MSVC x64 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' permissions: contents: write env: VCPKG_FEATURE_FLAGS: dependencygraph GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: ccache uses: hendrikmuhs/ccache-action@v1.2 - name: Install latest CMake uses: lukka/get-cmake@latest - name: Restore or setup vcpkg uses: lukka/run-vcpkg@v11.5 with: vcpkgGitCommitId: 'a62ce77d56ee07513b4b67de1ec2daeaebfae51a' - name: Fetch test data run: | mkdir build-ninja-vcpkg-relwithdebinfo Invoke-WebRequest -Uri "https://github.com/diasurgical/devilutionx-assets/releases/download/v2/spawn.mpq" -OutFile "build-ninja-vcpkg-relwithdebinfo/spawn.mpq" -Resume - name: Get CMakePresets.json run: cp Packaging/windows/CMakePresets.json . - name: Run CMake consuming CMakePresets.json and vcpkg.json by mean of vcpkg. uses: lukka/run-cmake@v10 with: configurePreset: 'ninja-vcpkg-relwithdebinfo' buildPreset: 'ninja-vcpkg-relwithdebinfo' testPreset: 'ninja-vcpkg-relwithdebinfo' - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx.exe path: | build-ninja-vcpkg-relwithdebinfo/devilutionx.exe build-ninja-vcpkg-relwithdebinfo/bz2.dll build-ninja-vcpkg-relwithdebinfo/fmt.dll build-ninja-vcpkg-relwithdebinfo/libpng16.dll build-ninja-vcpkg-relwithdebinfo/libsodium.dll build-ninja-vcpkg-relwithdebinfo/SDL2.dll build-ninja-vcpkg-relwithdebinfo/SDL2_image.dll build-ninja-vcpkg-relwithdebinfo/zlib1.dll ================================================ FILE: .github/workflows/Windows_MinGW_x64.yml ================================================ name: Windows MinGW x64 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y cmake gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 pkg-config-mingw-w64-x86-64 libz-mingw-w64-dev gettext dpkg-dev wget git sudo smpq && sudo Packaging/windows/mingw-prep64.sh - name: Configure CMake shell: bash working-directory: ${{github.workspace}} run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc64.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON -DSCREEN_READER_INTEGRATION=ON - name: Build working-directory: ${{github.workspace}} shell: bash run: cmake --build build -j $(nproc) --target package - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx_x64.zip path: build/devilutionx.zip - name: Move artifacts to new folder and split exe and other data into two folders if: ${{ !env.ACT && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'diasurgical/devilutionX'}} working-directory: ${{github.workspace}} shell: bash run: mkdir artifacts_dir && unzip build/devilutionx.zip -d artifacts_dir && mkdir artifacts_dir/exe_dir && mv artifacts_dir/devilutionx/devilutionx.exe artifacts_dir/exe_dir && zip -m artifacts_dir/exe_dir/build.zip artifacts_dir/exe_dir/devilutionx.exe && rm artifacts_dir/devilutionx/libTolk.dll - name: Pushes DLLs and devilutionx.mpq to another repository if: ${{ !env.ACT && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'diasurgical/devilutionX' }} uses: cpina/github-action-push-to-another-repository@main env: SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} with: source-directory: artifacts_dir/devilutionx/ destination-github-username: 'artifacts-storage' destination-repository-name: 'devilutionx-artifacts' target-directory: data commit-message: "[DATA] ${{ github.event.head_commit.message }}" target-branch: master - name: Pushes exe to another repository if: ${{ !env.ACT && github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'diasurgical/devilutionX' }} uses: cpina/github-action-push-to-another-repository@main env: SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} with: source-directory: artifacts_dir/exe_dir destination-github-username: 'artifacts-storage' destination-repository-name: 'devilutionx-artifacts' target-directory: ${{ github.sha }} commit-message: "[EXE] ${{ github.event.head_commit.message }}" target-branch: master ================================================ FILE: .github/workflows/Windows_MinGW_x86.yml ================================================ name: Windows MinGW x86 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt update && sudo apt install -y cmake gcc-mingw-w64-i686 g++-mingw-w64-i686 pkg-config-mingw-w64-i686 libz-mingw-w64-dev gettext dpkg-dev wget git sudo smpq && sudo Packaging/windows/mingw-prep.sh - name: Configure CMake shell: bash working-directory: ${{github.workspace}} run: cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=RelWithDebInfo -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DDEVILUTIONX_SYSTEM_BZIP2=OFF -DDEVILUTIONX_STATIC_LIBSODIUM=ON -DDISCORD_INTEGRATION=ON -DSCREEN_READER_INTEGRATION=ON - name: Build working-directory: ${{github.workspace}} shell: bash run: cmake --build build -j $(nproc) --target package - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx_x86.zip path: build/devilutionx.zip ================================================ FILE: .github/workflows/Windows_XP_32bit.yml ================================================ name: Windows XP MinGW on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt update && sudo apt install -y cmake gcc-mingw-w64-i686 g++-mingw-w64-i686 pkg-config-mingw-w64-i686 libz-mingw-w64-dev gettext dpkg-dev wget git sudo smpq && sudo rm /usr/i686-w64-mingw32/lib/libz.dll.a && sudo Packaging/windows/mingw-prep.sh - name: Configure CMake shell: bash working-directory: ${{github.workspace}} run: cmake -S. -Bbuild-windowsxp -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DCPACK=ON -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/mingwcc.toolchain.cmake -DTARGET_PLATFORM=windowsXP - name: Build working-directory: ${{github.workspace}} shell: bash run: | cmake --build build-windowsxp -j $(nproc) --target package mv build-windowsxp/devilutionx.zip devilutionx-windows-xp-32bit.zip - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: path: devilutionx-windows-xp-32bit.zip - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-windows-xp-32bit.zip overwrite: true ================================================ FILE: .github/workflows/amiga-m68k.yml ================================================ --- name: Amiga M68K on: # yamllint disable-line rule:truthy push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest container: amigadev/crosstools:m68k-amigaos-gcc10@sha256:e0ee27eb4174ba8f95143273dd2ecb0ffa23ca6a195e013030a681ab541b4a37 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Run prep.sh script run: ./Packaging/amiga/prep.sh - name: Configure CMake run: | cmake \ -S . \ -B build \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DM68K_COMMON="-s -fbbb=- -ffast-math" \ -DM68K_CPU=68040 \ -DM68K_FPU=hard - name: Build DevilutionX run: cmake --build build - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx_m68k path: ./build/devilutionx - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-amiga-m68k file: ./build/devilutionx overwrite: true ================================================ FILE: .github/workflows/cache-cleanup.yml ================================================ name: Delete cache for a closed Pull Request on: pull_request: types: - closed jobs: cleanup: runs-on: ubuntu-latest steps: - name: Check out code uses: actions/checkout@v6 # https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries - name: Cleanup run: | gh extension install actions/gh-actions-cache REPO=${{ github.repository }} BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge" echo "Fetching list of cache key" cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 ) ## Setting this to not fail the workflow while deleting cache keys. set +e echo "Deleting caches..." for cacheKey in $cacheKeysForPR; do gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm done echo "Done" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/clang-format-check.yml ================================================ name: clang-format check on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: formatting-check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Formatting Check (Source) uses: jidicula/clang-format-action@v4.17.0 with: clang-format-version: '18' check-path: 'Source' - name: Formatting Check (test) uses: jidicula/clang-format-action@v4.17.0 with: clang-format-version: '18' check-path: 'test' ================================================ FILE: .github/workflows/clang-tidy-check.yml ================================================ name: clang-tidy check on: push: branches: - master paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake', '.github/workflows/clang-tidy-check.yml'] pull_request: types: [ opened, synchronize ] paths: ['**.c', '**.cpp', '**.h', '**.hpp', '**.cxx', '**.hxx', '**.cc', '**.hh', '**CMakeLists.txt', 'meson.build', '**.cmake', '.github/workflows/clang-tidy-check.yml'] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: tidy-check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y cmake curl g++ git libgtest-dev libgmock-dev libbenchmark-dev libfmt-dev libsdl2-dev libsodium-dev libpng-dev libbz2-dev wget - name: CMake Configure run: cmake -S. -Bbuild - name: clang-tidy Check uses: cpp-linter/cpp-linter-action@v2 id: linter with: database: build # directory containing compile_commands.json style: '' # using an empty string here disables clang-format checks. We leave the existing clang-format-check workflow to run clang-format instead tidy-checks: '' # using an empty string here instructs the action to use only checks from the .clang-tidy file ignore-tidy: '3rdParty' ================================================ FILE: .github/workflows/iOS.yml ================================================ name: iOS on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: # The CMake configure and build commands are platform agnostic and should work equally # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix runs-on: macos-14 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v5-${{ github.sha }} restore-keys: ${{ github.workflow }}-v5- - name: Configure CMake # Use a bash shell so we can use the same syntax for environment variable # access regardless of the host operating system shell: bash working-directory: ${{github.workspace}} run: cmake -S. -Bbuild -DCMAKE_TOOLCHAIN_FILE=../CMake/platforms/ios.toolchain.cmake -DENABLE_BITCODE=0 -DPLATFORM=OS64 - name: Build working-directory: ${{github.workspace}} shell: bash run: > cmake --build build -j $(sysctl -n hw.physicalcpu) --config Release && cd build && mkdir Payload && mv devilutionx.app Payload && zip -r devilutionx-iOS.ipa Payload - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-iOS.ipa path: build/devilutionx-iOS.ipa - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: build/devilutionx-iOS.ipa overwrite: true - name: Clean up artifacts run: rm -rf build/Payload build/*.ipa ================================================ FILE: .github/workflows/macOS_arm64.yml ================================================ name: macOS arm64 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: macos-14 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: brew bundle install - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v2-${{ github.sha }} restore-keys: ${{ github.workflow }}-v2- - name: Clean previous DMG working-directory: ${{github.workspace}} run: rm -f build/*.dmg - name: Build working-directory: ${{github.workspace}} shell: bash env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} run: | cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DBUILD_TESTING=OFF \ -DMACOSX_STANDALONE_APP_BUNDLE=ON && \ cmake --build build -j $(sysctl -n hw.physicalcpu) --target package && \ mv build/devilutionx.dmg build/devilutionx-arm64-macOS.dmg - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-arm64-macOS.dmg path: build/devilutionx-arm64-macOS.dmg - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/*.dmg ================================================ FILE: .github/workflows/macOS_x86_64.yml ================================================ name: macOS x86_64 on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: macos-15-intel steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: brew bundle install - name: Cache CMake build folder uses: actions/cache@v5 with: path: build key: ${{ github.workflow }}-v4-${{ github.sha }} restore-keys: ${{ github.workflow }}-v4- - name: Clean previous DMG working-directory: ${{github.workspace}} run: rm -f build/*.dmg - name: Build working-directory: ${{github.workspace}} shell: bash env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} run: | cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DBUILD_TESTING=OFF \ -DMACOSX_STANDALONE_APP_BUNDLE=ON -DDISCORD_INTEGRATION=ON && \ cmake --build build -j $(sysctl -n hw.physicalcpu) --target package && \ mv build/devilutionx.dmg build/devilutionx-x86_64-macOS.dmg - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-x86_64-macOS.dmg path: build/devilutionx-x86_64-macOS.dmg - name: Clean up artifacts run: rm -rf build/_CPack_Packages build/*.dmg ================================================ FILE: .github/workflows/miyoo_mini_release.yml ================================================ name: Miyoo Mini Release Build on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: miyoo-mini: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install toolchain working-directory: ${{github.workspace}} run: sudo Packaging/miyoo_mini/setup_toolchain.sh - name: Build working-directory: ${{github.workspace}} run: Packaging/miyoo_mini/build.sh - name: Upload-OnionOS-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-miyoo-mini-onion-os.zip path: build-miyoo-mini/devilutionx-miyoo-mini-onion-os.zip - name: Upload-miniUI-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-miyoo-mini-miniui.zip path: build-miyoo-mini/devilutionx-miyoo-mini-miniui.zip - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "build-miyoo-mini/devilutionx-*.zip" file_glob: true overwrite: true ================================================ FILE: .github/workflows/opendingux_release.yml ================================================ name: OpenDingux Release Build on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: gcw0: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y curl cmake git squashfs-tools && curl -L https://github.com/OpenDingux/buildroot/releases/download/od-2022.09.22/opendingux-gcw0-toolchain.2022-09-22.tar.xz -o gcw0-toolchain.tar.xz && sudo mkdir -p /opt/gcw0-toolchain && sudo chown -R "${USER}:" /opt/gcw0-toolchain && tar -C /opt -xf gcw0-toolchain.tar.xz && cd /opt/gcw0-toolchain && ./relocate-sdk.sh - name: Build working-directory: ${{github.workspace}} shell: bash run: Packaging/OpenDingux/build.sh rg350 env: TOOLCHAIN: /opt/gcw0-toolchain - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-rg350.opk.zip path: build-rg350/devilutionx-rg350.opk - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "build-rg350/devilutionx-rg350.opk" overwrite: true lepus: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt update && sudo apt install -y curl cmake git squashfs-tools && curl -L https://github.com/OpenDingux/buildroot/releases/download/od-2022.09.22/opendingux-lepus-toolchain.2022-09-22.tar.xz -o lepus-toolchain.tar.xz && sudo mkdir -p /opt/lepus-toolchain && sudo chown -R "${USER}:" /opt/lepus-toolchain && tar -C /opt -xf lepus-toolchain.tar.xz && cd /opt/lepus-toolchain && ./relocate-sdk.sh - name: Build working-directory: ${{github.workspace}} shell: bash run: Packaging/OpenDingux/build.sh lepus env: TOOLCHAIN: /opt/lepus-toolchain - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-lepus.opk.zip path: build-lepus/devilutionx-lepus.opk - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "build-lepus/devilutionx-lepus.opk" overwrite: true ================================================ FILE: .github/workflows/retrofw_release.yml ================================================ name: RetroFW Release Build on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: retrofw: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y curl cmake git squashfs-tools gettext && curl -L https://github.com/retrofw/retrofw.github.io/releases/download/v2.3/mipsel-RetroFW-linux-uclibc_sdk-buildroot.tar.gz -o retrofw-toolchain.tar.gz && sudo mkdir -p /opt/retrofw-toolchain && sudo chown -R "${USER}:" /opt/retrofw-toolchain && tar -C /opt/retrofw-toolchain --strip-components=1 -xf retrofw-toolchain.tar.gz && cd /opt/retrofw-toolchain && ./relocate-sdk.sh - name: Build working-directory: ${{github.workspace}} shell: bash run: Packaging/OpenDingux/build.sh retrofw env: TOOLCHAIN: /opt/retrofw-toolchain - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-retrofw.opk.zip path: build-retrofw/devilutionx-retrofw.opk - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "build-retrofw/devilutionx-retrofw.opk" overwrite: true ================================================ FILE: .github/workflows/s390x_qemu_big_endian_tests.yml ================================================ name: s390x qemu tests (big-endian) # The test suite takes ~50m to run so we only trigger it manually on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: big-endian-qemu: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Cache .ccache uses: actions/cache@v5 with: path: .ccache key: ${{ github.workflow }}-v1-${{ github.sha }} restore-keys: ${{ github.workflow }}-v1- - name: Get the qemu container run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - name: Run tests run: > docker run --platform linux/s390x --rm --interactive --mount type=bind,source=$(pwd),target=/host s390x/alpine sh -c " apk add --update-cache g++ ninja cmake ccache sdl2-dev sdl2_image-dev fmt-dev libpng-dev jpeg-dev bzip2-dev gtest-dev wget && cd /host && export CCACHE_DIR=/host/.ccache && cmake -S. -Bbuild -G Ninja -DNONET=ON -DNOSOUND=ON && wget -nv -nc https://github.com/diasurgical/devilutionx-assets/releases/download/v2/spawn.mpq -P build && cmake --build build -j $(nproc) && ctest --test-dir build --output-on-failure -j $(nproc) " ================================================ FILE: .github/workflows/src_dist_release.yml ================================================ name: Build source tarball on: release: types: [published] paths-ignore: - '*.md' - 'docs/**' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: make_src_dist: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y cmake curl libsdl2-dev libsdl2-image-dev libfmt-dev libsodium-dev libbz2-dev libgtest-dev libgmock-dev libbenchmark-dev git smpq gettext python-is-python3 - name: Build working-directory: ${{github.workspace}} run: tools/make_src_dist.py && mv build-src-dist/devilutionx-*.tar.xz devilutionx-src.tar.xz - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-src.tar.xz path: devilutionx-src.tar.xz - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "devilutionx-src.tar.xz" overwrite: true make_src_dist_fully_vendored: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Create Build Environment run: > sudo apt-get update && sudo apt-get install -y cmake curl libsdl2-dev libsdl2-image-dev libfmt-dev libsodium-dev libbz2-dev libgtest-dev libgmock-dev libbenchmark-dev git smpq gettext python-is-python3 - name: Build working-directory: ${{github.workspace}} run: tools/make_src_dist.py --fully_vendored && mv build-src-dist/devilutionx-*.tar.xz devilutionx-src-fully-vendored.tar.xz - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-src-fully-vendored.tar.xz path: devilutionx-src-fully-vendored.tar.xz - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: "devilutionx-src-fully-vendored.tar.xz" overwrite: true ================================================ FILE: .github/workflows/switch.yml ================================================ --- name: Nintendo Switch on: # yamllint disable-line rule:truthy push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest container: devkitpro/devkita64:latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Configure CMake run: | cmake \ -S . \ -B build \ -D CMAKE_BUILD_TYPE=RelWithDebInfo \ -D CMAKE_TOOLCHAIN_FILE=/opt/devkitpro/cmake/Switch.cmake - name: Build DevilutionX run: cmake --build build -j$(nproc) - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx.nro path: ./build/devilutionx.nro - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-switch.nro file: ./build/devilutionx.nro overwrite: true ... ================================================ FILE: .github/workflows/translations.yml ================================================ name: Validate translations on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [ opened, synchronize ] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: fmt-check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install polib run: sudo apt-get update && sudo apt-get install -y python3-polib - name: Check working-directory: ${{github.workspace}} shell: bash run: python3 tools/validate_translations.py ================================================ FILE: .github/workflows/vita.yml ================================================ --- name: Sony PlayStation Vita on: # yamllint disable-line rule:truthy push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest container: vitasdk/vitasdk:latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install dependencies run: | apk add \ gettext \ git \ samurai \ patch - name: Configure CMake run: | cmake \ -S . \ -B build \ -G Ninja \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_TOOLCHAIN_FILE=${VITASDK}/share/vita.toolchain.cmake - name: Build DevilutionX run: cmake --build build - name: Upload Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx.vpk path: ./build/devilutionx.vpk - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: asset_name: devilutionx-vita.vpk file: ./build/devilutionx.vpk overwrite: true ... ================================================ FILE: .github/workflows/xbox_nxdk.yml ================================================ name: Xbox on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: xbox: runs-on: ubuntu-22.04 env: NXDK_DIR: /opt/nxdk steps: - name: Install APT packages run: sudo apt-get update && sudo apt-get install -y clang llvm lld bison flex cmake git gettext - name: Clone nxdk Repo shell: bash run: git clone --recursive --depth 1 https://github.com/XboxDev/nxdk.git "$NXDK_DIR" - name: Build nxdk shell: bash run: PATH="${NXDK_DIR}/bin:$PATH" make -j $(nproc) -C "$NXDK_DIR" NXDK_ONLY=1 CFLAGS=-O2 CXXFLAGS=-O2 all cxbe - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Configure CMake working-directory: ${{github.workspace}} env: CMAKE_BUILD_TYPE: ${{github.event_name == 'release' && 'Release' || 'RelWithDebInfo'}} run: cmake -S. -Bbuild-xbox -DCMAKE_BUILD_TYPE=${{env.CMAKE_BUILD_TYPE}} -DCMAKE_TOOLCHAIN_FILE=/opt/nxdk/share/toolchain-nxdk.cmake - name: Build working-directory: ${{github.workspace}} shell: bash run: cmake --build build-xbox -j $(nproc) --target nxdk_xbe - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-xbox path: build-xbox/pkg/ - name: Package Release if: ${{ github.event_name == 'release' && !env.ACT }} working-directory: ${{github.workspace}}/build-xbox/pkg run: zip -r ../../devilutionx-xbox.zip . - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-xbox.zip overwrite: true ================================================ FILE: .github/workflows/xbox_one.yml ================================================ name: Xbox One / Series on: push: branches: - master paths-ignore: - '*.md' - 'docs/**' pull_request: types: [opened, synchronize] paths-ignore: - '*.md' - 'docs/**' release: types: [published] paths-ignore: - '*.md' - 'docs/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: windows-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: ccache uses: hendrikmuhs/ccache-action@v1.2 - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v2 with: msbuild-architecture: x64 - name: Configure git shell: cmd run: | git config --global core.filemode false git config --global core.autocrlf false - name: Install gettext run: > New-Item -Path . -Name tools-gettext -ItemType Directory -Force && Invoke-WebRequest -Uri "https://github.com/vslavik/gettext-tools-windows/releases/download/v0.22.5/gettext-tools-windows-0.22.5.zip" -OutFile tools-gettext.zip -Resume && Expand-Archive tools-gettext.zip && Remove-Item tools-gettext.zip && Add-Content $env:GITHUB_PATH "$((Get-Item tools-gettext).FullName)\bin" - name: Build shell: cmd run: | cd ${{ github.workspace }}\Packaging\xbox-one build.bat - name: Copy artifacts to package directory run: > New-Item -Path build\uwp-project -Name pkg -ItemType Directory -Force && Get-Childitem -Path build\uwp-project\AppxPackages, build\uwp-project\Release -Include Microsoft.VCLibs.x64.*.appx, devilutionX_*_x64.appx -File -Recurse | Copy-Item -Destination build\uwp-project\pkg\ - name: Upload-Package if: ${{ !env.ACT }} uses: actions/upload-artifact@v7 with: name: devilutionx-xbox-one-series if-no-files-found: error path: | build/uwp-project/pkg/ - name: Package Release if: ${{ github.event_name == 'release' && !env.ACT }} working-directory: ${{github.workspace}}/build/uwp-project/pkg/ run: 7z a -r ../../../devilutionx-xbox-one-series.zip . - name: Update Release if: ${{ github.event_name == 'release' && !env.ACT }} uses: svenstaro/upload-release-action@v2 with: file: devilutionx-xbox-one-series.zip overwrite: true ================================================ FILE: .gitignore ================================================ # Generated by VC++ 6 builds /vc60.idb *.asm *.idb # macOS .DS_Store # CodeLite .CodeLite *.project *.workspace # Devilution Comparer devilution-comparer comparer-config.toml #ignore cmake cache /build-*/ .vscode/tasks.json # Extra files in the source distribution (see make_src_dist.py) /dist/ *.appimage *.AppImage # ELF object file, shared library and object archive. *.o *.so *.a # PE shared library and associated files. *.lib *.exp *.dll # PE executable. *.exe # GCC dependency file. *.d # Resource file. *.res # Created by https://www.gitignore.io/api/visualstudio ### VisualStudio ### ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ WinDebug/ WinRel/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Vim swap files *.swp # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # Visual Studio Edit & Continue enc_temp_folder # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # except for a top-level .cache directory (clangd uses /.cache/clangd for temporary files) /.cache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # CodeRush .cr/ # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # macOS .DS_Store # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ ### VisualStudio Patch ### # By default, sensitive information, such as encrypted password # should be stored in the .pubxml.user file. *.pubxml.user # End of https://www.gitignore.io/api/visualstudio # Created by https://www.gitignore.io/api/xcode # Edit at https://www.gitignore.io/?templates=xcode ### Visual Code /.vscode ### Xcode ### # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ### Xcode Patch ### *.xcodeproj/* !*.xcodeproj/project.pbxproj !*.xcodeproj/xcshareddata/ !*.xcworkspace/contents.xcworkspacedata /*.gcno **/xcshareddata/WorkspaceSettings.xcsettings /libs # End of https://www.gitignore.io/api/xcode # Don't accidently commit the diabdat.mpq or any other MPQ files *.mpq ### Nintendo Switch ### exefs/main /out/isenseconfig/CPI-Debug /docs/html/ # ddraw wrapper configuration file ddraw_settings.ini Brewfile.lock.json .vscode/settings.json # GPerf heap profile dumps *.heap *.mo # UWP copied assets uwp-project/Assets/data uwp-project/Assets/fonts uwp-project/Assets/gendata uwp-project/Assets/ui_art !uwp-project/devilutionX_TemporaryKey.pfx /.s390x-ccache/ # Temporary directory for prep scripts and other purposes. /tmp/ /compile_commands.json /tools/tmp ================================================ FILE: .lldbinit ================================================ script topsrcdir = topsrcdir if "topsrcdir" in locals() else os.getcwd() script sys.path.append(os.path.join(topsrcdir, "tools/lldb")) script import devilution_lldb script devilution_lldb.init() ================================================ FILE: 3rdParty/Lua/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) # Workaround for deprecation of older CMake versions set(CMAKE_POLICY_VERSION_MINIMUM 3.22) set(LUA_ENABLE_TESTING OFF) set(LUA_BUILD_COMPILER OFF) if(DEVILUTIONX_STATIC_LUA) set(LUA_ENABLE_SHARED OFF) else() set(LUA_ENABLE_SHARED ON) endif() include(FetchContent) FetchContent_Declare_ExcludeFromAll(Lua URL https://github.com/walterschell/Lua/archive/3ed55a56eaa05c9221f40b3c07d0e908eb1067b0.tar.gz URL_HASH MD5=77907b8209f77c65cb681a5012f2d804 ) FetchContent_MakeAvailable_ExcludeFromAll(Lua) # Needed for sol2 set(LUA_VERSION_STRING "5.4.7" PARENT_SCOPE) if(CMAKE_SYSTEM_NAME MATCHES "Darwin" AND DARWIN_MAJOR_VERSION VERSION_EQUAL 8) # We need legacy-support from MacPorts for: # localtime_r gmtime_r find_package(MacportsLegacySupport REQUIRED) target_link_libraries(lua_static PRIVATE MacportsLegacySupport::MacportsLegacySupport) elseif(TARGET_PLATFORM STREQUAL "dos") target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89) elseif(ANDROID AND ("${ANDROID_ABI}" STREQUAL "armeabi-v7a" OR "${ANDROID_ABI}" STREQUAL "x86")) target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89) elseif(NINTENDO_3DS OR VITA OR NINTENDO_SWITCH OR NXDK) target_compile_definitions(lua_static PUBLIC -DLUA_USE_C89) elseif(IOS) target_compile_definitions(lua_static PUBLIC -DLUA_USE_IOS) endif() ================================================ FILE: 3rdParty/PKWare/CMakeLists.txt ================================================ add_library(PKWare STATIC explode.cpp implode.cpp) target_include_directories(PKWare PUBLIC .) ================================================ FILE: 3rdParty/PKWare/Makefile ================================================ VC5_DIR ?= $(HOME)/DevStudio_5.10/VC # The $(VS6_DIR) directory is a copy of the "Microsoft Visual Studio" directory. # # To get a working setup on Linux or other "portable" copies of VS, # the following DLLs have to be copied to the # $(VS6_DIR)/VC98/Bin directory. # # - $(VS6_DIR)/Common/MSDev98/Bin/MSPDB60.DLL # # And to the $(VC5_DIR)/bin directory. # # - $(VC5_DIR)/SharedIDE/bin/MSDIS100.DLL # - $(VC5_DIR)/SharedIDE/bin/MSPDB50.DLL VS6_DIR ?= $(HOME)/VS6 VC6_DIR = $(VS6_DIR)/VC98 VC6_BIN_DIR = $(VC6_DIR)/Bin VC6_INC_DIR = $(VC6_DIR)/Include VC6_LIB_DIR = $(VC6_DIR)/Lib VC5_LIB_DIR = $(VC5_DIR)/lib IDE_DIR ?= $(VS6_DIR)/Common/MSDev98 IDE_BIN_DIR = $(IDE_DIR)/bin ifeq ($(OS),Windows_NT) CL = $(VC6_BIN_DIR)/CL.EXE RC = $(IDE_BIN_DIR)/RC.EXE VC5_LINK = $(VC5_DIR)/bin/link.exe VC6_LINK = $(VC6_BIN_DIR)/link.exe else CL = wine $(VC6_BIN_DIR)/CL.EXE RC = wine $(IDE_BIN_DIR)/RC.EXE VC5_LINK = wine $(VC5_DIR)/bin/link.exe VC6_LINK = wine $(VC6_BIN_DIR)/link.exe endif CFLAGS=/nologo /c /GX /W3 /O1 /I $(VC6_INC_DIR) /FD /MT /D "NDEBUG" /D "WIN32" /D "_WINDOWS" /YX /Gm /Zi LINKFLAGS=/nologo /subsystem:windows /machine:I386 /incremental:no VC_LINK=$(VC5_LINK) LINKFLAGS+= /LIBPATH:$(VC5_LIB_DIR) all: pkware.lib PKWARE_SRC=$(sort $(wildcard *.cpp)) PKWARE_OBJS=$(PKWARE_SRC:.cpp=.obj) pkware.lib: $(PKWARE_OBJS) $(VC_LINK) -lib /OUT:$@ $^ /nologo %.obj: %.cpp $(CL) $(CFLAGS) /Fo$@ $< clean: @$(RM) -v $(PKWARE_OBJS) pkware.lib vc60.{idb,pch,pdb} .PHONY: clean all ================================================ FILE: 3rdParty/PKWare/PKWare.vcxproj ================================================ Debug Win32 Release Win32 15.0 {C7F9F3B4-2F7C-4672-9586-94D8BA0950B6} Win32Proj PKWare 10.0.17763.0 StaticLibrary true v141 Unicode StaticLibrary false v141 true Unicode true .\WinDebug\ .\WinDebug\ false .\WinRel .\WinRel NotUsing Level3 Disabled true WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) true MultiThreadedDebug Windows true NotUsing Level3 MaxSpeed true true true WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) true MultiThreaded Windows true true true ================================================ FILE: 3rdParty/PKWare/PKWare.vcxproj.filters ================================================  {4FC737F1-C7A5-4376-A066-2A32D752A2FF} cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx {93995380-89BD-4b04-88EB-625FBE52EBFB} h;hh;hpp;hxx;hm;inl;inc;ipp;xsd {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms Source Files Source Files Header Files ================================================ FILE: 3rdParty/PKWare/Pkware.dsp ================================================ # Microsoft Developer Studio Project File - Name="Pkware" - Package Owner=<4> # Microsoft Developer Studio Generated Build File, Format Version 6.00 # ** DO NOT EDIT ** # TARGTYPE "Win32 (x86) Static Library" 0x0104 CFG=Pkware - Win32 Debug !MESSAGE This is not a valid makefile. To build this project using NMAKE, !MESSAGE use the Export Makefile command and run !MESSAGE !MESSAGE NMAKE /f "Pkware.mak". !MESSAGE !MESSAGE You can specify a configuration when running NMAKE !MESSAGE by defining the macro CFG on the command line. For example: !MESSAGE !MESSAGE NMAKE /f "Pkware.mak" CFG="Pkware - Win32 Debug" !MESSAGE !MESSAGE Possible choices for configuration are: !MESSAGE !MESSAGE "Pkware - Win32 Release" (based on "Win32 (x86) Static Library") !MESSAGE "Pkware - Win32 Debug" (based on "Win32 (x86) Static Library") !MESSAGE # Begin Project # PROP AllowPerConfigDependencies 0 # PROP Scc_ProjName "" # PROP Scc_LocalPath "" CPP=cl.exe RSC=rc.exe !IF "$(CFG)" == "Pkware - Win32 Release" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 0 # PROP BASE Output_Dir "WinRel" # PROP BASE Intermediate_Dir "WinRel" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 0 # PROP Output_Dir "WinRel" # PROP Intermediate_Dir "WinRel" # PROP Target_Dir "" # ADD BASE CPP /nologo /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_MBCS" /D "_LIB" /YX /FD /c # ADD CPP /nologo /MT /W3 /GX /O2 /D "WIN32" /D "NDEBUG" /D "_MBCS" /D "_LIB" /YX /FD /c # ADD BASE RSC /l 0x409 /d "NDEBUG" # ADD RSC /l 0x409 /d "NDEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LIB32=link.exe -lib # ADD BASE LIB32 /nologo # ADD LIB32 /nologo !ELSEIF "$(CFG)" == "Pkware - Win32 Debug" # PROP BASE Use_MFC 0 # PROP BASE Use_Debug_Libraries 1 # PROP BASE Output_Dir "WinDebug" # PROP BASE Intermediate_Dir "WinDebug" # PROP BASE Target_Dir "" # PROP Use_MFC 0 # PROP Use_Debug_Libraries 1 # PROP Output_Dir "WinDebug" # PROP Intermediate_Dir "WinDebug" # PROP Target_Dir "" # ADD BASE CPP /nologo /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_MBCS" /D "_LIB" /YX /FD /GZ /c # ADD CPP /nologo /MTd /W3 /Gm /GX /ZI /Od /D "WIN32" /D "_DEBUG" /D "_MBCS" /D "_LIB" /YX /FD /GZ /c # ADD BASE RSC /l 0x409 /d "_DEBUG" # ADD RSC /l 0x409 /d "_DEBUG" BSC32=bscmake.exe # ADD BASE BSC32 /nologo # ADD BSC32 /nologo LIB32=link.exe -lib # ADD BASE LIB32 /nologo # ADD LIB32 /nologo !ENDIF # Begin Target # Name "Pkware - Win32 Release" # Name "Pkware - Win32 Debug" # Begin Group "Source Files" # PROP Default_Filter "cpp;c;cxx;rc;def;r;odl;idl;hpj;bat" # Begin Source File SOURCE=.\explode.cpp # End Source File # Begin Source File SOURCE=.\implode.cpp # End Source File # End Group # Begin Group "Header Files" # PROP Default_Filter "h;hpp;hxx;hm;inl" # End Group # End Target # End Project ================================================ FILE: 3rdParty/PKWare/explode.cpp ================================================ /*****************************************************************************/ /* explode.cpp Copyright (c) Ladislav Zezula 2003 */ /*---------------------------------------------------------------------------*/ /* Implode function of PKWARE Data Compression library */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 11.03.03 1.00 Lad Splitted from Pkware.cpp */ /* 08.04.03 1.01 Lad Renamed to explode.cpp to be compatible with pkware */ /* 02.05.03 1.01 Lad Stress test done */ /* 22.04.10 1.01 Lad Documented */ /*****************************************************************************/ #include #include #include "pkware.h" #define PKDCL_OK 0 #define PKDCL_STREAM_END 1 // All data from the input stream is read #define PKDCL_NEED_DICT 2 // Need more data (dictionary) #define PKDCL_CONTINUE 10 // Internal flag, not returned to user #define PKDCL_GET_INPUT 11 // Internal flag, not returned to user static char CopyrightPkware[] = "PKWARE Data Compression Library for Win32\r\n" "Copyright 1989-1995 PKWARE Inc. All Rights Reserved\r\n" "Patent No. 5,051,745\r\n" "PKWARE Data Compression Library Reg. U.S. Pat. and Tm. Off.\r\n" "Version 1.11\r\n"; //----------------------------------------------------------------------------- // Tables static unsigned char DistBits[] = { 0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 }; static unsigned char DistCode[] = { 0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A, 0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C, 0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08, 0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00 }; static unsigned char ExLenBits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; static unsigned short LenBase[] = { 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008, 0x000A, 0x000E, 0x0016, 0x0026, 0x0046, 0x0086, 0x0106 }; static unsigned char LenBits[] = { 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07 }; static unsigned char LenCode[] = { 0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14, 0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00 }; static unsigned char ChBitsAsc[] = { 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08, 0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B, 0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06, 0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x09, 0x0C, 0x08, 0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05, 0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D }; static unsigned short ChCodeAsc[] = { 0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0, 0x01E0, 0x00B8, 0x0062, 0x0EE0, 0x06E0, 0x0022, 0x0AE0, 0x02E0, 0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360, 0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60, 0x000F, 0x0250, 0x0038, 0x0260, 0x0050, 0x0C60, 0x0390, 0x00D8, 0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098, 0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C, 0x0074, 0x00E8, 0x0068, 0x0460, 0x0090, 0x0034, 0x00B0, 0x0710, 0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8, 0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E, 0x0024, 0x0510, 0x000E, 0x0036, 0x0016, 0x0044, 0x0030, 0x00C8, 0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088, 0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A, 0x000A, 0x000B, 0x0210, 0x0004, 0x0013, 0x0032, 0x0003, 0x001D, 0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078, 0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0, 0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040, 0x0040, 0x1F80, 0x0F80, 0x1780, 0x0780, 0x1B80, 0x0B80, 0x1380, 0x0380, 0x1D80, 0x0D80, 0x1580, 0x0580, 0x1980, 0x0980, 0x1180, 0x0180, 0x1E80, 0x0E80, 0x1680, 0x0680, 0x1A80, 0x0A80, 0x1280, 0x0280, 0x1C80, 0x0C80, 0x1480, 0x0480, 0x1880, 0x0880, 0x1080, 0x0080, 0x1F00, 0x0F00, 0x1700, 0x0700, 0x1B00, 0x0B00, 0x1300, 0x0DA0, 0x05A0, 0x09A0, 0x01A0, 0x0EA0, 0x06A0, 0x0AA0, 0x02A0, 0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320, 0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220, 0x0C20, 0x0420, 0x0820, 0x0020, 0x0FC0, 0x07C0, 0x0BC0, 0x03C0, 0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0, 0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340, 0x0300, 0x0D40, 0x1D00, 0x0D00, 0x1500, 0x0540, 0x0500, 0x1900, 0x0900, 0x0940, 0x1100, 0x0100, 0x1E00, 0x0E00, 0x0140, 0x1600, 0x0600, 0x1A00, 0x0E40, 0x0640, 0x0A40, 0x0A00, 0x1200, 0x0200, 0x1C00, 0x0C00, 0x1400, 0x0400, 0x1800, 0x0800, 0x1000, 0x0000 }; //----------------------------------------------------------------------------- // Local functions static void PKWAREAPI GenDecodeTabs( unsigned char * positions, // [out] Table of positions unsigned char * start_indexes, // [in] Table of start indexes unsigned char * length_bits, // [in] Table of lengths. Each length is stored as number of bits size_t elements) // [in] Number of elements in start_indexes and length_bits { unsigned int index; unsigned int length; size_t i; for(i = 0; i < elements; i++) { length = 1 << length_bits[i]; // Get the length in bytes for(index = start_indexes[i]; index < 0x100; index += length) { positions[index] = (unsigned char)i; } } } static void PKWAREAPI GenAscTabs(TDcmpStruct * pWork) { unsigned short * pChCodeAsc = &ChCodeAsc[0xFF]; unsigned int acc, add; unsigned short count; for(count = 0x00FF; pChCodeAsc >= ChCodeAsc; pChCodeAsc--, count--) { unsigned char * pChBitsAsc = pWork->ChBitsAsc + count; unsigned char bits_asc = *pChBitsAsc; if(bits_asc <= 8) { add = (1 << bits_asc); acc = *pChCodeAsc; do { pWork->offs2C34[acc] = (unsigned char)count; acc += add; } while(acc < 0x100); } else if((acc = (*pChCodeAsc & 0xFF)) != 0) { pWork->offs2C34[acc] = 0xFF; if(*pChCodeAsc & 0x3F) { bits_asc -= 4; *pChBitsAsc = bits_asc; add = (1 << bits_asc); acc = *pChCodeAsc >> 4; do { pWork->offs2D34[acc] = (unsigned char)count; acc += add; } while(acc < 0x100); } else { bits_asc -= 6; *pChBitsAsc = bits_asc; add = (1 << bits_asc); acc = *pChCodeAsc >> 6; do { pWork->offs2E34[acc] = (unsigned char)count; acc += add; } while(acc < 0x80); } } else { bits_asc -= 8; *pChBitsAsc = bits_asc; add = (1 << bits_asc); acc = *pChCodeAsc >> 8; do { pWork->offs2EB4[acc] = (unsigned char)count; acc += add; } while(acc < 0x100); } } } //----------------------------------------------------------------------------- // Removes given number of bits in the bit buffer. New bits are reloaded from // the input buffer, if needed. // Returns: PKDCL_OK: Operation was successful // PKDCL_STREAM_END: There are no more bits in the input buffer static int PKWAREAPI WasteBits(TDcmpStruct * pWork, unsigned int nBits) { // If number of bits required is less than number of (bits in the buffer) ? if(nBits <= pWork->extra_bits) { pWork->extra_bits -= nBits; pWork->bit_buff >>= nBits; return PKDCL_OK; } // Load input buffer if necessary pWork->bit_buff >>= pWork->extra_bits; if(pWork->in_pos == pWork->in_bytes) { pWork->in_pos = sizeof(pWork->in_buff); if((pWork->in_bytes = pWork->read_buf((char *)pWork->in_buff, &pWork->in_pos, pWork->param)) == 0) return PKDCL_STREAM_END; pWork->in_pos = 0; } // Update bit buffer pWork->bit_buff |= (pWork->in_buff[pWork->in_pos++] << 8); pWork->bit_buff >>= (nBits - pWork->extra_bits); pWork->extra_bits = (pWork->extra_bits - nBits) + 8; return PKDCL_OK; } //----------------------------------------------------------------------------- // Decodes next literal from the input (compressed) data. // Returns : 0x000: One byte 0x00 // 0x001: One byte 0x01 // ... // 0x0FF: One byte 0xFF // 0x100: Repetition, length of 0x02 bytes // 0x101: Repetition, length of 0x03 bytes // ... // 0x304: Repetition, length of 0x206 bytes // 0x305: End of stream // 0x306: Error static unsigned int PKWAREAPI DecodeLit(TDcmpStruct * pWork) { unsigned int extra_length_bits; // Number of bits of extra literal length unsigned int length_code; // Length code unsigned int value; // Test the current bit in byte buffer. If is not set, simply return the next 8 bits. if(pWork->bit_buff & 1) { // Remove one bit from the input data if(WasteBits(pWork, 1)) return 0x306; // The next 8 bits hold the index to the length code table length_code = pWork->LengthCodes[pWork->bit_buff & 0xFF]; // Remove the apropriate number of bits if(WasteBits(pWork, pWork->LenBits[length_code])) return 0x306; // Are there some extra bits for the obtained length code ? if((extra_length_bits = pWork->ExLenBits[length_code]) != 0) { unsigned int extra_length = pWork->bit_buff & ((1 << extra_length_bits) - 1); if(WasteBits(pWork, extra_length_bits)) { if((length_code + extra_length) != 0x10E) return 0x306; } length_code = pWork->LenBase[length_code] + extra_length; } // In order to distinguish uncompressed byte from repetition length, // we have to add 0x100 to the length. return length_code + 0x100; } // Remove one bit from the input data if(WasteBits(pWork, 1)) return 0x306; // If the binary compression type, read 8 bits and return them as one byte. if(pWork->ctype == CMP_BINARY) { unsigned int uncompressed_byte = pWork->bit_buff & 0xFF; if(WasteBits(pWork, 8)) return 0x306; return uncompressed_byte; } // When ASCII compression ... if(pWork->bit_buff & 0xFF) { value = pWork->offs2C34[pWork->bit_buff & 0xFF]; if(value == 0xFF) { if(pWork->bit_buff & 0x3F) { if(WasteBits(pWork, 4)) return 0x306; value = pWork->offs2D34[pWork->bit_buff & 0xFF]; } else { if(WasteBits(pWork, 6)) return 0x306; value = pWork->offs2E34[pWork->bit_buff & 0x7F]; } } } else { if(WasteBits(pWork, 8)) return 0x306; value = pWork->offs2EB4[pWork->bit_buff & 0xFF]; } return WasteBits(pWork, pWork->ChBitsAsc[value]) ? 0x306 : value; } //----------------------------------------------------------------------------- // Decodes the distance of the repetition, backwards relative to the // current output buffer position static unsigned int PKWAREAPI DecodeDist(TDcmpStruct * pWork, unsigned int rep_length) { unsigned int dist_pos_code; // Distance position code unsigned int dist_pos_bits; // Number of bits of distance position unsigned int distance; // Distance position // Next 2-8 bits in the input buffer is the distance position code dist_pos_code = pWork->DistPosCodes[pWork->bit_buff & 0xFF]; dist_pos_bits = pWork->DistBits[dist_pos_code]; if(WasteBits(pWork, dist_pos_bits)) return 0; if(rep_length == 2) { // If the repetition is only 2 bytes length, // then take 2 bits from the stream in order to get the distance distance = (dist_pos_code << 2) | (pWork->bit_buff & 0x03); if(WasteBits(pWork, 2)) return 0; } else { // If the repetition is more than 2 bytes length, // then take "dsize_bits" bits in order to get the distance distance = (dist_pos_code << pWork->dsize_bits) | (pWork->bit_buff & pWork->dsize_mask); if(WasteBits(pWork, pWork->dsize_bits)) return 0; } return distance + 1; } static unsigned int PKWAREAPI Expand(TDcmpStruct * pWork) { unsigned int next_literal; // Literal decoded from the compressed data unsigned int result; // Value to be returned unsigned int copyBytes; // Number of bytes to copy to the output buffer pWork->outputPos = 0x1000; // Initialize output buffer position // Decode the next literal from the input data. // The returned literal can either be an uncompressed byte (next_literal < 0x100) // or an encoded length of the repeating byte sequence that // is to be copied to the current buffer position while((result = next_literal = DecodeLit(pWork)) < 0x305) { // If the literal is greater than 0x100, it holds length // of repeating byte sequence // literal of 0x100 means repeating sequence of 0x2 bytes // literal of 0x101 means repeating sequence of 0x3 bytes // ... // literal of 0x305 means repeating sequence of 0x207 bytes if(next_literal >= 0x100) { unsigned char * source; unsigned char * target; unsigned int rep_length; // Length of the repetition, in bytes unsigned int minus_dist; // Backward distance to the repetition, relative to the current buffer position // Get the length of the repeating sequence. // Note that the repeating block may overlap the current output position, // for example if there was a sequence of equal bytes rep_length = next_literal - 0xFE; // Get backward distance to the repetition if((minus_dist = DecodeDist(pWork, rep_length)) == 0) { result = 0x306; break; } // Target and source pointer target = &pWork->out_buff[pWork->outputPos]; source = target - minus_dist; // Update buffer output position pWork->outputPos += rep_length; // Copy the repeating sequence while(rep_length-- > 0) *target++ = *source++; } else { pWork->out_buff[pWork->outputPos++] = (unsigned char)next_literal; } // Flush the output buffer, if number of extracted bytes has reached the end if(pWork->outputPos >= 0x2000) { // Copy decompressed data into user buffer copyBytes = 0x1000; pWork->write_buf((char *)&pWork->out_buff[0x1000], ©Bytes, pWork->param); // Now copy the decompressed data to the first half of the buffer. // This is needed because the decompression might reuse them as repetitions. // Note that if the output buffer overflowed previously, the extra decompressed bytes // are stored in "out_buff_overflow", and they will now be // within decompressed part of the output buffer. memmove(pWork->out_buff, &pWork->out_buff[0x1000], pWork->outputPos - 0x1000); pWork->outputPos -= 0x1000; } } // Flush any remaining decompressed bytes copyBytes = pWork->outputPos - 0x1000; pWork->write_buf((char *)&pWork->out_buff[0x1000], ©Bytes, pWork->param); return result; } //----------------------------------------------------------------------------- // Main exploding function. unsigned int PKWAREAPI explode( unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param), void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param), char *work_buf, void *param) { TDcmpStruct * pWork = (TDcmpStruct *)work_buf; // Initialize work struct and load compressed data // Note: The caller must zero the "work_buff" before passing it to explode pWork->read_buf = read_buf; pWork->write_buf = write_buf; pWork->param = param; pWork->in_pos = sizeof(pWork->in_buff); pWork->in_bytes = pWork->read_buf((char *)pWork->in_buff, &pWork->in_pos, pWork->param); if(pWork->in_bytes <= 4) return CMP_BAD_DATA; pWork->ctype = pWork->in_buff[0]; // Get the compression type (CMP_BINARY or CMP_ASCII) pWork->dsize_bits = pWork->in_buff[1]; // Get the dictionary size pWork->bit_buff = pWork->in_buff[2]; // Initialize 16-bit bit buffer pWork->extra_bits = 0; // Extra (over 8) bits pWork->in_pos = 3; // Position in input buffer // Test for the valid dictionary size if(4 > pWork->dsize_bits || pWork->dsize_bits > 6) return CMP_INVALID_DICTSIZE; pWork->dsize_mask = 0xFFFF >> (0x10 - pWork->dsize_bits); // Shifted by 'sar' instruction if(pWork->ctype != CMP_BINARY) { if(pWork->ctype != CMP_ASCII) return CMP_INVALID_MODE; memcpy(pWork->ChBitsAsc, ChBitsAsc, sizeof(pWork->ChBitsAsc)); GenAscTabs(pWork); } memcpy(pWork->LenBits, LenBits, sizeof(pWork->LenBits)); GenDecodeTabs(pWork->LengthCodes, LenCode, pWork->LenBits, sizeof(pWork->LenBits)); memcpy(pWork->ExLenBits, ExLenBits, sizeof(pWork->ExLenBits)); memcpy(pWork->LenBase, LenBase, sizeof(pWork->LenBase)); memcpy(pWork->DistBits, DistBits, sizeof(pWork->DistBits)); GenDecodeTabs(pWork->DistPosCodes, DistCode, pWork->DistBits, sizeof(pWork->DistBits)); if(Expand(pWork) != 0x306) return CMP_NO_ERROR; return CMP_ABORT; } ================================================ FILE: 3rdParty/PKWare/implode.cpp ================================================ /*****************************************************************************/ /* implode.cpp Copyright (c) Ladislav Zezula 2003 */ /*---------------------------------------------------------------------------*/ /* Implode function of PKWARE Data Compression library */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 11.04.03 1.00 Lad First version of implode.cpp */ /* 02.05.03 1.00 Lad Stress test done */ /* 22.04.10 1.01 Lad Documented */ /*****************************************************************************/ #include #include #include "pkware.h" #if ((1200 < _MSC_VER) && (_MSC_VER < 1400)) #pragma optimize("", off) #endif //----------------------------------------------------------------------------- // Defines #define MAX_REP_LENGTH 0x204 // The longest allowed repetition static char CopyrightPkware[] = "PKWARE Data Compression Library for Win32\r\n" "Copyright 1989-1995 PKWARE Inc. All Rights Reserved\r\n" "Patent No. 5,051,745\r\n" "PKWARE Data Compression Library Reg. U.S. Pat. and Tm. Off.\r\n" "Version 1.11\r\n"; //----------------------------------------------------------------------------- // Tables static unsigned char DistBits[] = { 0x02, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08 }; static unsigned char DistCode[] = { 0x03, 0x0D, 0x05, 0x19, 0x09, 0x11, 0x01, 0x3E, 0x1E, 0x2E, 0x0E, 0x36, 0x16, 0x26, 0x06, 0x3A, 0x1A, 0x2A, 0x0A, 0x32, 0x12, 0x22, 0x42, 0x02, 0x7C, 0x3C, 0x5C, 0x1C, 0x6C, 0x2C, 0x4C, 0x0C, 0x74, 0x34, 0x54, 0x14, 0x64, 0x24, 0x44, 0x04, 0x78, 0x38, 0x58, 0x18, 0x68, 0x28, 0x48, 0x08, 0xF0, 0x70, 0xB0, 0x30, 0xD0, 0x50, 0x90, 0x10, 0xE0, 0x60, 0xA0, 0x20, 0xC0, 0x40, 0x80, 0x00 }; static unsigned char ExLenBits[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 }; static unsigned char LenBits[] = { 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07 }; static unsigned char LenCode[] = { 0x05, 0x03, 0x01, 0x06, 0x0A, 0x02, 0x0C, 0x14, 0x04, 0x18, 0x08, 0x30, 0x10, 0x20, 0x40, 0x00 }; static unsigned char ChBitsAsc[] = { 0x0B, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x07, 0x0C, 0x0C, 0x07, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x04, 0x0A, 0x08, 0x0C, 0x0A, 0x0C, 0x0A, 0x08, 0x07, 0x07, 0x08, 0x09, 0x07, 0x06, 0x07, 0x08, 0x07, 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x07, 0x07, 0x08, 0x08, 0x0C, 0x0B, 0x07, 0x09, 0x0B, 0x0C, 0x06, 0x07, 0x06, 0x06, 0x05, 0x07, 0x08, 0x08, 0x06, 0x0B, 0x09, 0x06, 0x07, 0x06, 0x06, 0x07, 0x0B, 0x06, 0x06, 0x06, 0x07, 0x09, 0x08, 0x09, 0x09, 0x0B, 0x08, 0x0B, 0x09, 0x0C, 0x08, 0x0C, 0x05, 0x06, 0x06, 0x06, 0x05, 0x06, 0x06, 0x06, 0x05, 0x0B, 0x07, 0x05, 0x06, 0x05, 0x05, 0x06, 0x0A, 0x05, 0x05, 0x05, 0x05, 0x08, 0x07, 0x08, 0x08, 0x0A, 0x0B, 0x0B, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0C, 0x0D, 0x0D, 0x0D, 0x0C, 0x0C, 0x0C, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D, 0x0D }; static unsigned short ChCodeAsc[] = { 0x0490, 0x0FE0, 0x07E0, 0x0BE0, 0x03E0, 0x0DE0, 0x05E0, 0x09E0, 0x01E0, 0x00B8, 0x0062, 0x0EE0, 0x06E0, 0x0022, 0x0AE0, 0x02E0, 0x0CE0, 0x04E0, 0x08E0, 0x00E0, 0x0F60, 0x0760, 0x0B60, 0x0360, 0x0D60, 0x0560, 0x1240, 0x0960, 0x0160, 0x0E60, 0x0660, 0x0A60, 0x000F, 0x0250, 0x0038, 0x0260, 0x0050, 0x0C60, 0x0390, 0x00D8, 0x0042, 0x0002, 0x0058, 0x01B0, 0x007C, 0x0029, 0x003C, 0x0098, 0x005C, 0x0009, 0x001C, 0x006C, 0x002C, 0x004C, 0x0018, 0x000C, 0x0074, 0x00E8, 0x0068, 0x0460, 0x0090, 0x0034, 0x00B0, 0x0710, 0x0860, 0x0031, 0x0054, 0x0011, 0x0021, 0x0017, 0x0014, 0x00A8, 0x0028, 0x0001, 0x0310, 0x0130, 0x003E, 0x0064, 0x001E, 0x002E, 0x0024, 0x0510, 0x000E, 0x0036, 0x0016, 0x0044, 0x0030, 0x00C8, 0x01D0, 0x00D0, 0x0110, 0x0048, 0x0610, 0x0150, 0x0060, 0x0088, 0x0FA0, 0x0007, 0x0026, 0x0006, 0x003A, 0x001B, 0x001A, 0x002A, 0x000A, 0x000B, 0x0210, 0x0004, 0x0013, 0x0032, 0x0003, 0x001D, 0x0012, 0x0190, 0x000D, 0x0015, 0x0005, 0x0019, 0x0008, 0x0078, 0x00F0, 0x0070, 0x0290, 0x0410, 0x0010, 0x07A0, 0x0BA0, 0x03A0, 0x0240, 0x1C40, 0x0C40, 0x1440, 0x0440, 0x1840, 0x0840, 0x1040, 0x0040, 0x1F80, 0x0F80, 0x1780, 0x0780, 0x1B80, 0x0B80, 0x1380, 0x0380, 0x1D80, 0x0D80, 0x1580, 0x0580, 0x1980, 0x0980, 0x1180, 0x0180, 0x1E80, 0x0E80, 0x1680, 0x0680, 0x1A80, 0x0A80, 0x1280, 0x0280, 0x1C80, 0x0C80, 0x1480, 0x0480, 0x1880, 0x0880, 0x1080, 0x0080, 0x1F00, 0x0F00, 0x1700, 0x0700, 0x1B00, 0x0B00, 0x1300, 0x0DA0, 0x05A0, 0x09A0, 0x01A0, 0x0EA0, 0x06A0, 0x0AA0, 0x02A0, 0x0CA0, 0x04A0, 0x08A0, 0x00A0, 0x0F20, 0x0720, 0x0B20, 0x0320, 0x0D20, 0x0520, 0x0920, 0x0120, 0x0E20, 0x0620, 0x0A20, 0x0220, 0x0C20, 0x0420, 0x0820, 0x0020, 0x0FC0, 0x07C0, 0x0BC0, 0x03C0, 0x0DC0, 0x05C0, 0x09C0, 0x01C0, 0x0EC0, 0x06C0, 0x0AC0, 0x02C0, 0x0CC0, 0x04C0, 0x08C0, 0x00C0, 0x0F40, 0x0740, 0x0B40, 0x0340, 0x0300, 0x0D40, 0x1D00, 0x0D00, 0x1500, 0x0540, 0x0500, 0x1900, 0x0900, 0x0940, 0x1100, 0x0100, 0x1E00, 0x0E00, 0x0140, 0x1600, 0x0600, 0x1A00, 0x0E40, 0x0640, 0x0A40, 0x0A00, 0x1200, 0x0200, 0x1C00, 0x0C00, 0x1400, 0x0400, 0x1800, 0x0800, 0x1000, 0x0000 }; //----------------------------------------------------------------------------- // Macros // Macro for calculating hash of the current byte pair. // Note that most exact byte pair hash would be buffer[0] + buffer[1] << 0x08, // but even this way gives nice indication of equal byte pairs, with significantly // smaller size of the array that holds numbers of those hashes #define BYTE_PAIR_HASH(buffer) ((buffer[0] * 4) + (buffer[1] * 5)) //----------------------------------------------------------------------------- // Local functions // Builds the "hash_to_index" table and "pair_hash_offsets" table. // Every element of "hash_to_index" will contain lowest index to the // "pair_hash_offsets" table, effectively giving offset of the first // occurence of the given PAIR_HASH in the input data. static void PKWAREAPI SortBuffer(TCmpStruct * pWork, unsigned char * buffer_begin, unsigned char * buffer_end) { unsigned short * phash_to_index; unsigned char * buffer_ptr; unsigned short total_sum = 0; unsigned long byte_pair_hash; // Hash value of the byte pair unsigned short byte_pair_offs; // Offset of the byte pair, relative to "work_buff" // Zero the entire "phash_to_index" table memset(pWork->phash_to_index, 0, sizeof(pWork->phash_to_index)); // Step 1: Count amount of each PAIR_HASH in the input buffer // The table will look like this: // offs 0x000: Number of occurences of PAIR_HASH 0 // offs 0x001: Number of occurences of PAIR_HASH 1 // ... // offs 0x8F7: Number of occurences of PAIR_HASH 0x8F7 (the highest hash value) for(buffer_ptr = buffer_begin; buffer_ptr < buffer_end; buffer_ptr++) pWork->phash_to_index[BYTE_PAIR_HASH(buffer_ptr)]++; // Step 2: Convert the table to the array of PAIR_HASH amounts. // Each element contains count of PAIR_HASHes that is less or equal // to element index // The table will look like this: // offs 0x000: Number of occurences of PAIR_HASH 0 or lower // offs 0x001: Number of occurences of PAIR_HASH 1 or lower // ... // offs 0x8F7: Number of occurences of PAIR_HASH 0x8F7 or lower for(phash_to_index = pWork->phash_to_index; phash_to_index < &pWork->phash_to_index_end; phash_to_index++) { total_sum = total_sum + phash_to_index[0]; phash_to_index[0] = total_sum; } // Step 3: Convert the table to the array of indexes. // Now, each element contains index to the first occurence of given PAIR_HASH for(buffer_end--; buffer_end >= buffer_begin; buffer_end--) { byte_pair_hash = BYTE_PAIR_HASH(buffer_end); byte_pair_offs = (unsigned short)(buffer_end - pWork->work_buff); pWork->phash_to_index[byte_pair_hash]--; pWork->phash_offs[pWork->phash_to_index[byte_pair_hash]] = byte_pair_offs; } } static void PKWAREAPI FlushBuf(TCmpStruct * pWork) { unsigned char save_ch1; unsigned char save_ch2; unsigned int size = 0x800; pWork->write_buf(pWork->out_buff, &size, pWork->param); save_ch1 = pWork->out_buff[0x800]; save_ch2 = pWork->out_buff[pWork->out_bytes]; pWork->out_bytes -= 0x800; memset(pWork->out_buff, 0, sizeof(pWork->out_buff)); if(pWork->out_bytes != 0) pWork->out_buff[0] = save_ch1; if(pWork->out_bits != 0) pWork->out_buff[pWork->out_bytes] = save_ch2; } static void PKWAREAPI OutputBits(TCmpStruct * pWork, unsigned int nbits, unsigned long bit_buff) { unsigned int out_bits; // If more than 8 bits to output, do recursion if(nbits > 8) { OutputBits(pWork, 8, bit_buff); bit_buff >>= 8; nbits -= 8; } // Add bits to the last out byte in out_buff; out_bits = pWork->out_bits; pWork->out_buff[pWork->out_bytes] |= (unsigned char)(bit_buff << out_bits); pWork->out_bits += nbits; // If 8 or more bits, increment number of bytes if(pWork->out_bits > 8) { pWork->out_bytes++; bit_buff >>= (8 - out_bits); pWork->out_buff[pWork->out_bytes] = (unsigned char)bit_buff; pWork->out_bits &= 7; } else { pWork->out_bits &= 7; if(pWork->out_bits == 0) pWork->out_bytes++; } // If there is enough compressed bytes, flush them if(pWork->out_bytes >= 0x800) FlushBuf(pWork); } // This function searches for a repetition // (a previous occurence of the current byte sequence) // Returns length of the repetition, and stores the backward distance // to pWork structure. static unsigned int PKWAREAPI FindRep(TCmpStruct * pWork, unsigned char * input_data) { unsigned short * phash_to_index; // Pointer into pWork->phash_to_index table unsigned short * phash_offs; // Pointer to the table containing offsets of each PAIR_HASH unsigned char * repetition_limit; // An eventual repetition must be at position below this pointer unsigned char * prev_repetition; // Pointer to the previous occurence of the current PAIR_HASH unsigned char * prev_rep_end; // End of the previous repetition unsigned char * input_data_ptr; unsigned short phash_offs_index; // Index to the table with PAIR_HASH positions unsigned short min_phash_offs; // The lowest allowed hash offset unsigned short offs_in_rep; // Offset within found repetition unsigned int equal_byte_count; // Number of bytes that are equal to the previous occurence unsigned int rep_length = 1; // Length of the found repetition unsigned int rep_length2; // Secondary repetition unsigned char pre_last_byte; // Last but one byte from a repetion unsigned short di_val; // Calculate the previous position of the PAIR_HASH phash_to_index = pWork->phash_to_index + BYTE_PAIR_HASH(input_data); min_phash_offs = (unsigned short)((input_data - pWork->work_buff) - pWork->dsize_bytes + 1); phash_offs_index = phash_to_index[0]; // If the PAIR_HASH offset is below the limit, find a next one phash_offs = pWork->phash_offs + phash_offs_index; if(*phash_offs < min_phash_offs) { while(*phash_offs < min_phash_offs) { phash_offs_index++; phash_offs++; } *phash_to_index = phash_offs_index; } // Get the first location of the PAIR_HASH, // and thus the first eventual location of byte repetition phash_offs = pWork->phash_offs + phash_offs_index; prev_repetition = pWork->work_buff + phash_offs[0]; repetition_limit = input_data - 1; // If the current PAIR_HASH was not encountered before, // we haven't found a repetition. if(prev_repetition >= repetition_limit) return 0; // We have found a match of a PAIR_HASH. Now we have to make sure // that it is also a byte match, because PAIR_HASH is not unique. // We compare the bytes and count the length of the repetition input_data_ptr = input_data; for(;;) { // If the first byte of the repetition and the so-far-last byte // of the repetition are equal, we will compare the blocks. if(*input_data_ptr == *prev_repetition && input_data_ptr[rep_length-1] == prev_repetition[rep_length-1]) { // Skip the current byte prev_repetition++; input_data_ptr++; equal_byte_count = 2; // Now count how many more bytes are equal while(equal_byte_count < MAX_REP_LENGTH) { prev_repetition++; input_data_ptr++; // Are the bytes different ? if(*prev_repetition != *input_data_ptr) break; equal_byte_count++; } // If we found a repetition of at least the same length, take it. // If there are multiple repetitions in the input buffer, this will // make sure that we find the most recent one, which in turn allows // us to store backward length in less amount of bits input_data_ptr = input_data; if(equal_byte_count >= rep_length) { // Calculate the backward distance of the repetition. // Note that the distance is stored as decremented by 1 pWork->distance = (unsigned int)(input_data - prev_repetition + equal_byte_count - 1); // Repetitions longer than 10 bytes will be stored in more bits, // so they need a bit different handling if((rep_length = equal_byte_count) > 10) break; } } // Move forward in the table of PAIR_HASH repetitions. // There might be a more recent occurence of the same repetition. phash_offs_index++; phash_offs++; prev_repetition = pWork->work_buff + phash_offs[0]; // If the next repetition is beyond the minimum allowed repetition, we are done. if(prev_repetition >= repetition_limit) { // A repetition must have at least 2 bytes, otherwise it's not worth it return (rep_length >= 2) ? rep_length : 0; } } // If the repetition has max length of 0x204 bytes, we can't go any fuhrter if(equal_byte_count == MAX_REP_LENGTH) { pWork->distance--; return equal_byte_count; } // Check for possibility of a repetition that occurs at more recent position phash_offs = pWork->phash_offs + phash_offs_index; if(pWork->work_buff + phash_offs[1] >= repetition_limit) return rep_length; // // The following part checks if there isn't a longer repetition at // a latter offset, that would lead to better compression. // // Example of data that can trigger this optimization: // // "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEQQQQQQQQQQQQ" // "XYZ" // "EEEEEEEEEEEEEEEEQQQQQQQQQQQQ"; // // Description of data in this buffer // [0x00] Single byte "E" // [0x01] Single byte "E" // [0x02] Repeat 0x1E bytes from [0x00] // [0x20] Single byte "X" // [0x21] Single byte "Y" // [0x22] Single byte "Z" // [0x23] 17 possible previous repetitions of length at least 0x10 bytes: // - Repetition of 0x10 bytes from [0x00] "EEEEEEEEEEEEEEEE" // - Repetition of 0x10 bytes from [0x01] "EEEEEEEEEEEEEEEE" // - Repetition of 0x10 bytes from [0x02] "EEEEEEEEEEEEEEEE" // ... // - Repetition of 0x10 bytes from [0x0F] "EEEEEEEEEEEEEEEE" // - Repetition of 0x1C bytes from [0x10] "EEEEEEEEEEEEEEEEQQQQQQQQQQQQ" // The last repetition is the best one. // pWork->offs09BC[0] = 0xFFFF; pWork->offs09BC[1] = 0x0000; di_val = 0; // Note: I failed to figure out what does the table "offs09BC" mean. // If anyone has an idea, let me know to zezula_at_volny_dot_cz for(offs_in_rep = 1; offs_in_rep < rep_length; ) { if(input_data[offs_in_rep] != input_data[di_val]) { di_val = pWork->offs09BC[di_val]; if(di_val != 0xFFFF) continue; } pWork->offs09BC[++offs_in_rep] = ++di_val; } // // Now go through all the repetitions from the first found one // to the current input data, and check if any of them migh be // a start of a greater sequence match. // prev_repetition = pWork->work_buff + phash_offs[0]; prev_rep_end = prev_repetition + rep_length; rep_length2 = rep_length; for(;;) { rep_length2 = pWork->offs09BC[rep_length2]; if(rep_length2 == 0xFFFF) rep_length2 = 0; // Get the pointer to the previous repetition phash_offs = pWork->phash_offs + phash_offs_index; // Skip those repetitions that don't reach the end // of the first found repetition do { phash_offs++; phash_offs_index++; prev_repetition = pWork->work_buff + *phash_offs; if(prev_repetition >= repetition_limit) return rep_length; } while(prev_repetition + rep_length2 < prev_rep_end); // Verify if the last but one byte from the repetition matches // the last but one byte from the input data. // If not, find a next repetition pre_last_byte = input_data[rep_length - 2]; if(pre_last_byte == prev_repetition[rep_length - 2]) { // If the new repetition reaches beyond the end // of previously found repetition, reset the repetition length to zero. if(prev_repetition + rep_length2 != prev_rep_end) { prev_rep_end = prev_repetition; rep_length2 = 0; } } else { phash_offs = pWork->phash_offs + phash_offs_index; do { phash_offs++; phash_offs_index++; prev_repetition = pWork->work_buff + *phash_offs; if(prev_repetition >= repetition_limit) return rep_length; } while(prev_repetition[rep_length - 2] != pre_last_byte || prev_repetition[0] != input_data[0]); // Reset the length of the repetition to 2 bytes only prev_rep_end = prev_repetition + 2; rep_length2 = 2; } // Find out how many more characters are equal to the first repetition. while(*prev_rep_end == input_data[rep_length2]) { if(++rep_length2 >= 0x204) break; prev_rep_end++; } // Is the newly found repetion at least as long as the previous one ? if(rep_length2 >= rep_length) { // Calculate the distance of the new repetition pWork->distance = (unsigned int)(input_data - prev_repetition - 1); if((rep_length = rep_length2) == 0x204) return rep_length; // Update the additional elements in the "offs09BC" table // to reflect new rep length while(offs_in_rep < rep_length2) { if(input_data[offs_in_rep] != input_data[di_val]) { di_val = pWork->offs09BC[di_val]; if(di_val != 0xFFFF) continue; } pWork->offs09BC[++offs_in_rep] = ++di_val; } } } } static void PKWAREAPI WriteCmpData(TCmpStruct * pWork) { unsigned char * input_data_end; // Pointer to the end of the input data unsigned char * input_data = pWork->work_buff + pWork->dsize_bytes + 0x204; unsigned int input_data_ended = 0; // If 1, then all data from the input stream have been already loaded unsigned int save_rep_length; // Saved length of current repetition unsigned int save_distance = 0; // Saved distance of current repetition unsigned int rep_length; // Length of the found repetition unsigned int phase = 0; // // Store the compression type and dictionary size pWork->out_buff[0] = (char)pWork->ctype; pWork->out_buff[1] = (char)pWork->dsize_bits; pWork->out_bytes = 2; // Reset output buffer to zero memset(&pWork->out_buff[2], 0, sizeof(pWork->out_buff) - 2); pWork->out_bits = 0; while(input_data_ended == 0) { unsigned int bytes_to_load = 0x1000; int total_loaded = 0; int bytes_loaded; // Load the bytes from the input stream, up to 0x1000 bytes while(bytes_to_load != 0) { bytes_loaded = pWork->read_buf((char *)pWork->work_buff + pWork->dsize_bytes + 0x204 + total_loaded, &bytes_to_load, pWork->param); if(bytes_loaded == 0) { if(total_loaded == 0 && phase == 0) goto __Exit; input_data_ended = 1; break; } else { bytes_to_load -= bytes_loaded; total_loaded += bytes_loaded; } } input_data_end = pWork->work_buff + pWork->dsize_bytes + total_loaded; if(input_data_ended) input_data_end += 0x204; // // Warning: The end of the buffer passed to "SortBuffer" is actually 2 bytes beyond // valid data. It is questionable if this is actually a bug or not, // but it might cause the compressed data output to be dependent on random bytes // that are in the buffer. // To prevent that, the calling application must always zero the compression // buffer before passing it to "implode" // // Search the PAIR_HASHes of the loaded blocks. Also, include // previously compressed data, if any. switch(phase) { case 0: SortBuffer(pWork, input_data, input_data_end + 1); phase++; if(pWork->dsize_bytes != 0x1000) phase++; break; case 1: SortBuffer(pWork, input_data - pWork->dsize_bytes + 0x204, input_data_end + 1); phase++; break; default: SortBuffer(pWork, input_data - pWork->dsize_bytes, input_data_end + 1); break; } // Perform the compression of the current block while(input_data < input_data_end) { // Find if the current byte sequence wasn't there before. rep_length = FindRep(pWork, input_data); while(rep_length != 0) { // If we found repetition of 2 bytes, that is 0x100 or fuhrter back, // don't bother. Storing the distance of 0x100 bytes would actually // take more space than storing the 2 bytes as-is. if(rep_length == 2 && pWork->distance >= 0x100) break; // When we are at the end of the input data, we cannot allow // the repetition to go past the end of the input data. if(input_data_ended && input_data + rep_length > input_data_end) { // Shorten the repetition length so that it only covers valid data rep_length = (unsigned long)(input_data_end - input_data); if(rep_length < 2) break; // If we got repetition of 2 bytes, that is 0x100 or more backward, don't bother if(rep_length == 2 && pWork->distance >= 0x100) break; goto __FlushRepetition; } if(rep_length >= 8 || input_data + 1 >= input_data_end) goto __FlushRepetition; // Try to find better repetition 1 byte later. // Example: "ARROCKFORT" "AROCKFORT" // When "input_data" points to the second string, FindRep // returns the occurence of "AR". But there is longer repetition "ROCKFORT", // beginning 1 byte after. save_rep_length = rep_length; save_distance = pWork->distance; rep_length = FindRep(pWork, input_data + 1); // Only use the new repetition if it's length is greater than the previous one if(rep_length > save_rep_length) { // If the new repetition if only 1 byte better // and the previous distance is less than 0x80 bytes, use the previous repetition if(rep_length > save_rep_length + 1 || save_distance > 0x80) { // Flush one byte, so that input_data will point to the secondary repetition OutputBits(pWork, pWork->nChBits[*input_data], pWork->nChCodes[*input_data]); input_data++; continue; } } // Revert to the previous repetition rep_length = save_rep_length; pWork->distance = save_distance; __FlushRepetition: OutputBits(pWork, pWork->nChBits[rep_length + 0xFE], pWork->nChCodes[rep_length + 0xFE]); if(rep_length == 2) { OutputBits(pWork, pWork->dist_bits[pWork->distance >> 2], pWork->dist_codes[pWork->distance >> 2]); OutputBits(pWork, 2, pWork->distance & 3); } else { OutputBits(pWork, pWork->dist_bits[pWork->distance >> pWork->dsize_bits], pWork->dist_codes[pWork->distance >> pWork->dsize_bits]); OutputBits(pWork, pWork->dsize_bits, pWork->dsize_mask & pWork->distance); } // Move the begin of the input data by the length of the repetition input_data += rep_length; goto _00402252; } // If there was no previous repetition for the current position in the input data, // just output the 9-bit literal for the one character OutputBits(pWork, pWork->nChBits[*input_data], pWork->nChCodes[*input_data]); input_data++; _00402252:; } if(input_data_ended == 0) { input_data -= 0x1000; memmove(pWork->work_buff, pWork->work_buff + 0x1000, pWork->dsize_bytes + 0x204); } } __Exit: // Write the termination literal OutputBits(pWork, pWork->nChBits[0x305], pWork->nChCodes[0x305]); if(pWork->out_bits != 0) pWork->out_bytes++; pWork->write_buf(pWork->out_buff, &pWork->out_bytes, pWork->param); return; } //----------------------------------------------------------------------------- // Main imploding function unsigned int PKWAREAPI implode( unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param), void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param), char *work_buf, void *param, unsigned int *type, unsigned int *dsize) { TCmpStruct * pWork = (TCmpStruct *)work_buf; unsigned int nChCode; unsigned int nCount; unsigned int i; int nCount2; // Fill the work buffer information // Note: The caller must zero the "work_buff" before passing it to implode pWork->read_buf = read_buf; pWork->write_buf = write_buf; pWork->dsize_bytes = *dsize; pWork->ctype = *type; pWork->param = param; pWork->dsize_bits = 4; pWork->dsize_mask = 0x0F; // Test dictionary size switch(*dsize) { case CMP_IMPLODE_DICT_SIZE3: // 0x1000 bytes pWork->dsize_bits++; pWork->dsize_mask |= 0x20; // No break here !!! case CMP_IMPLODE_DICT_SIZE2: // 0x800 bytes pWork->dsize_bits++; pWork->dsize_mask |= 0x10; // No break here !!! case CMP_IMPLODE_DICT_SIZE1: // 0x400 break; default: return CMP_INVALID_DICTSIZE; } // Test the compression type switch(*type) { case CMP_BINARY: // We will compress data with binary compression type for(nChCode = 0, nCount = 0; nCount < 0x100; nCount++) { pWork->nChBits[nCount] = 9; pWork->nChCodes[nCount] = (unsigned short)nChCode; nChCode = (nChCode & 0x0000FFFF) + 2; } break; case CMP_ASCII: // We will compress data with ASCII compression type for(nCount = 0; nCount < 0x100; nCount++) { pWork->nChBits[nCount] = (unsigned char )(ChBitsAsc[nCount] + 1); pWork->nChCodes[nCount] = (unsigned short)(ChCodeAsc[nCount] * 2); } break; default: return CMP_INVALID_MODE; } for(i = 0; i < 0x10; i++) { if(1 << ExLenBits[i]) { for(nCount2 = 0; nCount2 < (1 << ExLenBits[i]); nCount2++) { pWork->nChBits[nCount] = (unsigned char)(ExLenBits[i] + LenBits[i] + 1); pWork->nChCodes[nCount] = (unsigned short)((nCount2 << (LenBits[i] + 1)) | ((LenCode[i] & 0xFFFF00FF) * 2) | 1); nCount++; } } } // Copy the distance codes and distance bits and perform the compression memcpy(&pWork->dist_codes, DistCode, sizeof(DistCode)); memcpy(&pWork->dist_bits, DistBits, sizeof(DistBits)); WriteCmpData(pWork); return CMP_NO_ERROR; } ================================================ FILE: 3rdParty/PKWare/pkware.h ================================================ /*****************************************************************************/ /* pkware.h Copyright (c) Ladislav Zezula 2003 */ /*---------------------------------------------------------------------------*/ /* Header file for PKWARE Data Compression Library */ /*---------------------------------------------------------------------------*/ /* Date Ver Who Comment */ /* -------- ---- --- ------- */ /* 31.03.03 1.00 Lad The first version of pkware.h */ /*****************************************************************************/ #ifndef __PKWARE_H__ #define __PKWARE_H__ //----------------------------------------------------------------------------- // Defines #define CMP_BINARY 0 // Binary compression #define CMP_ASCII 1 // Ascii compression #define CMP_NO_ERROR 0 #define CMP_INVALID_DICTSIZE 1 #define CMP_INVALID_MODE 2 #define CMP_BAD_DATA 3 #define CMP_ABORT 4 #define CMP_IMPLODE_DICT_SIZE1 1024 // Dictionary size of 1024 #define CMP_IMPLODE_DICT_SIZE2 2048 // Dictionary size of 2048 #define CMP_IMPLODE_DICT_SIZE3 4096 // Dictionary size of 4096 //----------------------------------------------------------------------------- // Define calling convention #ifndef PKWAREAPI #ifdef WIN32 #define PKWAREAPI __cdecl // Use for normal __cdecl calling #else #define PKWAREAPI #endif #endif //----------------------------------------------------------------------------- // Internal structures // Compression structure typedef struct { unsigned int distance; // 0000: Backward distance of the currently found repetition, decreased by 1 unsigned int out_bytes; // 0004: # bytes available in out_buff unsigned int out_bits; // 0008: # of bits available in the last out byte unsigned int dsize_bits; // 000C: Number of bits needed for dictionary size. 4 = 0x400, 5 = 0x800, 6 = 0x1000 unsigned int dsize_mask; // 0010: Bit mask for dictionary. 0x0F = 0x400, 0x1F = 0x800, 0x3F = 0x1000 unsigned int ctype; // 0014: Compression type (CMP_ASCII or CMP_BINARY) unsigned int dsize_bytes; // 0018: Dictionary size in bytes unsigned char dist_bits[0x40]; // 001C: Distance bits unsigned char dist_codes[0x40]; // 005C: Distance codes unsigned char nChBits[0x306]; // 009C: Table of literal bit lengths to be put to the output stream unsigned short nChCodes[0x306]; // 03A2: Table of literal codes to be put to the output stream unsigned short offs09AE; // 09AE: void * param; // 09B0: User parameter unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param); // 9B4 void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param); // 9B8 unsigned short offs09BC[0x204]; // 09BC: unsigned long offs0DC4; // 0DC4: unsigned short phash_to_index[0x900]; // 0DC8: Array of indexes (one for each PAIR_HASH) to the "pair_hash_offsets" table unsigned short phash_to_index_end; // 1FC8: End marker for "phash_to_index" table char out_buff[0x802]; // 1FCA: Compressed data unsigned char work_buff[0x2204]; // 27CC: Work buffer // + DICT_OFFSET => Dictionary // + UNCMP_OFFSET => Uncompressed data unsigned short phash_offs[0x2204]; // 49D0: Table of offsets for each PAIR_HASH } TCmpStruct; #define CMP_BUFFER_SIZE sizeof(TCmpStruct) // Size of compression structure. // Defined as 36312 in pkware header file // Decompression structure typedef struct { unsigned long offs0000; // 0000 unsigned long ctype; // 0004: Compression type (CMP_BINARY or CMP_ASCII) unsigned long outputPos; // 0008: Position in output buffer unsigned long dsize_bits; // 000C: Dict size (4, 5, 6 for 0x400, 0x800, 0x1000) unsigned long dsize_mask; // 0010: Dict size bitmask (0x0F, 0x1F, 0x3F for 0x400, 0x800, 0x1000) unsigned long bit_buff; // 0014: 16-bit buffer for processing input data unsigned long extra_bits; // 0018: Number of extra (above 8) bits in bit buffer unsigned int in_pos; // 001C: Position in in_buff unsigned long in_bytes; // 0020: Number of bytes in input buffer void * param; // 0024: Custom parameter unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param); // Pointer to function that reads data from the input stream void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param);// Pointer to function that writes data to the output stream unsigned char out_buff[0x2204]; // 0030: Output circle buffer. // 0x0000 - 0x0FFF: Previous uncompressed data, kept for repetitions // 0x1000 - 0x1FFF: Currently decompressed data // 0x2000 - 0x2203: Reserve space for the longest possible repetition unsigned char in_buff[0x800]; // 2234: Buffer for data to be decompressed unsigned char DistPosCodes[0x100]; // 2A34: Table of distance position codes unsigned char LengthCodes[0x100]; // 2B34: Table of length codes unsigned char offs2C34[0x100]; // 2C34: Buffer for unsigned char offs2D34[0x100]; // 2D34: Buffer for unsigned char offs2E34[0x80]; // 2EB4: Buffer for unsigned char offs2EB4[0x100]; // 2EB4: Buffer for unsigned char ChBitsAsc[0x100]; // 2FB4: Buffer for unsigned char DistBits[0x40]; // 30B4: Numbers of bytes to skip copied block length unsigned char LenBits[0x10]; // 30F4: Numbers of bits for skip copied block length unsigned char ExLenBits[0x10]; // 3104: Number of valid bits for copied block unsigned short LenBase[0x10]; // 3114: Buffer for } TDcmpStruct; #define EXP_BUFFER_SIZE sizeof(TDcmpStruct) // Size of decompression structure // Defined as 12596 in pkware headers //----------------------------------------------------------------------------- // Public functions #ifdef __cplusplus extern "C" { #endif unsigned int PKWAREAPI implode( unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param), void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param), char *work_buf, void *param, unsigned int *type, unsigned int *dsize); unsigned int PKWAREAPI explode( unsigned int (PKWAREAPI *read_buf)(char *buf, unsigned int *size, void *param), void (PKWAREAPI *write_buf)(char *buf, unsigned int *size, void *param), char *work_buf, void *param); #ifdef __cplusplus } // End of 'extern "C"' declaration #endif #endif // __PKWARE_H__ ================================================ FILE: 3rdParty/PicoSHA2/picosha2.h ================================================ /* The MIT License (MIT) Copyright (C) 2017 okdshin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef PICOSHA2_H #define PICOSHA2_H // picosha2:20140213 #ifndef PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR #define PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR \ 1048576 //=1024*1024: default is 1MB memory #endif #include #include #include #include #include #include namespace picosha2 { typedef unsigned long word_t; typedef unsigned char byte_t; static const size_t k_digest_size = 32; namespace detail { inline byte_t mask_8bit(byte_t x) { return x & 0xff; } inline word_t mask_32bit(word_t x) { return x & 0xffffffff; } const word_t add_constant[64] = { 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; const word_t initial_message_digest[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19}; inline word_t ch(word_t x, word_t y, word_t z) { return (x & y) ^ ((~x) & z); } inline word_t maj(word_t x, word_t y, word_t z) { return (x & y) ^ (x & z) ^ (y & z); } inline word_t rotr(word_t x, std::size_t n) { assert(n < 32); return mask_32bit((x >> n) | (x << (32 - n))); } inline word_t bsig0(word_t x) { return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); } inline word_t bsig1(word_t x) { return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); } inline word_t shr(word_t x, std::size_t n) { assert(n < 32); return x >> n; } inline word_t ssig0(word_t x) { return rotr(x, 7) ^ rotr(x, 18) ^ shr(x, 3); } inline word_t ssig1(word_t x) { return rotr(x, 17) ^ rotr(x, 19) ^ shr(x, 10); } template void hash256_block(RaIter1 message_digest, RaIter2 first, RaIter2 last) { assert(first + 64 == last); static_cast(last); // for avoiding unused-variable warning word_t w[64]; std::fill(w, w + 64, 0); for (std::size_t i = 0; i < 16; ++i) { w[i] = (static_cast(mask_8bit(*(first + i * 4))) << 24) | (static_cast(mask_8bit(*(first + i * 4 + 1))) << 16) | (static_cast(mask_8bit(*(first + i * 4 + 2))) << 8) | (static_cast(mask_8bit(*(first + i * 4 + 3)))); } for (std::size_t i = 16; i < 64; ++i) { w[i] = mask_32bit(ssig1(w[i - 2]) + w[i - 7] + ssig0(w[i - 15]) + w[i - 16]); } word_t a = *message_digest; word_t b = *(message_digest + 1); word_t c = *(message_digest + 2); word_t d = *(message_digest + 3); word_t e = *(message_digest + 4); word_t f = *(message_digest + 5); word_t g = *(message_digest + 6); word_t h = *(message_digest + 7); for (std::size_t i = 0; i < 64; ++i) { word_t temp1 = h + bsig1(e) + ch(e, f, g) + add_constant[i] + w[i]; word_t temp2 = bsig0(a) + maj(a, b, c); h = g; g = f; f = e; e = mask_32bit(d + temp1); d = c; c = b; b = a; a = mask_32bit(temp1 + temp2); } *message_digest += a; *(message_digest + 1) += b; *(message_digest + 2) += c; *(message_digest + 3) += d; *(message_digest + 4) += e; *(message_digest + 5) += f; *(message_digest + 6) += g; *(message_digest + 7) += h; for (std::size_t i = 0; i < 8; ++i) { *(message_digest + i) = mask_32bit(*(message_digest + i)); } } } // namespace detail template void output_hex(InIter first, InIter last, std::ostream& os) { os.setf(std::ios::hex, std::ios::basefield); while (first != last) { os.width(2); os.fill('0'); os << static_cast(*first); ++first; } os.setf(std::ios::dec, std::ios::basefield); } template void bytes_to_hex_string(InIter first, InIter last, std::string& hex_str) { std::ostringstream oss; output_hex(first, last, oss); hex_str.assign(oss.str()); } template void bytes_to_hex_string(const InContainer& bytes, std::string& hex_str) { bytes_to_hex_string(bytes.begin(), bytes.end(), hex_str); } template std::string bytes_to_hex_string(InIter first, InIter last) { std::string hex_str; bytes_to_hex_string(first, last, hex_str); return hex_str; } template std::string bytes_to_hex_string(const InContainer& bytes) { std::string hex_str; bytes_to_hex_string(bytes, hex_str); return hex_str; } class hash256_one_by_one { public: hash256_one_by_one() { init(); } void init() { buffer_.clear(); std::fill(data_length_digits_, data_length_digits_ + 4, 0); std::copy(detail::initial_message_digest, detail::initial_message_digest + 8, h_); } template void process(RaIter first, RaIter last) { add_to_data_length(static_cast(std::distance(first, last))); std::copy(first, last, std::back_inserter(buffer_)); std::size_t i = 0; for (; i + 64 <= buffer_.size(); i += 64) { detail::hash256_block(h_, buffer_.begin() + i, buffer_.begin() + i + 64); } buffer_.erase(buffer_.begin(), buffer_.begin() + i); } void finish() { byte_t temp[64]; std::fill(temp, temp + 64, 0); std::size_t remains = buffer_.size(); std::copy(buffer_.begin(), buffer_.end(), temp); temp[remains] = 0x80; if (remains > 55) { std::fill(temp + remains + 1, temp + 64, 0); detail::hash256_block(h_, temp, temp + 64); std::fill(temp, temp + 64 - 4, 0); } else { std::fill(temp + remains + 1, temp + 64 - 4, 0); } write_data_bit_length(&(temp[56])); detail::hash256_block(h_, temp, temp + 64); } template void get_hash_bytes(OutIter first, OutIter last) const { for (const word_t* iter = h_; iter != h_ + 8; ++iter) { for (std::size_t i = 0; i < 4 && first != last; ++i) { *(first++) = detail::mask_8bit( static_cast((*iter >> (24 - 8 * i)))); } } } private: void add_to_data_length(word_t n) { word_t carry = 0; data_length_digits_[0] += n; for (std::size_t i = 0; i < 4; ++i) { data_length_digits_[i] += carry; if (data_length_digits_[i] >= 65536u) { carry = data_length_digits_[i] >> 16; data_length_digits_[i] &= 65535u; } else { break; } } } void write_data_bit_length(byte_t* begin) { word_t data_bit_length_digits[4]; std::copy(data_length_digits_, data_length_digits_ + 4, data_bit_length_digits); // convert byte length to bit length (multiply 8 or shift 3 times left) word_t carry = 0; for (std::size_t i = 0; i < 4; ++i) { word_t before_val = data_bit_length_digits[i]; data_bit_length_digits[i] <<= 3; data_bit_length_digits[i] |= carry; data_bit_length_digits[i] &= 65535u; carry = (before_val >> (16 - 3)) & 65535u; } // write data_bit_length for (int i = 3; i >= 0; --i) { (*begin++) = static_cast(data_bit_length_digits[i] >> 8); (*begin++) = static_cast(data_bit_length_digits[i]); } } std::vector buffer_; word_t data_length_digits_[4]; // as 64bit integer (16bit x 4 integer) word_t h_[8]; }; inline void get_hash_hex_string(const hash256_one_by_one& hasher, std::string& hex_str) { byte_t hash[k_digest_size]; hasher.get_hash_bytes(hash, hash + k_digest_size); return bytes_to_hex_string(hash, hash + k_digest_size, hex_str); } inline std::string get_hash_hex_string(const hash256_one_by_one& hasher) { std::string hex_str; get_hash_hex_string(hasher, hex_str); return hex_str; } namespace impl { template void hash256_impl(RaIter first, RaIter last, OutIter first2, OutIter last2, int, std::random_access_iterator_tag) { hash256_one_by_one hasher; // hasher.init(); hasher.process(first, last); hasher.finish(); hasher.get_hash_bytes(first2, last2); } template void hash256_impl(InputIter first, InputIter last, OutIter first2, OutIter last2, int buffer_size, std::input_iterator_tag) { std::vector buffer(buffer_size); hash256_one_by_one hasher; // hasher.init(); while (first != last) { int size = buffer_size; for (int i = 0; i != buffer_size; ++i, ++first) { if (first == last) { size = i; break; } buffer[i] = *first; } hasher.process(buffer.begin(), buffer.begin() + size); } hasher.finish(); hasher.get_hash_bytes(first2, last2); } } template void hash256(InIter first, InIter last, OutIter first2, OutIter last2, int buffer_size = PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR) { picosha2::impl::hash256_impl( first, last, first2, last2, buffer_size, typename std::iterator_traits::iterator_category()); } template void hash256(InIter first, InIter last, OutContainer& dst) { hash256(first, last, dst.begin(), dst.end()); } template void hash256(const InContainer& src, OutIter first, OutIter last) { hash256(src.begin(), src.end(), first, last); } template void hash256(const InContainer& src, OutContainer& dst) { hash256(src.begin(), src.end(), dst.begin(), dst.end()); } template void hash256_hex_string(InIter first, InIter last, std::string& hex_str) { byte_t hashed[k_digest_size]; hash256(first, last, hashed, hashed + k_digest_size); std::ostringstream oss; output_hex(hashed, hashed + k_digest_size, oss); hex_str.assign(oss.str()); } template std::string hash256_hex_string(InIter first, InIter last) { std::string hex_str; hash256_hex_string(first, last, hex_str); return hex_str; } inline void hash256_hex_string(const std::string& src, std::string& hex_str) { hash256_hex_string(src.begin(), src.end(), hex_str); } template void hash256_hex_string(const InContainer& src, std::string& hex_str) { hash256_hex_string(src.begin(), src.end(), hex_str); } template std::string hash256_hex_string(const InContainer& src) { return hash256_hex_string(src.begin(), src.end()); } templatevoid hash256(std::ifstream& f, OutIter first, OutIter last){ hash256(std::istreambuf_iterator(f), std::istreambuf_iterator(), first,last); } }// namespace picosha2 #endif // PICOSHA2_H ================================================ FILE: 3rdParty/SDL2/CMake/FindSDL2.cmake ================================================ if(UWP_LIB AND NOT TARGET SDL2::SDL2-static) set(SDL_BUILD_TYPE "Release") if(CMAKE_BUILD_TYPE MATCHES "Debug") set(SDL_BUILD_TYPE "Debug") endif() set(SDL2_LIBRARY "${UWP_SDL2_DIR}/VisualC-WinRT/x64/${SDL_BUILD_TYPE}/SDL-UWP") set(SDL2_INCLUDE_DIR "${UWP_SDL2_DIR}/include") add_library(SDL2_lib STATIC IMPORTED) set_property(TARGET SDL2_lib PROPERTY IMPORTED_LOCATION "${SDL2_LIBRARY}/SDL2.lib") set_property(TARGET SDL2_lib PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${SDL2_INCLUDE_DIR}) add_library(SDL2::SDL2-static ALIAS SDL2_lib) mark_as_advanced(SDL2_INCLUDE_DIR SDL2_LIBRARY) set(SDL2_LIBRARIES ${SDL2_LIBRARY}) set(SDL2_INCLUDE_DIRS ${SDL2_INCLUDE_DIR}) endif() ================================================ FILE: 3rdParty/SDL2/CMakeLists.txt ================================================ if(DEVILUTIONX_STATIC_SDL2) set(BUILD_SHARED_LIBS OFF) set(SDL_SHARED OFF) set(SDL_STATIC ON) if(PIE) set(SDL_STATIC_PIC ON) endif() else() set(BUILD_SHARED_LIBS ON) set(SDL_SHARED ON) set(SDL_STATIC OFF) endif() set(SDL_TEST_ENABLED_BY_DEFAULT OFF) include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) if(TARGET_PLATFORM STREQUAL "dos") set(DOS ON) FetchContent_Declare(SDL2 # branch: dos-vbe-rebase URL https://github.com/diasurgical/SDL/archive/20b47a62a12f0c7be4052fb5b32eb21d709d6f06.tar.gz URL_HASH MD5=6e77a1d1a79d7f0dd6231b39515a341f ) else() FetchContent_Declare(SDL2 URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.8/SDL2-2.32.8.tar.gz URL_HASH SHA256=0ca83e9c9b31e18288c7ec811108e58bac1f1bb5ec6577ad386830eac51c787e ) endif() FetchContent_MakeAvailable_ExcludeFromAll(SDL2) ================================================ FILE: 3rdParty/SDL3/CMakeLists.txt ================================================ if(DEVILUTIONX_STATIC_SDL3) set(BUILD_SHARED_LIBS OFF) set(SDL_SHARED OFF) set(SDL_STATIC ON) if(PIE) set(SDL_STATIC_PIC ON) endif() else() set(BUILD_SHARED_LIBS ON) set(SDL_SHARED ON) set(SDL_STATIC OFF) endif() set(SDL_TEST_ENABLED_BY_DEFAULT OFF) include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare(SDL3 URL https://github.com/libsdl-org/SDL/archive/f173fd28f04cb64ae054d6a97edb5d33925f539b.tar.gz URL_HASH SHA256=f7501d84c1a7f168567c002f4e1db4f220c4a34c51f7fa7d199962d0ed5fb42c ) FetchContent_MakeAvailable_ExcludeFromAll(SDL3) ================================================ FILE: 3rdParty/SDL3_image/CMakeLists.txt ================================================ include(functions/dependency_options) if(NOT DEFINED DEVILUTIONX_SYSTEM_LIBPNG) find_package(PNG QUIET) if(PNG_FOUND) message("-- Found png ${PNG_VERSION_STRING}") else() message("-- Suitable system png package not found, will use png from source") set(DEVILUTIONX_SYSTEM_LIBPNG OFF) endif() endif() dependency_options("libpng" DEVILUTIONX_SYSTEM_LIBPNG ON DEVILUTIONX_STATIC_LIBPNG) if(DEVILUTIONX_SYSTEM_LIBPNG) find_package(PNG REQUIRED) else() add_subdirectory(../libpng libpng) endif() include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) if(DEVILUTIONX_STATIC_SDL_IMAGE) set(BUILD_SHARED_LIBS OFF) else() set(BUILD_SHARED_LIBS ON) endif() set(SDLIMAGE_BACKEND_STB OFF) set(SDLIMAGE_AVIF OFF) set(SDLIMAGE_BMP OFF) set(SDLIMAGE_GIF OFF) set(SDLIMAGE_JPG OFF) set(SDLIMAGE_JXL OFF) set(SDLIMAGE_LBM OFF) set(SDLIMAGE_PCX OFF) set(SDLIMAGE_PNG ON) set(SDLIMAGE_PNM OFF) set(SDLIMAGE_QOI OFF) set(SDLIMAGE_SVG OFF) set(SDLIMAGE_TGA OFF) set(SDLIMAGE_TIF OFF) set(SDLIMAGE_WEBP OFF) set(SDLIMAGE_XCF OFF) set(SDLIMAGE_XPM OFF) set(SDLIMAGE_XV OFF) set(SDLIMAGE_AVIF_SAVE OFF) set(SDLIMAGE_JPG_SAVE OFF) set(SDLIMAGE_PNG_SAVE ON) FetchContent_Declare_ExcludeFromAll(SDL_image URL https://github.com/libsdl-org/SDL_image/releases/download/release-3.2.4/SDL3_image-3.2.4.tar.gz URL_HASH SHA256=a725bd6d04261fdda0dd8d950659e1dc15a8065d025275ef460d32ae7dcfc182 ) FetchContent_MakeAvailable_ExcludeFromAll(SDL_image) ================================================ FILE: 3rdParty/SDL3_mixer/CMakeLists.txt ================================================ include(functions/dependency_options) include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) if(DEVILUTIONX_STATIC_SDL_MIXER) set(BUILD_SHARED_LIBS OFF) else() set(BUILD_SHARED_LIBS ON) endif() set(SDLMIXER_AIFF OFF) set(SDLMIXER_WAVE ON) set(SDLMIXER_VOC OFF) set(SDLMIXER_AU OFF) set(SDLMIXER_FLAC_LIBFLAC OFF) set(SDLMIXER_FLAC_DRFLAC OFF) set(SDLMIXER_GME OFF) set(SDLMIXER_MOD_XMP OFF) set(SDLMIXER_MP3_DRMP3 ON) set(SDLMIXER_MP3_MPG123 OFF) set(SDLMIXER_MIDI_FLUIDSYNTH OFF) set(SDLMIXER_MIDI_TIMIDITY OFF) set(SDLMIXER_OPUS OFF) set(SDLMIXER_VORBIS_STB OFF) set(SDLMIXER_VORBIS_VORBISFILE OFF) set(SDLMIXER_VORBIS_TREMOR OFF) set(SDLMIXER_WAVPACK OFF) FetchContent_Declare_ExcludeFromAll(SDL_mixer URL https://github.com/libsdl-org/SDL_mixer/archive/7d37755016f0952c32c9483c556d8608da7ee82f.tar.gz URL_HASH SHA256=2fa63f1eb623e3acd0012a461771eb93332e2026205f9487da3a3a75bc790111 ) FetchContent_MakeAvailable_ExcludeFromAll(SDL_mixer) ================================================ FILE: 3rdParty/SDL_audiolib/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) if(DEVILUTIONX_STATIC_SDL_AUDIOLIB) set(BUILD_SHARED_LIBS OFF) else() set(BUILD_SHARED_LIBS ON) endif() # Will use our `fmt::fmt` target if it exists. set(WITH_SYSTEM_FMTLIB ON) # No need for the libsamplerate resampler: set(USE_RESAMP_SRC OFF) # No need for the SOX resampler: set(USE_RESAMP_SOXR OFF) # Exceptions are only used for fatal errors which we can't handle anyway: set(DISABLE_EXCEPTIONS ON) # We do not need any of the audio formats except WAV and mp3: set(USE_DEC_DRWAV ON) set(USE_DEC_DRFLAC OFF) set(USE_DEC_OPENMPT OFF) set(USE_DEC_XMP OFF) set(USE_DEC_MODPLUG OFF) set(USE_DEC_MPG123 OFF) set(USE_DEC_SNDFILE OFF) set(USE_DEC_LIBVORBIS OFF) set(USE_DEC_LIBOPUSFILE OFF) set(USE_DEC_FLAC OFF) set(USE_DEC_MUSEPACK OFF) set(USE_DEC_FLUIDSYNTH OFF) set(USE_DEC_BASSMIDI OFF) set(USE_DEC_WILDMIDI OFF) set(USE_DEC_ADLMIDI OFF) set(USE_DEC_DRMP3 ON) include(FetchContent) FetchContent_Declare_ExcludeFromAll(SDL_audiolib URL https://github.com/realnc/SDL_audiolib/archive/cc1bb6af8d4cf5e200259072bde1edd1c8c5137e.tar.gz URL_HASH MD5=0e8174264ac9c6b314c6b2d9a5f72efd) FetchContent_MakeAvailable_ExcludeFromAll(SDL_audiolib) add_library(SDL_audiolib::SDL_audiolib ALIAS SDL_audiolib) # External library, disable warnings. target_compile_options(SDL_audiolib PRIVATE -w) ================================================ FILE: 3rdParty/SDL_image/CMakeLists.txt ================================================ include(functions/dependency_options) if(NOT DEFINED DEVILUTIONX_SYSTEM_LIBPNG) find_package(PNG QUIET) if(PNG_FOUND) message("-- Found png ${PNG_VERSION_STRING}") else() message("-- Suitable system png package not found, will use png from source") set(DEVILUTIONX_SYSTEM_LIBPNG OFF) endif() endif() dependency_options("libpng" DEVILUTIONX_SYSTEM_LIBPNG ON DEVILUTIONX_STATIC_LIBPNG) if(DEVILUTIONX_SYSTEM_LIBPNG) find_package(PNG REQUIRED) else() add_subdirectory(../libpng libpng) endif() include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(SDL_image URL https://github.com/libsdl-org/SDL_image/archive/refs/tags/release-2.0.5.tar.gz URL_HASH MD5=3446ed7ee3c700065dcb33426a9b0c6e ) FetchContent_MakeAvailable_ExcludeFromAll(SDL_image) if(DEVILUTIONX_STATIC_SDL_IMAGE) add_library(SDL_image STATIC ${CMAKE_CURRENT_LIST_DIR}/IMG.c ${sdl_image_SOURCE_DIR}/IMG_png.c) else() add_library(SDL_image SHARED ${CMAKE_CURRENT_LIST_DIR}/IMG.c ${sdl_image_SOURCE_DIR}/IMG_png.c) endif() target_include_directories(SDL_image PRIVATE ${sdl_image_SOURCE_DIR}) target_compile_definitions(SDL_image PRIVATE LOAD_PNG SDL_IMAGE_USE_COMMON_BACKEND) target_link_libraries(SDL_image PNG::PNG) if(TARGET SDL2::SDL2 AND NOT (DEVILUTIONX_STATIC_SDL2 AND TARGET SDL2::SDL2-static)) target_link_libraries(SDL_image SDL2::SDL2) add_library(SDL2::SDL2_image ALIAS SDL_image) elseif(TARGET SDL2::SDL2-static) target_link_libraries(SDL_image SDL2::SDL2-static) add_library(SDL2::SDL2_image ALIAS SDL_image) endif() ================================================ FILE: 3rdParty/SDL_image/IMG.c ================================================ /* SDL_image: An example image loading library for use with SDL Copyright (C) 1997-2021 Sam Lantinga This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. */ /* This is a heavily reduced version of IMG.c including only PNG support */ #include "SDL_image.h" extern int IMG_InitPNG(void); extern void IMG_QuitPNG(void); static int initialized = 0; int IMG_Init(int flags) { int result = 0; /* Passing 0 returns the currently initialized loaders */ if (!flags) { return initialized; } if (flags & IMG_INIT_PNG) { if ((initialized & IMG_INIT_PNG) || IMG_InitPNG() == 0) { result |= IMG_INIT_PNG; } } initialized |= result; return result; } void IMG_Quit() { if (initialized & IMG_INIT_PNG) { IMG_QuitPNG(); } initialized = 0; } ================================================ FILE: 3rdParty/SheenBidi/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) set(BUILD_TESTING OFF) set(ASAN OFF) set(UBSAN OFF) FetchContent_Declare_ExcludeFromAll(SheenBidi URL https://github.com/Tehreer/SheenBidi/archive/refs/tags/v2.9.0.tar.gz URL_HASH MD5=2c22cfad93a52afa7bd25ad56a3a4089 ) FetchContent_MakeAvailable_ExcludeFromAll(SheenBidi) ================================================ FILE: 3rdParty/asio/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(asio URL https://github.com/diasurgical/asio/archive/4bcf552fcea3e1ae555dde2ab33bc9fa6770da4d.tar.gz URL_HASH MD5=7ffee993fc21b1115abf485958d03ac8 ) FetchContent_MakeAvailable_ExcludeFromAll(asio) add_library(asio STATIC ${CMAKE_CURRENT_LIST_DIR}/asio_handle_exception.cpp) target_compile_definitions(asio PUBLIC ASIO_NO_EXCEPTIONS) target_include_directories(asio PUBLIC ${asio_SOURCE_DIR}/asio/include ${CMAKE_CURRENT_LIST_DIR}) if(NINTENDO_3DS OR NINTENDO_SWITCH) include(asio_defs REQUIRED) endif() ================================================ FILE: 3rdParty/asio/asio_handle_exception.cpp ================================================ #include #define ErrAsio(message) devilution::ErrDlg("ASIO Error", message, __FILE__, __LINE__) namespace devilution { extern void ErrDlg(const char* title, std::string_view error, std::string_view logFilePath, int logLineNr); } // namespace devilution namespace asio::detail { void fatal_exception(const char* message) { ErrAsio(message); } } // namespace asio::detail ================================================ FILE: 3rdParty/asio/asio_handle_exception.hpp ================================================ #pragma once #include namespace asio::detail { void fatal_exception(const char *message); template void throw_exception( const Exception &e ASIO_SOURCE_LOCATION_PARAM) { fatal_exception(e.what()); } } // namespace asio::detail ================================================ FILE: 3rdParty/benchmark/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) FetchContent_Declare_ExcludeFromAll( benchmark URL https://github.com/google/benchmark/archive/refs/tags/v1.8.5.tar.gz URL_HASH MD5=708d91ce255e8af4c1d7dfec50dff178 ) set(INSTALL_GTEST OFF) set(BENCHMARK_ENABLE_TESTING OFF) set(BENCHMARK_ENABLE_EXCEPTIONS OFF) set(BENCHMARK_ENABLE_WERROR OFF) set(BENCHMARK_ENABLE_INSTALL OFF) FetchContent_MakeAvailable_ExcludeFromAll(benchmark) ================================================ FILE: 3rdParty/bzip2/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(bzip2 GIT_REPOSITORY https://gitlab.com/bzip2/bzip2 GIT_TAG bzip2-1.0.8 ) FetchContent_MakeAvailable_ExcludeFromAll(bzip2) if(DEVILUTIONX_STATIC_BZIP2) set(_lib_type STATIC) else() set(_lib_type SHARED) endif() add_library(BZip2 ${_lib_type} ${bzip2_SOURCE_DIR}/blocksort.c ${bzip2_SOURCE_DIR}/bzlib.c ${bzip2_SOURCE_DIR}/compress.c ${bzip2_SOURCE_DIR}/crctable.c ${bzip2_SOURCE_DIR}/decompress.c ${bzip2_SOURCE_DIR}/huffman.c ${bzip2_SOURCE_DIR}/randtable.c ${bzip2_SOURCE_DIR}/bzlib.h ${bzip2_SOURCE_DIR}/bzlib_private.h ${CMAKE_CURRENT_LIST_DIR}/bz_internal_error.c ) target_compile_definitions(BZip2 PRIVATE -DBZ_NO_STDIO) target_include_directories(BZip2 PUBLIC ${bzip2_SOURCE_DIR}) add_library(BZip2::BZip2 ALIAS BZip2) ================================================ FILE: 3rdParty/bzip2/bz_internal_error.c ================================================ #include void bz_internal_error(int errcode) { fprintf(stderr, "BZip2 fatal error %d\n", errcode); } ================================================ FILE: 3rdParty/discord/.editorconfig ================================================ [*.patch] end_of_line = lf ================================================ FILE: 3rdParty/discord/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) find_package(Patch REQUIRED) set(Discord_SDK_URL "https://dl-game-sdk.discordapp.net/3.2.1/discord_game_sdk.zip") set(Discord_SDK_HASH "73e5e1b3f8413a2c7184ef17476822f2") if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") FetchContent_Declare_ExcludeFromAll(discordsrc URL ${Discord_SDK_URL} URL_HASH MD5=${Discord_SDK_HASH} ) else() FetchContent_Declare_ExcludeFromAll(discordsrc URL ${Discord_SDK_URL} URL_HASH MD5=${Discord_SDK_HASH} PATCH_COMMAND "${Patch_EXECUTABLE}" -p1 -N < "${CMAKE_CURRENT_LIST_DIR}/fixes.patch" || true ) endif() FetchContent_MakeAvailable_ExcludeFromAll(discordsrc) file(GLOB discord_SRCS ${discordsrc_SOURCE_DIR}/cpp/*.cpp) add_library(discord STATIC ${discord_SRCS}) target_include_directories(discord INTERFACE "${discordsrc_SOURCE_DIR}/..") if(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") set(DISCORD_LIB_DIR "${discordsrc_SOURCE_DIR}/lib/aarch64") elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) set(DISCORD_LIB_DIR "${discordsrc_SOURCE_DIR}/lib/x86") else() set(DISCORD_LIB_DIR "${discordsrc_SOURCE_DIR}/lib/x86_64") endif() set(DISCORD_SHARED_LIB_FILENAME "discord_game_sdk${CMAKE_SHARED_LIBRARY_SUFFIX}") set(DISCORD_SHARED_LIB "${DISCORD_LIB_DIR}/${DISCORD_SHARED_LIB_FILENAME}") set(DISCORD_SHARED_LIB "${DISCORD_SHARED_LIB}" PARENT_SCOPE) find_library( DISCORD_LIB NAMES "${DISCORD_SHARED_LIB_FILENAME}" PATHS "${DISCORD_LIB_DIR}" REQUIRED NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH ) message("-- 📚 discord_game_sdk: ${DISCORD_LIB}") # Copy the dll so that tests and Debug builds can find it on Windows. # We also need it at this location to link to `discord_game_sdk.so` rather # than `_deps/discordsrc-src/lib/x86_64/discord_game_sdk.so` on Linux. file(COPY "${DISCORD_SHARED_LIB}" DESTINATION "${CMAKE_BINARY_DIR}") add_library(discord_game_sdk SHARED IMPORTED GLOBAL) set_target_properties(discord_game_sdk PROPERTIES IMPORTED_IMPLIB "${DISCORD_LIB}" IMPORTED_LOCATION "${CMAKE_BINARY_DIR}/${DISCORD_SHARED_LIB_FILENAME}" ) ================================================ FILE: 3rdParty/discord/fixes.patch ================================================ From 767aff84177e8805933ac4f980d9fc36a8b02af6 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sat, 21 Jan 2023 13:09:38 +0000 Subject: [PATCH] Various fixes 1. Replaces `#include ` with `#include ` for compatibility with case-sensitive file systems (e.g. MinGW cross-compilation on a Linux host). 2. Adds missing `#include ` to `cpp/types.h`. 3. Fixes calling convention for callback lambdas. Signed-off-by: Gleb Mazovetskiy --- c/discord_game_sdk.h | 2 +- cpp/achievement_manager.cpp | 4 ++-- cpp/activity_manager.cpp | 10 +++++----- cpp/application_manager.cpp | 6 +++--- cpp/core.cpp | 2 +- cpp/ffi.h | 2 +- cpp/image_manager.cpp | 2 +- cpp/lobby_manager.cpp | 22 +++++++++++----------- cpp/overlay_manager.cpp | 12 ++++++------ cpp/relationship_manager.cpp | 2 +- cpp/storage_manager.cpp | 6 +++--- cpp/store_manager.cpp | 6 +++--- cpp/types.h | 4 +++- cpp/user_manager.cpp | 2 +- cpp/voice_manager.cpp | 2 +- examples/c/main.c | 2 +- 16 files changed, 44 insertions(+), 42 deletions(-) diff --git a/c/discord_game_sdk.h b/c/discord_game_sdk.h index 4618756..f4c209a 100644 --- a/c/discord_game_sdk.h +++ b/c/discord_game_sdk.h @@ -2,7 +2,7 @@ #define _DISCORD_GAME_SDK_H_ #ifdef _WIN32 -#include +#include #include #endif diff --git a/cpp/achievement_manager.cpp b/cpp/achievement_manager.cpp index 43a6d4c..9e92ad6 100644 --- a/cpp/achievement_manager.cpp +++ b/cpp/achievement_manager.cpp @@ -34,7 +34,7 @@ void AchievementManager::SetUserAchievement(Snowflake achievementId, std::uint8_t percentComplete, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -50,7 +50,7 @@ void AchievementManager::SetUserAchievement(Snowflake achievementId, void AchievementManager::FetchUserAchievements(std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/activity_manager.cpp b/cpp/activity_manager.cpp index 3c20074..074f784 100644 --- a/cpp/activity_manager.cpp +++ b/cpp/activity_manager.cpp @@ -84,7 +84,7 @@ Result ActivityManager::RegisterSteam(std::uint32_t steamId) void ActivityManager::UpdateActivity(Activity const& activity, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -102,7 +102,7 @@ void ActivityManager::UpdateActivity(Activity const& activity, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -119,7 +119,7 @@ void ActivityManager::SendRequestReply(UserId userId, ActivityJoinRequestReply reply, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -141,7 +141,7 @@ void ActivityManager::SendInvite(UserId userId, char const* content, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -161,7 +161,7 @@ void ActivityManager::SendInvite(UserId userId, void ActivityManager::AcceptInvite(UserId userId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/application_manager.cpp b/cpp/application_manager.cpp index 0e05f3f..98dac16 100644 --- a/cpp/application_manager.cpp +++ b/cpp/application_manager.cpp @@ -13,7 +13,7 @@ namespace discord { void ApplicationManager::ValidateOrExit(std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -47,7 +47,7 @@ void ApplicationManager::GetCurrentBranch(char branch[4096]) void ApplicationManager::GetOAuth2Token(std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, DiscordOAuth2Token* oauth2Token) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordOAuth2Token* oauth2Token) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -62,7 +62,7 @@ void ApplicationManager::GetOAuth2Token(std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result, char const* data) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, char const* data) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/core.cpp b/cpp/core.cpp index 110c9ef..56b2801 100644 --- a/cpp/core.cpp +++ b/cpp/core.cpp @@ -59,7 +59,7 @@ void Core::SetLogHook(LogLevel minLevel, std::function void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordLogLevel level, char const* message) -> void { auto cb(reinterpret_cast(callbackData)); if (!cb) { return; diff --git a/cpp/ffi.h b/cpp/ffi.h index 4a21057..3d2dd9d 100644 --- a/cpp/ffi.h +++ b/cpp/ffi.h @@ -2,7 +2,7 @@ #define _DISCORD_GAME_SDK_H_ #ifdef _WIN32 -#include +#include #include #endif diff --git a/cpp/image_manager.cpp b/cpp/image_manager.cpp index 03b1db4..c90f337 100644 --- a/cpp/image_manager.cpp +++ b/cpp/image_manager.cpp @@ -16,7 +16,7 @@ void ImageManager::Fetch(ImageHandle handle, std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, DiscordImageHandle handleResult) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordImageHandle handleResult) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/lobby_manager.cpp b/cpp/lobby_manager.cpp index 3a95b1a..4000032 100644 --- a/cpp/lobby_manager.cpp +++ b/cpp/lobby_manager.cpp @@ -167,7 +167,7 @@ void LobbyManager::CreateLobby(LobbyTransaction const& transaction, std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -185,7 +185,7 @@ void LobbyManager::UpdateLobby(LobbyId lobbyId, LobbyTransaction const& transaction, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -204,7 +204,7 @@ void LobbyManager::UpdateLobby(LobbyId lobbyId, void LobbyManager::DeleteLobby(LobbyId lobbyId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -222,7 +222,7 @@ void LobbyManager::ConnectLobby(LobbyId lobbyId, std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -240,7 +240,7 @@ void LobbyManager::ConnectLobbyWithActivitySecret( std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordLobby* lobby) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -256,7 +256,7 @@ void LobbyManager::ConnectLobbyWithActivitySecret( void LobbyManager::DisconnectLobby(LobbyId lobbyId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -403,7 +403,7 @@ void LobbyManager::UpdateMember(LobbyId lobbyId, LobbyMemberTransaction const& transaction, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -426,7 +426,7 @@ void LobbyManager::SendLobbyMessage(LobbyId lobbyId, std::uint32_t dataLength, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -452,7 +452,7 @@ Result LobbyManager::GetSearchQuery(LobbySearchQuery* query) void LobbyManager::Search(LobbySearchQuery const& query, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -487,7 +487,7 @@ Result LobbyManager::GetLobbyId(std::int32_t index, LobbyId* lobbyId) void LobbyManager::ConnectVoice(LobbyId lobbyId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -502,7 +502,7 @@ void LobbyManager::ConnectVoice(LobbyId lobbyId, std::function cal void LobbyManager::DisconnectVoice(LobbyId lobbyId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/overlay_manager.cpp b/cpp/overlay_manager.cpp index f4b1fba..d1bffc0 100644 --- a/cpp/overlay_manager.cpp +++ b/cpp/overlay_manager.cpp @@ -49,7 +49,7 @@ void OverlayManager::IsLocked(bool* locked) void OverlayManager::SetLocked(bool locked, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -65,7 +65,7 @@ void OverlayManager::SetLocked(bool locked, std::function callback void OverlayManager::OpenActivityInvite(ActivityActionType type, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -81,7 +81,7 @@ void OverlayManager::OpenActivityInvite(ActivityActionType type, void OverlayManager::OpenGuildInvite(char const* code, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -96,7 +96,7 @@ void OverlayManager::OpenGuildInvite(char const* code, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -182,7 +182,7 @@ void OverlayManager::SetImeCompositionRangeCallback( std::function onImeCompositionRangeChanged) { - static auto wrapper = [](void* callbackData, + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, int32_t from, int32_t to, DiscordRect* bounds, @@ -205,7 +205,7 @@ void OverlayManager::SetImeSelectionBoundsCallback( std::function onImeSelectionBoundsChanged) { static auto wrapper = - [](void* callbackData, DiscordRect anchor, DiscordRect focus, bool isAnchorFirst) -> void { + [] DISCORD_CALLBACK (void* callbackData, DiscordRect anchor, DiscordRect focus, bool isAnchorFirst) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/relationship_manager.cpp b/cpp/relationship_manager.cpp index dce874e..a427996 100644 --- a/cpp/relationship_manager.cpp +++ b/cpp/relationship_manager.cpp @@ -44,7 +44,7 @@ IDiscordRelationshipEvents RelationshipManager::events_{ void RelationshipManager::Filter(std::function filter) { - static auto wrapper = [](void* callbackData, DiscordRelationship* relationship) -> bool { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, DiscordRelationship* relationship) -> bool { auto cb(reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { return {}; diff --git a/cpp/storage_manager.cpp b/cpp/storage_manager.cpp index fbf9ca7..f114665 100644 --- a/cpp/storage_manager.cpp +++ b/cpp/storage_manager.cpp @@ -32,7 +32,7 @@ void StorageManager::ReadAsync(char const* name, std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, uint8_t* data, uint32_t dataLength) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, uint8_t* data, uint32_t dataLength) -> void { std::unique_ptr> cb( reinterpret_cast*>( callbackData)); @@ -53,7 +53,7 @@ void StorageManager::ReadAsyncPartial( std::function callback) { static auto wrapper = - [](void* callbackData, EDiscordResult result, uint8_t* data, uint32_t dataLength) -> void { + [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, uint8_t* data, uint32_t dataLength) -> void { std::unique_ptr> cb( reinterpret_cast*>( callbackData)); @@ -80,7 +80,7 @@ void StorageManager::WriteAsync(char const* name, std::uint32_t dataLength, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/store_manager.cpp b/cpp/store_manager.cpp index 40c7e65..dd13cf9 100644 --- a/cpp/store_manager.cpp +++ b/cpp/store_manager.cpp @@ -45,7 +45,7 @@ IDiscordStoreEvents StoreManager::events_{ void StoreManager::FetchSkus(std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -89,7 +89,7 @@ Result StoreManager::GetSkuAt(std::int32_t index, Sku* sku) void StoreManager::FetchEntitlements(std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { @@ -146,7 +146,7 @@ Result StoreManager::HasSkuEntitlement(Snowflake skuId, bool* hasEntitlement) void StoreManager::StartPurchase(Snowflake skuId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/types.h b/cpp/types.h index 76c4311..4e78f54 100644 --- a/cpp/types.h +++ b/cpp/types.h @@ -1,9 +1,11 @@ #pragma once +#include + #include "ffi.h" #include "event.h" #ifdef _WIN32 -#include +#include #include #endif diff --git a/cpp/user_manager.cpp b/cpp/user_manager.cpp index ddb6d5c..8617ec9 100644 --- a/cpp/user_manager.cpp +++ b/cpp/user_manager.cpp @@ -42,7 +42,7 @@ Result UserManager::GetCurrentUser(User* currentUser) void UserManager::GetUser(UserId userId, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result, DiscordUser* user) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result, DiscordUser* user) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/cpp/voice_manager.cpp b/cpp/voice_manager.cpp index 014ceb3..69d4904 100644 --- a/cpp/voice_manager.cpp +++ b/cpp/voice_manager.cpp @@ -42,7 +42,7 @@ Result VoiceManager::GetInputMode(InputMode* inputMode) void VoiceManager::SetInputMode(InputMode inputMode, std::function callback) { - static auto wrapper = [](void* callbackData, EDiscordResult result) -> void { + static auto wrapper = [] DISCORD_CALLBACK (void* callbackData, EDiscordResult result) -> void { std::unique_ptr> cb( reinterpret_cast*>(callbackData)); if (!cb || !(*cb)) { diff --git a/examples/c/main.c b/examples/c/main.c index 197c26d..45d8811 100644 --- a/examples/c/main.c +++ b/examples/c/main.c @@ -3,7 +3,7 @@ #include #include "discord_game_sdk.h" #ifdef _WIN32 -#include +#include #else #include #include -- 2.37.2 ================================================ FILE: 3rdParty/find_steam_game/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(find_steam_game URL https://github.com/cxong/find_steam_game/archive/a2bd6273fc002214052c2ee3bd48d7c1e7d3f366.tar.gz URL_HASH MD5=a6950ce5d9ced8a259752bc2dc7f5311 ) FetchContent_MakeAvailable_ExcludeFromAll(find_steam_game) add_library(find_steam_game INTERFACE) target_include_directories(find_steam_game INTERFACE ${find_steam_game_SOURCE_DIR}) ================================================ FILE: 3rdParty/googletest/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) FetchContent_Declare_ExcludeFromAll( googletest URL https://github.com/google/googletest/releases/download/v1.15.2/googletest-1.15.2.tar.gz URL_HASH MD5=7e11f6cfcf6498324ac82d567dcb891e ) set(INSTALL_GTEST OFF) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable_ExcludeFromAll(googletest) ================================================ FILE: 3rdParty/libfmt/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) if(NOT WIN32 AND NOT APPLE AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL FreeBSD) # Enable POSIX extensions such as `readlink` and `ftruncate`. add_definitions(-D_POSIX_C_SOURCE=200809L) endif() # Disable fmt/os.h functionality. # We do not use it and it is not supported on some systems. set(FMT_OS OFF) if(DEVILUTIONX_STATIC_LIBFMT) set(BUILD_SHARED_LIBS OFF) else() set(BUILD_SHARED_LIBS ON) endif() include(FetchContent) FetchContent_Declare_ExcludeFromAll(libfmt URL https://github.com/fmtlib/fmt/releases/download/12.0.0/fmt-12.0.0.zip URL_HASH SHA256=1c32293203449792bf8e94c7f6699c643887e826f2d66a80869b4f279fb07d25 ) FetchContent_MakeAvailable_ExcludeFromAll(libfmt) # We do not use locale-specific features of libfmt and disabling them reduces the size. target_compile_definitions(fmt PUBLIC FMT_USE_LOCALE=0) if(DEVILUTIONX_WINDOWS_NO_WCHAR) target_compile_definitions(fmt PUBLIC FMT_USE_WRITE_CONSOLE) endif() # Reduces the overall binary size by 8 KiB. if(TARGET_PLATFORM STREQUAL "rg99") target_compile_definitions(fmt PUBLIC FMT_BUILTIN_TYPES=0) endif() # https://github.com/fmtlib/fmt/issues/4189 if(NINTENDO_3DS OR NINTENDO_SWITCH OR VITA) target_compile_definitions(fmt PUBLIC FMT_USE_FALLBACK_FILE=1) endif() ================================================ FILE: 3rdParty/libmpq/CMakeLists.txt ================================================ if(NOT TARGET ZLIB::ZLIB) find_package(ZLIB REQUIRED) endif() if(NOT TARGET BZip2::BZip2) find_package(BZip2 REQUIRED) endif() include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(libmpq URL https://github.com/diasurgical/libmpq/archive/7c2924d4553513eba1a70bbdb558198dd8c2726a.tar.gz URL_HASH MD5=315c88c02b45851cdfee8460322de044 ) FetchContent_MakeAvailable_ExcludeFromAll(libmpq) add_library(libmpq STATIC ${libmpq_SOURCE_DIR}/libmpq/common.c ${libmpq_SOURCE_DIR}/libmpq/explode.c ${libmpq_SOURCE_DIR}/libmpq/extract.c ${libmpq_SOURCE_DIR}/libmpq/huffman.c ${libmpq_SOURCE_DIR}/libmpq/mpq.c ${libmpq_SOURCE_DIR}/libmpq/wave.c ) target_include_directories(libmpq PUBLIC ${libmpq_SOURCE_DIR}) target_include_directories(libmpq PRIVATE ${CMAKE_CURRENT_LIST_DIR}) target_link_libraries(libmpq PRIVATE ZLIB::ZLIB BZip2::BZip2) if(LIBMPQ_FILE_BUFFER_SIZE) target_compile_definitions(libmpq PRIVATE "LIBMPQ_FILE_BUFFER_SIZE=${LIBMPQ_FILE_BUFFER_SIZE}") endif() if(DEVILUTIONX_WINDOWS_NO_WCHAR) target_compile_definitions(libmpq PRIVATE LIBMPQ_WINDOWS_NO_WCHAR) endif() ================================================ FILE: 3rdParty/libmpq/config.h ================================================ #define VERSION "0.4.2" ================================================ FILE: 3rdParty/libpng/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) # Workaround for deprecation of older CMake versions set(CMAKE_POLICY_VERSION_MINIMUM 3.22) if(NOT DISABLE_LTO) # Force CMake to raise an error if INTERPROCEDURAL_OPTIMIZATION # is enabled and compiler does not support IPO set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) endif() if(DEVILUTIONX_STATIC_LIBPNG) set(PNG_LIBRARY png_static) set(PNG_STATIC ON) set(PNG_SHARED OFF) else() set(PNG_LIBRARY png) set(PNG_STATIC OFF) set(PNG_SHARED ON) endif() set(PNG_TESTS OFF) set(PNG_EXECUTABLES OFF) set(SKIP_INSTALL_ALL ON) if(APPLE AND "${CMAKE_OSX_ARCHITECTURES}" STREQUAL "arm64") set(PNG_ARM_NEON "on" CACHE STRING "" FORCE) endif() include(FetchContent) FetchContent_Declare_ExcludeFromAll(libpng URL https://github.com/glennrp/libpng/archive/0a158f3506502dfa23edfc42790dfaed82efba17.tar.gz URL_HASH MD5=6d705417242732e8e081bff752c98c18 ) FetchContent_MakeAvailable_ExcludeFromAll(libpng) target_include_directories(${PNG_LIBRARY} INTERFACE $ $) add_library(PNG::PNG ALIAS ${PNG_LIBRARY}) ================================================ FILE: 3rdParty/libsmackerdec/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(libsmackerdec URL https://github.com/diasurgical/libsmackerdec/archive/0aaaf8c94a097b009d844db0d44dd7cd0ff81922.tar.gz URL_HASH SHA256=c3a7fbc91531eee8989b1d281415adc0209d84ffc0437c6b0b4f677ae7fd3b41 ) FetchContent_MakeAvailable_ExcludeFromAll(libsmackerdec) add_library(libsmackerdec STATIC ${libsmackerdec_SOURCE_DIR}/src/BitReader.cpp ${libsmackerdec_SOURCE_DIR}/src/FileStream.cpp ${libsmackerdec_SOURCE_DIR}/src/LogError.cpp ${libsmackerdec_SOURCE_DIR}/src/SmackerDecoder.cpp) target_include_directories(libsmackerdec PUBLIC ${libsmackerdec_SOURCE_DIR}/include) if(USE_SDL1) target_link_libraries(libsmackerdec PUBLIC ${SDL_LIBRARY}) elseif(USE_SDL3) target_link_libraries(libsmackerdec PUBLIC SDL3::SDL3) target_compile_definitions(libsmackerdec PUBLIC USE_SDL3) elseif(TARGET SDL2::SDL2 AND NOT (DEVILUTIONX_STATIC_SDL2 AND TARGET SDL2::SDL2-static)) target_link_libraries(libsmackerdec PUBLIC SDL2::SDL2) elseif(TARGET SDL2::SDL2-static) target_link_libraries(libsmackerdec PUBLIC SDL2::SDL2-static) endif() ================================================ FILE: 3rdParty/libsodium/CMakeLists.txt ================================================ if(NOT DEVILUTIONX_SYSTEM_LIBSODIUM) include(functions/FetchContent_ExcludeFromAll_backport) set(SODIUM_MINIMAL ON) set(SODIUM_ENABLE_BLOCKING_RANDOM OFF) set(SODIUM_DISABLE_TESTS ON) if(DEVILUTIONX_STATIC_LIBSODIUM) set(BUILD_SHARED_LIBS OFF) else() set(BUILD_SHARED_LIBS ON) endif() include(FetchContent) FetchContent_Declare_ExcludeFromAll(libsodium GIT_REPOSITORY https://github.com/robinlinden/libsodium-cmake.git GIT_TAG a8ac4509b22b84d6c2eb7d7448f08678e4a67da6 ) FetchContent_MakeAvailable_ExcludeFromAll(libsodium) endif() ================================================ FILE: 3rdParty/libzt/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) set(BUILD_HOST_SELFTEST OFF) include(FetchContent) FetchContent_Declare_ExcludeFromAll(libzt GIT_REPOSITORY https://github.com/diasurgical/libzt.git GIT_TAG 1a9d83b8c4c2bdcd7ea6d8ab1dd2771b16eb4e13) FetchContent_MakeAvailable_ExcludeFromAll(libzt) if(NOT ANDROID) set(libzt_LIB_NAME zt-static) else() set(libzt_LIB_NAME zt-shared) endif() foreach( lib_name zto_obj libnatpmp_obj libzt_obj lwip_obj miniupnpc_obj zto_pic zt_pic natpmp_pic lwip_pic miniupnpc_pic ztcore ${libzt_LIB_NAME} ) if(TARGET ${lib_name}) # External library, ignore all warnings target_compile_options(${lib_name} PRIVATE -w) endif() endforeach(lib_name) target_include_directories(${libzt_LIB_NAME} INTERFACE "${libzt_SOURCE_DIR}/include" "${libzt_SOURCE_DIR}/src" "${libzt_SOURCE_DIR}/ext/lwip/src/include") if(WIN32) target_include_directories(${libzt_LIB_NAME} INTERFACE "${libzt_SOURCE_DIR}/ext/lwip-contrib/ports/win32/include") else() target_include_directories(${libzt_LIB_NAME} INTERFACE "${libzt_SOURCE_DIR}/ext/lwip-contrib/ports/unix/port/include") endif() if(MINGW_CROSS) include(zt_defs REQUIRED) endif() if(MSVC) target_compile_definitions(libnatpmp_obj PRIVATE -DSTATICLIB) endif() ================================================ FILE: 3rdParty/magic_enum/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) FetchContent_Declare_ExcludeFromAll(magic_enum URL https://github.com/Neargye/magic_enum/archive/refs/tags/v0.9.7.tar.gz URL_HASH MD5=5afd218c48c3f7cf094889a182842a50 ) FetchContent_MakeAvailable_ExcludeFromAll(magic_enum) ================================================ FILE: 3rdParty/sol2/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) set(SOL2_ENABLE_INSTALL OFF) set(SOL2_BUILD_LUA OFF) set(SOL2_LUA_VERSION "${LUA_VERSION_STRING}") FetchContent_Declare_ExcludeFromAll(sol2 # branch: devilutionx # This is upstream c1f95a773c6f8f4fde8ca3efe872e7286afe4444 with the following PRs merged on top: # https://github.com/ThePhD/sol2/pull/1712 # https://github.com/ThePhD/sol2/pull/1722 # https://github.com/ThePhD/sol2/pull/1676 # https://github.com/ThePhD/sol2/pull/1704 # https://github.com/ThePhD/sol2/pull/1705 # https://github.com/ThePhD/sol2/pull/1716 # https://github.com/ThePhD/sol2/pull/1674 # https://github.com/ThePhD/sol2/pull/1722 URL https://github.com/diasurgical/sol2/archive/832ac772c2cd3d9620d447e9e77897f7b5e806e3.tar.gz URL_HASH MD5=06c9c0f3242ee803f50e8fd210fdfe4e ) FetchContent_MakeAvailable_ExcludeFromAll(sol2) target_include_directories(sol2 SYSTEM BEFORE INTERFACE ${CMAKE_CURRENT_LIST_DIR}/sol_config) target_compile_definitions(sol2 INTERFACE SOL_NO_EXCEPTIONS=1) ================================================ FILE: 3rdParty/sol2/sol_config/sol/config.hpp ================================================ #pragma once #define SOL_SAFE_USERTYPE 1 #define SOL_SAFE_REFERENCES 1 #define SOL_SAFE_FUNCTION_CALLS 1 #define SOL_SAFE_FUNCTION 1 #define SOL_SAFE_NUMERICS 1 #define SOL_IN_DEBUG_DETECTED 0 ================================================ FILE: 3rdParty/sol2/sol_config/sol/debug.hpp ================================================ #pragma once // sol2 uses std::cout for debug logging by default. // We want to use SDL logging instead for better compatibility. #include #include #include namespace devilutionx { void Sol2DebugPrintStack(lua_State *L); void Sol2DebugPrintSection(const std::string &message, lua_State *L); } // namespace devilutionx namespace sol::detail::debug { inline std::string dump_types(lua_State *L) { std::string visual; std::size_t size = lua_gettop(L) + 1; for (std::size_t i = 1; i < size; ++i) { if (i != 1) { visual += " | "; } visual += type_name(L, stack::get(L, static_cast(i))); } return visual; } inline void print_stack(lua_State *L) { ::devilutionx::Sol2DebugPrintStack(L); } inline void print_section(const std::string &message, lua_State *L) { ::devilutionx::Sol2DebugPrintSection(message, L); } } // namespace sol::detail::debug ================================================ FILE: 3rdParty/tl/CMakeLists.txt ================================================ add_library(tl INTERFACE) target_include_directories(tl INTERFACE ${CMAKE_CURRENT_LIST_DIR}) ================================================ FILE: 3rdParty/tl/expected.hpp ================================================ /// // expected - An implementation of std::expected with extensions // Written in 2017 by Sy Brand (tartanllama@gmail.com, @TartanLlama) // // Documentation available at http://tl.tartanllama.xyz/ // // To the extent possible under law, the author(s) have dedicated all // copyright and related and neighboring rights to this software to the // public domain worldwide. This software is distributed without any warranty. // // You should have received a copy of the CC0 Public Domain Dedication // along with this software. If not, see // . /// #ifndef TL_EXPECTED_HPP #define TL_EXPECTED_HPP #define TL_EXPECTED_VERSION_MAJOR 1 #define TL_EXPECTED_VERSION_MINOR 1 #define TL_EXPECTED_VERSION_PATCH 0 #include #include #include #include #if defined(__EXCEPTIONS) || defined(_CPPUNWIND) #define TL_EXPECTED_EXCEPTIONS_ENABLED #endif #if (defined(_MSC_VER) && _MSC_VER == 1900) #define TL_EXPECTED_MSVC2015 #define TL_EXPECTED_MSVC2015_CONSTEXPR #else #define TL_EXPECTED_MSVC2015_CONSTEXPR constexpr #endif #if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ !defined(__clang__)) #define TL_EXPECTED_GCC49 #endif #if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \ !defined(__clang__)) #define TL_EXPECTED_GCC54 #endif #if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 5 && \ !defined(__clang__)) #define TL_EXPECTED_GCC55 #endif #if !defined(TL_ASSERT) // can't have assert in constexpr in C++11 and GCC 4.9 has a compiler bug #if (__cplusplus > 201103L) && !defined(TL_EXPECTED_GCC49) #include #define TL_ASSERT(x) assert(x) #else #define TL_ASSERT(x) #endif #endif #if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ !defined(__clang__)) // GCC < 5 doesn't support overloading on const&& for member functions #define TL_EXPECTED_NO_CONSTRR // GCC < 5 doesn't support some standard C++11 type traits #define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ std::has_trivial_copy_constructor #define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ std::has_trivial_copy_assign // This one will be different for GCC 5.7 if it's ever supported #define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \ std::is_trivially_destructible // GCC 5 < v < 8 has a bug in is_trivially_copy_constructible which breaks // std::vector for non-copyable types #elif (defined(__GNUC__) && __GNUC__ < 8 && !defined(__clang__)) #ifndef TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX #define TL_GCC_LESS_8_TRIVIALLY_COPY_CONSTRUCTIBLE_MUTEX namespace tl { namespace detail { template struct is_trivially_copy_constructible : std::is_trivially_copy_constructible {}; #ifdef _GLIBCXX_VECTOR template struct is_trivially_copy_constructible> : std::false_type {}; #endif } // namespace detail } // namespace tl #endif #define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ tl::detail::is_trivially_copy_constructible #define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ std::is_trivially_copy_assignable #define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \ std::is_trivially_destructible #else #define TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T) \ std::is_trivially_copy_constructible #define TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T) \ std::is_trivially_copy_assignable #define TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T) \ std::is_trivially_destructible #endif #if __cplusplus > 201103L #define TL_EXPECTED_CXX14 #endif #ifdef TL_EXPECTED_GCC49 #define TL_EXPECTED_GCC49_CONSTEXPR #else #define TL_EXPECTED_GCC49_CONSTEXPR constexpr #endif #if (__cplusplus == 201103L || defined(TL_EXPECTED_MSVC2015) || \ defined(TL_EXPECTED_GCC49)) #define TL_EXPECTED_11_CONSTEXPR #else #define TL_EXPECTED_11_CONSTEXPR constexpr #endif namespace tl { template class expected; #ifndef TL_MONOSTATE_INPLACE_MUTEX #define TL_MONOSTATE_INPLACE_MUTEX class monostate {}; struct in_place_t { explicit in_place_t() = default; }; static constexpr in_place_t in_place{}; #endif template class unexpected { public: static_assert(!std::is_same::value, "E must not be void"); unexpected() = delete; constexpr explicit unexpected(const E &e) : m_val(e) {} constexpr explicit unexpected(E &&e) : m_val(std::move(e)) {} template ::value>::type * = nullptr> constexpr explicit unexpected(Args &&...args) : m_val(std::forward(args)...) {} template < class U, class... Args, typename std::enable_if &, Args &&...>::value>::type * = nullptr> constexpr explicit unexpected(std::initializer_list l, Args &&...args) : m_val(l, std::forward(args)...) {} constexpr const E &value() const & { return m_val; } TL_EXPECTED_11_CONSTEXPR E &value() & { return m_val; } TL_EXPECTED_11_CONSTEXPR E &&value() && { return std::move(m_val); } constexpr const E &&value() const && { return std::move(m_val); } private: E m_val; }; #ifdef __cpp_deduction_guides template unexpected(E) -> unexpected; #endif template constexpr bool operator==(const unexpected &lhs, const unexpected &rhs) { return lhs.value() == rhs.value(); } template constexpr bool operator!=(const unexpected &lhs, const unexpected &rhs) { return lhs.value() != rhs.value(); } template constexpr bool operator<(const unexpected &lhs, const unexpected &rhs) { return lhs.value() < rhs.value(); } template constexpr bool operator<=(const unexpected &lhs, const unexpected &rhs) { return lhs.value() <= rhs.value(); } template constexpr bool operator>(const unexpected &lhs, const unexpected &rhs) { return lhs.value() > rhs.value(); } template constexpr bool operator>=(const unexpected &lhs, const unexpected &rhs) { return lhs.value() >= rhs.value(); } template unexpected::type> make_unexpected(E &&e) { return unexpected::type>(std::forward(e)); } struct unexpect_t { unexpect_t() = default; }; static constexpr unexpect_t unexpect{}; namespace detail { template [[noreturn]] TL_EXPECTED_11_CONSTEXPR void throw_exception(E &&e) { #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED throw std::forward(e); #else (void)e; #ifdef _MSC_VER __assume(0); #else __builtin_unreachable(); #endif #endif } #ifndef TL_TRAITS_MUTEX #define TL_TRAITS_MUTEX // C++14-style aliases for brevity template using remove_const_t = typename std::remove_const::type; template using remove_reference_t = typename std::remove_reference::type; template using decay_t = typename std::decay::type; template using enable_if_t = typename std::enable_if::type; template using conditional_t = typename std::conditional::type; // std::conjunction from C++17 template struct conjunction : std::true_type {}; template struct conjunction : B {}; template struct conjunction : std::conditional, B>::type {}; #if defined(_LIBCPP_VERSION) && __cplusplus == 201103L #define TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND #endif // In C++11 mode, there's an issue in libc++'s std::mem_fn // which results in a hard-error when using it in a noexcept expression // in some cases. This is a check to workaround the common failing case. #ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND template struct is_pointer_to_non_const_member_func : std::false_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_pointer_to_non_const_member_func : std::true_type {}; template struct is_const_or_const_ref : std::false_type {}; template struct is_const_or_const_ref : std::true_type {}; template struct is_const_or_const_ref : std::true_type {}; #endif // std::invoke from C++17 // https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround template < typename Fn, typename... Args, #ifdef TL_TRAITS_LIBCXX_MEM_FN_WORKAROUND typename = enable_if_t::value && is_const_or_const_ref::value)>, #endif typename = enable_if_t>::value>, int = 0> constexpr auto invoke(Fn &&f, Args &&...args) noexcept( noexcept(std::mem_fn(f)(std::forward(args)...))) -> decltype(std::mem_fn(f)(std::forward(args)...)) { return std::mem_fn(f)(std::forward(args)...); } template >::value>> constexpr auto invoke(Fn &&f, Args &&...args) noexcept( noexcept(std::forward(f)(std::forward(args)...))) -> decltype(std::forward(f)(std::forward(args)...)) { return std::forward(f)(std::forward(args)...); } // std::invoke_result from C++17 template struct invoke_result_impl; template struct invoke_result_impl< F, decltype(detail::invoke(std::declval(), std::declval()...), void()), Us...> { using type = decltype(detail::invoke(std::declval(), std::declval()...)); }; template using invoke_result = invoke_result_impl; template using invoke_result_t = typename invoke_result::type; #if defined(_MSC_VER) && _MSC_VER <= 1900 // TODO make a version which works with MSVC 2015 template struct is_swappable : std::true_type {}; template struct is_nothrow_swappable : std::true_type {}; #else // https://stackoverflow.com/questions/26744589/what-is-a-proper-way-to-implement-is-swappable-to-test-for-the-swappable-concept namespace swap_adl_tests { // if swap ADL finds this then it would call std::swap otherwise (same // signature) struct tag {}; template tag swap(T &, T &); template tag swap(T (&a)[N], T (&b)[N]); // helper functions to test if an unqualified swap is possible, and if it // becomes std::swap template std::false_type can_swap(...) noexcept(false); template (), std::declval()))> std::true_type can_swap(int) noexcept(noexcept(swap(std::declval(), std::declval()))); template std::false_type uses_std(...); template std::is_same(), std::declval())), tag> uses_std(int); template struct is_std_swap_noexcept : std::integral_constant::value && std::is_nothrow_move_assignable::value> {}; template struct is_std_swap_noexcept : is_std_swap_noexcept {}; template struct is_adl_swap_noexcept : std::integral_constant(0))> {}; } // namespace swap_adl_tests template struct is_swappable : std::integral_constant< bool, decltype(detail::swap_adl_tests::can_swap(0))::value && (!decltype(detail::swap_adl_tests::uses_std(0))::value || (std::is_move_assignable::value && std::is_move_constructible::value))> {}; template struct is_swappable : std::integral_constant< bool, decltype(detail::swap_adl_tests::can_swap(0))::value && (!decltype(detail::swap_adl_tests::uses_std( 0))::value || is_swappable::value)> {}; template struct is_nothrow_swappable : std::integral_constant< bool, is_swappable::value && ((decltype(detail::swap_adl_tests::uses_std(0))::value && detail::swap_adl_tests::is_std_swap_noexcept::value) || (!decltype(detail::swap_adl_tests::uses_std(0))::value && detail::swap_adl_tests::is_adl_swap_noexcept::value))> {}; #endif #endif // Trait for checking if a type is a tl::expected template struct is_expected_impl : std::false_type {}; template struct is_expected_impl> : std::true_type {}; template using is_expected = is_expected_impl>; template using expected_enable_forward_value = detail::enable_if_t< std::is_constructible::value && !std::is_same, in_place_t>::value && !std::is_same, detail::decay_t>::value && !std::is_same, detail::decay_t>::value>; template using expected_enable_from_other = detail::enable_if_t< std::is_constructible::value && std::is_constructible::value && !std::is_constructible &>::value && !std::is_constructible &&>::value && !std::is_constructible &>::value && !std::is_constructible &&>::value && !std::is_convertible &, T>::value && !std::is_convertible &&, T>::value && !std::is_convertible &, T>::value && !std::is_convertible &&, T>::value>; template using is_void_or = conditional_t::value, std::true_type, U>; template using is_copy_constructible_or_void = is_void_or>; template using is_move_constructible_or_void = is_void_or>; template using is_copy_assignable_or_void = is_void_or>; template using is_move_assignable_or_void = is_void_or>; } // namespace detail namespace detail { struct no_init_t {}; static constexpr no_init_t no_init{}; // Implements the storage of the values, and ensures that the destructor is // trivial if it can be. // // This specialization is for where neither `T` or `E` is trivially // destructible, so the destructors must be called on destruction of the // `expected` template ::value, bool = std::is_trivially_destructible::value> struct expected_storage_base { constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} template ::value> * = nullptr> constexpr expected_storage_base(in_place_t, Args &&...args) : m_val(std::forward(args)...), m_has_val(true) {} template &, Args &&...>::value> * = nullptr> constexpr expected_storage_base(in_place_t, std::initializer_list il, Args &&...args) : m_val(il, std::forward(args)...), m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() { if (m_has_val) { m_val.~T(); } else { m_unexpect.~unexpected(); } } union { T m_val; unexpected m_unexpect; char m_no_init; }; bool m_has_val; }; // This specialization is for when both `T` and `E` are trivially-destructible, // so the destructor of the `expected` can be trivial. template struct expected_storage_base { constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} template ::value> * = nullptr> constexpr expected_storage_base(in_place_t, Args &&...args) : m_val(std::forward(args)...), m_has_val(true) {} template &, Args &&...>::value> * = nullptr> constexpr expected_storage_base(in_place_t, std::initializer_list il, Args &&...args) : m_val(il, std::forward(args)...), m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() = default; union { T m_val; unexpected m_unexpect; char m_no_init; }; bool m_has_val; }; // T is trivial, E is not. template struct expected_storage_base { constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} TL_EXPECTED_MSVC2015_CONSTEXPR expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} template ::value> * = nullptr> constexpr expected_storage_base(in_place_t, Args &&...args) : m_val(std::forward(args)...), m_has_val(true) {} template &, Args &&...>::value> * = nullptr> constexpr expected_storage_base(in_place_t, std::initializer_list il, Args &&...args) : m_val(il, std::forward(args)...), m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() { if (!m_has_val) { m_unexpect.~unexpected(); } } union { T m_val; unexpected m_unexpect; char m_no_init; }; bool m_has_val; }; // E is trivial, T is not. template struct expected_storage_base { constexpr expected_storage_base() : m_val(T{}), m_has_val(true) {} constexpr expected_storage_base(no_init_t) : m_no_init(), m_has_val(false) {} template ::value> * = nullptr> constexpr expected_storage_base(in_place_t, Args &&...args) : m_val(std::forward(args)...), m_has_val(true) {} template &, Args &&...>::value> * = nullptr> constexpr expected_storage_base(in_place_t, std::initializer_list il, Args &&...args) : m_val(il, std::forward(args)...), m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() { if (m_has_val) { m_val.~T(); } } union { T m_val; unexpected m_unexpect; char m_no_init; }; bool m_has_val; }; // `T` is `void`, `E` is trivially-destructible template struct expected_storage_base { #if __GNUC__ <= 5 // no constexpr for GCC 4/5 bug #else TL_EXPECTED_MSVC2015_CONSTEXPR #endif expected_storage_base() : m_has_val(true) {} constexpr expected_storage_base(no_init_t) : m_val(), m_has_val(false) {} constexpr expected_storage_base(in_place_t) : m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() = default; struct dummy {}; union { unexpected m_unexpect; dummy m_val; }; bool m_has_val; }; // `T` is `void`, `E` is not trivially-destructible template struct expected_storage_base { constexpr expected_storage_base() : m_dummy(), m_has_val(true) {} constexpr expected_storage_base(no_init_t) : m_dummy(), m_has_val(false) {} constexpr expected_storage_base(in_place_t) : m_dummy(), m_has_val(true) {} template ::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, Args &&...args) : m_unexpect(std::forward(args)...), m_has_val(false) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected_storage_base(unexpect_t, std::initializer_list il, Args &&...args) : m_unexpect(il, std::forward(args)...), m_has_val(false) {} ~expected_storage_base() { if (!m_has_val) { m_unexpect.~unexpected(); } } union { unexpected m_unexpect; char m_dummy; }; bool m_has_val; }; // This base class provides some handy member functions which can be used in // further derived classes template struct expected_operations_base : expected_storage_base { using expected_storage_base::expected_storage_base; template void construct(Args &&...args) noexcept { new (std::addressof(this->m_val)) T(std::forward(args)...); this->m_has_val = true; } template void construct_with(Rhs &&rhs) noexcept { new (std::addressof(this->m_val)) T(std::forward(rhs).get()); this->m_has_val = true; } template void construct_error(Args &&...args) noexcept { new (std::addressof(this->m_unexpect)) unexpected(std::forward(args)...); this->m_has_val = false; } #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED // These assign overloads ensure that the most efficient assignment // implementation is used while maintaining the strong exception guarantee. // The problematic case is where rhs has a value, but *this does not. // // This overload handles the case where we can just copy-construct `T` // directly into place without throwing. template ::value> * = nullptr> void assign(const expected_operations_base &rhs) noexcept { if (!this->m_has_val && rhs.m_has_val) { geterr().~unexpected(); construct(rhs.get()); } else { assign_common(rhs); } } // This overload handles the case where we can attempt to create a copy of // `T`, then no-throw move it into place if the copy was successful. template ::value && std::is_nothrow_move_constructible::value> * = nullptr> void assign(const expected_operations_base &rhs) noexcept { if (!this->m_has_val && rhs.m_has_val) { T tmp = rhs.get(); geterr().~unexpected(); construct(std::move(tmp)); } else { assign_common(rhs); } } // This overload is the worst-case, where we have to move-construct the // unexpected value into temporary storage, then try to copy the T into place. // If the construction succeeds, then everything is fine, but if it throws, // then we move the old unexpected value back into place before rethrowing the // exception. template ::value && !std::is_nothrow_move_constructible::value> * = nullptr> void assign(const expected_operations_base &rhs) { if (!this->m_has_val && rhs.m_has_val) { auto tmp = std::move(geterr()); geterr().~unexpected(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { construct(rhs.get()); } catch (...) { geterr() = std::move(tmp); throw; } #else construct(rhs.get()); #endif } else { assign_common(rhs); } } // These overloads do the same as above, but for rvalues template ::value> * = nullptr> void assign(expected_operations_base &&rhs) noexcept { if (!this->m_has_val && rhs.m_has_val) { geterr().~unexpected(); construct(std::move(rhs).get()); } else { assign_common(std::move(rhs)); } } template ::value> * = nullptr> void assign(expected_operations_base &&rhs) { if (!this->m_has_val && rhs.m_has_val) { auto tmp = std::move(geterr()); geterr().~unexpected(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { construct(std::move(rhs).get()); } catch (...) { geterr() = std::move(tmp); throw; } #else construct(std::move(rhs).get()); #endif } else { assign_common(std::move(rhs)); } } #else // If exceptions are disabled then we can just copy-construct void assign(const expected_operations_base &rhs) noexcept { if (!this->m_has_val && rhs.m_has_val) { geterr().~unexpected(); construct(rhs.get()); } else { assign_common(rhs); } } void assign(expected_operations_base &&rhs) noexcept { if (!this->m_has_val && rhs.m_has_val) { geterr().~unexpected(); construct(std::move(rhs).get()); } else { assign_common(std::move(rhs)); } } #endif // The common part of move/copy assigning template void assign_common(Rhs &&rhs) { if (this->m_has_val) { if (rhs.m_has_val) { get() = std::forward(rhs).get(); } else { destroy_val(); construct_error(std::forward(rhs).geterr()); } } else { if (!rhs.m_has_val) { geterr() = std::forward(rhs).geterr(); } } } bool has_value() const { return this->m_has_val; } TL_EXPECTED_11_CONSTEXPR T &get() & { return this->m_val; } constexpr const T &get() const & { return this->m_val; } TL_EXPECTED_11_CONSTEXPR T &&get() && { return std::move(this->m_val); } #ifndef TL_EXPECTED_NO_CONSTRR constexpr const T &&get() const && { return std::move(this->m_val); } #endif TL_EXPECTED_11_CONSTEXPR unexpected &geterr() & { return this->m_unexpect; } constexpr const unexpected &geterr() const & { return this->m_unexpect; } TL_EXPECTED_11_CONSTEXPR unexpected &&geterr() && { return std::move(this->m_unexpect); } #ifndef TL_EXPECTED_NO_CONSTRR constexpr const unexpected &&geterr() const && { return std::move(this->m_unexpect); } #endif TL_EXPECTED_11_CONSTEXPR void destroy_val() { get().~T(); } }; // This base class provides some handy member functions which can be used in // further derived classes template struct expected_operations_base : expected_storage_base { using expected_storage_base::expected_storage_base; template void construct() noexcept { this->m_has_val = true; } // This function doesn't use its argument, but needs it so that code in // levels above this can work independently of whether T is void template void construct_with(Rhs &&) noexcept { this->m_has_val = true; } template void construct_error(Args &&...args) noexcept { new (std::addressof(this->m_unexpect)) unexpected(std::forward(args)...); this->m_has_val = false; } template void assign(Rhs &&rhs) noexcept { if (!this->m_has_val) { if (rhs.m_has_val) { geterr().~unexpected(); construct(); } else { geterr() = std::forward(rhs).geterr(); } } else { if (!rhs.m_has_val) { construct_error(std::forward(rhs).geterr()); } } } bool has_value() const { return this->m_has_val; } TL_EXPECTED_11_CONSTEXPR unexpected &geterr() & { return this->m_unexpect; } constexpr const unexpected &geterr() const & { return this->m_unexpect; } TL_EXPECTED_11_CONSTEXPR unexpected &&geterr() && { return std::move(this->m_unexpect); } #ifndef TL_EXPECTED_NO_CONSTRR constexpr const unexpected &&geterr() const && { return std::move(this->m_unexpect); } #endif TL_EXPECTED_11_CONSTEXPR void destroy_val() { // no-op } }; // This class manages conditionally having a trivial copy constructor // This specialization is for when T and E are trivially copy constructible template :: value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value> struct expected_copy_base : expected_operations_base { using expected_operations_base::expected_operations_base; }; // This specialization is for when T or E are not trivially copy constructible template struct expected_copy_base : expected_operations_base { using expected_operations_base::expected_operations_base; expected_copy_base() = default; expected_copy_base(const expected_copy_base &rhs) : expected_operations_base(no_init) { if (rhs.has_value()) { this->construct_with(rhs); } else { this->construct_error(rhs.geterr()); } } expected_copy_base(expected_copy_base &&rhs) = default; expected_copy_base &operator=(const expected_copy_base &rhs) = default; expected_copy_base &operator=(expected_copy_base &&rhs) = default; }; // This class manages conditionally having a trivial move constructor // Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it // doesn't implement an analogue to std::is_trivially_move_constructible. We // have to make do with a non-trivial move constructor even if T is trivially // move constructible #ifndef TL_EXPECTED_GCC49 template >::value &&std::is_trivially_move_constructible::value> struct expected_move_base : expected_copy_base { using expected_copy_base::expected_copy_base; }; #else template struct expected_move_base; #endif template struct expected_move_base : expected_copy_base { using expected_copy_base::expected_copy_base; expected_move_base() = default; expected_move_base(const expected_move_base &rhs) = default; expected_move_base(expected_move_base &&rhs) noexcept( std::is_nothrow_move_constructible::value) : expected_copy_base(no_init) { if (rhs.has_value()) { this->construct_with(std::move(rhs)); } else { this->construct_error(std::move(rhs.geterr())); } } expected_move_base &operator=(const expected_move_base &rhs) = default; expected_move_base &operator=(expected_move_base &&rhs) = default; }; // This class manages conditionally having a trivial copy assignment operator template >::value &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value> struct expected_copy_assign_base : expected_move_base { using expected_move_base::expected_move_base; }; template struct expected_copy_assign_base : expected_move_base { using expected_move_base::expected_move_base; expected_copy_assign_base() = default; expected_copy_assign_base(const expected_copy_assign_base &rhs) = default; expected_copy_assign_base(expected_copy_assign_base &&rhs) = default; expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) { this->assign(rhs); return *this; } expected_copy_assign_base & operator=(expected_copy_assign_base &&rhs) = default; }; // This class manages conditionally having a trivial move assignment operator // Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it // doesn't implement an analogue to std::is_trivially_move_assignable. We have // to make do with a non-trivial move assignment operator even if T is trivially // move assignable #ifndef TL_EXPECTED_GCC49 template , std::is_trivially_move_constructible, std::is_trivially_move_assignable>>:: value &&std::is_trivially_destructible::value &&std::is_trivially_move_constructible::value &&std::is_trivially_move_assignable::value> struct expected_move_assign_base : expected_copy_assign_base { using expected_copy_assign_base::expected_copy_assign_base; }; #else template struct expected_move_assign_base; #endif template struct expected_move_assign_base : expected_copy_assign_base { using expected_copy_assign_base::expected_copy_assign_base; expected_move_assign_base() = default; expected_move_assign_base(const expected_move_assign_base &rhs) = default; expected_move_assign_base(expected_move_assign_base &&rhs) = default; expected_move_assign_base & operator=(const expected_move_assign_base &rhs) = default; expected_move_assign_base & operator=(expected_move_assign_base &&rhs) noexcept( std::is_nothrow_move_constructible::value &&std::is_nothrow_move_assignable::value) { this->assign(std::move(rhs)); return *this; } }; // expected_delete_ctor_base will conditionally delete copy and move // constructors depending on whether T is copy/move constructible template ::value && std::is_copy_constructible::value), bool EnableMove = (is_move_constructible_or_void::value && std::is_move_constructible::value)> struct expected_delete_ctor_base { expected_delete_ctor_base() = default; expected_delete_ctor_base(const expected_delete_ctor_base &) = default; expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default; expected_delete_ctor_base & operator=(const expected_delete_ctor_base &) = default; expected_delete_ctor_base & operator=(expected_delete_ctor_base &&) noexcept = default; }; template struct expected_delete_ctor_base { expected_delete_ctor_base() = default; expected_delete_ctor_base(const expected_delete_ctor_base &) = default; expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete; expected_delete_ctor_base & operator=(const expected_delete_ctor_base &) = default; expected_delete_ctor_base & operator=(expected_delete_ctor_base &&) noexcept = default; }; template struct expected_delete_ctor_base { expected_delete_ctor_base() = default; expected_delete_ctor_base(const expected_delete_ctor_base &) = delete; expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = default; expected_delete_ctor_base & operator=(const expected_delete_ctor_base &) = default; expected_delete_ctor_base & operator=(expected_delete_ctor_base &&) noexcept = default; }; template struct expected_delete_ctor_base { expected_delete_ctor_base() = default; expected_delete_ctor_base(const expected_delete_ctor_base &) = delete; expected_delete_ctor_base(expected_delete_ctor_base &&) noexcept = delete; expected_delete_ctor_base & operator=(const expected_delete_ctor_base &) = default; expected_delete_ctor_base & operator=(expected_delete_ctor_base &&) noexcept = default; }; // expected_delete_assign_base will conditionally delete copy and move // constructors depending on whether T and E are copy/move constructible + // assignable template ::value && std::is_copy_constructible::value && is_copy_assignable_or_void::value && std::is_copy_assignable::value), bool EnableMove = (is_move_constructible_or_void::value && std::is_move_constructible::value && is_move_assignable_or_void::value && std::is_move_assignable::value)> struct expected_delete_assign_base { expected_delete_assign_base() = default; expected_delete_assign_base(const expected_delete_assign_base &) = default; expected_delete_assign_base(expected_delete_assign_base &&) noexcept = default; expected_delete_assign_base & operator=(const expected_delete_assign_base &) = default; expected_delete_assign_base & operator=(expected_delete_assign_base &&) noexcept = default; }; template struct expected_delete_assign_base { expected_delete_assign_base() = default; expected_delete_assign_base(const expected_delete_assign_base &) = default; expected_delete_assign_base(expected_delete_assign_base &&) noexcept = default; expected_delete_assign_base & operator=(const expected_delete_assign_base &) = default; expected_delete_assign_base & operator=(expected_delete_assign_base &&) noexcept = delete; }; template struct expected_delete_assign_base { expected_delete_assign_base() = default; expected_delete_assign_base(const expected_delete_assign_base &) = default; expected_delete_assign_base(expected_delete_assign_base &&) noexcept = default; expected_delete_assign_base & operator=(const expected_delete_assign_base &) = delete; expected_delete_assign_base & operator=(expected_delete_assign_base &&) noexcept = default; }; template struct expected_delete_assign_base { expected_delete_assign_base() = default; expected_delete_assign_base(const expected_delete_assign_base &) = default; expected_delete_assign_base(expected_delete_assign_base &&) noexcept = default; expected_delete_assign_base & operator=(const expected_delete_assign_base &) = delete; expected_delete_assign_base & operator=(expected_delete_assign_base &&) noexcept = delete; }; // This is needed to be able to construct the expected_default_ctor_base which // follows, while still conditionally deleting the default constructor. struct default_constructor_tag { explicit constexpr default_constructor_tag() = default; }; // expected_default_ctor_base will ensure that expected has a deleted default // consturctor if T is not default constructible. // This specialization is for when T is default constructible template ::value || std::is_void::value> struct expected_default_ctor_base { constexpr expected_default_ctor_base() noexcept = default; constexpr expected_default_ctor_base( expected_default_ctor_base const &) noexcept = default; constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept = default; expected_default_ctor_base & operator=(expected_default_ctor_base const &) noexcept = default; expected_default_ctor_base & operator=(expected_default_ctor_base &&) noexcept = default; constexpr explicit expected_default_ctor_base(default_constructor_tag) {} }; // This specialization is for when T is not default constructible template struct expected_default_ctor_base { constexpr expected_default_ctor_base() noexcept = delete; constexpr expected_default_ctor_base( expected_default_ctor_base const &) noexcept = default; constexpr expected_default_ctor_base(expected_default_ctor_base &&) noexcept = default; expected_default_ctor_base & operator=(expected_default_ctor_base const &) noexcept = default; expected_default_ctor_base & operator=(expected_default_ctor_base &&) noexcept = default; constexpr explicit expected_default_ctor_base(default_constructor_tag) {} }; } // namespace detail template class bad_expected_access : public std::exception { public: explicit bad_expected_access(E e) : m_val(std::move(e)) {} virtual const char *what() const noexcept override { return "Bad expected access"; } const E &error() const & { return m_val; } E &error() & { return m_val; } const E &&error() const && { return std::move(m_val); } E &&error() && { return std::move(m_val); } private: E m_val; }; /// An `expected` object is an object that contains the storage for /// another object and manages the lifetime of this contained object `T`. /// Alternatively it could contain the storage for another unexpected object /// `E`. The contained object may not be initialized after the expected object /// has been initialized, and may not be destroyed before the expected object /// has been destroyed. The initialization state of the contained object is /// tracked by the expected object. template class expected : private detail::expected_move_assign_base, private detail::expected_delete_ctor_base, private detail::expected_delete_assign_base, private detail::expected_default_ctor_base { static_assert(!std::is_reference::value, "T must not be a reference"); static_assert(!std::is_same::type>::value, "T must not be in_place_t"); static_assert(!std::is_same::type>::value, "T must not be unexpect_t"); static_assert( !std::is_same>::type>::value, "T must not be unexpected"); static_assert(!std::is_reference::value, "E must not be a reference"); T *valptr() { return std::addressof(this->m_val); } const T *valptr() const { return std::addressof(this->m_val); } unexpected *errptr() { return std::addressof(this->m_unexpect); } const unexpected *errptr() const { return std::addressof(this->m_unexpect); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR U &val() { return this->m_val; } TL_EXPECTED_11_CONSTEXPR unexpected &err() { return this->m_unexpect; } template ::value> * = nullptr> constexpr const U &val() const { return this->m_val; } constexpr const unexpected &err() const { return this->m_unexpect; } using impl_base = detail::expected_move_assign_base; using ctor_base = detail::expected_default_ctor_base; public: typedef T value_type; typedef E error_type; typedef unexpected unexpected_type; #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & { return and_then_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && { return and_then_impl(std::move(*this), std::forward(f)); } template constexpr auto and_then(F &&f) const & { return and_then_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr auto and_then(F &&f) const && { return and_then_impl(std::move(*this), std::forward(f)); } #endif #else template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) & -> decltype(and_then_impl(std::declval(), std::forward(f))) { return and_then_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto and_then(F &&f) && -> decltype(and_then_impl(std::declval(), std::forward(f))) { return and_then_impl(std::move(*this), std::forward(f)); } template constexpr auto and_then(F &&f) const & -> decltype(and_then_impl( std::declval(), std::forward(f))) { return and_then_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr auto and_then(F &&f) const && -> decltype(and_then_impl( std::declval(), std::forward(f))) { return and_then_impl(std::move(*this), std::forward(f)); } #endif #endif #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template TL_EXPECTED_11_CONSTEXPR auto map(F &&f) & { return expected_map_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto map(F &&f) && { return expected_map_impl(std::move(*this), std::forward(f)); } template constexpr auto map(F &&f) const & { return expected_map_impl(*this, std::forward(f)); } template constexpr auto map(F &&f) const && { return expected_map_impl(std::move(*this), std::forward(f)); } #else template TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl( std::declval(), std::declval())) map(F &&f) & { return expected_map_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(std::declval(), std::declval())) map(F &&f) && { return expected_map_impl(std::move(*this), std::forward(f)); } template constexpr decltype(expected_map_impl(std::declval(), std::declval())) map(F &&f) const & { return expected_map_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr decltype(expected_map_impl(std::declval(), std::declval())) map(F &&f) const && { return expected_map_impl(std::move(*this), std::forward(f)); } #endif #endif #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) & { return expected_map_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto transform(F &&f) && { return expected_map_impl(std::move(*this), std::forward(f)); } template constexpr auto transform(F &&f) const & { return expected_map_impl(*this, std::forward(f)); } template constexpr auto transform(F &&f) const && { return expected_map_impl(std::move(*this), std::forward(f)); } #else template TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl( std::declval(), std::declval())) transform(F &&f) & { return expected_map_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR decltype(expected_map_impl(std::declval(), std::declval())) transform(F &&f) && { return expected_map_impl(std::move(*this), std::forward(f)); } template constexpr decltype(expected_map_impl(std::declval(), std::declval())) transform(F &&f) const & { return expected_map_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr decltype(expected_map_impl(std::declval(), std::declval())) transform(F &&f) const && { return expected_map_impl(std::move(*this), std::forward(f)); } #endif #endif #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) & { return map_error_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto map_error(F &&f) && { return map_error_impl(std::move(*this), std::forward(f)); } template constexpr auto map_error(F &&f) const & { return map_error_impl(*this, std::forward(f)); } template constexpr auto map_error(F &&f) const && { return map_error_impl(std::move(*this), std::forward(f)); } #else template TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), std::declval())) map_error(F &&f) & { return map_error_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), std::declval())) map_error(F &&f) && { return map_error_impl(std::move(*this), std::forward(f)); } template constexpr decltype(map_error_impl(std::declval(), std::declval())) map_error(F &&f) const & { return map_error_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr decltype(map_error_impl(std::declval(), std::declval())) map_error(F &&f) const && { return map_error_impl(std::move(*this), std::forward(f)); } #endif #endif #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template TL_EXPECTED_11_CONSTEXPR auto transform_error(F &&f) & { return map_error_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR auto transform_error(F &&f) && { return map_error_impl(std::move(*this), std::forward(f)); } template constexpr auto transform_error(F &&f) const & { return map_error_impl(*this, std::forward(f)); } template constexpr auto transform_error(F &&f) const && { return map_error_impl(std::move(*this), std::forward(f)); } #else template TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), std::declval())) transform_error(F &&f) & { return map_error_impl(*this, std::forward(f)); } template TL_EXPECTED_11_CONSTEXPR decltype(map_error_impl(std::declval(), std::declval())) transform_error(F &&f) && { return map_error_impl(std::move(*this), std::forward(f)); } template constexpr decltype(map_error_impl(std::declval(), std::declval())) transform_error(F &&f) const & { return map_error_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template constexpr decltype(map_error_impl(std::declval(), std::declval())) transform_error(F &&f) const && { return map_error_impl(std::move(*this), std::forward(f)); } #endif #endif template expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) & { return or_else_impl(*this, std::forward(f)); } template expected TL_EXPECTED_11_CONSTEXPR or_else(F &&f) && { return or_else_impl(std::move(*this), std::forward(f)); } template expected constexpr or_else(F &&f) const & { return or_else_impl(*this, std::forward(f)); } #ifndef TL_EXPECTED_NO_CONSTRR template expected constexpr or_else(F &&f) const && { return or_else_impl(std::move(*this), std::forward(f)); } #endif constexpr expected() = default; constexpr expected(const expected &rhs) = default; constexpr expected(expected &&rhs) = default; expected &operator=(const expected &rhs) = default; expected &operator=(expected &&rhs) = default; template ::value> * = nullptr> constexpr expected(in_place_t, Args &&...args) : impl_base(in_place, std::forward(args)...), ctor_base(detail::default_constructor_tag{}) {} template &, Args &&...>::value> * = nullptr> constexpr expected(in_place_t, std::initializer_list il, Args &&...args) : impl_base(in_place, il, std::forward(args)...), ctor_base(detail::default_constructor_tag{}) {} template ::value> * = nullptr, detail::enable_if_t::value> * = nullptr> explicit constexpr expected(const unexpected &e) : impl_base(unexpect, e.value()), ctor_base(detail::default_constructor_tag{}) {} template < class G = E, detail::enable_if_t::value> * = nullptr, detail::enable_if_t::value> * = nullptr> constexpr expected(unexpected const &e) : impl_base(unexpect, e.value()), ctor_base(detail::default_constructor_tag{}) {} template < class G = E, detail::enable_if_t::value> * = nullptr, detail::enable_if_t::value> * = nullptr> explicit constexpr expected(unexpected &&e) noexcept( std::is_nothrow_constructible::value) : impl_base(unexpect, std::move(e.value())), ctor_base(detail::default_constructor_tag{}) {} template < class G = E, detail::enable_if_t::value> * = nullptr, detail::enable_if_t::value> * = nullptr> constexpr expected(unexpected &&e) noexcept( std::is_nothrow_constructible::value) : impl_base(unexpect, std::move(e.value())), ctor_base(detail::default_constructor_tag{}) {} template ::value> * = nullptr> constexpr explicit expected(unexpect_t, Args &&...args) : impl_base(unexpect, std::forward(args)...), ctor_base(detail::default_constructor_tag{}) {} template &, Args &&...>::value> * = nullptr> constexpr explicit expected(unexpect_t, std::initializer_list il, Args &&...args) : impl_base(unexpect, il, std::forward(args)...), ctor_base(detail::default_constructor_tag{}) {} template ::value && std::is_convertible::value)> * = nullptr, detail::expected_enable_from_other * = nullptr> explicit TL_EXPECTED_11_CONSTEXPR expected(const expected &rhs) : ctor_base(detail::default_constructor_tag{}) { if (rhs.has_value()) { this->construct(*rhs); } else { this->construct_error(rhs.error()); } } template ::value && std::is_convertible::value)> * = nullptr, detail::expected_enable_from_other * = nullptr> TL_EXPECTED_11_CONSTEXPR expected(const expected &rhs) : ctor_base(detail::default_constructor_tag{}) { if (rhs.has_value()) { this->construct(*rhs); } else { this->construct_error(rhs.error()); } } template < class U, class G, detail::enable_if_t::value && std::is_convertible::value)> * = nullptr, detail::expected_enable_from_other * = nullptr> explicit TL_EXPECTED_11_CONSTEXPR expected(expected &&rhs) : ctor_base(detail::default_constructor_tag{}) { if (rhs.has_value()) { this->construct(std::move(*rhs)); } else { this->construct_error(std::move(rhs.error())); } } template < class U, class G, detail::enable_if_t<(std::is_convertible::value && std::is_convertible::value)> * = nullptr, detail::expected_enable_from_other * = nullptr> TL_EXPECTED_11_CONSTEXPR expected(expected &&rhs) : ctor_base(detail::default_constructor_tag{}) { if (rhs.has_value()) { this->construct(std::move(*rhs)); } else { this->construct_error(std::move(rhs.error())); } } template < class U = T, detail::enable_if_t::value> * = nullptr, detail::expected_enable_forward_value * = nullptr> explicit TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v) : expected(in_place, std::forward(v)) {} template < class U = T, detail::enable_if_t::value> * = nullptr, detail::expected_enable_forward_value * = nullptr> TL_EXPECTED_MSVC2015_CONSTEXPR expected(U &&v) : expected(in_place, std::forward(v)) {} template < class U = T, class G = T, detail::enable_if_t::value> * = nullptr, detail::enable_if_t::value> * = nullptr, detail::enable_if_t< (!std::is_same, detail::decay_t>::value && !detail::conjunction, std::is_same>>::value && std::is_constructible::value && std::is_assignable::value && std::is_nothrow_move_constructible::value)> * = nullptr> expected &operator=(U &&v) { if (has_value()) { val() = std::forward(v); } else { err().~unexpected(); ::new (valptr()) T(std::forward(v)); this->m_has_val = true; } return *this; } template < class U = T, class G = T, detail::enable_if_t::value> * = nullptr, detail::enable_if_t::value> * = nullptr, detail::enable_if_t< (!std::is_same, detail::decay_t>::value && !detail::conjunction, std::is_same>>::value && std::is_constructible::value && std::is_assignable::value && std::is_nothrow_move_constructible::value)> * = nullptr> expected &operator=(U &&v) { if (has_value()) { val() = std::forward(v); } else { auto tmp = std::move(err()); err().~unexpected(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { ::new (valptr()) T(std::forward(v)); this->m_has_val = true; } catch (...) { err() = std::move(tmp); throw; } #else ::new (valptr()) T(std::forward(v)); this->m_has_val = true; #endif } return *this; } template ::value && std::is_assignable::value> * = nullptr> expected &operator=(const unexpected &rhs) { if (!has_value()) { err() = rhs; } else { this->destroy_val(); ::new (errptr()) unexpected(rhs); this->m_has_val = false; } return *this; } template ::value && std::is_move_assignable::value> * = nullptr> expected &operator=(unexpected &&rhs) noexcept { if (!has_value()) { err() = std::move(rhs); } else { this->destroy_val(); ::new (errptr()) unexpected(std::move(rhs)); this->m_has_val = false; } return *this; } template ::value> * = nullptr> void emplace(Args &&...args) { if (has_value()) { val().~T(); } else { err().~unexpected(); this->m_has_val = true; } ::new (valptr()) T(std::forward(args)...); } template ::value> * = nullptr> void emplace(Args &&...args) { if (has_value()) { val().~T(); ::new (valptr()) T(std::forward(args)...); } else { auto tmp = std::move(err()); err().~unexpected(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { ::new (valptr()) T(std::forward(args)...); this->m_has_val = true; } catch (...) { err() = std::move(tmp); throw; } #else ::new (valptr()) T(std::forward(args)...); this->m_has_val = true; #endif } } template &, Args &&...>::value> * = nullptr> void emplace(std::initializer_list il, Args &&...args) { if (has_value()) { T t(il, std::forward(args)...); val() = std::move(t); } else { err().~unexpected(); ::new (valptr()) T(il, std::forward(args)...); this->m_has_val = true; } } template &, Args &&...>::value> * = nullptr> void emplace(std::initializer_list il, Args &&...args) { if (has_value()) { T t(il, std::forward(args)...); val() = std::move(t); } else { auto tmp = std::move(err()); err().~unexpected(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { ::new (valptr()) T(il, std::forward(args)...); this->m_has_val = true; } catch (...) { err() = std::move(tmp); throw; } #else ::new (valptr()) T(il, std::forward(args)...); this->m_has_val = true; #endif } } private: using t_is_void = std::true_type; using t_is_not_void = std::false_type; using t_is_nothrow_move_constructible = std::true_type; using move_constructing_t_can_throw = std::false_type; using e_is_nothrow_move_constructible = std::true_type; using move_constructing_e_can_throw = std::false_type; void swap_where_both_have_value(expected & /*rhs*/, t_is_void) noexcept { // swapping void is a no-op } void swap_where_both_have_value(expected &rhs, t_is_not_void) { using std::swap; swap(val(), rhs.val()); } void swap_where_only_one_has_value(expected &rhs, t_is_void) noexcept( std::is_nothrow_move_constructible::value) { ::new (errptr()) unexpected_type(std::move(rhs.err())); rhs.err().~unexpected_type(); std::swap(this->m_has_val, rhs.m_has_val); } void swap_where_only_one_has_value(expected &rhs, t_is_not_void) { swap_where_only_one_has_value_and_t_is_not_void( rhs, typename std::is_nothrow_move_constructible::type{}, typename std::is_nothrow_move_constructible::type{}); } void swap_where_only_one_has_value_and_t_is_not_void( expected &rhs, t_is_nothrow_move_constructible, e_is_nothrow_move_constructible) noexcept { auto temp = std::move(val()); val().~T(); ::new (errptr()) unexpected_type(std::move(rhs.err())); rhs.err().~unexpected_type(); ::new (rhs.valptr()) T(std::move(temp)); std::swap(this->m_has_val, rhs.m_has_val); } void swap_where_only_one_has_value_and_t_is_not_void( expected &rhs, t_is_nothrow_move_constructible, move_constructing_e_can_throw) { auto temp = std::move(val()); val().~T(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { ::new (errptr()) unexpected_type(std::move(rhs.err())); rhs.err().~unexpected_type(); ::new (rhs.valptr()) T(std::move(temp)); std::swap(this->m_has_val, rhs.m_has_val); } catch (...) { val() = std::move(temp); throw; } #else ::new (errptr()) unexpected_type(std::move(rhs.err())); rhs.err().~unexpected_type(); ::new (rhs.valptr()) T(std::move(temp)); std::swap(this->m_has_val, rhs.m_has_val); #endif } void swap_where_only_one_has_value_and_t_is_not_void( expected &rhs, move_constructing_t_can_throw, e_is_nothrow_move_constructible) { auto temp = std::move(rhs.err()); rhs.err().~unexpected_type(); #ifdef TL_EXPECTED_EXCEPTIONS_ENABLED try { ::new (rhs.valptr()) T(std::move(val())); val().~T(); ::new (errptr()) unexpected_type(std::move(temp)); std::swap(this->m_has_val, rhs.m_has_val); } catch (...) { rhs.err() = std::move(temp); throw; } #else ::new (rhs.valptr()) T(std::move(val())); val().~T(); ::new (errptr()) unexpected_type(std::move(temp)); std::swap(this->m_has_val, rhs.m_has_val); #endif } public: template detail::enable_if_t::value && detail::is_swappable::value && (std::is_nothrow_move_constructible::value || std::is_nothrow_move_constructible::value)> swap(expected &rhs) noexcept( std::is_nothrow_move_constructible::value &&detail::is_nothrow_swappable::value &&std::is_nothrow_move_constructible::value &&detail::is_nothrow_swappable::value) { if (has_value() && rhs.has_value()) { swap_where_both_have_value(rhs, typename std::is_void::type{}); } else if (!has_value() && rhs.has_value()) { rhs.swap(*this); } else if (has_value()) { swap_where_only_one_has_value(rhs, typename std::is_void::type{}); } else { using std::swap; swap(err(), rhs.err()); } } constexpr const T *operator->() const { TL_ASSERT(has_value()); return valptr(); } TL_EXPECTED_11_CONSTEXPR T *operator->() { TL_ASSERT(has_value()); return valptr(); } template ::value> * = nullptr> constexpr const U &operator*() const & { TL_ASSERT(has_value()); return val(); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR U &operator*() & { TL_ASSERT(has_value()); return val(); } template ::value> * = nullptr> constexpr const U &&operator*() const && { TL_ASSERT(has_value()); return std::move(val()); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR U &&operator*() && { TL_ASSERT(has_value()); return std::move(val()); } constexpr bool has_value() const noexcept { return this->m_has_val; } constexpr explicit operator bool() const noexcept { return this->m_has_val; } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR const U &value() const & { if (!has_value()) detail::throw_exception(bad_expected_access(err().value())); return val(); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR U &value() & { if (!has_value()) detail::throw_exception(bad_expected_access(err().value())); return val(); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR const U &&value() const && { if (!has_value()) detail::throw_exception(bad_expected_access(std::move(err()).value())); return std::move(val()); } template ::value> * = nullptr> TL_EXPECTED_11_CONSTEXPR U &&value() && { if (!has_value()) detail::throw_exception(bad_expected_access(std::move(err()).value())); return std::move(val()); } constexpr const E &error() const & { TL_ASSERT(!has_value()); return err().value(); } TL_EXPECTED_11_CONSTEXPR E &error() & { TL_ASSERT(!has_value()); return err().value(); } constexpr const E &&error() const && { TL_ASSERT(!has_value()); return std::move(err().value()); } TL_EXPECTED_11_CONSTEXPR E &&error() && { TL_ASSERT(!has_value()); return std::move(err().value()); } template constexpr T value_or(U &&v) const & { static_assert(std::is_copy_constructible::value && std::is_convertible::value, "T must be copy-constructible and convertible to from U&&"); return bool(*this) ? **this : static_cast(std::forward(v)); } template TL_EXPECTED_11_CONSTEXPR T value_or(U &&v) && { static_assert(std::is_move_constructible::value && std::is_convertible::value, "T must be move-constructible and convertible to from U&&"); return bool(*this) ? std::move(**this) : static_cast(std::forward(v)); } }; namespace detail { template using exp_t = typename detail::decay_t::value_type; template using err_t = typename detail::decay_t::error_type; template using ret_t = expected>; #ifdef TL_EXPECTED_CXX14 template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), *std::declval()))> constexpr auto and_then_impl(Exp &&exp, F &&f) { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? detail::invoke(std::forward(f), *std::forward(exp)) : Ret(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval()))> constexpr auto and_then_impl(Exp &&exp, F &&f) { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? detail::invoke(std::forward(f)) : Ret(unexpect, std::forward(exp).error()); } #else template struct TC; template (), *std::declval())), detail::enable_if_t>::value> * = nullptr> auto and_then_impl(Exp &&exp, F &&f) -> Ret { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? detail::invoke(std::forward(f), *std::forward(exp)) : Ret(unexpect, std::forward(exp).error()); } template ())), detail::enable_if_t>::value> * = nullptr> constexpr auto and_then_impl(Exp &&exp, F &&f) -> Ret { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? detail::invoke(std::forward(f)) : Ret(unexpect, std::forward(exp).error()); } #endif #ifdef TL_EXPECTED_CXX14 template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), *std::declval())), detail::enable_if_t::value> * = nullptr> constexpr auto expected_map_impl(Exp &&exp, F &&f) { using result = ret_t>; return exp.has_value() ? result(detail::invoke(std::forward(f), *std::forward(exp))) : result(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), *std::declval())), detail::enable_if_t::value> * = nullptr> auto expected_map_impl(Exp &&exp, F &&f) { using result = expected>; if (exp.has_value()) { detail::invoke(std::forward(f), *std::forward(exp)); return result(); } return result(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval())), detail::enable_if_t::value> * = nullptr> constexpr auto expected_map_impl(Exp &&exp, F &&f) { using result = ret_t>; return exp.has_value() ? result(detail::invoke(std::forward(f))) : result(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval())), detail::enable_if_t::value> * = nullptr> auto expected_map_impl(Exp &&exp, F &&f) { using result = expected>; if (exp.has_value()) { detail::invoke(std::forward(f)); return result(); } return result(unexpect, std::forward(exp).error()); } #else template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), *std::declval())), detail::enable_if_t::value> * = nullptr> constexpr auto expected_map_impl(Exp &&exp, F &&f) -> ret_t> { using result = ret_t>; return exp.has_value() ? result(detail::invoke(std::forward(f), *std::forward(exp))) : result(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), *std::declval())), detail::enable_if_t::value> * = nullptr> auto expected_map_impl(Exp &&exp, F &&f) -> expected> { if (exp.has_value()) { detail::invoke(std::forward(f), *std::forward(exp)); return {}; } return unexpected>(std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval())), detail::enable_if_t::value> * = nullptr> constexpr auto expected_map_impl(Exp &&exp, F &&f) -> ret_t> { using result = ret_t>; return exp.has_value() ? result(detail::invoke(std::forward(f))) : result(unexpect, std::forward(exp).error()); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval())), detail::enable_if_t::value> * = nullptr> auto expected_map_impl(Exp &&exp, F &&f) -> expected> { if (exp.has_value()) { detail::invoke(std::forward(f)); return {}; } return unexpected>(std::forward(exp).error()); } #endif #if defined(TL_EXPECTED_CXX14) && !defined(TL_EXPECTED_GCC49) && \ !defined(TL_EXPECTED_GCC54) && !defined(TL_EXPECTED_GCC55) template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> constexpr auto map_error_impl(Exp &&exp, F &&f) { using result = expected, detail::decay_t>; return exp.has_value() ? result(*std::forward(exp)) : result(unexpect, detail::invoke(std::forward(f), std::forward(exp).error())); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> auto map_error_impl(Exp &&exp, F &&f) { using result = expected, monostate>; if (exp.has_value()) { return result(*std::forward(exp)); } detail::invoke(std::forward(f), std::forward(exp).error()); return result(unexpect, monostate{}); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> constexpr auto map_error_impl(Exp &&exp, F &&f) { using result = expected, detail::decay_t>; return exp.has_value() ? result() : result(unexpect, detail::invoke(std::forward(f), std::forward(exp).error())); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> auto map_error_impl(Exp &&exp, F &&f) { using result = expected, monostate>; if (exp.has_value()) { return result(); } detail::invoke(std::forward(f), std::forward(exp).error()); return result(unexpect, monostate{}); } #else template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> constexpr auto map_error_impl(Exp &&exp, F &&f) -> expected, detail::decay_t> { using result = expected, detail::decay_t>; return exp.has_value() ? result(*std::forward(exp)) : result(unexpect, detail::invoke(std::forward(f), std::forward(exp).error())); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> auto map_error_impl(Exp &&exp, F &&f) -> expected, monostate> { using result = expected, monostate>; if (exp.has_value()) { return result(*std::forward(exp)); } detail::invoke(std::forward(f), std::forward(exp).error()); return result(unexpect, monostate{}); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> constexpr auto map_error_impl(Exp &&exp, F &&f) -> expected, detail::decay_t> { using result = expected, detail::decay_t>; return exp.has_value() ? result() : result(unexpect, detail::invoke(std::forward(f), std::forward(exp).error())); } template >::value> * = nullptr, class Ret = decltype(detail::invoke(std::declval(), std::declval().error())), detail::enable_if_t::value> * = nullptr> auto map_error_impl(Exp &&exp, F &&f) -> expected, monostate> { using result = expected, monostate>; if (exp.has_value()) { return result(); } detail::invoke(std::forward(f), std::forward(exp).error()); return result(unexpect, monostate{}); } #endif #ifdef TL_EXPECTED_CXX14 template (), std::declval().error())), detail::enable_if_t::value> * = nullptr> constexpr auto or_else_impl(Exp &&exp, F &&f) { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? std::forward(exp) : detail::invoke(std::forward(f), std::forward(exp).error()); } template (), std::declval().error())), detail::enable_if_t::value> * = nullptr> detail::decay_t or_else_impl(Exp &&exp, F &&f) { return exp.has_value() ? std::forward(exp) : (detail::invoke(std::forward(f), std::forward(exp).error()), std::forward(exp)); } #else template (), std::declval().error())), detail::enable_if_t::value> * = nullptr> auto or_else_impl(Exp &&exp, F &&f) -> Ret { static_assert(detail::is_expected::value, "F must return an expected"); return exp.has_value() ? std::forward(exp) : detail::invoke(std::forward(f), std::forward(exp).error()); } template (), std::declval().error())), detail::enable_if_t::value> * = nullptr> detail::decay_t or_else_impl(Exp &&exp, F &&f) { return exp.has_value() ? std::forward(exp) : (detail::invoke(std::forward(f), std::forward(exp).error()), std::forward(exp)); } #endif } // namespace detail template constexpr bool operator==(const expected &lhs, const expected &rhs) { return (lhs.has_value() != rhs.has_value()) ? false : (!lhs.has_value() ? lhs.error() == rhs.error() : *lhs == *rhs); } template constexpr bool operator!=(const expected &lhs, const expected &rhs) { return (lhs.has_value() != rhs.has_value()) ? true : (!lhs.has_value() ? lhs.error() != rhs.error() : *lhs != *rhs); } template constexpr bool operator==(const expected &lhs, const expected &rhs) { return (lhs.has_value() != rhs.has_value()) ? false : (!lhs.has_value() ? lhs.error() == rhs.error() : true); } template constexpr bool operator!=(const expected &lhs, const expected &rhs) { return (lhs.has_value() != rhs.has_value()) ? true : (!lhs.has_value() ? lhs.error() == rhs.error() : false); } template constexpr bool operator==(const expected &x, const U &v) { return x.has_value() ? *x == v : false; } template constexpr bool operator==(const U &v, const expected &x) { return x.has_value() ? *x == v : false; } template constexpr bool operator!=(const expected &x, const U &v) { return x.has_value() ? *x != v : true; } template constexpr bool operator!=(const U &v, const expected &x) { return x.has_value() ? *x != v : true; } template constexpr bool operator==(const expected &x, const unexpected &e) { return x.has_value() ? false : x.error() == e.value(); } template constexpr bool operator==(const unexpected &e, const expected &x) { return x.has_value() ? false : x.error() == e.value(); } template constexpr bool operator!=(const expected &x, const unexpected &e) { return x.has_value() ? true : x.error() != e.value(); } template constexpr bool operator!=(const unexpected &e, const expected &x) { return x.has_value() ? true : x.error() != e.value(); } template ::value || std::is_move_constructible::value) && detail::is_swappable::value && std::is_move_constructible::value && detail::is_swappable::value> * = nullptr> void swap(expected &lhs, expected &rhs) noexcept(noexcept(lhs.swap(rhs))) { lhs.swap(rhs); } } // namespace tl #endif ================================================ FILE: 3rdParty/tl/function_ref.hpp ================================================ /// // function_ref - A low-overhead non-owning function // Written in 2017 by Simon Brand (@TartanLlama) // // To the extent possible under law, the author(s) have dedicated all // copyright and related and neighboring rights to this software to the // public domain worldwide. This software is distributed without any warranty. // // You should have received a copy of the CC0 Public Domain Dedication // along with this software. If not, see // . /// #ifndef TL_FUNCTION_REF_HPP #define TL_FUNCTION_REF_HPP #define TL_FUNCTION_REF_VERSION_MAJOR 1 #define TL_FUNCTION_REF_VERSION_MINOR 0 #define TL_FUNCTION_REF_VERSION_PATCH 0 #if (defined(_MSC_VER) && _MSC_VER == 1900) /// \exclude #define TL_FUNCTION_REF_MSVC2015 #endif #if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ !defined(__clang__)) /// \exclude #define TL_FUNCTION_REF_GCC49 #endif #if (defined(__GNUC__) && __GNUC__ == 5 && __GNUC_MINOR__ <= 4 && \ !defined(__clang__)) /// \exclude #define TL_FUNCTION_REF_GCC54 #endif #if (defined(__GNUC__) && __GNUC__ == 4 && __GNUC_MINOR__ <= 9 && \ !defined(__clang__)) // GCC < 5 doesn't support overloading on const&& for member functions /// \exclude #define TL_FUNCTION_REF_NO_CONSTRR #endif #if __cplusplus > 201103L /// \exclude #define TL_FUNCTION_REF_CXX14 #endif // constexpr implies const in C++11, not C++14 #if (__cplusplus == 201103L || defined(TL_FUNCTION_REF_MSVC2015) || \ defined(TL_FUNCTION_REF_GCC49)) && \ !defined(TL_FUNCTION_REF_GCC54) /// \exclude #define TL_FUNCTION_REF_11_CONSTEXPR #else /// \exclude #define TL_FUNCTION_REF_11_CONSTEXPR constexpr #endif #include #include namespace tl { namespace detail { namespace fnref { // C++14-style aliases for brevity template using remove_const_t = typename std::remove_const::type; template using remove_reference_t = typename std::remove_reference::type; template using decay_t = typename std::decay::type; template using enable_if_t = typename std::enable_if::type; template using conditional_t = typename std::conditional::type; // std::invoke from C++17 // https://stackoverflow.com/questions/38288042/c11-14-invoke-workaround template >::value>, int = 0> constexpr auto invoke(Fn &&f, Args &&... args) noexcept( noexcept(std::mem_fn(f)(std::forward(args)...))) -> decltype(std::mem_fn(f)(std::forward(args)...)) { return std::mem_fn(f)(std::forward(args)...); } template >{}>> constexpr auto invoke(Fn &&f, Args &&... args) noexcept( noexcept(std::forward(f)(std::forward(args)...))) -> decltype(std::forward(f)(std::forward(args)...)) { return std::forward(f)(std::forward(args)...); } // std::invoke_result from C++17 template struct invoke_result_impl; template struct invoke_result_impl< F, decltype(tl::detail::fnref::invoke(std::declval(), std::declval()...), void()), Us...> { using type = decltype(tl::detail::fnref::invoke(std::declval(), std::declval()...)); }; template using invoke_result = invoke_result_impl; template using invoke_result_t = typename invoke_result::type; template struct is_invocable_r_impl : std::false_type {}; template struct is_invocable_r_impl< typename std::is_convertible, R>::type, R, F, Args...> : std::true_type {}; template using is_invocable_r = is_invocable_r_impl; } // namespace detail } // namespace fnref /// A lightweight non-owning reference to a callable. /// /// Example usage: /// /// ```cpp /// void foo (function_ref func) { /// std::cout << "Result is " << func(21); //42 /// } /// /// foo([](int i) { return i*2; }); template class function_ref; /// Specialization for function types. template class function_ref { public: constexpr function_ref() noexcept = delete; /// Creates a `function_ref` which refers to the same callable as `rhs`. constexpr function_ref(const function_ref &rhs) noexcept = default; /// Constructs a `function_ref` referring to `f`. /// /// \synopsis template constexpr function_ref(F &&f) noexcept template , function_ref>::value && detail::fnref::is_invocable_r::value> * = nullptr> TL_FUNCTION_REF_11_CONSTEXPR function_ref(F &&f) noexcept : obj_(const_cast(reinterpret_cast(std::addressof(f)))) { callback_ = [](void *obj, Args... args) -> R { return detail::fnref::invoke( *reinterpret_cast::type>(obj), std::forward(args)...); }; } /// Makes `*this` refer to the same callable as `rhs`. TL_FUNCTION_REF_11_CONSTEXPR function_ref & operator=(const function_ref &rhs) noexcept = default; /// Makes `*this` refer to `f`. /// /// \synopsis template constexpr function_ref &operator=(F &&f) noexcept; template ::value> * = nullptr> TL_FUNCTION_REF_11_CONSTEXPR function_ref &operator=(F &&f) noexcept { obj_ = reinterpret_cast(std::addressof(f)); callback_ = [](void *obj, Args... args) { return detail::fnref::invoke( *reinterpret_cast::type>(obj), std::forward(args)...); }; return *this; } /// Swaps the referred callables of `*this` and `rhs`. constexpr void swap(function_ref &rhs) noexcept { std::swap(obj_, rhs.obj_); std::swap(callback_, rhs.callback_); } /// Call the stored callable with the given arguments. R operator()(Args... args) const { return callback_(obj_, std::forward(args)...); } private: void *obj_ = nullptr; R (*callback_)(void *, Args...) = nullptr; }; /// Swaps the referred callables of `lhs` and `rhs`. template constexpr void swap(function_ref &lhs, function_ref &rhs) noexcept { lhs.swap(rhs); } #if __cplusplus >= 201703L template function_ref(R (*)(Args...))->function_ref; // TODO, will require some kind of callable traits // template // function_ref(F) -> function_ref; #endif } // namespace tl #endif ================================================ FILE: 3rdParty/tolk/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) # Workaround for deprecation of older CMake versions set(CMAKE_POLICY_VERSION_MINIMUM 3.22) include(FetchContent) FetchContent_Declare_ExcludeFromAll(Tolk URL https://github.com/sig-a11y/tolk/archive/89de98779e3b6365dc1688538d5de4ecba3fdbab.tar.gz URL_HASH MD5=724f6022186573dd9c5c2c92ed9e21e6 ) FetchContent_MakeAvailable_ExcludeFromAll(Tolk) target_include_directories(Tolk PUBLIC ${libTolk_SOURCE_DIR}/src) if(CMAKE_SIZEOF_VOID_P EQUAL 4) set(TOLK_LIB_DIR "${Tolk_SOURCE_DIR}/libs/x86") else() set(TOLK_LIB_DIR "${Tolk_SOURCE_DIR}/libs/x64") endif() file(GLOB TOLK_DLLS LIST_DIRECTORIES false "${TOLK_LIB_DIR}/*.dll" "${TOLK_LIB_DIR}/*.ini") foreach(_TOLK_DLL_PATH ${TOLK_DLLS}) install(FILES "${_TOLK_DLL_PATH}" DESTINATION "." ) endforeach() ================================================ FILE: 3rdParty/unordered_dense/0001-Disable-PMR-support-for-mingw-std-threads-compat.patch ================================================ From 78eb7bc8a64099235daa3a8cb249a5ce35a4db07 Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Sun, 4 Aug 2024 12:32:50 +0100 Subject: [PATCH] Disable PMR support for mingw-std-threads compat --- include/ankerl/unordered_dense.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/ankerl/unordered_dense.h b/include/ankerl/unordered_dense.h index 2aaacd6..e6fee6e 100644 --- a/include/ankerl/unordered_dense.h +++ b/include/ankerl/unordered_dense.h @@ -98,6 +98,8 @@ # include // for abort # endif +// DevilutionX disables PMR support because it does not play well with mingw-std-threads. +#if false # if defined(__has_include) # if __has_include() # define ANKERL_UNORDERED_DENSE_PMR std::pmr // NOLINT(cppcoreguidelines-macro-usage) @@ -107,6 +109,7 @@ # include // for polymorphic_allocator # endif # endif +#endif # if defined(_MSC_VER) && defined(_M_X64) # include -- 2.43.0 ================================================ FILE: 3rdParty/unordered_dense/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) include(FetchContent) set(unordered_dense_URL "https://github.com/martinus/unordered_dense/archive/refs/tags/v4.4.0.tar.gz") set(unordered_dense_HASH "f33c294a010540434b272754f937decf") if(MINGW_CROSS) find_package(Patch REQUIRED) FetchContent_Declare_ExcludeFromAll(unordered_dense URL ${unordered_dense_URL} URL_HASH MD5=${unordered_dense_HASH} PATCH_COMMAND "${Patch_EXECUTABLE}" -p1 -N < "${CMAKE_CURRENT_LIST_DIR}/0001-Disable-PMR-support-for-mingw-std-threads-compat.patch" || true ) else() FetchContent_Declare_ExcludeFromAll(unordered_dense URL ${unordered_dense_URL} URL_HASH MD5=${unordered_dense_HASH} ) endif() FetchContent_MakeAvailable_ExcludeFromAll(unordered_dense) ================================================ FILE: 3rdParty/zlib/CMake/FindZLIB.cmake ================================================ # No-op, ZLIB::ZLIB is built from source. ================================================ FILE: 3rdParty/zlib/CMakeLists.txt ================================================ include(functions/FetchContent_ExcludeFromAll_backport) set(CMAKE_POLICY_DEFAULT_CMP0048 NEW) include(FetchContent) FetchContent_Declare_ExcludeFromAll(zlib URL https://www.zlib.net/zlib-1.3.tar.gz https://www.zlib.net/fossils/zlib-1.3.tar.gz https://github.com/madler/zlib/releases/download/v1.3/zlib-1.3.tar.gz URL_HASH MD5=60373b133d630f74f4a1f94c1185a53f ) FetchContent_MakeAvailable_ExcludeFromAll(zlib) if(DEVILUTIONX_STATIC_ZLIB) add_library(ZLIB::ZLIB ALIAS zlibstatic) target_include_directories(zlibstatic INTERFACE ${zlib_BINARY_DIR} ${zlib_SOURCE_DIR}) else() add_library(ZLIB::ZLIB ALIAS zlib) target_include_directories(zlib INTERFACE ${zlib_BINARY_DIR} ${zlib_SOURCE_DIR}) endif() # 1. Set the variables that are usually set by FindZLIB.cmake. # 2. Add the module that stubs out `find_package(ZLIB ...)` calls. set(ZLIB_FOUND ON PARENT_SCOPE) set(ZLIB_LIBRARY ZLIB::ZLIB PARENT_SCOPE) set(ZLIB_LIBRARIES ZLIB::ZLIB PARENT_SCOPE) set(ZLIB_INCLUDE_DIR ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR} PARENT_SCOPE) set(ZLIB_INCLUDE_DIRS ${zlib_SOURCE_DIR} ${zlib_BINARY_DIR} PARENT_SCOPE) set(CMAKE_MODULE_PATH "${CMAKE_MODULE_PATH};${CMAKE_CURRENT_LIST_DIR}/CMake" PARENT_SCOPE) ================================================ FILE: Brewfile ================================================ brew "cmake" brew "fmt" brew "sdl2" brew "libsodium" brew "pkg-config" brew "googletest" brew "google-benchmark" brew "magic_enum" ================================================ FILE: CMake/Assets.cmake ================================================ include(functions/copy_files) include(functions/trim_retired_files) if(NOT DEFINED DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY) set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/assets") endif() set(devilutionx_langs be bg cs da de el es et fi fr hr hu it ja ko pl pt_BR ro ru uk sv tr zh_CN zh_TW) if(USE_GETTEXT_FROM_VCPKG) # vcpkg doesn't add its own tools directory to the search path list(APPEND Gettext_ROOT ${CMAKE_CURRENT_BINARY_DIR}/vcpkg_installed/${VCPKG_TARGET_TRIPLET}/tools/gettext/bin) endif() find_package(Gettext) if (Gettext_FOUND) file(MAKE_DIRECTORY "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}") foreach(lang ${devilutionx_langs}) set(_po_file "${CMAKE_CURRENT_SOURCE_DIR}/Translations/${lang}.po") set(_gmo_file "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/${lang}.gmo") set(_lang_target devilutionx_lang_${lang}) add_custom_command( COMMAND "${GETTEXT_MSGFMT_EXECUTABLE}" -o "${_gmo_file}" "${_po_file}" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" OUTPUT "${_gmo_file}" MAIN_DEPENDENCY "${_po_file}" VERBATIM ) add_custom_target("${_lang_target}" DEPENDS "${_gmo_file}") list(APPEND devilutionx_lang_targets "${_lang_target}") list(APPEND devilutionx_lang_files "${_gmo_file}") if(APPLE) set_source_files_properties("${_gmo_file}" PROPERTIES MACOSX_PACKAGE_LOCATION Resources XCODE_EXPLICIT_FILE_TYPE compiled) add_dependencies(libdevilutionx "${_lang_target}") add_dependencies(${BIN_TARGET} "${_lang_target}") target_sources(${BIN_TARGET} PRIVATE "${_gmo_file}") endif() if(VITA) list(APPEND VITA_TRANSLATIONS_LIST "FILE" "${_gmo_file}" "assets/${lang}.gmo") endif() endforeach() endif() set(devilutionx_assets ASSETS_VERSION arena/church.dun arena/circle_of_death.dun arena/hell.dun data/boxleftend.clx data/boxmiddle.clx data/boxrightend.clx data/charbg.clx data/dirtybuc.clx data/dirtybucp.clx data/healthbox.clx data/health.clx data/hintbox.clx data/hintboxbackground.clx data/hinticons.clx data/monstertags.clx data/panel8buc.clx data/panel8bucp.clx data/resistance.clx data/stash.clx data/stashnavbtns.clx data/talkbutton.clx data/xpbar.clx fonts/12-00.clx fonts/12-01.clx fonts/12-02.clx fonts/12-03.clx fonts/12-04.clx fonts/12-05.clx fonts/12-1f4.clx fonts/12-1f6.clx fonts/12-1f9.clx fonts/12-20.clx fonts/12-26.clx fonts/12-e0.clx fonts/22-00.clx fonts/22-01.clx fonts/22-02.clx fonts/22-03.clx fonts/22-04.clx fonts/22-05.clx fonts/22-20.clx fonts/24-00.clx fonts/24-01.clx fonts/24-02.clx fonts/24-03.clx fonts/24-04.clx fonts/24-05.clx fonts/24-1f4.clx fonts/24-1f6.clx fonts/24-1f9.clx fonts/24-20.clx fonts/24-26.clx fonts/24-e0.clx fonts/30-00.clx fonts/30-01.clx fonts/30-02.clx fonts/30-03.clx fonts/30-04.clx fonts/30-05.clx fonts/30-20.clx fonts/30-e0.clx fonts/42-00.clx fonts/42-01.clx fonts/42-02.clx fonts/42-03.clx fonts/42-04.clx fonts/42-05.clx fonts/42-20.clx fonts/46-00.clx fonts/46-01.clx fonts/46-02.clx fonts/46-03.clx fonts/46-04.clx fonts/46-05.clx fonts/46-20.clx fonts/black.trn fonts/blue.trn fonts/buttonface.trn fonts/buttonpushed.trn fonts/gamedialogwhite.trn fonts/gamedialogyellow.trn fonts/gamedialogred.trn fonts/golduis.trn fonts/goldui.trn fonts/grayuis.trn fonts/grayui.trn fonts/orange.trn fonts/red.trn fonts/tr/12-00.clx fonts/tr/24-00.clx fonts/tr/30-00.clx fonts/tr/42-00.clx fonts/tr/46-00.clx fonts/whitegold.trn fonts/white.trn fonts/yellow.trn gendata/cut2w.clx gendata/cut3w.clx gendata/cut4w.clx gendata/cutgatew.clx gendata/cutl1dw.clx gendata/cutportlw.clx gendata/cutportrw.clx gendata/cutstartw.clx gendata/cutttw.clx gendata/pause.trn levels/l1data/sklkngt.dun levels/l2data/bonechat.dun levels/towndata/automap.dun levels/towndata/automap.amp lua_internal/get_lua_function_signature.lua lua/devilutionx/events.lua lua/inspect.lua lua/mods/adria_refills_mana/init.lua lua/mods/clock/init.lua "lua/mods/Floating Numbers - Damage/init.lua" "lua/mods/Floating Numbers - XP/init.lua" lua/repl_prelude.lua plrgfx/warrior/whu/whufm.trn plrgfx/warrior/whu/whulm.trn plrgfx/warrior/whu/whuqm.trn txtdata/Experience.tsv txtdata/classes/barbarian/animations.tsv txtdata/classes/barbarian/attributes.tsv txtdata/classes/barbarian/sounds.tsv txtdata/classes/barbarian/sprites.tsv txtdata/classes/barbarian/starting_loadout.tsv txtdata/classes/bard/animations.tsv txtdata/classes/bard/attributes.tsv txtdata/classes/bard/sounds.tsv txtdata/classes/bard/sprites.tsv txtdata/classes/bard/starting_loadout.tsv txtdata/classes/monk/animations.tsv txtdata/classes/monk/attributes.tsv txtdata/classes/monk/sounds.tsv txtdata/classes/monk/sprites.tsv txtdata/classes/monk/starting_loadout.tsv txtdata/classes/rogue/animations.tsv txtdata/classes/rogue/attributes.tsv txtdata/classes/rogue/sounds.tsv txtdata/classes/rogue/sprites.tsv txtdata/classes/rogue/starting_loadout.tsv txtdata/classes/sorcerer/animations.tsv txtdata/classes/sorcerer/attributes.tsv txtdata/classes/sorcerer/sounds.tsv txtdata/classes/sorcerer/sprites.tsv txtdata/classes/sorcerer/starting_loadout.tsv txtdata/classes/warrior/animations.tsv txtdata/classes/warrior/attributes.tsv txtdata/classes/warrior/sounds.tsv txtdata/classes/warrior/sprites.tsv txtdata/classes/warrior/starting_loadout.tsv txtdata/classes/classdat.tsv txtdata/items/item_prefixes.tsv txtdata/items/item_suffixes.tsv txtdata/items/itemdat.tsv txtdata/items/unique_itemdat.tsv txtdata/missiles/misdat.tsv txtdata/missiles/missile_sprites.tsv txtdata/monsters/monstdat.tsv txtdata/monsters/unique_monstdat.tsv txtdata/objects/objdat.tsv txtdata/quests/questdat.tsv txtdata/sound/effects.tsv txtdata/spells/spelldat.tsv txtdata/text/textdat.tsv txtdata/towners/quest_dialog.tsv txtdata/towners/towners.tsv ui_art/diablo.pal ui_art/creditsw.clx ui_art/dvl_but_sml.clx ui_art/dvl_lrpopup.clx ui_art/mainmenuw.clx) if(NOT UNPACKED_MPQS) list(APPEND devilutionx_assets data/inv/objcurs-widths.txt) endif() if(NOT USE_SDL1 AND NOT VITA) list(APPEND devilutionx_assets ui_art/button.png ui_art/directions2.png ui_art/directions.png ui_art/menu-levelup.png ui_art/menu.png) endif() if(APPLE) foreach(asset_file ${devilutionx_assets}) set(src "${CMAKE_CURRENT_SOURCE_DIR}/assets/${asset_file}") get_filename_component(_asset_dir "${asset_file}" DIRECTORY) set_source_files_properties("${src}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/${_asset_dir}" XCODE_EXPLICIT_FILE_TYPE compiled) target_sources(${BIN_TARGET} PRIVATE "${src}") endforeach() else() # Copy assets to the build assets subdirectory. This serves two purposes: # - If smpq is installed, devilutionx.mpq is built from these files. # - If smpq is not installed, the game will load the assets directly from this directory. copy_files( FILES ${devilutionx_assets} SRC_PREFIX "assets/" OUTPUT_DIR "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}" OUTPUT_VARIABLE DEVILUTIONX_OUTPUT_ASSETS_FILES) set(DEVILUTIONX_MPQ_FILES ${devilutionx_assets}) if (Gettext_FOUND) foreach(lang ${devilutionx_langs}) list(APPEND DEVILUTIONX_MPQ_FILES "${lang}.gmo") endforeach() endif() add_trim_target(devilutionx_trim_assets ROOT_FOLDER "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}" CURRENT_FILES ${DEVILUTIONX_MPQ_FILES}) if(devilutionx_lang_targets) add_dependencies(devilutionx_trim_assets ${devilutionx_lang_targets}) endif() if(BUILD_ASSETS_MPQ) if(TARGET_PLATFORM STREQUAL "dos") set(DEVILUTIONX_MPQ "${CMAKE_CURRENT_BINARY_DIR}/devx.mpq") else() set(DEVILUTIONX_MPQ "${CMAKE_CURRENT_BINARY_DIR}/devilutionx.mpq") endif() add_custom_command( COMMENT "Building devilutionx.mpq" OUTPUT "${DEVILUTIONX_MPQ}" COMMAND ${CMAKE_COMMAND} -E remove -f "${DEVILUTIONX_MPQ}" COMMAND ${SMPQ} -A -M 1 -C BZIP2 -c "${DEVILUTIONX_MPQ}" ${DEVILUTIONX_MPQ_FILES} WORKING_DIRECTORY "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}" DEPENDS ${TRIM_COMMAND_BYPRODUCT} ${DEVILUTIONX_OUTPUT_ASSETS_FILES} ${devilutionx_lang_targets} ${devilutionx_lang_files} VERBATIM) add_custom_target(devilutionx_mpq DEPENDS "${DEVILUTIONX_MPQ}") add_dependencies(devilutionx_mpq devilutionx_trim_assets) add_dependencies(libdevilutionx devilutionx_mpq) else() add_custom_target(devilutionx_copied_assets DEPENDS ${DEVILUTIONX_OUTPUT_ASSETS_FILES} ${devilutionx_lang_targets}) add_dependencies(devilutionx_copied_assets devilutionx_trim_assets) add_dependencies(libdevilutionx devilutionx_copied_assets) endif() endif() ================================================ FILE: CMake/Definitions.cmake ================================================ # Defines without value foreach( def_name NOSOUND NONET NOEXIT PREFILL_PLAYER_NAME DISABLE_TCP DISABLE_ZERO_TIER DISABLE_STREAMING_MUSIC DISABLE_STREAMING_SOUNDS DISABLE_DEMOMODE BUILD_TESTING GPERF GPERF_HEAP_MAIN GPERF_HEAP_FIRST_GAME_ITERATION PACKET_ENCRYPTION DEVILUTIONX_RESAMPLER_SPEEX DEVILUTIONX_RESAMPLER_SDL DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT SCREEN_READER_INTEGRATION UNPACKED_MPQS UNPACKED_SAVES DEVILUTIONX_WINDOWS_NO_WCHAR ) if(${def_name}) list(APPEND DEVILUTIONX_DEFINITIONS ${def_name}) endif() endforeach(def_name) # Defines with value foreach( def_name DEFAULT_WIDTH DEFAULT_HEIGHT DEFAULT_AUDIO_SAMPLE_RATE DEFAULT_AUDIO_CHANNELS DEFAULT_AUDIO_BUFFER_SIZE DEFAULT_AUDIO_RESAMPLING_QUALITY DEFAULT_PER_PIXEL_LIGHTING SDL1_VIDEO_MODE_BPP SDL1_VIDEO_MODE_FLAGS SDL1_VIDEO_MODE_SVID_FLAGS SDL1_FORCE_SVID_VIDEO_MODE SDL1_FORCE_DIRECT_RENDER DEVILUTIONX_GAMEPAD_TYPE HAS_KBCTRL KBCTRL_BUTTON_DPAD_LEFT KBCTRL_BUTTON_DPAD_RIGHT KBCTRL_BUTTON_DPAD_UP KBCTRL_BUTTON_DPAD_DOWN KBCTRL_BUTTON_B KBCTRL_BUTTON_A KBCTRL_BUTTON_Y KBCTRL_BUTTON_X KBCTRL_BUTTON_LEFTSTICK KBCTRL_BUTTON_RIGHTSTICK KBCTRL_BUTTON_RIGHTSHOULDER KBCTRL_BUTTON_LEFTSHOULDER KBCTRL_BUTTON_TRIGGERLEFT KBCTRL_BUTTON_TRIGGERRIGHT KBCTRL_BUTTON_START KBCTRL_BUTTON_BACK KBCTRL_IGNORE_1 JOY_AXIS_LEFTX JOY_AXIS_LEFTY JOY_AXIS_RIGHTX JOY_AXIS_RIGHTY JOY_HAT_DPAD_UP_HAT JOY_HAT_DPAD_UP JOY_HAT_DPAD_DOWN_HAT JOY_HAT_DPAD_DOWN JOY_HAT_DPAD_LEFT_HAT JOY_HAT_DPAD_LEFT JOY_HAT_DPAD_RIGHT_HAT JOY_HAT_DPAD_RIGHT JOY_BUTTON_DPAD_LEFT JOY_BUTTON_DPAD_RIGHT JOY_BUTTON_DPAD_UP JOY_BUTTON_DPAD_DOWN JOY_BUTTON_B JOY_BUTTON_A JOY_BUTTON_Y JOY_BUTTON_X JOY_BUTTON_LEFTSTICK JOY_BUTTON_RIGHTSTICK JOY_BUTTON_RIGHTSHOULDER JOY_BUTTON_LEFTSHOULDER JOY_BUTTON_TRIGGERLEFT JOY_BUTTON_TRIGGERRIGHT JOY_BUTTON_START JOY_BUTTON_BACK REMAP_KEYBOARD_KEYS DEVILUTIONX_DEFAULT_RESAMPLER STREAM_ALL_AUDIO_MIN_FILE_SIZE DEVILUTIONX_DISPLAY_PIXELFORMAT # SDL2-only DEVILUTIONX_DISPLAY_TEXTURE_FORMAT # SDL2-only DEVILUTIONX_SCREENSHOT_FORMAT DARWIN_MAJOR_VERSION DARWIN_MINOR_VERSION ) if(DEFINED ${def_name} AND NOT ${def_name} STREQUAL "") list(APPEND DEVILUTIONX_DEFINITIONS ${def_name}=${${def_name}}) endif() endforeach(def_name) ================================================ FILE: CMake/Dependencies.cmake ================================================ # Options that control whether to use system dependencies or build them from source, # and whether to link them statically. include(functions/dependency_options) include(functions/emscripten_system_library) if(EMSCRIPTEN) emscripten_system_library("zlib" ZLIB::ZLIB USE_ZLIB=1) else() dependency_options("zlib" DEVILUTIONX_SYSTEM_ZLIB ON DEVILUTIONX_STATIC_ZLIB) if(NOT DEVILUTIONX_SYSTEM_ZLIB) add_subdirectory(3rdParty/zlib) endif() endif() if(SUPPORTS_MPQ) # bzip2 is a libmpq dependency. if(EMSCRIPTEN) emscripten_system_library("bzip2" BZip2::BZip2 USE_BZIP2=1) else() dependency_options("bzip2" DEVILUTIONX_SYSTEM_BZIP2 ON DEVILUTIONX_STATIC_BZIP2) if(NOT DEVILUTIONX_SYSTEM_BZIP2) add_subdirectory(3rdParty/bzip2) endif() endif() endif() find_package(Lua 5.4 QUIET) if(LUA_FOUND) message("-- Found Lua ${LUA_VERSION_STRING}") else() if(NOT DEFINED DEVILUTIONX_SYSTEM_LUA) message("-- Suitable system Lua package not found, will use Lua from source") set(DEVILUTIONX_SYSTEM_LUA OFF) endif() endif() dependency_options("lua" DEVILUTIONX_SYSTEM_LUA ON DEVILUTIONX_STATIC_LUA) if(NOT DEVILUTIONX_SYSTEM_LUA) add_subdirectory(3rdParty/Lua) if(DEVILUTIONX_STATIC_LUA) set(LUA_LIBRARIES lua_static) else() set(LUA_LIBRARIES lua_shared) endif() else() find_package(Lua 5.4 REQUIRED) include_directories(${LUA_INCLUDE_DIR}) endif() add_subdirectory(3rdParty/sol2) if(SCREEN_READER_INTEGRATION) if(WIN32) add_subdirectory(3rdParty/tolk) else() find_package(Speechd REQUIRED) endif() endif() if(EMSCRIPTEN) # We use `USE_PTHREADS=1` here to get a version of SDL2 that supports threads. emscripten_system_library("SDL2" SDL2::SDL2 USE_SDL=2 USE_PTHREADS=1) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/SDL2/CMake") elseif(USE_SDL1) find_package(SDL 1.2.10 REQUIRED) include_directories(${SDL_INCLUDE_DIR}) elseif(USE_SDL3) dependency_options("SDL3" DEVILUTIONX_SYSTEM_SDL3 ON DEVILUTIONX_STATIC_SDL3) if(DEVILUTIONX_SYSTEM_SDL3) find_package(SDL3 3.4.0 REQUIRED CONFIG REQUIRED COMPONENTS SDL3) else() add_subdirectory(3rdParty/SDL3) endif() else() dependency_options("SDL2" DEVILUTIONX_SYSTEM_SDL2 ON DEVILUTIONX_STATIC_SDL2) if(DEVILUTIONX_SYSTEM_SDL2) find_package(SDL2 REQUIRED) if(TARGET SDL2::SDL2) if(TARGET SDL2::SDL2main) set(SDL2_MAIN SDL2::SDL2main) endif() elseif(TARGET SDL2::SDL2-static) # On some distros, such as vitasdk, only the SDL2::SDL2-static target is available. # Alias to SDL2::SDL2 because some finder scripts may refer to SDL2::SDL2. add_library(SDL2::SDL2 ALIAS SDL2::SDL2-static) if(TARGET SDL2::SDL2main) set(SDL2_MAIN SDL2::SDL2main) endif() else() # Assume an older Debian derivate that comes with an sdl2-config.cmake # that only defines `SDL2_LIBRARIES` (as -lSDL2) and `SDL2_INCLUDE_DIRS`. add_library(SDL2_lib INTERFACE) target_link_libraries(SDL2_lib INTERFACE ${SDL2_LIBRARIES}) target_include_directories(SDL2_lib INTERFACE ${SDL2_INCLUDE_DIRS}) # Can't define an INTERFACE target with ::, so alias instead add_library(SDL2::SDL2 ALIAS SDL2_lib) endif() elseif(UWP_LIB) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/SDL2/CMake") find_package(SDL2 REQUIRED) else() add_subdirectory(3rdParty/SDL2) if(TARGET SDL2::SDL2main) set(SDL2_MAIN SDL2::SDL2main) endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/SDL2/CMake") endif() endif() add_library(DevilutionX::SDL INTERFACE IMPORTED GLOBAL) if(USE_SDL1) target_link_libraries(DevilutionX::SDL INTERFACE ${SDL_LIBRARY}) target_compile_definitions(DevilutionX::SDL INTERFACE USE_SDL1) elseif(USE_SDL3) target_link_libraries(DevilutionX::SDL INTERFACE SDL3::SDL3) target_compile_definitions(DevilutionX::SDL INTERFACE USE_SDL3) else() if(TARGET SDL2::SDL2 AND NOT (DEVILUTIONX_STATIC_SDL2 AND TARGET SDL2::SDL2-static)) target_link_libraries(DevilutionX::SDL INTERFACE SDL2::SDL2) elseif(TARGET SDL2::SDL2-static) target_link_libraries(DevilutionX::SDL INTERFACE SDL2::SDL2-static) endif() if(NOT UWP_LIB) target_link_libraries(DevilutionX::SDL INTERFACE ${SDL2_MAIN}) endif() endif() macro(_find_SDL_image QUIET_OR_REQUIRED) # vcpkg uses sdl2-image as the package name find_package(sdl2-image QUIET) set(SDL_image_FOUND ${sdl2-image_FOUND}) if(NOT SDL_image_FOUND) # Fall back on PkgConfig via FindSDL2_image.cmake find_package(SDL2_image ${QUIET_OR_REQUIRED}) set(SDL_image_FOUND ${SDL2_image_FOUND}) endif() endmacro() if(USE_SDL1) # No need for SDL_image. elseif(USE_SDL3) if(NOT DEFINED DEVILUTIONX_SYSTEM_SDL_IMAGE) find_package(SDL3_image QUIET CONFIG) if(SDL3_image_FOUND) message("-- Found SDL3_image") else() message("-- Suitable system SDL3_image package not found, will use SDL3_image from source") set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) endif() endif() dependency_options("SDL3_image" DEVILUTIONX_SYSTEM_SDL_IMAGE ON DEVILUTIONX_STATIC_SDL_IMAGE) if(DEVILUTIONX_SYSTEM_SDL_IMAGE) find_package(SDL3_image REQUIRED CONFIG) else() # Must be the same, see: # https://github.com/libsdl-org/SDL_image/blob/11154afb7855293159588b245b446a4ef09e574f/CMakeLists.txt#L225-L233 set(DEVILUTIONX_STATIC_SDL_IMAGE ${DEVILUTIONX_STATIC_SDL3}) add_subdirectory(3rdParty/SDL3_image) endif() else() if(EMSCRIPTEN) emscripten_system_library("SDL_image" SDL2::SDL2_image USE_SDL_IMAGE=2 "SDL2_IMAGE_FORMATS='[\"png\"]'") else() if(NOT DEFINED DEVILUTIONX_SYSTEM_SDL_IMAGE) _find_SDL_image(QUIET) if(SDL_image_FOUND) message("-- Found SDL_image") else() message("-- Suitable system SDL_image package not found, will use SDL_image from source") set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) endif() endif() dependency_options("SDL_image" DEVILUTIONX_SYSTEM_SDL_IMAGE ON DEVILUTIONX_STATIC_SDL_IMAGE) if(DEVILUTIONX_SYSTEM_SDL_IMAGE) _find_SDL_image(REQUIRED) else() add_subdirectory(3rdParty/SDL_image) endif() endif() endif() if(NOT DEFINED DEVILUTIONX_SYSTEM_LIBFMT) find_package(fmt 8.0.0 QUIET) if(fmt_FOUND) message("-- Found fmt ${fmt_VERSION}") else() message("-- Suitable system fmt package not found, will use fmt from source") set(DEVILUTIONX_SYSTEM_LIBFMT OFF) endif() endif() dependency_options("libfmt" DEVILUTIONX_SYSTEM_LIBFMT ON DEVILUTIONX_STATIC_LIBFMT) if(DEVILUTIONX_SYSTEM_LIBFMT) find_package(fmt 8.0.0 REQUIRED) else() add_subdirectory(3rdParty/libfmt) endif() if(NOT NOSOUND) if(USE_SDL3) if(NOT DEFINED DEVILUTIONX_SYSTEM_SDL_MIXER) find_package(SDL3_mixer QUIET CONFIG) if(SDL3_mixer_FOUND) message("-- Found SDL3_mixer") else() message("-- Suitable system SDL3_mixer package not found, will use SDL3_mixer from source") set(DEVILUTIONX_SYSTEM_SDL_MIXER OFF) endif() endif() dependency_options("SDL3_mixer" DEVILUTIONX_SYSTEM_SDL_MIXER ON DEVILUTIONX_STATIC_SDL_MIXER) if(DEVILUTIONX_SYSTEM_SDL_MIXER) find_package(SDL3_mixer REQUIRED CONFIG) else() add_subdirectory(3rdParty/SDL3_mixer) endif() else() if(NOT DEFINED DEVILUTIONX_SYSTEM_SDL_AUDIOLIB) find_package(SDL_audiolib QUIET) if(SDL_audiolib_FOUND) message("-- Found SDL_audiolib") else() message("-- Suitable system SDL_audiolib package not found, will use SDL_audiolib from source") set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) endif() endif() dependency_options("SDL_audiolib" DEVILUTIONX_SYSTEM_SDL_AUDIOLIB ON DEVILUTIONX_STATIC_SDL_AUDIOLIB) if(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB) find_package(SDL_audiolib REQUIRED) else() add_subdirectory(3rdParty/SDL_audiolib) endif() endif() endif() if(PACKET_ENCRYPTION) dependency_options("libsodium" DEVILUTIONX_SYSTEM_LIBSODIUM ON DEVILUTIONX_STATIC_LIBSODIUM) if(DEVILUTIONX_SYSTEM_LIBSODIUM) set(sodium_USE_STATIC_LIBS ${DEVILUTIONX_STATIC_LIBSODIUM}) find_package(sodium REQUIRED) else() add_subdirectory(3rdParty/libsodium) endif() endif() add_subdirectory(3rdParty/libsmackerdec) if(WIN32 AND NOT UWP_LIB) add_subdirectory(3rdParty/find_steam_game) endif() if(SUPPORTS_MPQ) add_subdirectory(3rdParty/libmpq) endif() add_subdirectory(3rdParty/tl) if(NOT DEFINED DEVILUTIONX_SYSTEM_SHEENBIDI) find_package(SheenBidi 2.9.0 QUIET) if(SheenBidi_FOUND) message("-- Found SheenBidi ${SheenBidi_VERSION}") else() message("-- Suitable system SheenBidi package not found, will use SheenBidi from source") set(DEVILUTIONX_SYSTEM_SHEENBIDI OFF) endif() endif() dependency_options("SheenBidi" DEVILUTIONX_SYSTEM_SHEENBIDI ON DEVILUTIONX_STATIC_SHEENBIDI) if(DEVILUTIONX_SYSTEM_SHEENBIDI) find_package(SheenBidi REQUIRED) else() add_subdirectory(3rdParty/SheenBidi) endif() if(NOT DEFINED DEVILUTIONX_SYSTEM_UNORDERED_DENSE) find_package(unordered_dense CONFIG QUIET) if (unordered_dense_FOUND) message("-- Found unordered_dense") else() message("-- Suitable unordered_dense package not found, will use unordered_dense from source") set(DEVILUTIONX_SYSTEM_UNORDERED_DENSE OFF) endif() dependency_options("unordered_dense" DEVILUTIONX_SYSTEM_UNORDERED_DENSE ON DEVILUTIONX_STATIC_UNORDERED_DENSE) endif() if(DEVILUTIONX_SYSTEM_UNORDERED_DENSE) find_package(unordered_dense CONFIG REQUIRED) else() add_subdirectory(3rdParty/unordered_dense) endif() if(NOT DEFINED DEVILUTIONX_SYSTEM_MAGIC_ENUM) find_package(magic_enum 0.9.7 QUIET) if(magic_enum_FOUND) message("-- Found magic_enum ${magic_enum_VERSION}") else() message("-- Suitable system magic_enum package not found, will use magic_enum from source") set(DEVILUTIONX_SYSTEM_MAGIC_ENUM OFF) endif() endif() dependency_options("magic_enum" DEVILUTIONX_SYSTEM_MAGIC_ENUM ON DEVILUTIONX_STATIC_MAGIC_ENUM) if(DEVILUTIONX_SYSTEM_MAGIC_ENUM) find_package(magic_enum REQUIRED) else() add_subdirectory(3rdParty/magic_enum) endif() if(SUPPORTS_MPQ OR NOT NONET) add_subdirectory(3rdParty/PKWare) endif() if(NOT NONET AND NOT DISABLE_TCP) add_subdirectory(3rdParty/asio) endif() if(NOT NONET AND NOT DISABLE_ZERO_TIER) add_subdirectory(3rdParty/libzt) endif() if(DISCORD_INTEGRATION) add_subdirectory(3rdParty/discord) endif() if(BUILD_TESTING) dependency_options("googletest" DEVILUTIONX_SYSTEM_GOOGLETEST ON DEVILUTIONX_STATIC_GOOGLETEST) if(DEVILUTIONX_SYSTEM_GOOGLETEST) find_package(GTest REQUIRED) if(NOT TARGET GTest::gtest) add_library(GTest::gtest ALIAS GTest::GTest) endif() else() add_subdirectory(3rdParty/googletest) endif() dependency_options("benchmark" DEVILUTIONX_SYSTEM_BENCHMARK ON DEVILUTIONX_STATIC_BENCHMARK) if(DEVILUTIONX_SYSTEM_BENCHMARK) find_package(benchmark REQUIRED) else() add_subdirectory(3rdParty/benchmark) endif() endif() if(GPERF) find_package(Gperftools REQUIRED) message("INFO: ${GPERFTOOLS_LIBRARIES}") endif() ================================================ FILE: CMake/Mods.cmake ================================================ include(functions/copy_files) include(functions/trim_retired_files) if(NOT DEFINED DEVILUTIONX_MODS_OUTPUT_DIRECTORY) set(DEVILUTIONX_MODS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/mods") endif() set(hellfire_mod lua/mods/Hellfire/init.lua nlevels/cutl5w.clx nlevels/cutl6w.clx nlevels/l5data/cornerstone.dun nlevels/l5data/uberroom.dun txtdata/classes/sorcerer/starting_loadout.tsv txtdata/classes/classdat.tsv txtdata/items/item_prefixes.tsv txtdata/items/item_suffixes.tsv txtdata/items/unique_itemdat.tsv txtdata/missiles/misdat.tsv txtdata/missiles/missile_sprites.tsv txtdata/monsters/monstdat.tsv txtdata/sound/effects.tsv txtdata/spells/spelldat.tsv txtdata/towners/quest_dialog.tsv txtdata/towners/towners.tsv ui_art/diablo.pal ui_art/hf_titlew.clx ui_art/supportw.clx ui_art/mainmenuw.clx) if(NOT UNPACKED_MPQS) list(APPEND hellfire_mod data/inv/objcurs2-widths.txt) endif() if(APPLE) foreach(asset_file ${hellfire_mod}) set(src "${CMAKE_CURRENT_SOURCE_DIR}/mods/Hellfire/${asset_file}") get_filename_component(_asset_dir "${asset_file}" DIRECTORY) set_source_files_properties("${src}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/mods/Hellfire/${_asset_dir}" XCODE_EXPLICIT_FILE_TYPE compiled) target_sources(${BIN_TARGET} PRIVATE "${src}") endforeach() else() copy_files( FILES ${hellfire_mod} SRC_PREFIX "mods/Hellfire/" OUTPUT_DIR "${DEVILUTIONX_MODS_OUTPUT_DIRECTORY}/Hellfire" OUTPUT_VARIABLE HELLFIRE_OUTPUT_FILES) set(HELLFIRE_MPQ_FILES ${hellfire_mod}) add_trim_target(hellfire_trim_assets ROOT_FOLDER "${DEVILUTIONX_MODS_OUTPUT_DIRECTORY}/Hellfire" CURRENT_FILES ${HELLFIRE_MPQ_FILES}) if(BUILD_ASSETS_MPQ) set(HELLFIRE_MPQ "${DEVILUTIONX_MODS_OUTPUT_DIRECTORY}/Hellfire.mpq") add_custom_command( COMMENT "Building Hellfire.mpq" OUTPUT "${HELLFIRE_MPQ}" COMMAND ${CMAKE_COMMAND} -E remove -f "${HELLFIRE_MPQ}" COMMAND ${SMPQ} -A -M 1 -C BZIP2 -c "${HELLFIRE_MPQ}" ${HELLFIRE_MPQ_FILES} WORKING_DIRECTORY "${DEVILUTIONX_MODS_OUTPUT_DIRECTORY}/Hellfire" DEPENDS ${TRIM_COMMAND_BYPRODUCT} ${HELLFIRE_OUTPUT_FILES} VERBATIM) add_custom_target(hellfire_mpq DEPENDS "${HELLFIRE_MPQ}") add_dependencies(hellfire_mpq hellfire_trim_assets) add_dependencies(libdevilutionx hellfire_mpq) else() add_custom_target(hellfire_copied_assets DEPENDS ${HELLFIRE_OUTPUT_FILES}) add_dependencies(hellfire_copied_assets hellfire_trim_assets) add_dependencies(libdevilutionx hellfire_copied_assets) endif() endif() ================================================ FILE: CMake/MoldLinker.cmake ================================================ if(NOT CMAKE_CROSSCOMPILING) find_program( LD_MOLD_PATH ld PATHS ${CMAKE_INSTALL_PREFIX}/libexec/mold ENV LD_MOLD_PATH NO_DEFAULT_PATH ) if(NOT LD_MOLD_PATH STREQUAL "LD_MOLD_PATH-NOTFOUND") set(_have_ld_mold ON) else() set(_have_ld_mold OFF) endif() endif() option(USE_LD_MOLD "Use mold linker" ${_have_ld_mold}) if(USE_LD_MOLD) message("-- Using Mold linker (pass -DUSE_LD_MOLD=OFF to disable)") if(CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 12) if (_have_ld_mold) get_filename_component(_mold_dir ${LD_MOLD_PATH} DIRECTORY) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -B${_mold_dir}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -B${_mold_dir}") else() message(WARNING "Cannot use mold linker: mold ld directory not found") endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=mold") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=mold") endif() endif() ================================================ FILE: CMake/Platforms.cmake ================================================ if(HAIKU) include(platforms/haiku) endif() if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD|OpenBSD|DragonFly|NetBSD") if(CMAKE_SYSTEM_NAME MATCHES "NetBSD") add_definitions(-D_NETBSD_SOURCE) else() add_definitions(-D_BSD_SOURCE) set(UBSAN OFF) endif() set(ASAN OFF) add_definitions(-DO_LARGEFILE=0 -Dstat64=stat -Dlstat64=lstat -Dlseek64=lseek -Doff64_t=off_t -Dfstat64=fstat -Dftruncate64=ftruncate) endif() set(TARGET_PLATFORM host CACHE STRING "Target platform") set_property(CACHE TARGET_PLATFORM PROPERTY STRINGS host retrofw rg99 rg350 gkd350h cpigamesh miyoo_mini windows9x windowsXP) if(TARGET_PLATFORM STREQUAL "retrofw") include(platforms/retrofw) elseif(TARGET_PLATFORM STREQUAL "rg99") include(platforms/rg99) elseif(TARGET_PLATFORM STREQUAL "rg350") include(platforms/rg350) elseif(TARGET_PLATFORM STREQUAL "gkd350h") include(platforms/gkd350h) elseif(TARGET_PLATFORM STREQUAL "cpigamesh") include(platforms/cpigamesh) elseif(TARGET_PLATFORM STREQUAL "lepus") include(platforms/lepus) elseif(TARGET_PLATFORM STREQUAL "miyoo_mini") include(platforms/miyoo_mini) elseif(TARGET_PLATFORM STREQUAL "windows9x") include(platforms/windows9x) elseif(TARGET_PLATFORM STREQUAL "windowsXP") include(platforms/windowsXP) elseif(TARGET_PLATFORM STREQUAL "dos") include(platforms/dos) elseif(WIN32) include(platforms/windows) endif() if(NINTENDO_SWITCH) include(platforms/switch) endif() if(AMIGA) include(platforms/amiga) endif() if(NINTENDO_3DS) include(platforms/n3ds) endif() if(VITA) include("$ENV{VITASDK}/share/vita.cmake" REQUIRED) include(platforms/vita) endif() if(PS4) include(platforms/ps4) endif() if(ANDROID) include(platforms/android) endif() if(IOS) include(platforms/ios) endif() if(EMSCRIPTEN) include(platforms/emscripten) endif() if(UWP_LIB) include(platforms/uwp_lib) endif() if(NXDK) include(platforms/xbox_nxdk) endif() if(CMAKE_SYSTEM_NAME MATCHES "Darwin") # Some notable Darwin (macOS kernel) versions are: # 8.x == macOS 10.4 (Tiger) # 9.x == macOS 10.5 (Leopard) # # Importantly, a lot of the APIs first appeared in version 9, including # the feature availability API (the header). # # For Darwin 8 and below, we have to rely on the kernel version # to detect available APIs. string(REGEX REPLACE "^([0-9]+)\\.([0-9]+).*$" "\\1" DARWIN_MAJOR_VERSION "${CMAKE_SYSTEM_VERSION}") string(REGEX REPLACE "^([0-9]+)\\.([0-9]+).*$" "\\2" DARWIN_MINOR_VERSION "${CMAKE_SYSTEM_VERSION}") if(DARWIN_MAJOR_VERSION VERSION_EQUAL 8) include(platforms/macos_tiger) endif() # For older macOS, we assume MacPorts because Homebrew only supports newer version if(DARWIN_MAJOR_VERSION VERSION_LESS 11) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/platforms/macports/finders") # On MacPorts, libfmt is in a subdirectory: list(APPEND CMAKE_MODULE_PATH "/opt/local/lib/libfmt11/cmake") endif() endif() ================================================ FILE: CMake/Tests.cmake ================================================ include(GoogleTest) include(functions/copy_files) add_library(libdevilutionx_so SHARED) set_target_properties(libdevilutionx_so PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) target_link_dependencies(libdevilutionx_so PUBLIC libdevilutionx) set_target_properties(libdevilutionx_so PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) add_library(test_main OBJECT test/main.cpp) target_link_dependencies(test_main PUBLIC libdevilutionx_so GTest::gtest GTest::gmock) set(tests animationinfo_test appfat_test automap_test cursor_test dead_test diablo_test drlg_common_test drlg_l1_test drlg_l2_test drlg_l3_test drlg_l4_test effects_test inv_test items_test math_test missiles_test multi_logging_test pack_test player_test quests_test scrollrt_test stores_test tile_properties_test timedemo_test townerdat_test writehero_test vendor_test ) set(standalone_tests codec_test crawl_test data_file_test file_util_test format_int_test ini_test palette_blending_test parse_int_test path_test vision_test random_test rectangle_test static_vector_test str_cat_test utf8_test ) if(NOT USE_SDL1) list(APPEND standalone_tests text_render_integration_test) endif() set(benchmarks clx_render_benchmark crawl_benchmark dun_render_benchmark light_render_benchmark palette_blending_benchmark path_benchmark ) include(test/Fixtures.cmake) foreach(test_target ${tests} ${standalone_tests} ${benchmarks}) add_executable(${test_target} "test/${test_target}.cpp") set_target_properties(${test_target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) if(GPERF) target_link_libraries(${test_target} PUBLIC ${GPERFTOOLS_LIBRARIES}) endif() endforeach() foreach(test_target ${tests} ${standalone_tests}) gtest_discover_tests(${test_target}) endforeach() foreach(test_target ${tests}) target_link_libraries(${test_target} PRIVATE test_main) endforeach() foreach(test_target ${standalone_tests}) target_link_libraries(${test_target} PRIVATE GTest::gtest_main) target_include_directories(${test_target} PRIVATE "${PROJECT_SOURCE_DIR}/Source") endforeach() foreach(target ${benchmarks}) target_link_libraries(${target} PRIVATE benchmark::benchmark benchmark::benchmark_main) target_include_directories(${target} PRIVATE "${PROJECT_SOURCE_DIR}/Source") endforeach() add_library(app_fatal_for_testing OBJECT test/app_fatal_for_testing.cpp) target_sources(app_fatal_for_testing INTERFACE $) add_library(language_for_testing OBJECT test/language_for_testing.cpp) target_sources(language_for_testing INTERFACE $) target_link_dependencies(codec_test PRIVATE libdevilutionx_codec app_fatal_for_testing) target_link_dependencies(clx_render_benchmark PRIVATE DevilutionX::SDL tl app_fatal_for_testing language_for_testing libdevilutionx_clx_render libdevilutionx_load_clx libdevilutionx_log libdevilutionx_surface ) target_link_dependencies(crawl_test PRIVATE libdevilutionx_crawl) target_link_dependencies(crawl_benchmark PRIVATE libdevilutionx_crawl) target_link_dependencies(data_file_test PRIVATE libdevilutionx_txtdata app_fatal_for_testing language_for_testing) target_link_dependencies(dun_render_benchmark PRIVATE libdevilutionx_so) target_link_dependencies(file_util_test PRIVATE libdevilutionx_file_util app_fatal_for_testing) target_link_dependencies(format_int_test PRIVATE libdevilutionx_format_int language_for_testing) target_link_dependencies(ini_test PRIVATE libdevilutionx_ini app_fatal_for_testing) target_link_dependencies(light_render_benchmark PRIVATE libdevilutionx_light_render DevilutionX::SDL libdevilutionx_surface libdevilutionx_paths app_fatal_for_testing) target_link_dependencies(palette_blending_test PRIVATE libdevilutionx_palette_blending DevilutionX::SDL libdevilutionx_strings GTest::gmock app_fatal_for_testing) target_link_dependencies(palette_blending_benchmark PRIVATE DevilutionX::SDL libdevilutionx_palette_blending libdevilutionx_palette_kd_tree app_fatal_for_testing ) target_link_dependencies(parse_int_test PRIVATE libdevilutionx_parse_int) target_link_dependencies(path_test PRIVATE libdevilutionx_pathfinding libdevilutionx_direction app_fatal_for_testing) target_link_dependencies(vision_test PRIVATE libdevilutionx_vision) target_link_dependencies(path_benchmark PRIVATE libdevilutionx_pathfinding app_fatal_for_testing) target_link_dependencies(random_test PRIVATE libdevilutionx_random) target_link_dependencies(static_vector_test PRIVATE libdevilutionx_random app_fatal_for_testing) target_link_dependencies(str_cat_test PRIVATE libdevilutionx_strings) if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG AND NOT USE_SDL1) target_link_dependencies(text_render_integration_test PRIVATE DevilutionX::SDL GTest::gmock GTest::gtest fmt::fmt tl app_fatal_for_testing language_for_testing libdevilutionx_primitive_render libdevilutionx_strings libdevilutionx_surface libdevilutionx_surface_to_png libdevilutionx_text_render ) copy_files( FILES basic-colors.png basic.png horizontal_overflow.png horizontal_overflow-colors.png kerning_fit_spacing-colors.png kerning_fit_spacing.png kerning_fit_spacing__align_center-colors.png kerning_fit_spacing__align_center.png kerning_fit_spacing__align_center__newlines.png kerning_fit_spacing__align_center__newlines_in_fmt-colors.png kerning_fit_spacing__align_center__newlines_in_value-colors.png kerning_fit_spacing__align_right-colors.png kerning_fit_spacing__align_right.png vertical_overflow.png vertical_overflow-colors.png cursor-start.png cursor-middle.png cursor-end.png multiline_cursor-end_first_line.png multiline_cursor-start_second_line.png multiline_cursor-middle_second_line.png multiline_cursor-end_second_line.png highlight-partial.png highlight-full.png multiline_highlight.png SRC_PREFIX test/fixtures/text_render_integration_test/ OUTPUT_DIR "${DEVILUTIONX_TEST_FIXTURES_OUTPUT_DIRECTORY}/text_render_integration_test" OUTPUT_VARIABLE _text_render_integration_test_fixtures ) add_custom_target(text_render_integration_test_resources DEPENDS "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/fonts/12-00.clx" "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/fonts/goldui.trn" "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/fonts/golduis.trn" "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/fonts/grayuis.trn" "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/fonts/grayui.trn" "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}/ui_art/diablo.pal" ${_text_render_integration_test_fixtures} ) add_dependencies(text_render_integration_test text_render_integration_test_resources) endif() target_link_dependencies(utf8_test PRIVATE libdevilutionx_utf8) target_include_directories(writehero_test PRIVATE 3rdParty/PicoSHA2) ================================================ FILE: CMake/VcPkgManifestFeatures.cmake ================================================ # See https://vcpkg.readthedocs.io/en/latest/users/manifests/ if(USE_SDL1) list(APPEND VCPKG_MANIFEST_FEATURES "sdl1") elseif(USE_SDL3) list(APPEND VCPKG_MANIFEST_FEATURES "sdl3") else() list(APPEND VCPKG_MANIFEST_FEATURES "sdl2") endif() if(PACKET_ENCRYPTION) list(APPEND VCPKG_MANIFEST_FEATURES "encryption") endif() if(USE_GETTEXT_FROM_VCPKG) list(APPEND VCPKG_MANIFEST_FEATURES "translations") endif() if(BUILD_TESTING) list(APPEND VCPKG_MANIFEST_FEATURES "tests") endif() ================================================ FILE: CMake/finders/FindGperftools.cmake ================================================ # Based on https://github.com/baidu/braft/blob/e7776cd03ccc04f18d0f0911200617a89ac3cdf0/cmake/FindGperftools.cmake # Tries to find Gperftools. # # Usage of this module as follows: # # find_package(Gperftools) # # Variables used by this module, they can change the default behaviour and need # to be set before calling find_package: # # Gperftools_ROOT_DIR Set this variable to the root installation of # Gperftools if the module has problems finding # the proper installation path. # # Variables defined by this module: # # GPERFTOOLS_FOUND System has Gperftools libs/headers # GPERFTOOLS_LIBRARIES The Gperftools libraries (tcmalloc & profiler) # GPERFTOOLS_INCLUDE_DIR The location of Gperftools headers find_library(GPERFTOOLS_TCMALLOC NAMES tcmalloc HINTS ${Gperftools_ROOT_DIR}/lib) find_library(GPERFTOOLS_PROFILER NAMES profiler HINTS ${Gperftools_ROOT_DIR}/lib) find_library(GPERFTOOLS_TCMALLOC_AND_PROFILER NAMES tcmalloc_and_profiler HINTS ${Gperftools_ROOT_DIR}/lib) find_path(GPERFTOOLS_INCLUDE_DIR NAMES gperftools/heap-profiler.h HINTS ${Gperftools_ROOT_DIR}/include) set(GPERFTOOLS_LIBRARIES ${GPERFTOOLS_TCMALLOC_AND_PROFILER}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args( Gperftools DEFAULT_MSG GPERFTOOLS_LIBRARIES GPERFTOOLS_INCLUDE_DIR) mark_as_advanced( Gperftools_ROOT_DIR GPERFTOOLS_TCMALLOC GPERFTOOLS_PROFILER GPERFTOOLS_TCMALLOC_AND_PROFILER GPERFTOOLS_LIBRARIES GPERFTOOLS_INCLUDE_DIR) ================================================ FILE: CMake/finders/FindSDL2_image.cmake ================================================ find_package(SDL2_image CONFIG) if(TARGET SDL2_image::SDL2_image) if(NOT TARGET SDL2::SDL2_image) add_library(SDL2::SDL2_image ALIAS SDL2_image::SDL2_image) endif() else() find_package(PkgConfig) pkg_check_modules(PC_SDL2_image QUIET SDL2_image>=2.0.0) find_path(SDL2_image_INCLUDE_DIR SDL_image.h HINTS ${PC_SDL2_image_INCLUDEDIR} ${PC_SDL2_image_INCLUDE_DIRS}) find_library(SDL2_image_LIBRARY NAMES SDL2_image libSDL2_image HINTS ${PC_SDL2_image_LIBDIR} ${PC_SDL2_image_LIBRARY_DIRS}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(SDL2_image DEFAULT_MSG SDL2_image_INCLUDE_DIR SDL2_image_LIBRARY) if(SDL2_image_FOUND AND NOT TARGET SDL2::SDL2_image) add_library(SDL2::SDL2_image UNKNOWN IMPORTED) set_target_properties(SDL2::SDL2_image PROPERTIES IMPORTED_LOCATION ${SDL2_image_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${SDL2_image_INCLUDE_DIR}) endif() if(SDL2_image_FOUND) mark_as_advanced(SDL2_image_INCLUDE_DIR SDL2_image_LIBRARY) set(SDL2_image_LIBRARIES ${SDL2_image_LIBRARY}) set(SDL2_image_INCLUDE_DIRS ${SDL2_image_INCLUDE_DIR}) endif() endif() ================================================ FILE: CMake/finders/FindSDL_audiolib.cmake ================================================ find_package(PkgConfig) pkg_check_modules(PC_SDL_audiolib QUIET SDL_audiolib) find_path(SDL_audiolib_INCLUDE_DIR aulib.h HINTS ${PC_SDL_audiolib_INCLUDEDIR} ${PC_SDL_audiolib_INCLUDE_DIRS}) find_library(SDL_audiolib_LIBRARY NAMES SDL_audiolib libSDL_audiolib HINTS ${PC_SDL_audiolib_LIBDIR} ${PC_SDL_audiolib_LIBRARY_DIRS}) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(SDL_audiolib DEFAULT_MSG SDL_audiolib_INCLUDE_DIR SDL_audiolib_LIBRARY) if(SDL_audiolib_FOUND AND NOT TARGET SDL_audiolib::SDL_audiolib) add_library(SDL_audiolib::SDL_audiolib UNKNOWN IMPORTED) set_target_properties(SDL_audiolib::SDL_audiolib PROPERTIES IMPORTED_LOCATION ${SDL_audiolib_LIBRARY} INTERFACE_INCLUDE_DIRECTORIES ${SDL_audiolib_INCLUDE_DIR}) endif() if(SDL_audiolib_FOUND) mark_as_advanced(SDL_audiolib_INCLUDE_DIR SDL_audiolib_LIBRARY) set(SDL_audiolib_LIBRARIES ${SDL_audiolib_LIBRARY}) set(SDL_audiolib_INCLUDE_DIRS ${SDL_audiolib_INCLUDE_DIR}) endif() ================================================ FILE: CMake/finders/FindSpeechd.cmake ================================================ # find speech-dispatcher library and header if available # Copyright (c) 2009, Jeremy Whiting # Copyright (c) 2011, Raphael Kubo da Costa # This module defines # SPEECHD_INCLUDE_DIR, where to find libspeechd.h # SPEECHD_LIBRARIES, the libraries needed to link against speechd # SPEECHD_FOUND, If false, speechd was not found # # Redistribution and use is allowed according to the terms of the BSD license. # For details see the accompanying COPYING-CMAKE-SCRIPTS file. find_path(SPEECHD_INCLUDE_DIR libspeechd.h PATH_SUFFIXES speech-dispatcher) find_library(SPEECHD_LIBRARIES NAMES speechd) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Speechd REQUIRED_VARS SPEECHD_INCLUDE_DIR SPEECHD_LIBRARIES) ================================================ FILE: CMake/finders/Findsimpleini.cmake ================================================ find_package(PkgConfig QUIET) if(PKG_CONFIG_FOUND) pkg_check_modules(PC_simpleini QUIET simpleini) endif() find_path(simpleini_INCLUDE_DIR SimpleIni.h HINTS ${PC_simpleini_INCLUDEDIR} ${PC_simpleini_INCLUDE_DIRS} ${SIMPLEINI_INCLUDE_DIRS}) find_library(simpleini_LIBRARY simpleini HINTS ${PC_simpleini_LIBDIR} ${PC_simpleini_LIBRARY_DIRS} ${SIMPLEINI_LIBRARY_DIRS}) list(APPEND _required_vars "simpleini_INCLUDE_DIR") if(NOT simpleini_INCLUDE_DIR STREQUAL "simpleini_INCLUDE_DIR-NOTFOUND") file(READ "${simpleini_INCLUDE_DIR}/SimpleIni.h" _version_header_content) if(_version_header_content MATCHES "Version *([0-9.]+)") set(simpleini_VERSION "${CMAKE_MATCH_1}") endif() endif() # SimpleIni can be distributed as a header-only library, so the library is optional. if(NOT simpleini_LIBRARY STREQUAL "simpleini_LIBRARY-NOTFOUND") list(APPEND _required_vars "simpleini_LIBRARY") endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(simpleini REQUIRED_VARS ${_required_vars} VERSION_VAR simpleini_VERSION) if(simpleini_FOUND AND NOT TARGET simpleini::simpleini) if(simpleini_LIBRARY STREQUAL "simpleini_LIBRARY-NOTFOUND") # Header-only distribution. add_library(simpleini INTERFACE) target_include_directories(simpleini INTERFACE ${simpleini_INCLUDE_DIR}) add_library(simpleini::simpleini ALIAS simpleini) else() # A distribution with a library. add_library(simpleini::simpleini UNKNOWN IMPORTED) set_target_properties(simpleini::simpleini PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${simpleini_INCLUDE_DIR} IMPORTED_LOCATION ${simpleini_LIBRARY}) endif() endif() if(simpleini_FOUND) mark_as_advanced(simpleini_INCLUDE_DIR simpleini_LIBRARY) set(simpleini_LIBRARIES ${simpleini_LIBRARY}) set(simpleini_INCLUDE_DIRS ${simpleini_INCLUDE_DIR}) endif() ================================================ FILE: CMake/finders/Findsodium.cmake ================================================ # Written in 2016 by Henrik Steffen Gaßmann # # To the extent possible under law, the author(s) have dedicated all # copyright and related and neighboring rights to this software to the # public domain worldwide. This software is distributed without any warranty. # # You should have received a copy of the CC0 Public Domain Dedication # along with this software. If not, see # # https://creativecommons.org/publicdomain/zero/1.0/ # ######################################################################## # Tries to find the local libsodium installation. # # On Windows the sodium_DIR environment variable is used as a default # hint which can be overridden by setting the corresponding cmake variable. # # Once done the following variables will be defined: # # sodium_FOUND # sodium_INCLUDE_DIR # sodium_LIBRARY_DEBUG # sodium_LIBRARY_RELEASE # sodium_VERSION_STRING # # Furthermore an imported "sodium" target is created. # if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") set(_GCC_COMPATIBLE 1) endif() # static library option if (NOT DEFINED sodium_USE_STATIC_LIBS) option(sodium_USE_STATIC_LIBS "enable to statically link against sodium" OFF) endif() if(NOT (sodium_USE_STATIC_LIBS EQUAL sodium_USE_STATIC_LIBS_LAST)) unset(sodium_LIBRARY CACHE) unset(sodium_LIBRARY_DEBUG CACHE) unset(sodium_LIBRARY_RELEASE CACHE) unset(sodium_DLL_DEBUG CACHE) unset(sodium_DLL_RELEASE CACHE) set(sodium_USE_STATIC_LIBS_LAST ${sodium_USE_STATIC_LIBS} CACHE INTERNAL "internal change tracking variable") endif() ######################################################################## # UNIX if (UNIX OR CMAKE_SYSTEM_NAME STREQUAL "Generic" OR AMIGA) # import pkg-config find_package(PkgConfig QUIET) if (PKG_CONFIG_FOUND) pkg_check_modules(sodium_PKG QUIET libsodium) endif() if(sodium_USE_STATIC_LIBS) if(sodium_PKG_STATIC_FOUND) # Create a temporary list to manipulate the list of libraries we found set(sodium_PKG_STATIC_LIBRARIES_TMP "") # Mangle the library names into the format we need foreach(_libname ${sodium_PKG_STATIC_LIBRARIES}) if (NOT _libname MATCHES "^lib.*\\.a$") # ignore strings already ending with .a list(APPEND sodium_PKG_STATIC_LIBRARIES_TMP "lib${_libname}.a") endif() endforeach() list(REMOVE_DUPLICATES sodium_PKG_STATIC_LIBRARIES_TMP) # Replace the list with our processed one set(sodium_PKG_STATIC_LIBRARIES ${sodium_PKG_STATIC_LIBRARIES_TMP}) else() # if pkgconfig for libsodium doesn't provide # static lib info, then override PKG_STATIC here.. set(sodium_PKG_STATIC_LIBRARIES libsodium.a) endif() set(XPREFIX sodium_PKG_STATIC) else() if(sodium_PKG_FOUND) set(sodium_PKG_LIBRARIES sodium) endif() set(XPREFIX sodium_PKG) endif() # Feed pkgconfig results (if found) into standard find_* to populate # the right CMake cache variables find_path(sodium_INCLUDE_DIR sodium.h HINTS ${${XPREFIX}_INCLUDE_DIRS} ) find_library(sodium_LIBRARY_DEBUG NAMES ${${XPREFIX}_LIBRARIES} HINTS ${${XPREFIX}_LIBRARY_DIRS} ) find_library(sodium_LIBRARY_RELEASE NAMES ${${XPREFIX}_LIBRARIES} HINTS ${${XPREFIX}_LIBRARY_DIRS} ) ######################################################################## # Windows elseif (WIN32) set(sodium_DIR "$ENV{sodium_DIR}" CACHE FILEPATH "sodium install directory") mark_as_advanced(sodium_DIR) find_path(sodium_INCLUDE_DIR sodium.h HINTS ${sodium_DIR} PATH_SUFFIXES include ) if (MSVC) # detect target architecture file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/arch.c" [=[ #if defined _M_IX86 #error ARCH_VALUE x86_32 #elif defined _M_X64 #error ARCH_VALUE x86_64 #endif #error ARCH_VALUE unknown ]=]) try_compile(_UNUSED_VAR "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}/arch.c" OUTPUT_VARIABLE _COMPILATION_LOG ) # construct library path if (CMAKE_SIZEOF_VOID_P EQUAL 4) string(APPEND _PLATFORM_PATH "Win32") elseif(CMAKE_SIZEOF_VOID_P EQUAL 8) string(APPEND _PLATFORM_PATH "x64") else() message(FATAL_ERROR "Can't find target architecture. CMAKE_SIZEOF_VOID_P not 4 or 8.") endif() string(APPEND _PLATFORM_PATH "/$$CONFIG$$") if (MSVC_VERSION LESS 1900) math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 60") else() math(EXPR _VS_VERSION "${MSVC_VERSION} / 10 - 50") endif() string(APPEND _PLATFORM_PATH "/v${_VS_VERSION}") if (sodium_USE_STATIC_LIBS) string(APPEND _PLATFORM_PATH "/static") else() string(APPEND _PLATFORM_PATH "/dynamic") endif() string(REPLACE "$$CONFIG$$" "Debug" _DEBUG_PATH_SUFFIX "${_PLATFORM_PATH}") string(REPLACE "$$CONFIG$$" "Release" _RELEASE_PATH_SUFFIX "${_PLATFORM_PATH}") find_library(sodium_LIBRARY_DEBUG libsodium.lib HINTS ${sodium_DIR} PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} ) find_library(sodium_LIBRARY_RELEASE libsodium.lib HINTS ${sodium_DIR} PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} ) if (NOT sodium_USE_STATIC_LIBS) set(CMAKE_FIND_LIBRARY_SUFFIXES_BCK ${CMAKE_FIND_LIBRARY_SUFFIXES}) set(CMAKE_FIND_LIBRARY_SUFFIXES ".dll") find_library(sodium_DLL_DEBUG libsodium HINTS ${sodium_DIR} PATH_SUFFIXES ${_DEBUG_PATH_SUFFIX} ) find_library(sodium_DLL_RELEASE libsodium HINTS ${sodium_DIR} PATH_SUFFIXES ${_RELEASE_PATH_SUFFIX} ) set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES_BCK}) endif() elseif(_GCC_COMPATIBLE) if (sodium_USE_STATIC_LIBS) find_library(sodium_LIBRARY_DEBUG libsodium.a HINTS ${sodium_DIR} PATH_SUFFIXES lib ) find_library(sodium_LIBRARY_RELEASE libsodium.a HINTS ${sodium_DIR} PATH_SUFFIXES lib ) else() find_library(sodium_LIBRARY_DEBUG libsodium.dll.a HINTS ${sodium_DIR} PATH_SUFFIXES lib ) find_library(sodium_LIBRARY_RELEASE libsodium.dll.a HINTS ${sodium_DIR} PATH_SUFFIXES lib ) file(GLOB _DLL LIST_DIRECTORIES false RELATIVE "${sodium_DIR}/bin" "${sodium_DIR}/bin/libsodium*.dll" ) find_library(sodium_DLL_DEBUG ${_DLL} libsodium HINTS ${sodium_DIR} PATH_SUFFIXES bin ) find_library(sodium_DLL_RELEASE ${_DLL} libsodium HINTS ${sodium_DIR} PATH_SUFFIXES bin ) endif() else() message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") endif() ######################################################################## # unsupported else() message(FATAL_ERROR "this platform is not supported by FindSodium.cmake") endif() ######################################################################## # common stuff # extract sodium version if (sodium_INCLUDE_DIR) set(_VERSION_HEADER "${sodium_INCLUDE_DIR}/sodium/version.h") if(EXISTS "${_VERSION_HEADER}") file(READ "${_VERSION_HEADER}" _VERSION_HEADER_CONTENT) string(REGEX REPLACE ".*define[ \t]+SODIUM_VERSION_STRING[^\"]+\"([^\"]+)\".*" "\\1" sodium_VERSION_STRING "${_VERSION_HEADER_CONTENT}") set(sodium_VERSION_STRING "${sodium_VERSION_STRING}") endif() endif() # communicate results include(FindPackageHandleStandardArgs) find_package_handle_standard_args( sodium # The name must be either uppercase or match the filename case. REQUIRED_VARS sodium_LIBRARY_RELEASE sodium_LIBRARY_DEBUG sodium_INCLUDE_DIR VERSION_VAR sodium_VERSION_STRING ) # mark file paths as advanced mark_as_advanced(sodium_INCLUDE_DIR) mark_as_advanced(sodium_LIBRARY_DEBUG) mark_as_advanced(sodium_LIBRARY_RELEASE) if (WIN32) mark_as_advanced(sodium_DLL_DEBUG) mark_as_advanced(sodium_DLL_RELEASE) endif() # create imported target if(sodium_USE_STATIC_LIBS) set(_LIB_TYPE STATIC) else() set(_LIB_TYPE SHARED) endif() add_library(sodium ${_LIB_TYPE} IMPORTED) set_target_properties(sodium PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${sodium_INCLUDE_DIR}" IMPORTED_LINK_INTERFACE_LANGUAGES "C" ) if (sodium_USE_STATIC_LIBS) set_target_properties(sodium PROPERTIES INTERFACE_COMPILE_DEFINITIONS "SODIUM_STATIC" IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" ) else() if (UNIX) set_target_properties(sodium PROPERTIES IMPORTED_LOCATION "${sodium_LIBRARY_RELEASE}" IMPORTED_LOCATION_DEBUG "${sodium_LIBRARY_DEBUG}" ) elseif (WIN32) set_target_properties(sodium PROPERTIES IMPORTED_IMPLIB "${sodium_LIBRARY_RELEASE}" IMPORTED_IMPLIB_DEBUG "${sodium_LIBRARY_DEBUG}" ) if (NOT (sodium_DLL_DEBUG MATCHES ".*-NOTFOUND")) set_target_properties(sodium PROPERTIES IMPORTED_LOCATION_DEBUG "${sodium_DLL_DEBUG}" ) endif() if (NOT (sodium_DLL_RELEASE MATCHES ".*-NOTFOUND")) set_target_properties(sodium PROPERTIES IMPORTED_LOCATION_RELWITHDEBINFO "${sodium_DLL_RELEASE}" IMPORTED_LOCATION_MINSIZEREL "${sodium_DLL_RELEASE}" IMPORTED_LOCATION_RELEASE "${sodium_DLL_RELEASE}" ) endif() endif() endif() ================================================ FILE: CMake/functions/FetchContent_ExcludeFromAll_backport.cmake ================================================ if(CMAKE_VERSION VERSION_LESS "3.28.0") macro(FetchContent_Declare_ExcludeFromAll) FetchContent_Declare(${ARGV}) endmacro() # Like `FetchContent_MakeAvailable` but passes EXCLUDE_FROM_ALL to `add_subdirectory`. macro(FetchContent_MakeAvailable_ExcludeFromAll) foreach(name ${ARGV}) string(TOLOWER ${name} nameLower) FetchContent_GetProperties(${name}) if(NOT ${${nameLower}_POPULATED}) FetchContent_Populate(${name}) if(EXISTS ${${nameLower}_SOURCE_DIR}/CMakeLists.txt) add_subdirectory(${${nameLower}_SOURCE_DIR} ${${nameLower}_BINARY_DIR} EXCLUDE_FROM_ALL) endif() endif() endforeach() endmacro() else() macro(FetchContent_Declare_ExcludeFromAll) FetchContent_Declare(${ARGV} EXCLUDE_FROM_ALL) endmacro() macro(FetchContent_MakeAvailable_ExcludeFromAll) FetchContent_MakeAvailable(${ARGV}) endmacro() endif() ================================================ FILE: CMake/functions/copy_files.cmake ================================================ # copy_files( # FILES # OUTPUT_DIR # [SRC_PREFIX ] # [OUTPUT_VARIABLE ] # ) function(copy_files) set(options) set(oneValueArgs SRC_PREFIX OUTPUT_DIR OUTPUT_VARIABLE) set(multiValueArgs FILES) cmake_parse_arguments(PARSE_ARGV 0 arg "${options}" "${oneValueArgs}" "${multiValueArgs}" ) if(arg_UNPARSED_ARGUMENTS) message(FATAL "unknown arguments: ${arg_UNPARSED_ARGUMENTS}") endif() if(OUTPUT_DIR IN_LIST arg_KEYWORDS_MISSING_VALUES) message(FATAL "OUTPUT_DIR is required") endif() foreach(path ${arg_FILES}) set(src "${CMAKE_CURRENT_SOURCE_DIR}/${arg_SRC_PREFIX}${path}") set(dst "${arg_OUTPUT_DIR}/${path}") list(APPEND _out_paths "${dst}") add_custom_command( COMMENT "Copying ${arg_SRC_PREFIX}${path}" OUTPUT "${dst}" DEPENDS "${src}" COMMAND ${CMAKE_COMMAND} -E copy "${src}" "${dst}" VERBATIM ) endforeach() if(arg_OUTPUT_VARIABLE) set(${arg_OUTPUT_VARIABLE} ${_out_paths} PARENT_SCOPE) endif() endfunction() ================================================ FILE: CMake/functions/dependency_options.cmake ================================================ # This function defines 2 options for finding and linking a dependency: # # 1. ${SYSTEM_OPTION_NAME}: whether to use the system version of the dependency (default: ${DEFAULT_SYSTEM_VALUE}) # 2. ${STATIC_OPTION_NAME}: whether to link the dependency statically. # The default is ON if ${SYSTEM_OPTION_NAME} is OFF or if target does not support shared libraries. # # The ${LIB_NAME} argument is a human-readable library name only used in option description strings. function(dependency_options LIB_NAME SYSTEM_OPTION_NAME DEFAULT_SYSTEM_VALUE STATIC_OPTION_NAME) option(${SYSTEM_OPTION_NAME} "Use system-provided ${LIB_NAME}" ${DEFAULT_SYSTEM_VALUE}) get_property(_supports_shared_libs GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS) if(_supports_shared_libs) if(${SYSTEM_OPTION_NAME}) set(_static_default OFF) else() set(_static_default ON) endif() option(${STATIC_OPTION_NAME} "Link ${LIB_NAME} statically" ${_static_default}) else() set(${STATIC_OPTION_NAME} ON) set(${STATIC_OPTION_NAME} ON PARENT_SCOPE) endif() if(${STATIC_OPTION_NAME}) set(_msg_type "static") else() set(_msg_type "dynamic") endif() if(${SYSTEM_OPTION_NAME}) set(_msg_source "system library") else() set(_msg_source "library from source") endif() message("-- 📚 ${LIB_NAME}: ${_msg_type} ${_msg_source}") endfunction() ================================================ FILE: CMake/functions/devilutionx_library.cmake ================================================ include(functions/genex) include(functions/set_relative_file_macro) include(functions/object_libraries) # This function is equivalent to `add_library` but applies DevilutionX-specific # compilation flags to it. function(add_devilutionx_library NAME) add_library(${NAME} ${ARGN}) target_include_directories(${NAME} PUBLIC ${PROJECT_SOURCE_DIR}/Source) target_compile_definitions(${NAME} PUBLIC ${DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS}) target_compile_options(${NAME} PUBLIC ${DEVILUTIONX_PLATFORM_COMPILE_OPTIONS}) genex_for_option(DEBUG) target_compile_definitions(${NAME} PUBLIC "$<${DEBUG_GENEX}:_DEBUG>") if(NOT NONET AND NOT DISABLE_TCP) target_compile_definitions(${NAME} PUBLIC ASIO_STANDALONE) endif() genex_for_option(UBSAN) target_compile_options(${NAME} PUBLIC $<${UBSAN_GENEX}:-fsanitize=undefined>) target_link_libraries(${NAME} PUBLIC $<${UBSAN_GENEX}:-fsanitize=undefined>) if(TSAN) target_compile_options(${NAME} PUBLIC -fsanitize=thread) target_link_libraries(${NAME} PUBLIC -fsanitize=thread) else() genex_for_option(ASAN) target_compile_options(${NAME} PUBLIC "$<${ASAN_GENEX}:-fsanitize=address;-fsanitize-recover=address>") target_link_libraries(${NAME} PUBLIC "$<${ASAN_GENEX}:-fsanitize=address;-fsanitize-recover=address>") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") genex_for_option(DEVILUTIONX_STATIC_CXX_STDLIB) target_link_libraries(${NAME} PUBLIC $<${DEVILUTIONX_STATIC_CXX_STDLIB_GENEX}:-static-libgcc;-static-libstdc++>) endif() if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") # Note: For Valgrind support. genex_for_option(DEBUG) target_compile_options(${NAME} PUBLIC $<${DEBUG_GENEX}:-fno-omit-frame-pointer>) # Warnings for devilutionX target_compile_options(${NAME} PUBLIC -Wall -Wextra -Wno-unused-parameter) endif() if(NOT WIN32 AND NOT APPLE AND NOT ${CMAKE_SYSTEM_NAME} STREQUAL FreeBSD) # Enable POSIX extensions such as `readlink` and `ftruncate`. add_definitions(-D_POSIX_C_SOURCE=200809L) endif() if(BUILD_TESTING) if(ENABLE_CODECOVERAGE) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") message(WARNING "Codecoverage not supported with MSVC") else() target_compile_options(${NAME} PUBLIC --coverage) target_link_options(${NAME} PUBLIC --coverage) endif() endif() target_compile_definitions(${NAME} PRIVATE _DVL_EXPORTING) endif() target_compile_definitions(${NAME} PUBLIC ${DEVILUTIONX_DEFINITIONS}) set_relative_file_macro(${NAME}) endfunction() # Same as add_devilutionx_library(${NAME} OBJECT). function(add_devilutionx_object_library NAME) add_devilutionx_library(${NAME} OBJECT ${ARGN}) endfunction() ================================================ FILE: CMake/functions/emscripten_system_library.cmake ================================================ # This function defines a target that points to an Emscripten system library. # # Arguments: # LIB_NAME: a human-readable library name. # TARGET_NAME: the library target name # ...ARGN: Emscripten flags. # # Example: # emscripten_system_library("SDL2_image" SDL2::SDL2_image USE_SDL_IMAGE=2 "SDL2_IMAGE_FORMATS='[\"png\"]'") function(emscripten_system_library LIB_NAME TARGET_NAME) add_library(${TARGET_NAME} INTERFACE IMPORTED GLOBAL) foreach(arg ${ARGN}) target_compile_options(${TARGET_NAME} INTERFACE "SHELL:-s ${arg}") target_link_options(${TARGET_NAME} INTERFACE "SHELL:-s ${arg}") endforeach() message("-- 📚 ${LIB_NAME}: Emscripten system library via ${ARGN}") endfunction() ================================================ FILE: CMake/functions/genex.cmake ================================================ # Generator expression helpers # If "NEW", `set(CACHE ...)` does not override non-cache variables if(POLICY CMP0126) cmake_policy(GET CMP0126 _cache_does_not_override_normal_vars_policy) else() set(_cache_does_not_override_normal_vars_policy "OLD") endif() macro(GENEX_OPTION name default description) if(_cache_does_not_override_normal_vars_policy STREQUAL "NEW") set(_define_cache_var TRUE) elseif(DEFINED ${name}) get_property(_define_cache_var CACHE ${name} PROPERTY TYPE) endif() if(_define_cache_var) set(${name} ${default} CACHE STRING ${description}) set_property(CACHE ${name} PROPERTY STRINGS FOR_DEBUG FOR_RELEASE ON OFF) else() message("Skipping `set(CACHE ${name} ...)`: CMake is < 3.21 and a non-cache variable with the same name is already set (${name}=${${name}})") endif() endmacro() # Provide an option that defaults to ON in debug builds. macro(DEBUG_OPTION name description) GENEX_OPTION(${name} FOR_DEBUG ${description}) endmacro() # Provide an option that defaults to ON in non-debug builds. # Note that this applies to Release, RelWithDebInfo, and MinSizeRel. macro(RELEASE_OPTION name description) GENEX_OPTION(${name} FOR_RELEASE ${description}) endmacro() # Generate a generator expression for the given variable's current value. # # Supported variable values and what the resulting generator expression will evaluate to: # * FOR_DEBUG - 1 in Debug config. # * FOR_RELEASE - 1 in non-Debug config (Release, RelWithDebInfo). # * Boolean value (TRUE, FALSE, ON, 1, etc) - that value as 0 or 1. # # Result is set on ${option}_GENEX in the calling scope. function(genex_for_option name) set(value ${${name}}) set( ${name}_GENEX $,$,$,$>,$>> PARENT_SCOPE ) endfunction() ================================================ FILE: CMake/functions/git.cmake ================================================ function(get_git_commit_hash output_var) execute_process( COMMAND git log -1 --format=%h WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE GIT_COMMIT_HASH OUTPUT_STRIP_TRAILING_WHITESPACE) set(${output_var} ${GIT_COMMIT_HASH} PARENT_SCOPE) endfunction(get_git_commit_hash) ================================================ FILE: CMake/functions/object_libraries.cmake ================================================ # CMake has limited support for object libraries. # # The main limitation of CMake object libraries is the lack # of transitive dependency support. # The functions here provide a workaround for that. # # Use `target_link_dependencies` instead of `target_link_libraries` # # https://gitlab.kitware.com/cmake/cmake/-/issues/18090#note_861617 # # At the end of the main `CMakeLists.txt`, call `resolve_target_link_dependencies()`. # Behaves like target_link_libraries, but propagates OBJECT libraries' objects # up to the first non-object library. function(target_link_dependencies TARGET) # The library we're linking may not have been defined yet, # so we record it for now and resolve it later. # CMake <3.19 limits which property names are allowed on INTERFACE targets, # so we prefix the name with "INTERFACE_": # https://cmake.org/cmake/help/v3.18/manual/cmake-buildsystem.7.html#interface-libraries set_property(TARGET ${TARGET} APPEND PROPERTY INTERFACE_LINKED_DEPENDENCIES ${ARGN}) set_property(GLOBAL APPEND PROPERTY TARGETS_WITH_LINKED_DEPENDENCIES "${TARGET}") endfunction() # Transitively collects dependencies in topological order using depth-first search. function(_collect_linked_dependencies INITIAL_TARGET) set(MODES PUBLIC PRIVATE INTERFACE) list(APPEND STACK "${INITIAL_TARGET}") while(NOT STACK STREQUAL "") list(POP_BACK STACK TARGET) if(${TARGET} MATCHES "^\\$") set(FINALIZING ON) string(SUBSTRING "${TARGET}" 1 -1 TARGET) else() set(FINALIZING OFF) endif() get_target_property(LINKED_DEPENDENCIES ${TARGET} INTERFACE_LINKED_DEPENDENCIES) if(LINKED_DEPENDENCIES STREQUAL "LINKED_DEPENDENCIES-NOTFOUND") # Not a `target_link_dependencies` target, nothing to do. continue() endif() if(NOT FINALIZING) get_target_property(LINKED_DEPENDENCIES_COLLECTED ${TARGET} INTERFACE_LINKED_DEPENDENCIES_COLLECTED) if(NOT LINKED_DEPENDENCIES_COLLECTED STREQUAL "LINKED_DEPENDENCIES_COLLECTED-NOTFOUND") # Already processed. continue() endif() list(APPEND STACK "$${TARGET}") get_target_property(LINKED_DEPENDENCIES_COLLECTING ${TARGET} INTERFACE_LINKED_DEPENDENCIES_COLLECTING) if(NOT LINKED_DEPENDENCIES_COLLECTING STREQUAL "LINKED_DEPENDENCIES_COLLECTING-NOTFOUND") # A cycle. message(FATAL_ERROR "Dependency cycle for ${TARGET}: ${STACK}") endif() set_property(TARGET "${TARGET}" PROPERTY INTERFACE_LINKED_DEPENDENCIES_COLLECTING ON) endif() get_target_property(TARGET_TYPE ${TARGET} TYPE) get_target_property(LINKED_DEPENDENCIES ${TARGET} INTERFACE_LINKED_DEPENDENCIES) set(MODE PUBLIC) foreach(ARG ${LINKED_DEPENDENCIES}) if(ARG IN_LIST MODES) set(MODE ${ARG}) continue() endif() set(LIBRARY "${ARG}") if(TARGET ${LIBRARY}) if(NOT FINALIZING) list(APPEND STACK ${LIBRARY}) continue() endif() # When linking two OBJECT libraries together, record the input library objects in # a custom target property "LINKED_OBJECTS" together with any other existing ones # from the input library's LINKED_OBJECTS property. # Accumulate LINKED_OBJECTS until reaching a non-object target, and add them as # extra sources - this will de-duplicate the list and link it into the target. get_target_property(LIBRARY_TYPE ${LIBRARY} TYPE) if(LIBRARY_TYPE STREQUAL "OBJECT_LIBRARY") if(TARGET_TYPE STREQUAL "INTERFACE_LIBRARY") message(FATAL_ERROR "OBJECT to INTERFACE library linking is not supported.") endif() # All transitive dependencies of this object library: get_target_property(LIBRARY_LINKED_OBJECTS ${LIBRARY} LINKED_OBJECTS) if(LIBRARY_LINKED_OBJECTS STREQUAL "LIBRARY_LINKED_OBJECTS-NOTFOUND") set(LIBRARY_LINKED_OBJECTS) endif() # target_sources deduplicates the list but we also do it here for ease of debugging. get_target_property(TARGET_LINKED_OBJECTS ${TARGET} LINKED_OBJECTS) if(TARGET_LINKED_OBJECTS STREQUAL "TARGET_LINKED_OBJECTS-NOTFOUND") set(TARGET_LINKED_OBJECTS) endif() list(APPEND TARGET_LINKED_OBJECTS ${LIBRARY_LINKED_OBJECTS} $) list(REMOVE_DUPLICATES TARGET_LINKED_OBJECTS) if(TARGET_TYPE STREQUAL "OBJECT_LIBRARY") set_property(TARGET ${TARGET} PROPERTY LINKED_OBJECTS "${TARGET_LINKED_OBJECTS}") else() target_sources(${TARGET} PRIVATE ${TARGET_LINKED_OBJECTS}) endif() endif() endif() if(FINALIZING) target_link_libraries(${TARGET} ${MODE} "${LIBRARY}") endif() endforeach() if(FINALIZING) set_property(TARGET "${TARGET}" PROPERTY INTERFACE_LINKED_DEPENDENCIES_COLLECTED ON) endif() endwhile() endfunction() # Actually resolves the linked dependencies. function(resolve_target_link_dependencies) set(MODES PUBLIC PRIVATE INTERFACE) get_property(TARGETS GLOBAL PROPERTY TARGETS_WITH_LINKED_DEPENDENCIES) foreach(TARGET ${TARGETS}) _collect_linked_dependencies("${TARGET}" "") endforeach() set_property(GLOBAL PROPERTY TARGETS_WITH_LINKED_DEPENDENCIES) endfunction() ================================================ FILE: CMake/functions/set_relative_file_macro.cmake ================================================ # Sets the __FILE__ macro value to be relative to CMAKE_SOURCE_DIR. function(set_relative_file_macro TARGET) if(NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if((CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 12) OR (CMAKE_CXX_COMPILER_ID MATCHES "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 8)) target_compile_options(${TARGET} PUBLIC "-fmacro-prefix-map=${CMAKE_SOURCE_DIR}/=") else() get_target_property(_srcs ${TARGET} SOURCES) foreach(_src ${_srcs}) set_source_files_properties(${_src} PROPERTIES COMPILE_DEFINITIONS __FILE__="${_src}") endforeach() target_compile_options(${TARGET} PRIVATE -Wno-builtin-macro-redefined) endif() endif() endfunction() ================================================ FILE: CMake/functions/trim_retired_files.cmake ================================================ set(SCRIPT_CONTENT [=[ include(functions/trim_retired_files) trim_retired_files("${ROOT_FOLDER}" "${CURRENT_FILES}" "${OUTPUT_FILE}") ]=]) function(trim_retired_files root_folder current_files output_file) file( GLOB_RECURSE retired_files RELATIVE "${root_folder}" "${root_folder}/*") list(REMOVE_ITEM retired_files ${current_files}) list(LENGTH retired_files retired_file_count) foreach(retired_file ${retired_files}) file(REMOVE "${root_folder}/${retired_file}") endforeach() if(${retired_file_count} GREATER 0 OR NOT EXISTS ${output_file}) file(TOUCH ${output_file}) endif() endfunction(trim_retired_files) function(add_trim_target arg_TARGET_NAME) set(oneValueArgs ROOT_FOLDER BYPRODUCT SCRIPT_PATH) set(multiValueArgs CURRENT_FILES) cmake_parse_arguments(PARSE_ARGV 0 arg "" "${oneValueArgs}" "${multiValueArgs}") if(NOT arg_ROOT_FOLDER) message(FATAL_ERROR "add_trim_command: missing required parameter ROOT_FOLDER") endif() if(NOT arg_OUTPUT OR NOT arg_BYPRODUCT OR NOT arg_SCRIPT_PATH) cmake_path(GET arg_ROOT_FOLDER FILENAME root_filename) if(NOT arg_BYPRODUCT) set(arg_BYPRODUCT "${CMAKE_CURRENT_BINARY_DIR}/${root_filename}.rm") endif() if(NOT arg_SCRIPT_PATH) get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(is_multi_config) set(arg_SCRIPT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${root_filename}_$.cmake") else() set(arg_SCRIPT_PATH "${CMAKE_CURRENT_BINARY_DIR}/${root_filename}.cmake") endif() endif() endif() file(GENERATE OUTPUT "${arg_SCRIPT_PATH}" CONTENT "${SCRIPT_CONTENT}") add_custom_target("${arg_TARGET_NAME}" COMMENT "Trimming ${arg_ROOT_FOLDER}" BYPRODUCTS "${arg_BYPRODUCT}" COMMAND ${CMAKE_COMMAND} -D "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}" -D "ROOT_FOLDER=${arg_ROOT_FOLDER}" -D "CURRENT_FILES=${arg_CURRENT_FILES}" -D "OUTPUT_FILE=${arg_BYPRODUCT}" -P "${arg_SCRIPT_PATH}" VERBATIM) set(TRIM_COMMAND_BYPRODUCT "${arg_BYPRODUCT}" PARENT_SCOPE) endfunction(add_trim_target) ================================================ FILE: CMake/platforms/.editorconfig ================================================ [ios.toolchain.cmake] end_of_line = lf ================================================ FILE: CMake/platforms/aarch64-linux-gnu-clang-static-libc++.toolchain.cmake ================================================ set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(triple aarch64-linux-gnu) set(CMAKE_C_COMPILER "/usr/bin/clang") set(CMAKE_C_COMPILER_TARGET "${triple}") set(CMAKE_CXX_COMPILER "/usr/bin/clang++") set(CMAKE_CXX_FLAGS_INIT "-stdlib=libc++") set(CMAKE_CXX_COMPILER_TARGET "${triple}") set(CMAKE_ASM_COMPILER "/usr/bin/clang") set(CMAKE_ASM_COMPILER_TARGET "${triple}") set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=/usr/bin/ld.lld -static-libstdc++ -static-libgcc") set(CMAKE_FIND_ROOT_PATH "/usr/aarch64-linux-gnu;/usr") set(CMAKE_LIBRARY_ARCHITECTURE "${triple}") set(CMAKE_STRIP "/usr/bin/aarch64-linux-gnu-strip") set(PKG_CONFIG_EXECUTABLE "${CMAKE_CURRENT_LIST_DIR}/aarch64-linux-gnu-pkg-config" CACHE STRING "Path to pkg-config") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) ================================================ FILE: CMake/platforms/aarch64-linux-gnu.toolchain.cmake ================================================ set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CMAKE_C_COMPILER "/usr/bin/aarch64-linux-gnu-gcc") set(CMAKE_CXX_COMPILER "/usr/bin/aarch64-linux-gnu-g++") set(CMAKE_FIND_ROOT_PATH "/usr/aarch64-linux-gnu;/usr") set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu) set(CMAKE_STRIP "/usr/bin/aarch64-linux-gnu-strip") set(PKG_CONFIG_EXECUTABLE "${CMAKE_CURRENT_LIST_DIR}/aarch64-linux-gnu-pkg-config" CACHE STRING "Path to pkg-config") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE arm64) ================================================ FILE: CMake/platforms/amiga.cmake ================================================ set(BUILD_TESTING OFF) set(ASAN OFF) set(UBSAN OFF) set(NONET ON) set(USE_SDL1 ON) set(SDL1_VIDEO_MODE_BPP 8) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_SYSTEM_ZLIB OFF) # Lower the optimization level to O2 because there are issues with O3. set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2") # `fseeko` fails to link on Amiga. add_definitions(-Dfseeko=fseek) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES ZLIB::ZLIB) if(NOT WARPOS) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES -ldebug) endif() file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/amiga/devilutionx.info" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") ================================================ FILE: CMake/platforms/android.cmake ================================================ # General build options. set(BUILD_TESTING OFF) # Disable all system dependencies. # All of these will be fetched via FetchContent and linked statically. set(DEVILUTIONX_SYSTEM_SDL2 OFF) # JNI source directory list(APPEND DEVILUTIONX_PLATFORM_SUBDIRECTORIES platform/android) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES libdevilutionx_android) # Static SDL2 on Android requires Position Independent Code. set(SDL_STATIC_PIC ON) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBPNG OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) # Package the assets with the APK. set(BUILD_ASSETS_MPQ OFF) set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/android-project/app/src/main/assets") # Disable sanitizers. They're not supported out-of-the-box. set(ASAN OFF) set(UBSAN OFF) # Disable in-game options to exit the game. set(NOEXIT ON) ================================================ FILE: CMake/platforms/cpigamesh.cmake ================================================ set(BUILD_TESTING OFF) set(NONET ON) set(PREFILL_PLAYER_NAME ON) set(HAS_KBCTRL 1) set(LTO ON) set(DIST ON) set(DEBUG OFF) set(ASAN OFF) set(UBSAN OFF) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_X SDLK_i) set(KBCTRL_BUTTON_Y SDLK_u) set(KBCTRL_BUTTON_B SDLK_k) set(KBCTRL_BUTTON_A SDLK_j) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_l) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_h) set(KBCTRL_BUTTON_AXIS_TRIGGERLEFT SDLK_y) set(KBCTRL_BUTTON_AXIS_TRIGGERRIGHT SDLK_o) set(KBCTRL_BUTTON_LEFTSTICK SDLK_PAGEUP) set(KBCTRL_BUTTON_RIGHTSTICK SDLK_PAGEDOWN) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_BACK SDLK_SPACE) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") ================================================ FILE: CMake/platforms/ctr/Tools3DS.cmake ================================================ ############################################################################ # Various macros for 3DS homebrews tools # # add_3dsx_target # ^^^^^^^^^^^^^^^ # # This macro has two signatures : # # ## add_3dsx_target(target [NO_SMDH]) # # Adds a target that generates a .3dsx file from `target`. If NO_SMDH is specified, no .smdh file will be generated. # # You can set the following variables to change the SMDH file : # # * APP_TITLE is the name of the app stored in the SMDH file (Optional) # * APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) # * APP_AUTHOR is the author of the app stored in the SMDH file (Optional) # * APP_ICON is the filename of the icon (.png), relative to the project folder. # If not set, it attempts to use one of the following (in this order): # - $(target).png # - icon.png # - $(libctru folder)/default_icon.png # # ## add_3dsx_target(target APP_TITLE APP_DESCRIPTION APP_AUTHOR [APP_ICON]) # # This version will produce the SMDH with tha values passed as arguments. Tha APP_ICON is optional and follows the same rule as the other version of `add_3dsx_target`. # # add_cia_target(target RSF IMAGE SOUND [APP_TITLE APP_DESCRIPTION APP_AUTHOR [APP_ICON]]) # ^^^^^^^^^^^^^^ # # Same as add_3dsx_target but for CIA files. # # RSF is the .rsf file to be given to makerom. # IMAGE is either a .png or a cgfximage file. # SOUND is either a .wav or a cwavaudio file. # # add_netload_target(name target_or_file) # ^^^^^^^^^^^^^^^^^^ # # Adds a target `name` that sends a .3dsx using the homebrew launcher netload system (3dslink). # target_or_file is either the name of a target or of file. # # add_binary_library(target input1 [input2 ...]) # ^^^^^^^^^^^^^^^^^^ # # /!\ Requires ASM to be enabled ( `enable_language(ASM)` or `project(yourprojectname C CXX ASM)`) # # Converts the files given as input to arrays of their binary data. This is useful to embed resources into your project. # For example, logo.bmp will generate the array `u8 logo_bmp[]` and its size `logo_bmp_size`. By linking this library, you # will also have access to a generated header file called `logo_bmp.h` which contains the declarations you need to use it. # # Note : All dots in the filename are converted to `_`, and if it starts with a number, `_` will be prepended. # For example 8x8.gas.tex would give the name _8x8_gas_tex. # # target_embed_file(target input1 [input2 ...]) # ^^^^^^^^^^^^^^^^^ # # Same as add_binary_library(tempbinlib input1 [input2 ...]) + target_link_libraries(target tempbinlib) # # add_shbin(output input [entrypoint] [shader_type]) # ^^^^^^^^^^^^^^^^^^^^^^^ # # Assembles the shader given as `input` into the file `output`. No file extension is added. # You can choose the shader assembler by setting SHADER_AS to `picasso` or `nihstro`. # # If `nihstro` is set as the assembler, entrypoint and shader_type will be used. # entrypoint is set to `main` by default # shader_type can be either VSHADER or GSHADER. By default it is VSHADER. # # generate_shbins(input1 [input2 ...]) # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # # Assemble all the shader files given as input into .shbin files. Those will be located in the folder `shaders` of the build directory. # The names of the output files will be .shbin. vshader.pica will output shader.shbin but shader.vertex.pica will output shader.shbin too. # # add_shbin_library(target input1 [input2 ...]) # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # # /!\ Requires ASM to be enabled ( `enable_language(ASM)` or `project(yourprojectname C CXX ASM)`) # # This is the same as calling generate_shbins and add_binary_library. This is the function to be used to reproduce devkitArm makefiles behaviour. # For example, add_shbin_library(shaders data/my1stshader.vsh.pica) will generate the target library `shaders` and you # will be able to use the shbin in your program by linking it, including `my1stshader_pica.h` and using `my1stshader_pica[]` and `my1stshader_pica_size`. # # target_embed_shader(target input1 [input2 ...]) # ^^^^^^^^^^^^^^^^^ # # Same as add_shbin_library(tempbinlib input1 [input2 ...]) + target_link_libraries(target tempbinlib) # ############################################################################ if(NOT NINTENDO_3DS) message(WARNING "Those tools can only be used if you are using the 3DS toolchain file. Please erase this build directory or create another one, and then use -DCMAKE_TOOLCHAIN_FILE=DevkitArm3DS.cmake when calling cmake for the 1st time. For more information, see the Readme.md for more information.") endif() get_filename_component(__tools3dsdir ${CMAKE_CURRENT_LIST_FILE} PATH) # Used to locate files to be used with configure_file message(STATUS "Looking for 3ds tools...") ############## ## 3DSXTOOL ## ############## if(NOT _3DSXTOOL) # message(STATUS "Looking for 3dsxtool...") find_program(_3DSXTOOL 3dsxtool ${DEVKITPRO}/tools/bin) if(_3DSXTOOL) message(STATUS "3dsxtool: ${_3DSXTOOL} - found") else() message(WARNING "3dsxtool - not found") endif() endif() ############## ## SMDHTOOL ## ############## if(NOT SMDHTOOL) # message(STATUS "Looking for smdhtool...") find_program(SMDHTOOL smdhtool ${DEVKITPRO}/tools/bin) if(SMDHTOOL) message(STATUS "smdhtool: ${SMDHTOOL} - found") else() message(WARNING "smdhtool - not found") endif() endif() ################ ## BANNERTOOL ## ################ if(NOT BANNERTOOL) # message(STATUS "Looking for bannertool...") find_program(BANNERTOOL bannertool ${DEVKITPRO}/tools/bin /usr/local/bin) if(BANNERTOOL) message(STATUS "bannertool: ${BANNERTOOL} - found") else() message(WARNING "bannertool - not found") endif() endif() set(FORCE_SMDHTOOL FALSE CACHE BOOL "Force the use of smdhtool instead of bannertool") ############# ## MAKEROM ## ############# if(NOT MAKEROM) # message(STATUS "Looking for makerom...") find_program(MAKEROM makerom ${DEVKITPRO}/tools/bin /usr/local/bin) if(MAKEROM) message(STATUS "makerom: ${MAKEROM} - found") else() message(WARNING "makerom - not found") endif() endif() ############# ## STRIP ## ############# if(NOT STRIP) # message(STATUS "Looking for strip...") find_program(STRIP arm-none-eabi-strip ${DEVKITARM}/bin) if(STRIP) message(STATUS "strip: ${STRIP} - found") else() message(WARNING "strip - not found") endif() endif() ############# ## BIN2S ## ############# if(NOT BIN2S) # message(STATUS "Looking for bin2s...") find_program(BIN2S bin2s ${DEVKITPRO}/tools/bin) if(BIN2S) message(STATUS "bin2s: ${BIN2S} - found") else() message(WARNING "bin2s - not found") endif() endif() ############### ## 3DSLINK ## ############### if(NOT _3DSLINK) # message(STATUS "Looking for 3dslink...") find_program(_3DSLINK 3dslink ${DEVKITPRO}/tools/bin) if(_3DSLINK) message(STATUS "3dslink: ${_3DSLINK} - found") else() message(WARNING "3dslink - not found") endif() endif() ############# ## PICASSO ## ############# if(NOT PICASSO_EXE) # message(STATUS "Looking for Picasso...") find_program(PICASSO_EXE picasso ${DEVKITPRO}/tools/bin) if(PICASSO_EXE) message(STATUS "Picasso: ${PICASSO_EXE} - found") set(SHADER_AS picasso CACHE STRING "The shader assembler to be used. Allowed values are 'none', 'picasso' or 'nihstro'") else() message(STATUS "Picasso - not found") endif() endif() ############# ## NIHSTRO ## ############# if(NOT NIHSTRO_AS) # message(STATUS "Looking for nihstro...") find_program(NIHSTRO_AS nihstro ${DEVKITPRO}/tools/bin) if(NIHSTRO_AS) message(STATUS "nihstro: ${NIHSTRO_AS} - found") set(SHADER_AS nihstro CACHE STRING "The shader assembler to be used. Allowed values are 'none', 'picasso' or 'nihstro'") else() message(STATUS "nihstro - not found") endif() endif() set(SHADER_AS none CACHE STRING "The shader assembler to be used. Allowed values are 'none', 'picasso' or 'nihstro'") ############################### ############################### ######## MACROS ######### ############################### ############################### ################### ### EXECUTABLES ### ################### function(__add_smdh target APP_TITLE APP_DESCRIPTION APP_AUTHOR APP_ICON) if(BANNERTOOL AND NOT FORCE_SMDHTOOL) set(__SMDH_COMMAND ${BANNERTOOL} makesmdh -s ${APP_TITLE} -l ${APP_DESCRIPTION} -p ${APP_AUTHOR} -i ${APP_ICON} -o ${CMAKE_CURRENT_BINARY_DIR}/${target}) else() set(__SMDH_COMMAND ${SMDHTOOL} --create ${APP_TITLE} ${APP_DESCRIPTION} ${APP_AUTHOR} ${APP_ICON} ${CMAKE_CURRENT_BINARY_DIR}/${target}) endif() add_custom_command( OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target} COMMAND ${__SMDH_COMMAND} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} DEPENDS ${APP_ICON} VERBATIM ) endfunction() function(add_3dsx_target target) get_filename_component(target_we ${target} NAME_WE) if((NOT (${ARGC} GREATER 1 AND "${ARGV1}" STREQUAL "NO_SMDH") ) OR (${ARGC} GREATER 3) ) if(${ARGC} GREATER 3) set(APP_TITLE ${ARGV1}) set(APP_DESCRIPTION ${ARGV2}) set(APP_AUTHOR ${ARGV3}) endif() if(${ARGC} EQUAL 5) set(APP_ICON ${ARGV4}) endif() if(NOT APP_TITLE) set(APP_TITLE ${target}) endif() if(NOT APP_DESCRIPTION) set(APP_DESCRIPTION "Built with devkitARM & libctru") endif() if(NOT APP_AUTHOR) set(APP_AUTHOR "Unspecified Author") endif() if(NOT APP_ICON) if(EXISTS ${target}.png) set(APP_ICON ${target}.png) elseif(EXISTS icon.png) set(APP_ICON icon.png) elseif(CTRULIB) set(APP_ICON ${CTRULIB}/default_icon.png) else() message(FATAL_ERROR "No icon found ! Please use NO_SMDH or provide some icon.") endif() endif() if( NOT ${target_we}.smdh) __add_smdh(${target_we}.smdh ${APP_TITLE} ${APP_DESCRIPTION} ${APP_AUTHOR} ${APP_ICON}) endif() add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.3dsx COMMAND ${_3DSXTOOL} $ ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.3dsx --smdh=${CMAKE_CURRENT_BINARY_DIR}/${target_we}.smdh --romfs=${CMAKE_CURRENT_BINARY_DIR}/romfs DEPENDS ${target} ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.smdh VERBATIM ) else() message(STATUS "No smdh file will be generated") add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.3dsx COMMAND ${_3DSXTOOL} $ ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.3dsx --romfs=${CMAKE_CURRENT_BINARY_DIR}/romfs DEPENDS ${target} VERBATIM ) endif() add_custom_target(${target_we}_3dsx ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.3dsx) #set_target_properties(${target} PROPERTIES LINK_FLAGS "-specs=3dsx.specs") endfunction() function(__add_ncch_banner target IMAGE SOUND) if(IMAGE MATCHES ".*\\.png$") set(IMG_PARAM -i ${IMAGE}) else() set(IMG_PARAM -ci ${IMAGE}) endif() if(SOUND MATCHES ".*\\.wav$") set(SND_PARAM -a ${SOUND}) else() set(SND_PARAM -ca ${SOUND}) endif() add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target} COMMAND ${BANNERTOOL} makebanner -o ${CMAKE_CURRENT_BINARY_DIR}/${target} ${IMG_PARAM} ${SND_PARAM} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} DEPENDS ${IMAGE} ${SOUND} VERBATIM ) endfunction() function(add_cia_target target RSF IMAGE SOUND) get_filename_component(target_we ${target} NAME_WE) if(${ARGC} GREATER 6) set(APP_TITLE ${ARGV4}) set(APP_DESCRIPTION ${ARGV5}) set(APP_AUTHOR ${ARGV6}) endif() if(${ARGC} EQUAL 8) set(APP_ICON ${ARGV7}) endif() if(NOT APP_TITLE) set(APP_TITLE ${target}) endif() if(NOT APP_DESCRIPTION) set(APP_DESCRIPTION "Built with devkitARM & libctru") endif() if(NOT APP_AUTHOR) set(APP_AUTHOR "Unspecified Author") endif() if(NOT APP_ICON) if(EXISTS ${target}.png) set(APP_ICON ${target}.png) elseif(EXISTS icon.png) set(APP_ICON icon.png) elseif(CTRULIB) set(APP_ICON ${CTRULIB}/default_icon.png) else() message(FATAL_ERROR "No icon found ! Please use NO_SMDH or provide some icon.") endif() endif() if( NOT ${target_we}.smdh) __add_smdh(${target_we}.smdh ${APP_TITLE} ${APP_DESCRIPTION} ${APP_AUTHOR} ${APP_ICON}) endif() __add_ncch_banner(${target_we}.bnr ${IMAGE} ${SOUND}) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.cia COMMAND ${STRIP} -o $-stripped $ COMMAND ${MAKEROM} -f cia -target t -exefslogo -o ${target_we}.cia -elf $-stripped -rsf ${RSF} -banner ${target_we}.bnr -icon ${target_we}.smdh DEPENDS ${target} ${RSF} ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.bnr ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.smdh WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} VERBATIM ) add_custom_target(${target_we}_cia ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.cia) #set_target_properties(${target} PROPERTIES LINK_FLAGS "-specs=3dsx.specs") endfunction() macro(add_netload_target name target) set(NETLOAD_IP "" CACHE STRING "The ip address of the 3ds when using netload.") if(NETLOAD_IP) set(__NETLOAD_IP_OPTION -a ${NETLOAD_IP}) endif() if(NOT TARGET ${target}) message("NOT ${target}") set(FILE ${target}) else() set(FILE ${CMAKE_CURRENT_BINARY_DIR}/${target}.3dsx) endif() add_custom_target(${name} COMMAND ${_3DSLINK} ${FILE} ${__NETLOAD_IP_OPTION} DEPENDS ${FILE} ) endmacro() ###################### ### File embedding ### ###################### macro(add_binary_library libtarget) if(NOT ${ARGC} GREATER 1) message(FATAL_ERROR "add_binary_library : Argument error (no input files)") endif() get_cmake_property(ENABLED_LANGUAGES ENABLED_LANGUAGES) if(NOT ENABLED_LANGUAGES MATCHES ".*ASM.*") message(FATAL_ERROR "You have to enable ASM in order to use add_binary_library (or any target_embed_* which relies on it). Use enable_language(ASM) in your CMakeLists. Currently enabled languages are ${ENABLED_LANGUAGES}") endif() foreach(__file ${ARGN}) get_filename_component(__file_wd ${__file} NAME) string(REGEX REPLACE "^([0-9])" "_\\1" __BIN_FILE_NAME ${__file_wd}) # add '_' if the file name starts by a number string(REGEX REPLACE "[-./]" "_" __BIN_FILE_NAME ${__BIN_FILE_NAME}) #Generate the header file configure_file(${__tools3dsdir}/bin2s_header.h.in ${CMAKE_CURRENT_BINARY_DIR}/${libtarget}_include/${__BIN_FILE_NAME}.h) endforeach() file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/binaries_asm) # Generate the assembly file, and create the new target add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/binaries_asm/${libtarget}.s COMMAND ${BIN2S} ${ARGN} > ${CMAKE_CURRENT_BINARY_DIR}/binaries_asm/${libtarget}.s DEPENDS ${ARGN} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} ) add_library(${libtarget} ${CMAKE_CURRENT_BINARY_DIR}/binaries_asm/${libtarget}.s) target_include_directories(${libtarget} INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/${libtarget}_include) endmacro() macro(target_embed_file _target) if(NOT ${ARGC} GREATER 1) message(FATAL_ERROR "target_embed_file : Argument error (no input files)") endif() get_filename_component(__1st_file_wd ${ARGV1} NAME) add_binary_library(__${_target}_embed_${__1st_file_wd} ${ARGN}) target_link_libraries(${_target} __${_target}_embed_${__1st_file_wd}) endmacro() ################### ##### SHADERS ##### ################### macro(add_shbin OUTPUT INPUT ) if(SHADER_AS STREQUAL "picasso") if(${ARGC} GREATER 2) message(WARNING "Picasso doesn't support changing the entrypoint or shader type") endif() add_custom_command(OUTPUT ${OUTPUT} COMMAND ${PICASSO_EXE} -o ${OUTPUT} ${INPUT} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) elseif(SHADER_AS STREQUAL "nihstro") if(NOT NIHSTRO_AS) message(SEND_ERROR "SHADER_AS is set to nihstro, but nihstro wasn't found. Please set NIHSTRO_AS.") endif() if(${ARGC} GREATER 2) if(${ARGV2} EQUAL GSHADER) set(SHADER_TYPE_FLAG "-g") elseif(NOT ${ARGV2} EQUAL VSHADER) set(_ENTRYPOINT ${ARGV2}) endif() endif() if(${ARGC} GREATER 3) if(${ARGV2} EQUAL GSHADER) set(SHADER_TYPE_FLAG "-g") elseif(NOT ${ARGV3} EQUAL VSHADER) set(_ENTRYPOINT ${ARGV3}) endif() endif() if(NOT _ENTRYPOINT) set(_ENTRYPOINT "main") endif() add_custom_command(OUTPUT ${OUTPUT} COMMAND ${NIHSTRO_AS} ${INPUT} -o ${OUTPUT} -e ${_ENTRYPOINT} ${SHADER_TYPE_FLAG} WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) else() message(FATAL_ERROR "Please set SHADER_AS to 'picasso' or 'nihstro' if you use the shbin feature.") endif() endmacro() function(generate_shbins) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders) foreach(__shader_file ${ARGN}) get_filename_component(__shader_file_we ${__shader_file} NAME_WE) #Generate the shbin file list(APPEND __SHADERS_BIN_FILES ${CMAKE_CURRENT_BINARY_DIR}/shaders/${__shader_file_we}.shbin) add_shbin(${CMAKE_CURRENT_BINARY_DIR}/shaders/${__shader_file_we}.shbin ${__shader_file}) endforeach() endfunction() function(add_shbin_library libtarget) file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/shaders) foreach(__shader_file ${ARGN}) get_filename_component(__shader_file_we ${__shader_file} NAME_WE) #Generate the shbin file list(APPEND __SHADERS_BIN_FILES ${CMAKE_CURRENT_BINARY_DIR}/shaders/${__shader_file_we}.shbin) add_shbin(${CMAKE_CURRENT_BINARY_DIR}/shaders/${__shader_file_we}.shbin ${__shader_file}) endforeach() add_binary_library(${libtarget} ${__SHADERS_BIN_FILES}) endfunction() macro(target_embed_shader _target) if(NOT ${ARGC} GREATER 1) message(FATAL_ERROR "target_embed_shader : Argument error (no input files)") endif() get_filename_component(__1st_file_wd ${ARGV1} NAME) add_shbin_library(__${_target}_embed_${__1st_file_wd} ${ARGN}) target_link_libraries(${_target} __${_target}_embed_${__1st_file_wd}) endmacro() ================================================ FILE: CMake/platforms/ctr/asio_defs.cmake ================================================ target_compile_definitions(asio INTERFACE ASIO_DISABLE_THREADS=ON ASIO_HAS_UNISTD_H=ON) # Missing headers and declarations provided by DevilutionX target_include_directories(asio BEFORE INTERFACE ${PROJECT_SOURCE_DIR}/Source/platform/ctr/asio/include) ================================================ FILE: CMake/platforms/ctr/bin2s_header.h.in ================================================ extern const u8 @__BIN_FILE_NAME@_end[]; extern const u8 @__BIN_FILE_NAME@[]; extern const u32 @__BIN_FILE_NAME@_size; ================================================ FILE: CMake/platforms/ctr/modules/FindCITRO3D.cmake ================================================ # - Try to find citro3d # You can set CITRO3D_ROOT to specify a certain directory to look in first. # Once done this will define # CITRO3D_FOUND - System has citro3d # CITRO3D_INCLUDE_DIRS - The citro3d include directories # CITRO3D_LIBRARIES - The libraries needed to use citro3d # Unless we are unable to find CTRULIB # It also adds an imported target named `3ds::citro3d`, Linking against it is # equivalent to: # target_link_libraries(mytarget ${CITRO3D_LIBRARIES}) # target_include_directories(mytarget PRIVATE ${CITRO3D_INCLUDE_DIRS}) if(NOT NINTENDO_3DS) message(FATAL_ERROR "This module can only be used if you are using the 3DS toolchain file. Please erase this build directory or create another one, and then use -DCMAKE_TOOLCHAIN_FILE=DevkitArm3DS.cmake when calling cmake for the 1st time. For more information, see the Readme.md for more information.") endif() if(CITRO3D_INCLUDE_DIR) # Already in cache, be silent set(CITRO3D_FIND_QUIETLY TRUE) endif(CITRO3D_INCLUDE_DIR) include(LibFindMacros) include(try_add_imported_target) # citro3d requires ctrulib libfind_package(CITRO3D CTRULIB) set(_CITRO3D_SEARCHES) # Search CITRO3D_ROOT first if it is set. if(CITRO3D_ROOT) set(_CITRO3D_SEARCH_ROOT PATHS ${CITRO3D_ROOT} NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) list(APPEND _CITRO3D_SEARCHES _CITRO3D_SEARCH_ROOT) endif() # Search below ${DEVKITPRO}, ${DEVKITARM} etc. set(_CITRO3D_SEARCH_CMAKE_REROOTED PATHS / /citro3d /libctru /ctrulib NO_DEFAULT_PATH ONLY_CMAKE_FIND_ROOT_PATH) list(APPEND _CITRO3D_SEARCHES _CITRO3D_SEARCH_CMAKE_REROOTED) set(_CITRO3D_SEARCH_NORMAL NO_CMAKE_FIND_ROOT_PATH) list(APPEND _CITRO3D_SEARCHES _CITRO3D_SEARCH_NORMAL) foreach(search ${_CITRO3D_SEARCHES}) find_path(CITRO3D_INCLUDE_DIR NAMES citro3d.h ${${search}} PATH_SUFFIXES include) find_library(CITRO3D_LIBRARY NAMES citro3d libcitro3d.a ${${search}} PATH_SUFFIXES lib) endforeach() #find_library(LIBM_LIBRARY NAMES m libm.a # PATHS / /arm-none-eabi # PATH_SUFFIXES lib/armv6k/fpu) set(LIBM_LIBRARY m) set(CITRO3D_PROCESS_INCLUDES CITRO3D_INCLUDE_DIR) set(CITRO3D_PROCESS_LIBS CITRO3D_LIBRARY LIBM_LIBRARY) libfind_process(CITRO3D) try_add_imported_target(CITRO3D m 3ds::ctrulib) ================================================ FILE: CMake/platforms/ctr/modules/FindCTRULIB.cmake ================================================ # - Try to find ctrulib # Once done this will define # LIBCTRU_FOUND - System has ctrulib # LIBCTRU_INCLUDE_DIRS - The ctrulib include directories # LIBCTRU_LIBRARIES - The libraries needed to use ctrulib # # It also adds an imported target named `3ds::ctrulib`. # Linking it is the same as target_link_libraries(target ${LIBCTRU_LIBRARIES}) and target_include_directories(target ${LIBCTRU_INCLUDE_DIRS}) # DevkitPro paths are broken on windows, so we have to fix those macro(msys_to_cmake_path MsysPath ResultingPath) string(REGEX REPLACE "^/([a-zA-Z])/" "\\1:/" ${ResultingPath} "${MsysPath}") endmacro() if(NOT DEVKITPRO) msys_to_cmake_path("$ENV{DEVKITPRO}" DEVKITPRO) endif() set(CTRULIB_PATHS $ENV{CTRULIB} libctru ctrulib ${DEVKITPRO}/libctru ${DEVKITPRO}/ctrulib) find_path(LIBCTRU_INCLUDE_DIR 3ds.h PATHS ${CTRULIB_PATHS} PATH_SUFFIXES include libctru/include ) find_library(LIBCTRU_LIBRARY NAMES ctru libctru.a PATHS ${CTRULIB_PATHS} PATH_SUFFIXES lib libctru/lib ) set(LIBCTRU_LIBRARIES ${LIBCTRU_LIBRARY} ) set(LIBCTRU_INCLUDE_DIRS ${LIBCTRU_INCLUDE_DIR} ) include(FindPackageHandleStandardArgs) # handle the QUIETLY and REQUIRED arguments and set LIBCTRU_FOUND to TRUE # if all listed variables are TRUE find_package_handle_standard_args(CTRULIB DEFAULT_MSG LIBCTRU_LIBRARY LIBCTRU_INCLUDE_DIR) mark_as_advanced(LIBCTRU_INCLUDE_DIR LIBCTRU_LIBRARY ) if(CTRULIB_FOUND) set(CTRULIB ${LIBCTRU_INCLUDE_DIR}/..) message(STATUS "setting CTRULIB to ${CTRULIB}") add_library(3ds::ctrulib STATIC IMPORTED GLOBAL) set_target_properties(3ds::ctrulib PROPERTIES IMPORTED_LOCATION "${LIBCTRU_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${LIBCTRU_INCLUDE_DIR}" ) endif() ================================================ FILE: CMake/platforms/ctr/modules/FindPNG.cmake ================================================ # - Try to find png # You can set PNG_ROOT to specify a certain directory to look in first. # Once done this will define # PNG_FOUND - System has png # PNG_INCLUDE_DIRS - The png include directories # PNG_LIBRARIES - The libraries needed to use png # Unless we are unable to find ZLIB # It also adds an imported target named `3ds::png`, Linking against it is # equivalent to: # target_link_libraries(mytarget ${PNG_LIBRARIES}) # target_include_directories(mytarget PRIVATE ${PNG_INCLUDE_DIRS}) if(NOT NINTENDO_3DS) message(FATAL_ERROR "This module can only be used if you are using the 3DS toolchain file. Please erase this build directory or create another one, and then use -DCMAKE_TOOLCHAIN_FILE=DevkitArm3DS.cmake when calling cmake for the 1st time. For more information, see the Readme.md for more information.") endif() if(PNG_INCLUDE_DIR) # Already in cache, be silent set(PNG_FIND_QUIETLY TRUE) endif(PNG_INCLUDE_DIR) include(LibFindMacros) include(try_add_imported_target) libfind_package(PNG ZLIB) set(_PNG_SEARCHES) # Search PNG_ROOT first if it is set. if(PNG_ROOT) set(_PNG_SEARCH_ROOT PATHS ${PNG_ROOT} NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) list(APPEND _PNG_SEARCHES _PNG_SEARCH_ROOT) endif() # Search below ${DEVKITPRO}, ${DEVKITARM}, portlibs (if enabled) etc. set(_PNG_SEARCH_CMAKE_REROOTED PATHS / NO_DEFAULT_PATH ONLY_CMAKE_FIND_ROOT_PATH) list(APPEND _PNG_SEARCHES _PNG_SEARCH_CMAKE_REROOTED) set(_PNG_SEARCH_NORMAL NO_CMAKE_FIND_ROOT_PATH) list(APPEND _PNG_SEARCHES _PNG_SEARCH_NORMAL) foreach(search ${_PNG_SEARCHES}) find_path(PNG_INCLUDE_DIR NAMES png.h ${${search}} PATH_SUFFIXES include) find_library(PNG_LIBRARY NAMES png libpng.a ${${search}} PATH_SUFFIXES lib) endforeach() #find_library(LIBM_LIBRARY NAMES m libm.a # PATHS / /arm-none-eabi # PATH_SUFFIXES lib/armv6k/fpu) set(LIBM_LIBRARY m) set(PNG_PROCESS_INCLUDES PNG_INCLUDE_DIR) set(PNG_PROCESS_LIBS PNG_LIBRARY LIBM_LIBRARY) libfind_process(PNG) try_add_imported_target(PNG m 3ds::zlib) add_library(PNG::PNG ALIAS 3ds::png) ================================================ FILE: CMake/platforms/ctr/modules/FindZLIB.cmake ================================================ # - Try to find zlib # You can set ZLIB_ROOT to specify a certain directory to look in first. # Once done this will define # ZLIB_FOUND - System has zlib # ZLIB_INCLUDE_DIRS - The zlib include directories # ZLIB_LIBRARIES - The libraries needed to use zlib # It also adds an imported target named `3ds::zlib`, Linking against it is # equivalent to: # target_link_libraries(mytarget ${ZLIB_LIBRARY}) # target_include_directories(mytarget PRIVATE ${ZLIB_INCLUDE_DIRS}) if(NOT NINTENDO_3DS) message(FATAL_ERROR "This module can only be used if you are using the 3DS toolchain file. Please erase this build directory or create another one, and then use -DCMAKE_TOOLCHAIN_FILE=DevkitArm3DS.cmake when calling cmake for the 1st time. For more information, see the Readme.md for more information.") endif() if(ZLIB_INCLUDE_DIR) # Already in cache, be silent set(ZLIB_FIND_QUIETLY TRUE) endif(ZLIB_INCLUDE_DIR) include(LibFindMacros) include(try_add_imported_target) set(_ZLIB_SEARCHES) # Search ZLIB_ROOT first if it is set. if(ZLIB_ROOT) set(_ZLIB_SEARCH_ROOT PATHS ${ZLIB_ROOT} NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) list(APPEND _ZLIB_SEARCHES _ZLIB_SEARCH_ROOT) endif() # Search below ${DEVKITPRO}, ${DEVKITARM}, portlibs (if enabled) etc. set(_ZLIB_SEARCH_CMAKE_REROOTED PATHS / NO_DEFAULT_PATH ONLY_CMAKE_FIND_ROOT_PATH) list(APPEND _ZLIB_SEARCHES _ZLIB_SEARCH_CMAKE_REROOTED) set(_ZLIB_SEARCH_NORMAL NO_CMAKE_FIND_ROOT_PATH) list(APPEND _ZLIB_SEARCHES _ZLIB_SEARCH_NORMAL) foreach(search ${_ZLIB_SEARCHES}) find_path(ZLIB_INCLUDE_DIR NAMES zlib.h ${${search}} PATH_SUFFIXES include) find_library(ZLIB_LIBRARY NAMES z libz.a ${${search}} PATH_SUFFIXES lib) endforeach() set(ZLIB_PROCESS_INCLUDES ZLIB_INCLUDE_DIR) set(ZLIB_PROCESS_LIBS ZLIB_LIBRARY) libfind_process(ZLIB) try_add_imported_target(ZLIB) add_library(ZLIB::ZLIB ALIAS 3ds::zlib) ================================================ FILE: CMake/platforms/ctr/modules/LibFindMacros.cmake ================================================ # Version 2.2 # Public Domain, originally written by Lasse Kärkkäinen # Maintained at https://github.com/Tronic/cmake-modules # Please send your improvements as pull requests on Github. # Find another package and make it a dependency of the current package. # This also automatically forwards the "REQUIRED" argument. # Usage: libfind_package( [extra args to find_package]) macro (libfind_package PREFIX PKG) set(${PREFIX}_args ${PKG} ${ARGN}) if (${PREFIX}_FIND_REQUIRED) set(${PREFIX}_args ${${PREFIX}_args} REQUIRED) endif() find_package(${${PREFIX}_args}) set(${PREFIX}_DEPENDENCIES ${${PREFIX}_DEPENDENCIES};${PKG}) unset(${PREFIX}_args) endmacro() # A simple wrapper to make pkg-config searches a bit easier. # Works the same as CMake's internal pkg_check_modules but is always quiet. macro (libfind_pkg_check_modules) find_package(PkgConfig QUIET) if (PKG_CONFIG_FOUND) pkg_check_modules(${ARGN} QUIET) endif() endmacro() # Avoid useless copy&pasta by doing what most simple libraries do anyway: # pkg-config, find headers, find library. # Usage: libfind_pkg_detect( FIND_PATH [other args] FIND_LIBRARY [other args]) # E.g. libfind_pkg_detect(SDL2 sdl2 FIND_PATH SDL.h PATH_SUFFIXES SDL2 FIND_LIBRARY SDL2) function (libfind_pkg_detect PREFIX) # Parse arguments set(argname pkgargs) foreach (i ${ARGN}) if ("${i}" STREQUAL "FIND_PATH") set(argname pathargs) elseif ("${i}" STREQUAL "FIND_LIBRARY") set(argname libraryargs) else() set(${argname} ${${argname}} ${i}) endif() endforeach() if (NOT pkgargs) message(FATAL_ERROR "libfind_pkg_detect requires at least a pkg_config package name to be passed.") endif() # Find library libfind_pkg_check_modules(${PREFIX}_PKGCONF ${pkgargs}) if (pathargs) find_path(${PREFIX}_INCLUDE_DIR NAMES ${pathargs} HINTS ${${PREFIX}_PKGCONF_INCLUDE_DIRS}) endif() if (libraryargs) find_library(${PREFIX}_LIBRARY NAMES ${libraryargs} HINTS ${${PREFIX}_PKGCONF_LIBRARY_DIRS}) endif() endfunction() # Extracts a version #define from a version.h file, output stored to _VERSION. # Usage: libfind_version_header(Foobar foobar/version.h FOOBAR_VERSION_STR) # Fourth argument "QUIET" may be used for silently testing different define names. # This function does nothing if the version variable is already defined. function (libfind_version_header PREFIX VERSION_H DEFINE_NAME) # Skip processing if we already have a version or if the include dir was not found if (${PREFIX}_VERSION OR NOT ${PREFIX}_INCLUDE_DIR) return() endif() set(quiet ${${PREFIX}_FIND_QUIETLY}) # Process optional arguments foreach(arg ${ARGN}) if (arg STREQUAL "QUIET") set(quiet TRUE) else() message(AUTHOR_WARNING "Unknown argument ${arg} to libfind_version_header ignored.") endif() endforeach() # Read the header and parse for version number set(filename "${${PREFIX}_INCLUDE_DIR}/${VERSION_H}") if (NOT EXISTS ${filename}) if (NOT quiet) message(AUTHOR_WARNING "Unable to find ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}") endif() return() endif() file(READ "${filename}" header) string(REGEX REPLACE ".*#[ \t]*define[ \t]*${DEFINE_NAME}[ \t]*\"([^\n]*)\".*" "\\1" match "${header}") # No regex match? if (match STREQUAL header) if (NOT quiet) message(AUTHOR_WARNING "Unable to find \#define ${DEFINE_NAME} \"\" from ${${PREFIX}_INCLUDE_DIR}/${VERSION_H}") endif() return() endif() # Export the version string set(${PREFIX}_VERSION "${match}" PARENT_SCOPE) endfunction() # Do the final processing once the paths have been detected. # If include dirs are needed, ${PREFIX}_PROCESS_INCLUDES should be set to contain # all the variables, each of which contain one include directory. # Ditto for ${PREFIX}_PROCESS_LIBS and library files. # Will set ${PREFIX}_FOUND, ${PREFIX}_INCLUDE_DIRS and ${PREFIX}_LIBRARIES. # Also handles errors in case library detection was required, etc. function (libfind_process PREFIX) # Skip processing if already processed during this configuration run if (${PREFIX}_FOUND) return() endif() set(found TRUE) # Start with the assumption that the package was found # Did we find any files? Did we miss includes? These are for formatting better error messages. set(some_files FALSE) set(missing_headers FALSE) # Shorthands for some variables that we need often set(quiet ${${PREFIX}_FIND_QUIETLY}) set(required ${${PREFIX}_FIND_REQUIRED}) set(exactver ${${PREFIX}_FIND_VERSION_EXACT}) set(findver "${${PREFIX}_FIND_VERSION}") set(version "${${PREFIX}_VERSION}") # Lists of config option names (all, includes, libs) unset(configopts) set(includeopts ${${PREFIX}_PROCESS_INCLUDES}) set(libraryopts ${${PREFIX}_PROCESS_LIBS}) # Process deps to add to foreach (i ${PREFIX} ${${PREFIX}_DEPENDENCIES}) if (DEFINED ${i}_INCLUDE_OPTS OR DEFINED ${i}_LIBRARY_OPTS) # The package seems to export option lists that we can use, woohoo! list(APPEND includeopts ${${i}_INCLUDE_OPTS}) list(APPEND libraryopts ${${i}_LIBRARY_OPTS}) else() # If plural forms don't exist or they equal singular forms if ((NOT DEFINED ${i}_INCLUDE_DIRS AND NOT DEFINED ${i}_LIBRARIES) OR (${i}_INCLUDE_DIR STREQUAL ${i}_INCLUDE_DIRS AND ${i}_LIBRARY STREQUAL ${i}_LIBRARIES)) # Singular forms can be used if (DEFINED ${i}_INCLUDE_DIR) list(APPEND includeopts ${i}_INCLUDE_DIR) endif() if (DEFINED ${i}_LIBRARY) list(APPEND libraryopts ${i}_LIBRARY) endif() else() # Oh no, we don't know the option names message(FATAL_ERROR "We couldn't determine config variable names for ${i} includes and libs. Aieeh!") endif() endif() endforeach() if (includeopts) list(REMOVE_DUPLICATES includeopts) endif() if (libraryopts) list(REMOVE_DUPLICATES libraryopts) endif() string(REGEX REPLACE ".*[ ;]([^ ;]*(_INCLUDE_DIRS|_LIBRARIES))" "\\1" tmp "${includeopts} ${libraryopts}") if (NOT tmp STREQUAL "${includeopts} ${libraryopts}") message(AUTHOR_WARNING "Plural form ${tmp} found in config options of ${PREFIX}. This works as before but is now deprecated. Please only use singular forms INCLUDE_DIR and LIBRARY, and update your find scripts for LibFindMacros > 2.0 automatic dependency system (most often you can simply remove the PROCESS variables entirely).") endif() # Include/library names separated by spaces (notice: not CMake lists) unset(includes) unset(libs) # Process all includes and set found false if any are missing foreach (i ${includeopts}) list(APPEND configopts ${i}) if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND") list(APPEND includes "${${i}}") else() set(found FALSE) set(missing_headers TRUE) endif() endforeach() # Process all libraries and set found false if any are missing foreach (i ${libraryopts}) list(APPEND configopts ${i}) if (NOT "${${i}}" STREQUAL "${i}-NOTFOUND") list(APPEND libs "${${i}}") else() set (found FALSE) endif() endforeach() # Version checks if (found AND findver) if (NOT version) message(WARNING "The find module for ${PREFIX} does not provide version information, so we'll just assume that it is OK. Please fix the module or remove package version requirements to get rid of this warning.") elseif (version VERSION_LESS findver OR (exactver AND NOT version VERSION_EQUAL findver)) set(found FALSE) set(version_unsuitable TRUE) endif() endif() # If all-OK, hide all config options, export variables, print status and exit if (found) foreach (i ${configopts}) mark_as_advanced(${i}) endforeach() if (NOT quiet) message(STATUS "Found ${PREFIX} ${${PREFIX}_LIBRARY} ${${PREFIX}_VERSION}") if (LIBFIND_DEBUG) message(STATUS " ${PREFIX}_DEPENDENCIES=${${PREFIX}_DEPENDENCIES}") message(STATUS " ${PREFIX}_INCLUDE_OPTS=${includeopts}") message(STATUS " ${PREFIX}_INCLUDE_DIRS=${includes}") message(STATUS " ${PREFIX}_LIBRARY_OPTS=${libraryopts}") message(STATUS " ${PREFIX}_LIBRARIES=${libs}") endif() endif() set (${PREFIX}_INCLUDE_OPTS ${includeopts} PARENT_SCOPE) set (${PREFIX}_LIBRARY_OPTS ${libraryopts} PARENT_SCOPE) set (${PREFIX}_INCLUDE_DIRS ${includes} PARENT_SCOPE) set (${PREFIX}_LIBRARIES ${libs} PARENT_SCOPE) set (${PREFIX}_FOUND TRUE PARENT_SCOPE) return() endif() # Format messages for debug info and the type of error set(vars "Relevant CMake configuration variables:\n") foreach (i ${configopts}) mark_as_advanced(CLEAR ${i}) set(val ${${i}}) if ("${val}" STREQUAL "${i}-NOTFOUND") set (val "") elseif (val AND NOT EXISTS ${val}) set (val "${val} (does not exist)") else() set(some_files TRUE) endif() set(vars "${vars} ${i}=${val}\n") endforeach() set(vars "${vars}You may use CMake GUI, cmake -D or ccmake to modify the values. Delete CMakeCache.txt to discard all values and force full re-detection if necessary.\n") if (version_unsuitable) set(msg "${PREFIX} ${${PREFIX}_VERSION} was found but") if (exactver) set(msg "${msg} only version ${findver} is acceptable.") else() set(msg "${msg} version ${findver} is the minimum requirement.") endif() else() if (missing_headers) set(msg "We could not find development headers for ${PREFIX}. Do you have the necessary dev package installed?") elseif (some_files) set(msg "We only found some files of ${PREFIX}, not all of them. Perhaps your installation is incomplete or maybe we just didn't look in the right place?") if(findver) set(msg "${msg} This could also be caused by incompatible version (if it helps, at least ${PREFIX} ${findver} should work).") endif() else() set(msg "We were unable to find package ${PREFIX}.") endif() endif() # Fatal error out if REQUIRED if (required) set(msg "REQUIRED PACKAGE NOT FOUND\n${msg} This package is REQUIRED and you need to install it or adjust CMake configuration in order to continue building ${CMAKE_PROJECT_NAME}.") message(FATAL_ERROR "${msg}\n${vars}") endif() # Otherwise just print a nasty warning if (NOT quiet) message(WARNING "WARNING: MISSING PACKAGE\n${msg} This package is NOT REQUIRED and you may ignore this warning but by doing so you may miss some functionality of ${CMAKE_PROJECT_NAME}. \n${vars}") endif() endfunction() ================================================ FILE: CMake/platforms/ctr/modules/try_add_imported_target.cmake ================================================ macro(try_add_imported_target LIBNAME) string(TOLOWER ${LIBNAME} ${LIBNAME}_lwr) set(${LIBNAME}_TARGET 3ds::${${LIBNAME}_lwr}) if(${LIBNAME}_FOUND AND NOT TARGET ${${LIBNAME}_TARGET}) add_library(${${LIBNAME}_TARGET} STATIC IMPORTED GLOBAL) set_target_properties(${${LIBNAME}_TARGET} PROPERTIES IMPORTED_LOCATION "${${LIBNAME}_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${${LIBNAME}_INCLUDE_DIR}") if(${ARGC} GREATER 1) target_link_libraries(${${LIBNAME}_TARGET} INTERFACE ${ARGN}) endif() endif() endmacro() ================================================ FILE: CMake/platforms/debian-cross-pkg-config.sh ================================================ #!/bin/sh # pkg-config wrapper for cross-building # Sets pkg-config search path to search multiarch and historical cross-compiling paths. # https://gist.github.com/doug65536/ea9c52f9a65a655a2fd5cc4997e8443b # If the user has already set PKG_CONFIG_LIBDIR, believe it (even if empty): # it's documented to be an override if [ x"${PKG_CONFIG_LIBDIR+set}" = x ]; then # GNU triplet for the compiler, e.g. i486-linux-gnu for Debian i386, # i686-linux-gnu for Ubuntu i386 basename="`basename "$0"`" triplet="${basename%-pkg-config}" # Normalized multiarch path if any, e.g. i386-linux-gnu for i386 multiarch="`dpkg-architecture -t"${triplet}" -qDEB_HOST_MULTIARCH 2>/dev/null`" # Native multiarch path if [ -f /usr/lib/pkg-config.multiarch ]; then native_multiarch="$(cat /usr/lib/pkg-config.multiarch)" # This can be used for native builds as well, in that case, just exec pkg-config "$@" directly. if [ "$native_multiarch" = "$multiarch" ]; then exec pkg-config "$@" fi fi PKG_CONFIG_LIBDIR="/usr/local/${triplet}/lib/pkgconfig" # For a native build we would also want to append /usr/local/lib/pkgconfig # at this point; but this is a cross-building script, so don't PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/local/share/pkgconfig" if [ -n "$multiarch" ]; then PKG_CONFIG_LIBDIR="/usr/local/lib/${multiarch}/pkgconfig:$PKG_CONFIG_LIBDIR" PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/lib/${multiarch}/pkgconfig" fi PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/${triplet}/lib/pkgconfig" # For a native build we would also want to append /usr/lib/pkgconfig # at this point; but this is a cross-building script, so don't # If you want to allow use of un-multiarched -dev packages for crossing # (at the risk of finding build-arch stuff you didn't want, if not in a clean chroot) # Uncomment the next line: # PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/lib/pkgconfig" # ... but on Ubuntu we rely cross-building with non-multiarch libraries: if dpkg-vendor --derives-from Ubuntu; then PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/lib/pkgconfig" fi PKG_CONFIG_LIBDIR="$PKG_CONFIG_LIBDIR:/usr/share/pkgconfig" export PKG_CONFIG_LIBDIR fi exec pkg-config "$@" ================================================ FILE: CMake/platforms/djcpp.toolchain.cmake ================================================ set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_VERSION 1) if($ENV{DJGPP_PREFIX}) set(DJGPP_PREFIX "$ENV{DJGPP_PREFIX}") else() set(DJGPP_PREFIX "/opt/i386-pc-msdosdjgpp-toolchain") endif() set(CMAKE_C_COMPILER "${DJGPP_PREFIX}/bin/i386-pc-msdosdjgpp-gcc") set(CMAKE_CXX_COMPILER "${DJGPP_PREFIX}/bin/i386-pc-msdosdjgpp-g++") set(CMAKE_STRIP "${DJGPP_PREFIX}/bin/i386-pc-msdosdjgpp-strip") set(PKG_CONFIG_EXECUTABLE "${DJGPP_PREFIX}/bin/i386-pc-msdosdjgpp-pkg-config" CACHE STRING "Path to pkg-config") set(CMAKE_EXE_LINKER_FLAGS_INIT "-static") set(DJGPP_ROOT "${DJGPP_PREFIX}/i386-pc-msdosdjgpp") set(CMAKE_FIND_ROOT_PATH "${DJGPP_ROOT}") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) link_directories("${DJGPP_ROOT}/lib") include_directories(BEFORE SYSTEM "${DJGPP_ROOT}/sys-include" "${DJGPP_ROOT}/include") ================================================ FILE: CMake/platforms/dos.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DIST ON) set(NONET ON) set(NOSOUND ON) set(DEFAULT_PER_PIXEL_LIGHTING false) set(PREFILL_PLAYER_NAME ON) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_SYSTEM_ZLIB OFF) set(DEVILUTIONX_STATIC_ZLIB ON) set(DEVILUTIONX_SYSTEM_SDL2 OFF) set(DEVILUTIONX_STATIC_SDL2 ON) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_LIBPNG OFF) set(DEVILUTIONX_DISPLAY_PIXELFORMAT SDL_PIXELFORMAT_INDEX8) set(DEVILUTIONX_PLATFORM_FILE_UTIL_LINK_LIBRARIES "") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS $<$:-gstabs>) add_compile_definitions( SDL_DISABLE_IMMINTRIN_H SDL_DISABLE_XMMINTRIN_H SDL_DISABLE_EMMINTRIN_H SDL_DISABLE_PMMINTRIN_H SDL_DISABLE_MMINTRIN_H ) ================================================ FILE: CMake/platforms/emscripten.cmake ================================================ set(BUILD_TESTING OFF) set(BUILD_ASSETS_MPQ OFF) set(DISABLE_ZERO_TIER ON) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) # Emscripten ports do have a bzip2 but it fails to link with this error: # warning: _BZ2_bzDecompress may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library # error: undefined symbol: BZ2_bzDecompressEnd (referenced by top-level compiled C/C++ code) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/emscripten/index.html" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") ================================================ FILE: CMake/platforms/gkd350h.cmake ================================================ set(BUILD_ASSETS_MPQ OFF) set(DISABLE_ZERO_TIER ON) set(USE_SDL1 ON) # Do not warn about unknown attributes, such as [[nodiscard]]. # As this build uses an older compiler, there are lots of them. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-attributes") # GKD350h IPU scaler is broken at the moment set(DEFAULT_WIDTH 320) set(DEFAULT_HEIGHT 240) set(SDL1_VIDEO_MODE_BPP 16) set(PREFILL_PLAYER_NAME ON) # In joystick mode, GKD350h reports D-Pad as left stick, # so we have to use keyboard mode instead. set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_LCTRL) set(KBCTRL_BUTTON_A SDLK_LALT) set(KBCTRL_BUTTON_Y SDLK_SPACE) set(KBCTRL_BUTTON_X SDLK_LSHIFT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB) # We swap Select and Start because Start + D-Pad is overtaken by the kernel. set(KBCTRL_BUTTON_START SDLK_ESCAPE) # Select set(KBCTRL_BUTTON_BACK SDLK_RETURN) # Start set(JOY_AXIS_LEFTX 0) set(JOY_AXIS_LEFTY 1) # Unused joystick mappings (kept here for future reference). set(JOY_HAT_DPAD_UP_HAT 0) set(JOY_HAT_DPAD_UP 1) set(JOY_HAT_DPAD_DOWN_HAT 0) set(JOY_HAT_DPAD_DOWN 4) set(JOY_HAT_DPAD_LEFT_HAT 0) set(JOY_HAT_DPAD_LEFT 8) set(JOY_HAT_DPAD_RIGHT_HAT 0) set(JOY_HAT_DPAD_RIGHT 2) set(JOY_BUTTON_A 0) set(JOY_BUTTON_B 1) set(JOY_BUTTON_Y 2) set(JOY_BUTTON_X 3) set(JOY_BUTTON_RIGHTSHOULDER 7) set(JOY_BUTTON_LEFTSHOULDER 6) set(JOY_BUTTON_START 5) set(JOY_BUTTON_BACK 4) ================================================ FILE: CMake/platforms/haiku.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DEVILUTIONX_STATIC_CXX_STDLIB OFF) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES network) ================================================ FILE: CMake/platforms/ios.cmake ================================================ enable_language(OBJC) # General build options. set(BUILD_TESTING OFF) # Disable all system dependencies. # All of these will be fetched via FetchContent and linked statically. set(DEVILUTIONX_SYSTEM_SDL2 OFF) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBPNG OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(NOEXIT ON) # Disable sanitizers. They're not supported out-of-the-box. set(ASAN OFF) set(UBSAN OFF) ================================================ FILE: CMake/platforms/ios.toolchain.cmake ================================================ # This file is part of the ios-cmake project. It was retrieved from # https://github.com/leetal/ios-cmake.git, which is a fork of # https://github.com/gerstrong/ios-cmake.git, which is a fork of # https://github.com/cristeab/ios-cmake.git, which is a fork of # https://code.google.com/p/ios-cmake/. Which in turn is based off of # the Platform/Darwin.cmake and Platform/UnixPaths.cmake files which # are included with CMake 2.8.4 # # The ios-cmake project is licensed under the new BSD license. # # Copyright (c) 2014, Bogdan Cristea and LTE Engineering Software, # Kitware, Inc., Insight Software Consortium. All rights reserved. # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # 3. Neither the name of the copyright holder nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # This file is based on the Platform/Darwin.cmake and # Platform/UnixPaths.cmake files which are included with CMake 2.8.4 # It has been altered for iOS development. # # Updated by Alex Stewart (alexs.mac@gmail.com) # # ***************************************************************************** # Now maintained by Alexander Widerberg (widerbergaren [at] gmail.com) # under the BSD-3-Clause license # https://github.com/leetal/ios-cmake # ***************************************************************************** # # INFORMATION / HELP # ############################################################################### # OPTIONS # ############################################################################### # # PLATFORM: (default "OS64") # OS = Build for iPhoneOS. # OS64 = Build for arm64 iphoneOS. # OS64COMBINED = Build for arm64 x86_64 iphoneOS + iphoneOS Simulator. Combined into FAT STATIC lib (only supported on 3.14+ of CMake with "-G Xcode" argument in combination with the "cmake --install" CMake build step) # SIMULATOR = Build for x86 i386 iphoneOS Simulator. # SIMULATOR64 = Build for x86_64 iphoneOS Simulator. # SIMULATORARM64 = Build for arm64 iphoneOS Simulator. # SIMULATOR64COMBINED = Build for arm64 x86_64 iphoneOS Simulator. Combined into FAT STATIC lib (supported on 3.14+ of CMakewith "-G Xcode" argument ONLY) # TVOS = Build for arm64 tvOS. # TVOSCOMBINED = Build for arm64 x86_64 tvOS + tvOS Simulator. Combined into FAT STATIC lib (only supported on 3.14+ of CMake with "-G Xcode" argument in combination with the "cmake --install" CMake build step) # SIMULATOR_TVOS = Build for x86_64 tvOS Simulator. # SIMULATORARM64_TVOS = Build for arm64 tvOS Simulator. # VISIONOSCOMBINED = Build for arm64 visionOS + visionOS Simulator. Combined into FAT STATIC lib (only supported on 3.14+ of CMake with "-G Xcode" argument in combination with the "cmake --install" CMake build step) # VISIONOS = Build for arm64 visionOS. # SIMULATOR_VISIONOS = Build for arm64 visionOS Simulator. # WATCHOS = Build for armv7k arm64_32 for watchOS. # WATCHOSCOMBINED = Build for armv7k arm64_32 x86_64 watchOS + watchOS Simulator. Combined into FAT STATIC lib (only supported on 3.14+ of CMake with "-G Xcode" argument in combination with the "cmake --install" CMake build step) # SIMULATOR_WATCHOS = Build for x86_64 for watchOS Simulator. # SIMULATORARM64_WATCHOS = Build for arm64 for watchOS Simulator. # MAC = Build for x86_64 macOS. # MAC_ARM64 = Build for Apple Silicon macOS. # MAC_UNIVERSAL = Combined build for x86_64 and Apple Silicon on macOS. # MAC_CATALYST = Build for x86_64 macOS with Catalyst support (iOS toolchain on macOS). # Note: The build argument "MACOSX_DEPLOYMENT_TARGET" can be used to control min-version of macOS # MAC_CATALYST_ARM64 = Build for Apple Silicon macOS with Catalyst support (iOS toolchain on macOS). # Note: The build argument "MACOSX_DEPLOYMENT_TARGET" can be used to control min-version of macOS # MAC_CATALYST_UNIVERSAL = Combined build for x86_64 and Apple Silicon on Catalyst. # # CMAKE_OSX_SYSROOT: Path to the SDK to use. By default this is # automatically determined from PLATFORM and xcodebuild, but # can also be manually specified (although this should not be required). # # CMAKE_DEVELOPER_ROOT: Path to the Developer directory for the platform # being compiled for. By default, this is automatically determined from # CMAKE_OSX_SYSROOT, but can also be manually specified (although this should # not be required). # # DEPLOYMENT_TARGET: Minimum SDK version to target. Default 6.0 on watchOS, 13.0 on tvOS+iOS/iPadOS, 11.0 on macOS, 1.0 on visionOS # # NAMED_LANGUAGE_SUPPORT: # ON (default) = Will require "enable_language(OBJC) and/or enable_language(OBJCXX)" for full OBJC|OBJCXX support # OFF = Will embed the OBJC and OBJCXX flags into the CMAKE_C_FLAGS and CMAKE_CXX_FLAGS (legacy behavior, CMake version < 3.16) # # ENABLE_BITCODE: (ON|OFF) Enables or disables bitcode support. Default OFF # # ENABLE_ARC: (ON|OFF) Enables or disables ARC support. Default ON (ARC enabled by default) # # ENABLE_VISIBILITY: (ON|OFF) Enables or disables symbol visibility support. Default OFF (visibility hidden by default) # # ENABLE_STRICT_TRY_COMPILE: (ON|OFF) Enables or disables strict try_compile() on all Check* directives (will run linker # to actually check if linking is possible). Default OFF (will set CMAKE_TRY_COMPILE_TARGET_TYPE to STATIC_LIBRARY) # # ARCHS: (armv7 armv7s armv7k arm64 arm64_32 i386 x86_64) If specified, will override the default architectures for the given PLATFORM # OS = armv7 armv7s arm64 (if applicable) # OS64 = arm64 (if applicable) # SIMULATOR = i386 # SIMULATOR64 = x86_64 # SIMULATORARM64 = arm64 # TVOS = arm64 # SIMULATOR_TVOS = x86_64 (i386 has since long been deprecated) # SIMULATORARM64_TVOS = arm64 # WATCHOS = armv7k arm64_32 (if applicable) # SIMULATOR_WATCHOS = x86_64 (i386 has since long been deprecated) # SIMULATORARM64_WATCHOS = arm64 # MAC = x86_64 # MAC_ARM64 = arm64 # MAC_UNIVERSAL = x86_64 arm64 # MAC_CATALYST = x86_64 # MAC_CATALYST_ARM64 = arm64 # MAC_CATALYST_UNIVERSAL = x86_64 arm64 # # NOTE: When manually specifying ARCHS, put a semi-colon between the entries. E.g., -DARCHS="armv7;arm64" # ############################################################################### # END OPTIONS # ############################################################################### # # This toolchain defines the following properties (available via get_property()) for use externally: # # PLATFORM: The currently targeted platform. # XCODE_VERSION: Version number (not including Build version) of Xcode detected. # SDK_VERSION: Version of SDK being used. # OSX_ARCHITECTURES: Architectures being compiled for (generated from PLATFORM). # APPLE_TARGET_TRIPLE: Used by autoconf build systems. NOTE: If "ARCHS" is overridden, this will *NOT* be set! # # This toolchain defines the following macros for use externally: # # set_xcode_property (TARGET XCODE_PROPERTY XCODE_VALUE XCODE_VARIANT) # A convenience macro for setting xcode specific properties on targets. # Available variants are: All, Release, RelWithDebInfo, Debug, MinSizeRel # example: set_xcode_property (myioslib IPHONEOS_DEPLOYMENT_TARGET "3.1" "all"). # # find_host_package (PROGRAM ARGS) # A macro used to find executable programs on the host system, not within the # environment. Thanks to the android-cmake project for providing the # command. # cmake_minimum_required(VERSION 3.8.0) # CMake invokes the toolchain file twice during the first build, but only once during subsequent rebuilds. # NOTE: To improve single-library build-times, provide the flag "OS_SINGLE_BUILD" as a build argument. if(DEFINED OS_SINGLE_BUILD AND DEFINED ENV{_IOS_TOOLCHAIN_HAS_RUN}) return() endif() set(ENV{_IOS_TOOLCHAIN_HAS_RUN} true) # List of supported platform values list(APPEND _supported_platforms "OS" "OS64" "OS64COMBINED" "SIMULATOR" "SIMULATOR64" "SIMULATORARM64" "SIMULATOR64COMBINED" "TVOS" "TVOSCOMBINED" "SIMULATOR_TVOS" "SIMULATORARM64_TVOS" "WATCHOS" "WATCHOSCOMBINED" "SIMULATOR_WATCHOS" "SIMULATORARM64_WATCHOS" "MAC" "MAC_ARM64" "MAC_UNIVERSAL" "VISIONOS" "SIMULATOR_VISIONOS" "VISIONOSCOMBINED" "MAC_CATALYST" "MAC_CATALYST_ARM64" "MAC_CATALYST_UNIVERSAL") # Cache what generator is used set(USED_CMAKE_GENERATOR "${CMAKE_GENERATOR}") # Check if using a CMake version capable of building combined FAT builds (simulator and target slices combined in one static lib) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.14") set(MODERN_CMAKE YES) endif() # Get the Xcode version being used. # Problem: CMake runs toolchain files multiple times, but can't read cache variables on some runs. # Workaround: On the first run (in which cache variables are always accessible), set an intermediary environment variable. # # NOTE: This pattern is used in many places in this toolchain to speed up checks of all sorts if(DEFINED XCODE_VERSION_INT) # Environment variables are always preserved. set(ENV{_XCODE_VERSION_INT} "${XCODE_VERSION_INT}") elseif(DEFINED ENV{_XCODE_VERSION_INT}) set(XCODE_VERSION_INT "$ENV{_XCODE_VERSION_INT}") elseif(NOT DEFINED XCODE_VERSION_INT) find_program(XCODEBUILD_EXECUTABLE xcodebuild) if(NOT XCODEBUILD_EXECUTABLE) message(FATAL_ERROR "xcodebuild not found. Please install either the standalone commandline tools or Xcode.") endif() execute_process(COMMAND ${XCODEBUILD_EXECUTABLE} -version OUTPUT_VARIABLE XCODE_VERSION_INT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) string(REGEX MATCH "Xcode [0-9\\.]+" XCODE_VERSION_INT "${XCODE_VERSION_INT}") string(REGEX REPLACE "Xcode ([0-9\\.]+)" "\\1" XCODE_VERSION_INT "${XCODE_VERSION_INT}") set(XCODE_VERSION_INT "${XCODE_VERSION_INT}" CACHE INTERNAL "") endif() # Assuming that xcode 12.0 is installed you most probably have ios sdk 14.0 or later installed (tested on Big Sur) # if you don't set a deployment target it will be set the way you only get 64-bit builds #if(NOT DEFINED DEPLOYMENT_TARGET AND XCODE_VERSION_INT VERSION_GREATER 12.0) # Temporarily fix the arm64 issues in CMake install-combined by excluding arm64 for simulator builds (needed for Apple Silicon...) # set(CMAKE_XCODE_ATTRIBUTE_EXCLUDED_ARCHS[sdk=iphonesimulator*] "arm64") #endif() # Check if the platform variable is set if(DEFINED PLATFORM) # Environment variables are always preserved. set(ENV{_PLATFORM} "${PLATFORM}") elseif(DEFINED ENV{_PLATFORM}) set(PLATFORM "$ENV{_PLATFORM}") elseif(NOT DEFINED PLATFORM) message(FATAL_ERROR "PLATFORM argument not set. Bailing configure since I don't know what target you want to build for!") endif () if(PLATFORM MATCHES ".*COMBINED" AND NOT CMAKE_GENERATOR MATCHES "Xcode") message(FATAL_ERROR "The combined builds support requires Xcode to be used as a generator via '-G Xcode' command-line argument in CMake") endif() # Safeguard that the platform value is set and is one of the supported values list(FIND _supported_platforms ${PLATFORM} contains_PLATFORM) if("${contains_PLATFORM}" EQUAL "-1") string(REPLACE ";" "\n * " _supported_platforms_formatted "${_supported_platforms}") message(FATAL_ERROR " Invalid PLATFORM specified! Current value: ${PLATFORM}.\n" " Supported PLATFORM values: \n * ${_supported_platforms_formatted}") endif() # Check if Apple Silicon is supported if(PLATFORM MATCHES "^(MAC_ARM64)$|^(MAC_CATALYST_ARM64)$|^(MAC_UNIVERSAL)$|^(MAC_CATALYST_UNIVERSAL)$" AND ${CMAKE_VERSION} VERSION_LESS "3.19.5") message(FATAL_ERROR "Apple Silicon builds requires a minimum of CMake 3.19.5") endif() # Touch the toolchain variable to suppress the "unused variable" warning. # This happens if CMake is invoked with the same command line the second time. if(CMAKE_TOOLCHAIN_FILE) endif() # Fix for PThread library not in path set(CMAKE_THREAD_LIBS_INIT "-lpthread") set(CMAKE_HAVE_THREADS_LIBRARY 1) set(CMAKE_USE_WIN32_THREADS_INIT 0) set(CMAKE_USE_PTHREADS_INIT 1) # Specify named language support defaults. if(NOT DEFINED NAMED_LANGUAGE_SUPPORT AND ${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16") set(NAMED_LANGUAGE_SUPPORT ON) message(STATUS "[DEFAULTS] Using explicit named language support! E.g., enable_language(CXX) is needed in the project files.") elseif(NOT DEFINED NAMED_LANGUAGE_SUPPORT AND ${CMAKE_VERSION} VERSION_LESS "3.16") set(NAMED_LANGUAGE_SUPPORT OFF) message(STATUS "[DEFAULTS] Disabling explicit named language support. Falling back to legacy behavior.") elseif(DEFINED NAMED_LANGUAGE_SUPPORT AND ${CMAKE_VERSION} VERSION_LESS "3.16") message(FATAL_ERROR "CMake named language support for OBJC and OBJCXX was added in CMake 3.16.") endif() set(NAMED_LANGUAGE_SUPPORT_INT ${NAMED_LANGUAGE_SUPPORT} CACHE BOOL "Whether or not to enable explicit named language support" FORCE) # Specify the minimum version of the deployment target. if(NOT DEFINED DEPLOYMENT_TARGET) if (PLATFORM MATCHES "WATCHOS") # Unless specified, SDK version 4.0 is used by default as minimum target version (watchOS). set(DEPLOYMENT_TARGET "6.0") elseif(PLATFORM STREQUAL "MAC") # Unless specified, SDK version 10.13 (High Sierra) is used by default as the minimum target version (macos). set(DEPLOYMENT_TARGET "11.0") elseif(PLATFORM STREQUAL "VISIONOS" OR PLATFORM STREQUAL "SIMULATOR_VISIONOS" OR PLATFORM STREQUAL "VISIONOSCOMBINED") # Unless specified, SDK version 1.0 is used by default as minimum target version (visionOS). set(DEPLOYMENT_TARGET "1.0") elseif(PLATFORM STREQUAL "MAC_ARM64") # Unless specified, SDK version 11.0 (Big Sur) is used by default as the minimum target version (macOS on arm). set(DEPLOYMENT_TARGET "11.0") elseif(PLATFORM STREQUAL "MAC_UNIVERSAL") # Unless specified, SDK version 11.0 (Big Sur) is used by default as minimum target version for universal builds. set(DEPLOYMENT_TARGET "11.0") elseif(PLATFORM STREQUAL "MAC_CATALYST" OR PLATFORM STREQUAL "MAC_CATALYST_ARM64" OR PLATFORM STREQUAL "MAC_CATALYST_UNIVERSAL") # Unless specified, SDK version 13.0 is used by default as the minimum target version (mac catalyst minimum requirement). set(DEPLOYMENT_TARGET "13.1") else() # Unless specified, SDK version 11.0 is used by default as the minimum target version (iOS, tvOS). set(DEPLOYMENT_TARGET "13.0") endif() message(STATUS "[DEFAULTS] Using the default min-version since DEPLOYMENT_TARGET not provided!") elseif(DEFINED DEPLOYMENT_TARGET AND PLATFORM MATCHES "^MAC_CATALYST" AND ${DEPLOYMENT_TARGET} VERSION_LESS "13.1") message(FATAL_ERROR "Mac Catalyst builds requires a minimum deployment target of 13.1!") endif() # Store the DEPLOYMENT_TARGET in the cache set(DEPLOYMENT_TARGET "${DEPLOYMENT_TARGET}" CACHE INTERNAL "") # Handle the case where we are targeting iOS and a version above 10.3.4 (32-bit support dropped officially) if(PLATFORM STREQUAL "OS" AND DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 10.3.4) set(PLATFORM "OS64") message(STATUS "Targeting minimum SDK version ${DEPLOYMENT_TARGET}. Dropping 32-bit support.") elseif(PLATFORM STREQUAL "SIMULATOR" AND DEPLOYMENT_TARGET VERSION_GREATER_EQUAL 10.3.4) set(PLATFORM "SIMULATOR64") message(STATUS "Targeting minimum SDK version ${DEPLOYMENT_TARGET}. Dropping 32-bit support.") endif() set(PLATFORM_INT "${PLATFORM}") if(DEFINED ARCHS) string(REPLACE ";" "-" ARCHS_SPLIT "${ARCHS}") endif() # Determine the platform name and architectures for use in xcodebuild commands # from the specified PLATFORM_INT name. if(PLATFORM_INT STREQUAL "OS") set(SDK_NAME iphoneos) if(NOT ARCHS) set(ARCHS armv7 armv7s arm64) set(APPLE_TARGET_TRIPLE_INT arm-apple-ios${DEPLOYMENT_TARGET}) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}) endif() elseif(PLATFORM_INT STREQUAL "OS64") set(SDK_NAME iphoneos) if(NOT ARCHS) if (XCODE_VERSION_INT VERSION_GREATER 10.0) set(ARCHS arm64) # FIXME: Add arm64e when Apple has fixed the integration issues with it, libarclite_iphoneos.a is currently missing bitcode markers for example else() set(ARCHS arm64) endif() set(APPLE_TARGET_TRIPLE_INT arm64-apple-ios${DEPLOYMENT_TARGET}) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}) endif() elseif(PLATFORM_INT STREQUAL "OS64COMBINED") set(SDK_NAME iphoneos) if(MODERN_CMAKE) if(NOT ARCHS) if (XCODE_VERSION_INT VERSION_GREATER 12.0) set(ARCHS arm64 x86_64) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64 arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] "x86_64 arm64") else() set(ARCHS arm64 x86_64) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] "x86_64") endif() set(APPLE_TARGET_TRIPLE_INT arm64-x86_64-apple-ios${DEPLOYMENT_TARGET}) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}) endif() else() message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the OS64COMBINED setting work") endif() elseif(PLATFORM_INT STREQUAL "SIMULATOR64COMBINED") set(SDK_NAME iphonesimulator) if(MODERN_CMAKE) if(NOT ARCHS) if (XCODE_VERSION_INT VERSION_GREATER 12.0) set(ARCHS arm64 x86_64) # FIXME: Add arm64e when Apple have fixed the integration issues with it, libarclite_iphoneos.a is currently missing bitcode markers for example set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64 arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] "") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] "x86_64 arm64") else() set(ARCHS arm64 x86_64) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphoneos*] "") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=iphonesimulator*] "x86_64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphoneos*] "") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=iphonesimulator*] "x86_64") endif() set(APPLE_TARGET_TRIPLE_INT aarch64-x86_64-apple-ios${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-simulator) endif() else() message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the SIMULATOR64COMBINED setting work") endif() elseif(PLATFORM_INT STREQUAL "SIMULATOR") set(SDK_NAME iphonesimulator) if(NOT ARCHS) set(ARCHS i386) set(APPLE_TARGET_TRIPLE_INT i386-apple-ios${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-simulator) endif() message(DEPRECATION "SIMULATOR IS DEPRECATED. Consider using SIMULATOR64 instead.") elseif(PLATFORM_INT STREQUAL "SIMULATOR64") set(SDK_NAME iphonesimulator) if(NOT ARCHS) set(ARCHS x86_64) set(APPLE_TARGET_TRIPLE_INT x86_64-apple-ios${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "SIMULATORARM64") set(SDK_NAME iphonesimulator) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-ios${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "TVOS") set(SDK_NAME appletvos) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-tvos${DEPLOYMENT_TARGET}) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-tvos${DEPLOYMENT_TARGET}) endif() elseif (PLATFORM_INT STREQUAL "TVOSCOMBINED") set(SDK_NAME appletvos) if(MODERN_CMAKE) if(NOT ARCHS) set(ARCHS arm64 x86_64) set(APPLE_TARGET_TRIPLE_INT arm64-x86_64-apple-tvos${DEPLOYMENT_TARGET}) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=appletvos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=appletvsimulator*] "x86_64 arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=appletvos*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=appletvsimulator*] "x86_64 arm64") else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-tvos${DEPLOYMENT_TARGET}) endif() else() message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the TVOSCOMBINED setting work") endif() elseif(PLATFORM_INT STREQUAL "SIMULATOR_TVOS") set(SDK_NAME appletvsimulator) if(NOT ARCHS) set(ARCHS x86_64) set(APPLE_TARGET_TRIPLE_INT x86_64-apple-tvos${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-tvos${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "SIMULATORARM64_TVOS") set(SDK_NAME appletvsimulator) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-tvos${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-tvos${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "WATCHOS") set(SDK_NAME watchos) if(NOT ARCHS) if (XCODE_VERSION_INT VERSION_GREATER 10.0) set(ARCHS armv7k arm64_32) set(APPLE_TARGET_TRIPLE_INT arm64_32-apple-watchos${DEPLOYMENT_TARGET}) else() set(ARCHS armv7k) set(APPLE_TARGET_TRIPLE_INT arm-apple-watchos${DEPLOYMENT_TARGET}) endif() else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-watchos${DEPLOYMENT_TARGET}) endif() elseif(PLATFORM_INT STREQUAL "WATCHOSCOMBINED") set(SDK_NAME watchos) if(MODERN_CMAKE) if(NOT ARCHS) if (XCODE_VERSION_INT VERSION_GREATER 10.0) set(ARCHS armv7k arm64_32 i386) set(APPLE_TARGET_TRIPLE_INT arm64_32-i386-apple-watchos${DEPLOYMENT_TARGET}) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=watchos*] "armv7k arm64_32") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=watchsimulator*] "i386") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=watchos*] "armv7k arm64_32") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=watchsimulator*] "i386") else() set(ARCHS armv7k i386) set(APPLE_TARGET_TRIPLE_INT arm-i386-apple-watchos${DEPLOYMENT_TARGET}) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=watchos*] "armv7k") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=watchsimulator*] "i386") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=watchos*] "armv7k") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=watchsimulator*] "i386") endif() else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-watchos${DEPLOYMENT_TARGET}) endif() else() message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the WATCHOSCOMBINED setting work") endif() elseif(PLATFORM_INT STREQUAL "SIMULATOR_WATCHOS") set(SDK_NAME watchsimulator) if(NOT ARCHS) set(ARCHS i386) set(APPLE_TARGET_TRIPLE_INT i386-apple-watchos${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-watchos${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "SIMULATORARM64_WATCHOS") set(SDK_NAME watchsimulator) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-watchos${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-watchos${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "SIMULATOR_VISIONOS") set(SDK_NAME xrsimulator) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-xros${DEPLOYMENT_TARGET}-simulator) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-xros${DEPLOYMENT_TARGET}-simulator) endif() elseif(PLATFORM_INT STREQUAL "VISIONOS") set(SDK_NAME xros) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-xros${DEPLOYMENT_TARGET}) else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-xros${DEPLOYMENT_TARGET}) endif() elseif(PLATFORM_INT STREQUAL "VISIONOSCOMBINED") set(SDK_NAME xros) if(MODERN_CMAKE) if(NOT ARCHS) set(ARCHS arm64) set(APPLE_TARGET_TRIPLE_INT arm64-apple-xros${DEPLOYMENT_TARGET}) set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=xros*] "arm64") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=xrsimulator*] "arm64") else() set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-xros${DEPLOYMENT_TARGET}) endif() else() message(FATAL_ERROR "Please make sure that you are running CMake 3.14+ to make the VISIONOSCOMBINED setting work") endif() elseif(PLATFORM_INT STREQUAL "MAC" OR PLATFORM_INT STREQUAL "MAC_CATALYST") set(SDK_NAME macosx) if(NOT ARCHS) set(ARCHS x86_64) endif() string(REPLACE ";" "-" ARCHS_SPLIT "${ARCHS}") if(PLATFORM_INT STREQUAL "MAC") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-macosx${DEPLOYMENT_TARGET}) elseif(PLATFORM_INT STREQUAL "MAC_CATALYST") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-macabi) endif() elseif(PLATFORM_INT MATCHES "^(MAC_ARM64)$|^(MAC_CATALYST_ARM64)$") set(SDK_NAME macosx) if(NOT ARCHS) set(ARCHS arm64) endif() string(REPLACE ";" "-" ARCHS_SPLIT "${ARCHS}") if(PLATFORM_INT STREQUAL "MAC_ARM64") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-macosx${DEPLOYMENT_TARGET}) elseif(PLATFORM_INT STREQUAL "MAC_CATALYST_ARM64") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-macabi) endif() elseif(PLATFORM_INT STREQUAL "MAC_UNIVERSAL") set(SDK_NAME macosx) if(NOT ARCHS) set(ARCHS "x86_64;arm64") endif() string(REPLACE ";" "-" ARCHS_SPLIT "${ARCHS}") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-macosx${DEPLOYMENT_TARGET}) elseif(PLATFORM_INT STREQUAL "MAC_CATALYST_UNIVERSAL") set(SDK_NAME macosx) if(NOT ARCHS) set(ARCHS "x86_64;arm64") endif() string(REPLACE ";" "-" ARCHS_SPLIT "${ARCHS}") set(APPLE_TARGET_TRIPLE_INT ${ARCHS_SPLIT}-apple-ios${DEPLOYMENT_TARGET}-macabi) else() message(FATAL_ERROR "Invalid PLATFORM: ${PLATFORM_INT}") endif() string(REPLACE ";" " " ARCHS_SPACED "${ARCHS}") if(MODERN_CMAKE AND PLATFORM_INT MATCHES ".*COMBINED" AND NOT CMAKE_GENERATOR MATCHES "Xcode") message(FATAL_ERROR "The COMBINED options only work with Xcode generator, -G Xcode") endif() if(CMAKE_GENERATOR MATCHES "Xcode" AND PLATFORM_INT MATCHES "^MAC_CATALYST") set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") set(CMAKE_XCODE_ATTRIBUTE_SUPPORTED_PLATFORMS "macosx") set(CMAKE_XCODE_ATTRIBUTE_SUPPORTS_MACCATALYST "YES") if(NOT DEFINED MACOSX_DEPLOYMENT_TARGET) set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "10.15") else() set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET "${MACOSX_DEPLOYMENT_TARGET}") endif() elseif(CMAKE_GENERATOR MATCHES "Xcode") set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++") set(CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "${DEPLOYMENT_TARGET}") if(NOT PLATFORM_INT MATCHES ".*COMBINED") set(CMAKE_XCODE_ATTRIBUTE_ARCHS[sdk=${SDK_NAME}*] "${ARCHS_SPACED}") set(CMAKE_XCODE_ATTRIBUTE_VALID_ARCHS[sdk=${SDK_NAME}*] "${ARCHS_SPACED}") endif() endif() # If the user did not specify the SDK root to use, then query xcodebuild for it. if(DEFINED CMAKE_OSX_SYSROOT_INT) # Environment variables are always preserved. set(ENV{_CMAKE_OSX_SYSROOT_INT} "${CMAKE_OSX_SYSROOT_INT}") elseif(DEFINED ENV{_CMAKE_OSX_SYSROOT_INT}) set(CMAKE_OSX_SYSROOT_INT "$ENV{_CMAKE_OSX_SYSROOT_INT}") elseif(NOT DEFINED CMAKE_OSX_SYSROOT_INT) execute_process(COMMAND ${XCODEBUILD_EXECUTABLE} -version -sdk ${SDK_NAME} Path OUTPUT_VARIABLE CMAKE_OSX_SYSROOT_INT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) endif() if (NOT DEFINED CMAKE_OSX_SYSROOT_INT AND NOT DEFINED CMAKE_OSX_SYSROOT) message(SEND_ERROR "Please make sure that Xcode is installed and that the toolchain" "is pointing to the correct path. Please run:" "sudo xcode-select -s /Applications/Xcode.app/Contents/Developer" "and see if that fixes the problem for you.") message(FATAL_ERROR "Invalid CMAKE_OSX_SYSROOT: ${CMAKE_OSX_SYSROOT} " "does not exist.") elseif(DEFINED CMAKE_OSX_SYSROOT_INT) set(CMAKE_OSX_SYSROOT_INT "${CMAKE_OSX_SYSROOT_INT}" CACHE INTERNAL "") # Specify the location or name of the platform SDK to be used in CMAKE_OSX_SYSROOT. set(CMAKE_OSX_SYSROOT "${CMAKE_OSX_SYSROOT_INT}" CACHE INTERNAL "") endif() # Use bitcode or not if(NOT DEFINED ENABLE_BITCODE) message(STATUS "[DEFAULTS] Disabling bitcode support by default. ENABLE_BITCODE not provided for override!") set(ENABLE_BITCODE OFF) endif() set(ENABLE_BITCODE_INT ${ENABLE_BITCODE} CACHE BOOL "Whether or not to enable bitcode" FORCE) # Use ARC or not if(NOT DEFINED ENABLE_ARC) # Unless specified, enable ARC support by default set(ENABLE_ARC ON) message(STATUS "[DEFAULTS] Enabling ARC support by default. ENABLE_ARC not provided!") endif() set(ENABLE_ARC_INT ${ENABLE_ARC} CACHE BOOL "Whether or not to enable ARC" FORCE) # Use hidden visibility or not if(NOT DEFINED ENABLE_VISIBILITY) # Unless specified, disable symbols visibility by default set(ENABLE_VISIBILITY OFF) message(STATUS "[DEFAULTS] Hiding symbols visibility by default. ENABLE_VISIBILITY not provided!") endif() set(ENABLE_VISIBILITY_INT ${ENABLE_VISIBILITY} CACHE BOOL "Whether or not to hide symbols from the dynamic linker (-fvisibility=hidden)" FORCE) # Set strict compiler checks or not if(NOT DEFINED ENABLE_STRICT_TRY_COMPILE) # Unless specified, disable strict try_compile() set(ENABLE_STRICT_TRY_COMPILE OFF) message(STATUS "[DEFAULTS] Using NON-strict compiler checks by default. ENABLE_STRICT_TRY_COMPILE not provided!") endif() set(ENABLE_STRICT_TRY_COMPILE_INT ${ENABLE_STRICT_TRY_COMPILE} CACHE BOOL "Whether or not to use strict compiler checks" FORCE) # Get the SDK version information. if(DEFINED SDK_VERSION) # Environment variables are always preserved. set(ENV{_SDK_VERSION} "${SDK_VERSION}") elseif(DEFINED ENV{_SDK_VERSION}) set(SDK_VERSION "$ENV{_SDK_VERSION}") elseif(NOT DEFINED SDK_VERSION) execute_process(COMMAND ${XCODEBUILD_EXECUTABLE} -sdk ${CMAKE_OSX_SYSROOT_INT} -version SDKVersion OUTPUT_VARIABLE SDK_VERSION ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) endif() # Find the Developer root for the specific iOS platform being compiled for # from CMAKE_OSX_SYSROOT. Should be ../../ from SDK specified in # CMAKE_OSX_SYSROOT. There does not appear to be a direct way to obtain # this information from xcrun or xcodebuild. if (NOT DEFINED CMAKE_DEVELOPER_ROOT AND NOT CMAKE_GENERATOR MATCHES "Xcode") get_filename_component(PLATFORM_SDK_DIR ${CMAKE_OSX_SYSROOT_INT} PATH) get_filename_component(CMAKE_DEVELOPER_ROOT ${PLATFORM_SDK_DIR} PATH) if (NOT EXISTS "${CMAKE_DEVELOPER_ROOT}") message(FATAL_ERROR "Invalid CMAKE_DEVELOPER_ROOT: ${CMAKE_DEVELOPER_ROOT} does not exist.") endif() endif() # Find the C & C++ compilers for the specified SDK. if(DEFINED CMAKE_C_COMPILER) # Environment variables are always preserved. set(ENV{_CMAKE_C_COMPILER} "${CMAKE_C_COMPILER}") elseif(DEFINED ENV{_CMAKE_C_COMPILER}) set(CMAKE_C_COMPILER "$ENV{_CMAKE_C_COMPILER}") set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) elseif(NOT DEFINED CMAKE_C_COMPILER) execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT_INT} -find clang OUTPUT_VARIABLE CMAKE_C_COMPILER ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) endif() if(DEFINED CMAKE_CXX_COMPILER) # Environment variables are always preserved. set(ENV{_CMAKE_CXX_COMPILER} "${CMAKE_CXX_COMPILER}") elseif(DEFINED ENV{_CMAKE_CXX_COMPILER}) set(CMAKE_CXX_COMPILER "$ENV{_CMAKE_CXX_COMPILER}") elseif(NOT DEFINED CMAKE_CXX_COMPILER) execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT_INT} -find clang++ OUTPUT_VARIABLE CMAKE_CXX_COMPILER ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) endif() # Find (Apple's) libtool. if(DEFINED BUILD_LIBTOOL) # Environment variables are always preserved. set(ENV{_BUILD_LIBTOOL} "${BUILD_LIBTOOL}") elseif(DEFINED ENV{_BUILD_LIBTOOL}) set(BUILD_LIBTOOL "$ENV{_BUILD_LIBTOOL}") elseif(NOT DEFINED BUILD_LIBTOOL) execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT_INT} -find libtool OUTPUT_VARIABLE BUILD_LIBTOOL ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) endif() # Find the toolchain's provided install_name_tool if none is found on the host if(DEFINED CMAKE_INSTALL_NAME_TOOL) # Environment variables are always preserved. set(ENV{_CMAKE_INSTALL_NAME_TOOL} "${CMAKE_INSTALL_NAME_TOOL}") elseif(DEFINED ENV{_CMAKE_INSTALL_NAME_TOOL}) set(CMAKE_INSTALL_NAME_TOOL "$ENV{_CMAKE_INSTALL_NAME_TOOL}") elseif(NOT DEFINED CMAKE_INSTALL_NAME_TOOL) execute_process(COMMAND xcrun -sdk ${CMAKE_OSX_SYSROOT_INT} -find install_name_tool OUTPUT_VARIABLE CMAKE_INSTALL_NAME_TOOL_INT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) set(CMAKE_INSTALL_NAME_TOOL ${CMAKE_INSTALL_NAME_TOOL_INT} CACHE INTERNAL "") endif() # Configure libtool to be used instead of ar + ranlib to build static libraries. # This is required on Xcode 7+, but should also work on previous versions of # Xcode. get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES) foreach(lang ${languages}) set(CMAKE_${lang}_CREATE_STATIC_LIBRARY "${BUILD_LIBTOOL} -static -o " CACHE INTERNAL "") endforeach() # CMake 3.14+ support building for iOS, watchOS, and tvOS out of the box. if(MODERN_CMAKE) if(SDK_NAME MATCHES "iphone") set(CMAKE_SYSTEM_NAME iOS) elseif(SDK_NAME MATCHES "xros") set(CMAKE_SYSTEM_NAME visionOS) elseif(SDK_NAME MATCHES "xrsimulator") set(CMAKE_SYSTEM_NAME visionOS) elseif(SDK_NAME MATCHES "macosx") set(CMAKE_SYSTEM_NAME Darwin) elseif(SDK_NAME MATCHES "appletv") set(CMAKE_SYSTEM_NAME tvOS) elseif(SDK_NAME MATCHES "watch") set(CMAKE_SYSTEM_NAME watchOS) endif() # Provide flags for a combined FAT library build on newer CMake versions if(PLATFORM_INT MATCHES ".*COMBINED") set(CMAKE_IOS_INSTALL_COMBINED YES) if(CMAKE_GENERATOR MATCHES "Xcode") # Set the SDKROOT Xcode properties to a Xcode-friendly value (the SDK_NAME, E.g, iphoneos) # This way, Xcode will automatically switch between the simulator and device SDK when building. set(CMAKE_XCODE_ATTRIBUTE_SDKROOT "${SDK_NAME}") # Force to not build just one ARCH, but all! set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "NO") endif() endif() elseif(NOT DEFINED CMAKE_SYSTEM_NAME AND ${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.10") # Legacy code path prior to CMake 3.14 or fallback if no CMAKE_SYSTEM_NAME specified set(CMAKE_SYSTEM_NAME iOS) elseif(NOT DEFINED CMAKE_SYSTEM_NAME) # Legacy code path before CMake 3.14 or fallback if no CMAKE_SYSTEM_NAME specified set(CMAKE_SYSTEM_NAME Darwin) endif() # Standard settings. set(CMAKE_SYSTEM_VERSION ${SDK_VERSION} CACHE INTERNAL "") set(UNIX ON CACHE BOOL "") set(APPLE ON CACHE BOOL "") if(PLATFORM STREQUAL "MAC" OR PLATFORM STREQUAL "MAC_ARM64" OR PLATFORM STREQUAL "MAC_UNIVERSAL") set(IOS OFF CACHE BOOL "") set(MACOS ON CACHE BOOL "") elseif(PLATFORM STREQUAL "MAC_CATALYST" OR PLATFORM STREQUAL "MAC_CATALYST_ARM64" OR PLATFORM STREQUAL "MAC_CATALYST_UNIVERSAL") set(IOS ON CACHE BOOL "") set(MACOS ON CACHE BOOL "") elseif(PLATFORM STREQUAL "VISIONOS" OR PLATFORM STREQUAL "SIMULATOR_VISIONOS" OR PLATFORM STREQUAL "VISIONOSCOMBINED") set(IOS OFF CACHE BOOL "") set(VISIONOS ON CACHE BOOL "") else() set(IOS ON CACHE BOOL "") endif() # Set the architectures for which to build. set(CMAKE_OSX_ARCHITECTURES ${ARCHS} CACHE INTERNAL "") # Change the type of target generated for try_compile() so it'll work when cross-compiling, weak compiler checks if(NOT ENABLE_STRICT_TRY_COMPILE_INT) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) endif() # All iOS/Darwin specific settings - some may be redundant. if (NOT DEFINED CMAKE_MACOSX_BUNDLE) set(CMAKE_MACOSX_BUNDLE YES) endif() set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO") set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO") set(CMAKE_SHARED_LIBRARY_PREFIX "lib") set(CMAKE_SHARED_LIBRARY_SUFFIX ".dylib") set(CMAKE_EXTRA_SHARED_LIBRARY_SUFFIXES ".tbd" ".so") set(CMAKE_SHARED_MODULE_PREFIX "lib") set(CMAKE_SHARED_MODULE_SUFFIX ".so") set(CMAKE_C_COMPILER_ABI ELF) set(CMAKE_CXX_COMPILER_ABI ELF) set(CMAKE_C_HAS_ISYSROOT 1) set(CMAKE_CXX_HAS_ISYSROOT 1) set(CMAKE_MODULE_EXISTS 1) set(CMAKE_DL_LIBS "") set(CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG "-compatibility_version ") set(CMAKE_C_OSX_CURRENT_VERSION_FLAG "-current_version ") set(CMAKE_CXX_OSX_COMPATIBILITY_VERSION_FLAG "${CMAKE_C_OSX_COMPATIBILITY_VERSION_FLAG}") set(CMAKE_CXX_OSX_CURRENT_VERSION_FLAG "${CMAKE_C_OSX_CURRENT_VERSION_FLAG}") if(ARCHS MATCHES "((^|;|, )(arm64|arm64e|x86_64))+") set(CMAKE_C_SIZEOF_DATA_PTR 8) set(CMAKE_CXX_SIZEOF_DATA_PTR 8) if(ARCHS MATCHES "((^|;|, )(arm64|arm64e))+") set(CMAKE_SYSTEM_PROCESSOR "aarch64") else() set(CMAKE_SYSTEM_PROCESSOR "x86_64") endif() else() set(CMAKE_C_SIZEOF_DATA_PTR 4) set(CMAKE_CXX_SIZEOF_DATA_PTR 4) set(CMAKE_SYSTEM_PROCESSOR "arm") endif() # Note that only Xcode 7+ supports the newer more specific: # -m${SDK_NAME}-version-min flags, older versions of Xcode use: # -m(ios/ios-simulator)-version-min instead. if(${CMAKE_VERSION} VERSION_LESS "3.11") if(PLATFORM_INT STREQUAL "OS" OR PLATFORM_INT STREQUAL "OS64") if(XCODE_VERSION_INT VERSION_LESS 7.0) set(SDK_NAME_VERSION_FLAGS "-mios-version-min=${DEPLOYMENT_TARGET}") else() # Xcode 7.0+ uses flags we can build directly from SDK_NAME. set(SDK_NAME_VERSION_FLAGS "-m${SDK_NAME}-version-min=${DEPLOYMENT_TARGET}") endif() elseif(PLATFORM_INT STREQUAL "TVOS") set(SDK_NAME_VERSION_FLAGS "-mtvos-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "SIMULATOR_TVOS") set(SDK_NAME_VERSION_FLAGS "-mtvos-simulator-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "SIMULATORARM64_TVOS") set(SDK_NAME_VERSION_FLAGS "-mtvos-simulator-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "WATCHOS") set(SDK_NAME_VERSION_FLAGS "-mwatchos-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "SIMULATOR_WATCHOS") set(SDK_NAME_VERSION_FLAGS "-mwatchos-simulator-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "SIMULATORARM64_WATCHOS") set(SDK_NAME_VERSION_FLAGS "-mwatchos-simulator-version-min=${DEPLOYMENT_TARGET}") elseif(PLATFORM_INT STREQUAL "MAC") set(SDK_NAME_VERSION_FLAGS "-mmacosx-version-min=${DEPLOYMENT_TARGET}") else() # SIMULATOR or SIMULATOR64 both use -mios-simulator-version-min. set(SDK_NAME_VERSION_FLAGS "-mios-simulator-version-min=${DEPLOYMENT_TARGET}") endif() elseif(NOT PLATFORM_INT MATCHES "^MAC_CATALYST") # Newer versions of CMake sets the version min flags correctly, skip this for Mac Catalyst targets set(CMAKE_OSX_DEPLOYMENT_TARGET ${DEPLOYMENT_TARGET} CACHE INTERNAL "Minimum OS X deployment version") endif() if(DEFINED APPLE_TARGET_TRIPLE_INT) set(APPLE_TARGET_TRIPLE ${APPLE_TARGET_TRIPLE_INT} CACHE INTERNAL "") set(CMAKE_C_COMPILER_TARGET ${APPLE_TARGET_TRIPLE}) set(CMAKE_CXX_COMPILER_TARGET ${APPLE_TARGET_TRIPLE}) set(CMAKE_ASM_COMPILER_TARGET ${APPLE_TARGET_TRIPLE}) endif() if(PLATFORM_INT MATCHES "^MAC_CATALYST") set(C_TARGET_FLAGS "-isystem ${CMAKE_OSX_SYSROOT_INT}/System/iOSSupport/usr/include -iframework ${CMAKE_OSX_SYSROOT_INT}/System/iOSSupport/System/Library/Frameworks") endif() if(ENABLE_BITCODE_INT) set(BITCODE "-fembed-bitcode") set(CMAKE_XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode") set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "YES") else() set(BITCODE "") set(CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE "NO") endif() if(ENABLE_ARC_INT) set(FOBJC_ARC "-fobjc-arc") set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "YES") else() set(FOBJC_ARC "-fno-objc-arc") set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC "NO") endif() if(NAMED_LANGUAGE_SUPPORT_INT) set(OBJC_VARS "-fobjc-abi-version=2 -DOBJC_OLD_DISPATCH_PROTOTYPES=0") set(OBJC_LEGACY_VARS "") else() set(OBJC_VARS "") set(OBJC_LEGACY_VARS "-fobjc-abi-version=2 -DOBJC_OLD_DISPATCH_PROTOTYPES=0") endif() if(NOT ENABLE_VISIBILITY_INT) foreach(lang ${languages}) set(CMAKE_${lang}_VISIBILITY_PRESET "hidden" CACHE INTERNAL "") endforeach() set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "YES") set(VISIBILITY "-fvisibility=hidden -fvisibility-inlines-hidden") else() foreach(lang ${languages}) set(CMAKE_${lang}_VISIBILITY_PRESET "default" CACHE INTERNAL "") endforeach() set(CMAKE_XCODE_ATTRIBUTE_GCC_SYMBOLS_PRIVATE_EXTERN "NO") set(VISIBILITY "-fvisibility=default") endif() if(DEFINED APPLE_TARGET_TRIPLE) set(APPLE_TARGET_TRIPLE_FLAG "-target ${APPLE_TARGET_TRIPLE}") endif() #Check if Xcode generator is used since that will handle these flags automagically if(CMAKE_GENERATOR MATCHES "Xcode") message(STATUS "Not setting any manual command-line buildflags, since Xcode is selected as the generator. Modifying the Xcode build-settings directly instead.") else() set(CMAKE_C_FLAGS "${C_TARGET_FLAGS} ${APPLE_TARGET_TRIPLE_FLAG} ${SDK_NAME_VERSION_FLAGS} ${OBJC_LEGACY_VARS} ${BITCODE} ${VISIBILITY} ${CMAKE_C_FLAGS}" CACHE INTERNAL "Flags used by the compiler during all C build types.") set(CMAKE_C_FLAGS_DEBUG "-O0 -g ${CMAKE_C_FLAGS_DEBUG}") set(CMAKE_C_FLAGS_MINSIZEREL "-DNDEBUG -Os ${CMAKE_C_FLAGS_MINSIZEREL}") set(CMAKE_C_FLAGS_RELWITHDEBINFO "-DNDEBUG -O2 -g ${CMAKE_C_FLAGS_RELWITHDEBINFO}") set(CMAKE_C_FLAGS_RELEASE "-DNDEBUG -O3 ${CMAKE_C_FLAGS_RELEASE}") set(CMAKE_CXX_FLAGS "${C_TARGET_FLAGS} ${APPLE_TARGET_TRIPLE_FLAG} ${SDK_NAME_VERSION_FLAGS} ${OBJC_LEGACY_VARS} ${BITCODE} ${VISIBILITY} ${CMAKE_CXX_FLAGS}" CACHE INTERNAL "Flags used by the compiler during all CXX build types.") set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g ${CMAKE_CXX_FLAGS_DEBUG}") set(CMAKE_CXX_FLAGS_MINSIZEREL "-DNDEBUG -Os ${CMAKE_CXX_FLAGS_MINSIZEREL}") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-DNDEBUG -O2 -g ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}") set(CMAKE_CXX_FLAGS_RELEASE "-DNDEBUG -O3 ${CMAKE_CXX_FLAGS_RELEASE}") if(NAMED_LANGUAGE_SUPPORT_INT) set(CMAKE_OBJC_FLAGS "${C_TARGET_FLAGS} ${APPLE_TARGET_TRIPLE_FLAG} ${SDK_NAME_VERSION_FLAGS} ${BITCODE} ${VISIBILITY} ${FOBJC_ARC} ${OBJC_VARS} ${CMAKE_OBJC_FLAGS}" CACHE INTERNAL "Flags used by the compiler during all OBJC build types.") set(CMAKE_OBJC_FLAGS_DEBUG "-O0 -g ${CMAKE_OBJC_FLAGS_DEBUG}") set(CMAKE_OBJC_FLAGS_MINSIZEREL "-DNDEBUG -Os ${CMAKE_OBJC_FLAGS_MINSIZEREL}") set(CMAKE_OBJC_FLAGS_RELWITHDEBINFO "-DNDEBUG -O2 -g ${CMAKE_OBJC_FLAGS_RELWITHDEBINFO}") set(CMAKE_OBJC_FLAGS_RELEASE "-DNDEBUG -O3 ${CMAKE_OBJC_FLAGS_RELEASE}") set(CMAKE_OBJCXX_FLAGS "${C_TARGET_FLAGS} ${APPLE_TARGET_TRIPLE_FLAG} ${SDK_NAME_VERSION_FLAGS} ${BITCODE} ${VISIBILITY} ${FOBJC_ARC} ${OBJC_VARS} ${CMAKE_OBJCXX_FLAGS}" CACHE INTERNAL "Flags used by the compiler during all OBJCXX build types.") set(CMAKE_OBJCXX_FLAGS_DEBUG "-O0 -g ${CMAKE_OBJCXX_FLAGS_DEBUG}") set(CMAKE_OBJCXX_FLAGS_MINSIZEREL "-DNDEBUG -Os ${CMAKE_OBJCXX_FLAGS_MINSIZEREL}") set(CMAKE_OBJCXX_FLAGS_RELWITHDEBINFO "-DNDEBUG -O2 -g ${CMAKE_OBJCXX_FLAGS_RELWITHDEBINFO}") set(CMAKE_OBJCXX_FLAGS_RELEASE "-DNDEBUG -O3 ${CMAKE_OBJCXX_FLAGS_RELEASE}") endif() set(CMAKE_C_LINK_FLAGS "${C_TARGET_FLAGS} ${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_C_LINK_FLAGS}" CACHE INTERNAL "Flags used by the compiler for all C link types.") set(CMAKE_CXX_LINK_FLAGS "${C_TARGET_FLAGS} ${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_CXX_LINK_FLAGS}" CACHE INTERNAL "Flags used by the compiler for all CXX link types.") if(NAMED_LANGUAGE_SUPPORT_INT) set(CMAKE_OBJC_LINK_FLAGS "${C_TARGET_FLAGS} ${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_OBJC_LINK_FLAGS}" CACHE INTERNAL "Flags used by the compiler for all OBJC link types.") set(CMAKE_OBJCXX_LINK_FLAGS "${C_TARGET_FLAGS} ${SDK_NAME_VERSION_FLAGS} -Wl,-search_paths_first ${CMAKE_OBJCXX_LINK_FLAGS}" CACHE INTERNAL "Flags used by the compiler for all OBJCXX link types.") endif() set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -arch ${CMAKE_OSX_ARCHITECTURES} ${APPLE_TARGET_TRIPLE_FLAG}" CACHE INTERNAL "Flags used by the compiler for all ASM build types.") endif() ## Print status messages to inform of the current state message(STATUS "Configuring ${SDK_NAME} build for platform: ${PLATFORM_INT}, architecture(s): ${ARCHS}") message(STATUS "Using SDK: ${CMAKE_OSX_SYSROOT_INT}") message(STATUS "Using C compiler: ${CMAKE_C_COMPILER}") message(STATUS "Using CXX compiler: ${CMAKE_CXX_COMPILER}") message(STATUS "Using libtool: ${BUILD_LIBTOOL}") message(STATUS "Using install name tool: ${CMAKE_INSTALL_NAME_TOOL}") if(DEFINED APPLE_TARGET_TRIPLE) message(STATUS "Autoconf target triple: ${APPLE_TARGET_TRIPLE}") endif() message(STATUS "Using minimum deployment version: ${DEPLOYMENT_TARGET}" " (SDK version: ${SDK_VERSION})") if(MODERN_CMAKE) message(STATUS "Merging integrated CMake 3.14+ iOS,tvOS,watchOS,macOS toolchain(s) with this toolchain!") if(PLATFORM_INT MATCHES ".*COMBINED") message(STATUS "Will combine built (static) artifacts into FAT lib...") endif() endif() if(CMAKE_GENERATOR MATCHES "Xcode") message(STATUS "Using Xcode version: ${XCODE_VERSION_INT}") endif() message(STATUS "CMake version: ${CMAKE_VERSION}") if(DEFINED SDK_NAME_VERSION_FLAGS) message(STATUS "Using version flags: ${SDK_NAME_VERSION_FLAGS}") endif() message(STATUS "Using a data_ptr size of: ${CMAKE_CXX_SIZEOF_DATA_PTR}") if(ENABLE_BITCODE_INT) message(STATUS "Bitcode: Enabled") else() message(STATUS "Bitcode: Disabled") endif() if(ENABLE_ARC_INT) message(STATUS "ARC: Enabled") else() message(STATUS "ARC: Disabled") endif() if(ENABLE_VISIBILITY_INT) message(STATUS "Hiding symbols: Disabled") else() message(STATUS "Hiding symbols: Enabled") endif() # Set global properties set_property(GLOBAL PROPERTY PLATFORM "${PLATFORM}") set_property(GLOBAL PROPERTY APPLE_TARGET_TRIPLE "${APPLE_TARGET_TRIPLE_INT}") set_property(GLOBAL PROPERTY SDK_VERSION "${SDK_VERSION}") set_property(GLOBAL PROPERTY XCODE_VERSION "${XCODE_VERSION_INT}") set_property(GLOBAL PROPERTY OSX_ARCHITECTURES "${CMAKE_OSX_ARCHITECTURES}") # Export configurable variables for the try_compile() command. set(CMAKE_TRY_COMPILE_PLATFORM_VARIABLES PLATFORM XCODE_VERSION_INT SDK_VERSION NAMED_LANGUAGE_SUPPORT DEPLOYMENT_TARGET CMAKE_DEVELOPER_ROOT CMAKE_OSX_SYSROOT_INT ENABLE_BITCODE ENABLE_ARC CMAKE_ASM_COMPILER CMAKE_C_COMPILER CMAKE_C_COMPILER_TARGET CMAKE_CXX_COMPILER CMAKE_CXX_COMPILER_TARGET BUILD_LIBTOOL CMAKE_INSTALL_NAME_TOOL CMAKE_C_FLAGS CMAKE_C_DEBUG CMAKE_C_MINSIZEREL CMAKE_C_RELWITHDEBINFO CMAKE_C_RELEASE CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO CMAKE_CXX_FLAGS_RELEASE CMAKE_C_LINK_FLAGS CMAKE_CXX_LINK_FLAGS CMAKE_ASM_FLAGS ) if(NAMED_LANGUAGE_SUPPORT_INT) list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES CMAKE_OBJC_FLAGS CMAKE_OBJC_DEBUG CMAKE_OBJC_MINSIZEREL CMAKE_OBJC_RELWITHDEBINFO CMAKE_OBJC_RELEASE CMAKE_OBJCXX_FLAGS CMAKE_OBJCXX_DEBUG CMAKE_OBJCXX_MINSIZEREL CMAKE_OBJCXX_RELWITHDEBINFO CMAKE_OBJCXX_RELEASE CMAKE_OBJC_LINK_FLAGS CMAKE_OBJCXX_LINK_FLAGS ) endif() set(CMAKE_PLATFORM_HAS_INSTALLNAME 1) set(CMAKE_SHARED_LINKER_FLAGS "-rpath @executable_path/Frameworks -rpath @loader_path/Frameworks") set(CMAKE_SHARED_LIBRARY_CREATE_C_FLAGS "-dynamiclib -Wl,-headerpad_max_install_names") set(CMAKE_SHARED_MODULE_CREATE_C_FLAGS "-bundle -Wl,-headerpad_max_install_names") set(CMAKE_SHARED_MODULE_LOADER_C_FLAG "-Wl,-bundle_loader,") set(CMAKE_SHARED_MODULE_LOADER_CXX_FLAG "-Wl,-bundle_loader,") set(CMAKE_FIND_LIBRARY_SUFFIXES ".tbd" ".dylib" ".so" ".a") set(CMAKE_SHARED_LIBRARY_SONAME_C_FLAG "-install_name") # Set the find root to the SDK developer roots. # Note: CMAKE_FIND_ROOT_PATH is only useful when cross-compiling. Thus, do not set on macOS builds. if(NOT PLATFORM_INT MATCHES "^MAC.*$") list(APPEND CMAKE_FIND_ROOT_PATH "${CMAKE_OSX_SYSROOT_INT}" CACHE INTERNAL "") set(CMAKE_IGNORE_PATH "/System/Library/Frameworks;/usr/local/lib;/opt/homebrew" CACHE INTERNAL "") endif() # Default to searching for frameworks first. IF(NOT DEFINED CMAKE_FIND_FRAMEWORK) set(CMAKE_FIND_FRAMEWORK FIRST) ENDIF(NOT DEFINED CMAKE_FIND_FRAMEWORK) # Set up the default search directories for frameworks. if(PLATFORM_INT MATCHES "^MAC_CATALYST") set(CMAKE_FRAMEWORK_PATH ${CMAKE_DEVELOPER_ROOT}/Library/PrivateFrameworks ${CMAKE_OSX_SYSROOT_INT}/System/Library/Frameworks ${CMAKE_OSX_SYSROOT_INT}/System/iOSSupport/System/Library/Frameworks ${CMAKE_FRAMEWORK_PATH} CACHE INTERNAL "") else() set(CMAKE_FRAMEWORK_PATH ${CMAKE_DEVELOPER_ROOT}/Library/PrivateFrameworks ${CMAKE_OSX_SYSROOT_INT}/System/Library/Frameworks ${CMAKE_FRAMEWORK_PATH} CACHE INTERNAL "") endif() # By default, search both the specified iOS SDK and the remainder of the host filesystem. if(NOT CMAKE_FIND_ROOT_PATH_MODE_PROGRAM) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH CACHE INTERNAL "") endif() if(NOT CMAKE_FIND_ROOT_PATH_MODE_LIBRARY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH CACHE INTERNAL "") endif() if(NOT CMAKE_FIND_ROOT_PATH_MODE_INCLUDE) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH CACHE INTERNAL "") endif() if(NOT CMAKE_FIND_ROOT_PATH_MODE_PACKAGE) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH CACHE INTERNAL "") endif() # # Some helper-macros below to simplify and beautify the CMakeFile # # This little macro lets you set any Xcode specific property. macro(set_xcode_property TARGET XCODE_PROPERTY XCODE_VALUE XCODE_RELVERSION) set(XCODE_RELVERSION_I "${XCODE_RELVERSION}") if(XCODE_RELVERSION_I STREQUAL "All") set_property(TARGET ${TARGET} PROPERTY XCODE_ATTRIBUTE_${XCODE_PROPERTY} "${XCODE_VALUE}") else() set_property(TARGET ${TARGET} PROPERTY XCODE_ATTRIBUTE_${XCODE_PROPERTY}[variant=${XCODE_RELVERSION_I}] "${XCODE_VALUE}") endif() endmacro(set_xcode_property) # This macro lets you find executable programs on the host system. macro(find_host_package) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE NEVER) set(_TOOLCHAIN_IOS ${IOS}) set(IOS OFF) find_package(${ARGN}) set(IOS ${_TOOLCHAIN_IOS}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH) endmacro(find_host_package) ================================================ FILE: CMake/platforms/lepus.cmake ================================================ set(BUILD_ASSETS_MPQ OFF) set(USE_SDL1 ON) set(SDL1_VIDEO_MODE_BPP 8) set(SDL1_VIDEO_MODE_FLAGS SDL_HWSURFACE|SDL_TRIPLEBUF) set(SDL1_FORCE_SVID_VIDEO_MODE ON) set(PREFILL_PLAYER_NAME ON) set(JOY_AXIS_LEFTX 0) set(JOY_AXIS_LEFTY 1) set(JOY_AXIS_RIGHTX 2) set(JOY_AXIS_RIGHTY 3) # OpenDingux Beta does not currently support X-OD-NeedsJoystick, # so we use KBCTRL instead. Unfortunately, this partially breaks # external mouse and keyboard support. set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_LCTRL) set(KBCTRL_BUTTON_A SDLK_LALT) set(KBCTRL_BUTTON_Y SDLK_SPACE) set(KBCTRL_BUTTON_X SDLK_LSHIFT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_BACK SDLK_ESCAPE) # Select set(KBCTRL_BUTTON_TRIGGERLEFT SDLK_PAGEUP) set(KBCTRL_BUTTON_TRIGGERRIGHT SDLK_PAGEDOWN) set(KBCTRL_BUTTON_LEFTSTICK SDLK_KP_DIVIDE) set(KBCTRL_BUTTON_RIGHTSTICK SDLK_KP_PERIOD) # Joystick mappings that have no effect on OpenDingux Beta: set(JOY_HAT_DPAD_UP_HAT 0) set(JOY_HAT_DPAD_UP 1) set(JOY_HAT_DPAD_DOWN_HAT 0) set(JOY_HAT_DPAD_DOWN 4) set(JOY_HAT_DPAD_LEFT_HAT 0) set(JOY_HAT_DPAD_LEFT 8) set(JOY_HAT_DPAD_RIGHT_HAT 0) set(JOY_HAT_DPAD_RIGHT 2) set(JOY_BUTTON_A 0) set(JOY_BUTTON_B 1) set(JOY_BUTTON_Y 2) set(JOY_BUTTON_X 3) set(JOY_BUTTON_RIGHTSHOULDER 5) set(JOY_BUTTON_LEFTSHOULDER 4) set(JOY_BUTTON_TRIGGERLEFT 6) set(JOY_BUTTON_TRIGGERRIGHT 7) set(JOY_BUTTON_START 9) set(JOY_BUTTON_BACK 8) set(JOY_BUTTON_LEFTSTICK 10) set(JOY_BUTTON_RIGHTSTICK 11) # Map Power button to Esc (Menu in-game / Exit in-menu). set(REMAP_KEYBOARD_KEYS "{SDLK_HOME,SDLK_ESCAPE}") ================================================ FILE: CMake/platforms/linux_i386.toolchain.cmake ================================================ message(STATUS "Using 32-bit toolchain") set(CMAKE_CXX_FLAGS -m32 CACHE STRING "") set(CMAKE_C_FLAGS -m32 CACHE STRING "") # Affects pkg-config set_property(GLOBAL PROPERTY FIND_LIBRARY_USE_LIB32_PATHS TRUE) # Used by pkg-config on Debian set(CMAKE_LIBRARY_ARCHITECTURE i386-linux-gnu) # Silly hack required to get the pkg-config path code to activate list(APPEND CMAKE_PREFIX_PATH /usr) # Find where 32-bit CMake modules are stored find_path(DIR NAMES cmake PATHS /usr/lib32 /usr/lib/i386-linux-gnu NO_DEFAULT_PATH) if(DIR) message(STATUS "Using 32-bit libraries from ${DIR}") # Read CMake modules from 32-bit packages # set(CMAKE_FIND_ROOT_PATH ${DIR}) # set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) # set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) # set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER) endif() set(PKG_CONFIG_EXECUTABLE "${CMAKE_CURRENT_LIST_DIR}/i386-linux-gnu-pkg-config" CACHE STRING "Path to pkg-config") # 32-bit NASM set(CMAKE_ASM_NASM_OBJECT_FORMAT elf) set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE i386) ================================================ FILE: CMake/platforms/macos_tiger.cmake ================================================ # ASAN and UBSAN are not supported by macports gcc14 on PowerPC. set(ASAN OFF) set(UBSAN OFF) # SDL2 does not build for Tiger, so we use SDL1 instead. set(USE_SDL1 ON) # ZeroTier is yet to be tested. set(DISABLE_ZERO_TIER ON) # Use vendored libfmt until this issue is resolved: # https://trac.macports.org/ticket/71503 set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_STATIC_LIBFMT ON) # https://trac.macports.org/ticket/71511 set(DEVILUTIONX_SYSTEM_GOOGLETEST OFF) set(DEVILUTIONX_STATIC_GOOGLETEST OFF) set(DEVILUTIONX_SYSTEM_BENCHMARK OFF) set(DEVILUTIONX_STATIC_BENCHMARK OFF) ================================================ FILE: CMake/platforms/macports/finders/FindMacportsLegacySupport.cmake ================================================ # Provides missing functions, such as localtime_r if(NOT TARGET MacportsLegacySupport::MacportsLegacySupport) set(MacportsLegacySupport_INCLUDE_DIR /opt/local/include/LegacySupport) mark_as_advanced(MacportsLegacySupport_INCLUDE_DIR) find_library(MacportsLegacySupport_LIBRARY NAMES MacportsLegacySupport PATHS /opt/local/lib) mark_as_advanced(MacportsLegacySupport_LIBRARY) include(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS( MacportsLegacySupport DEFAULT_MSG MacportsLegacySupport_LIBRARY MacportsLegacySupport_INCLUDE_DIR) if(MacportsLegacySupport_FOUND) set(MacportsLegacySupport_LIBRARIES ${MacportsLegacySupport_LIBRARY}) set(MacportsLegacySupport_INCLUDE_DIRS ${MacportsLegacySupport_INCLUDE_DIR}) add_library(MacportsLegacySupport::MacportsLegacySupport UNKNOWN IMPORTED) set_target_properties( MacportsLegacySupport::MacportsLegacySupport PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${MacportsLegacySupport_INCLUDE_DIR}" ) set_target_properties( MacportsLegacySupport::MacportsLegacySupport PROPERTIES IMPORTED_LINK_INTERFACE_LANGUAGES "C" IMPORTED_LOCATION "${MacportsLegacySupport_LIBRARY}" ) endif() endif() ================================================ FILE: CMake/platforms/mingw/zt_defs.cmake ================================================ option(MINGW_STDTHREADS_GENERATE_STDHEADERS "" OFF) # Workaround for deprecation of older CMake versions set(CMAKE_POLICY_VERSION_MINIMUM 3.22) FetchContent_Declare_ExcludeFromAll(mingw-std-threads GIT_REPOSITORY https://github.com/meganz/mingw-std-threads GIT_TAG bee085c0a6cb32c59f0b55c7bba976fe6dcfca7f) FetchContent_MakeAvailable_ExcludeFromAll(mingw-std-threads) target_compile_definitions(libnatpmp_obj PRIVATE -D_WIN32_WINNT=0x601 -DSTATICLIB) target_compile_definitions(zto_obj PRIVATE -D_WIN32_WINNT=0x601) target_compile_definitions(zto_pic PRIVATE -D_WIN32_WINNT=0x601) target_compile_definitions(libzt_obj PRIVATE -D_WIN32_WINNT=0x601) target_compile_definitions(zt_pic PRIVATE -D_WIN32_WINNT=0x601) target_compile_definitions(${libzt_LIB_NAME} PRIVATE -D_WIN32_WINNT=0x601 -DADD_EXPORTS=1) target_compile_definitions(${libzt_LIB_NAME} PUBLIC -DADD_EXPORTS=1) target_link_libraries(libzt_obj PRIVATE mingw_stdthreads) target_link_libraries(${libzt_LIB_NAME} mingw_stdthreads) ================================================ FILE: CMake/platforms/mingw9x/include/windef.h ================================================ #ifndef _WINDEF_OVERRIDE_ #define _WINDEF_OVERRIDE_ #include_next // MinGW does not define these when _WIN32_WINNT < 0x0400 // but it declares functions that use it unconditionally. typedef enum _FINDEX_INFO_LEVELS { FindExInfoStandard, FindExInfoBasic, FindExInfoMaxInfoLevel } FINDEX_INFO_LEVELS; typedef enum _FINDEX_SEARCH_OPS { FindExSearchNameMatch, FindExSearchLimitToDirectories, FindExSearchLimitToDevices, FindExSearchMaxSearchOp } FINDEX_SEARCH_OPS; typedef void* SOLE_AUTHENTICATION_SERVICE; #endif /* _WINDEF_ */ ================================================ FILE: CMake/platforms/mingw9x.toolchain.cmake ================================================ SET(MINGW_CROSS TRUE) SET(CROSS_PREFIX "/usr" CACHE STRING "crosstool-NG prefix") SET(CMAKE_SYSTEM_NAME Windows) # workaround list(APPEND CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "${CROSS_PREFIX}/i686-w64-mingw32/include") list(PREPEND CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/mingw9x/include") list(PREPEND CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_LIST_DIR}/mingw9x/include") # work around https://gcc.gnu.org/bugzilla/show_bug.cgi?id=106103 set(CMAKE_CXX_FLAGS_MINSIZEREL_INIT "${CMAKE_CXX_FLAGS_MINSIZEREL_INIT} -fno-declone-ctor-dtor") SET(CMAKE_C_COMPILER "i686-w64-mingw32-gcc") SET(CMAKE_CXX_COMPILER "i686-w64-mingw32-g++") set(CMAKE_RC_COMPILER "i686-w64-mingw32-windres") set(CMAKE_STRIP "${CROSS_PREFIX}/i686-w64-mingw32/bin/strip") set(PKG_CONFIG_EXECUTABLE "${CROSS_PREFIX}/bin/i686-w64-mingw32-pkg-config" CACHE STRING "Path to pkg-config") SET(CMAKE_FIND_ROOT_PATH "${CROSS_PREFIX}/i686-w64-mingw32" "${CROSS_PREFIX}/i686-w64-mingw32/i686-w64-mingw32") SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(WIN32_INSTALL_DLLS "${CROSS_PREFIX}/i686-w64-mingw32/bin/SDL.dll") ================================================ FILE: CMake/platforms/mingwcc.toolchain.cmake ================================================ SET(MINGW_CROSS TRUE) SET(CROSS_PREFIX "/usr" CACHE STRING "crosstool-NG prefix") SET(CMAKE_SYSTEM_NAME Windows) # workaround list(APPEND CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "${CROSS_PREFIX}/i686-w64-mingw32/include") SET(CMAKE_C_COMPILER "i686-w64-mingw32-gcc") SET(CMAKE_CXX_COMPILER "i686-w64-mingw32-g++") set(CMAKE_RC_COMPILER "i686-w64-mingw32-windres") set(CMAKE_STRIP "${CROSS_PREFIX}/i686-w64-mingw32/bin/strip") set(PKG_CONFIG_EXECUTABLE "${CROSS_PREFIX}/bin/i686-w64-mingw32-pkg-config" CACHE STRING "Path to pkg-config") SET(CMAKE_FIND_ROOT_PATH "${CROSS_PREFIX}/i686-w64-mingw32" "${CROSS_PREFIX}/i686-w64-mingw32/i686-w64-mingw32") SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) ================================================ FILE: CMake/platforms/mingwcc64.toolchain.cmake ================================================ SET(MINGW_CROSS TRUE) SET(CROSS_PREFIX "/usr" CACHE STRING "crosstool-NG prefix") SET(CMAKE_SYSTEM_NAME Windows) # workaround list(APPEND CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "${CROSS_PREFIX}/x86_64-w64-mingw32/include") SET(CMAKE_C_COMPILER "x86_64-w64-mingw32-gcc") SET(CMAKE_CXX_COMPILER "x86_64-w64-mingw32-g++") set(CMAKE_RC_COMPILER "x86_64-w64-mingw32-windres") set(CMAKE_STRIP "${CROSS_PREFIX}/x86_64-w64-mingw32/bin/strip") set(PKG_CONFIG_EXECUTABLE "${CROSS_PREFIX}/bin/x86_64-w64-mingw32-pkg-config" CACHE STRING "Path to pkg-config") SET(CMAKE_FIND_ROOT_PATH "${CROSS_PREFIX}/x86_64-w64-mingw32" "${CROSS_PREFIX}/x86_64-w64-mingw32/x86_64-w64-mingw32") SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) SET(SYSTEM_BITS x64) ================================================ FILE: CMake/platforms/miyoo_mini.cmake ================================================ set(USE_SDL1 ON) set(NONET ON) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(SDL1_VIDEO_MODE_BPP 32) set(SDL1_VIDEO_MODE_FLAGS SDL_HWSURFACE) set(SDL1_FORCE_SVID_VIDEO_MODE ON) set(PREFILL_PLAYER_NAME ON) set(DEFAULT_AUDIO_SAMPLE_RATE 44100) # The mini's buttons are connected via GPIO and are mapped to keyboard inputs set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_SPACE) set(KBCTRL_BUTTON_A SDLK_LCTRL) set(KBCTRL_BUTTON_Y SDLK_LSHIFT) set(KBCTRL_BUTTON_X SDLK_LALT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_t) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_e) set(KBCTRL_BUTTON_TRIGGERRIGHT SDLK_BACKSPACE) set(KBCTRL_BUTTON_TRIGGERLEFT SDLK_TAB) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_BACK SDLK_RCTRL) ================================================ FILE: CMake/platforms/n3ds.cmake ================================================ #General compilation options set(ASAN OFF) set(UBSAN OFF) set(BUILD_TESTING OFF) set(BUILD_ASSETS_MPQ OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_STATIC_LIBSODIUM ON) set(DEVILUTIONX_STATIC_LIBFMT ON) set(DISABLE_ZERO_TIER ON) set(LIBMPQ_FILE_BUFFER_SIZE 32768) set(NOEXIT ON) # 3DS libraries and compile definitions list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/ctr") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/ctr/modules") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/threads-stub") find_package(CITRO3D REQUIRED) list(APPEND DEVILUTIONX_PLATFORM_SUBDIRECTORIES platform/ctr) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES libdevilutionx_ctr) list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS __3DS__) # The 3ds build handles the stripping in a custom way. set(DEVILUTIONX_DISABLE_STRIP ON) #SDL video mode parameters set(USE_SDL1 ON) set(SDL1_VIDEO_MODE_FLAGS SDL_DOUBLEBUF|SDL_HWSURFACE) set(SDL1_FORCE_SVID_VIDEO_MODE ON) set(SDL1_VIDEO_MODE_BPP 8) set(DEFAULT_WIDTH 800) set(DEFAULT_HEIGHT 480) set(DEFAULT_PER_PIXEL_LIGHTING false) #Deploy assets to romfs set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/romfs") #SDL Joystick axis mapping (circle-pad/C-stick) set(JOY_AXIS_LEFTX 0) set(JOY_AXIS_LEFTY 1) set(JOY_AXIS_RIGHTX 2) set(JOY_AXIS_RIGHTY 3) #SDL Joystick hat mapping (D-pad) set(JOY_HAT_DPAD_UP_HAT 0) set(JOY_HAT_DPAD_RIGHT_HAT 0) set(JOY_HAT_DPAD_DOWN_HAT 0) set(JOY_HAT_DPAD_LEFT_HAT 0) set(JOY_HAT_DPAD_UP 1) set(JOY_HAT_DPAD_RIGHT 2) set(JOY_HAT_DPAD_DOWN 4) set(JOY_HAT_DPAD_LEFT 8) #SDL Joystick button mapping (A / B and X / Y inverted) set(JOY_BUTTON_A 2) set(JOY_BUTTON_B 1) set(JOY_BUTTON_X 4) set(JOY_BUTTON_Y 3) set(JOY_BUTTON_LEFTSHOULDER 5) set(JOY_BUTTON_RIGHTSHOULDER 6) set(JOY_BUTTON_BACK 7) set(JOY_BUTTON_START 0) set(JOY_BUTTON_TRIGGERLEFT 8) set(JOY_BUTTON_TRIGGERRIGHT 9) #Additional gamepad related options set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(PREFILL_PLAYER_NAME ON) ================================================ FILE: CMake/platforms/ps4.cmake ================================================ set(DISCORD_INTEGRATION OFF) set(BUILD_TESTING OFF) set(ASAN OFF) set(UBSAN OFF) SET(DISABLE_LTO ON) set(NONET ON) set(NOEXIT ON) set(DEVILUTIONX_GAMEPAD_TYPE PlayStation) set(BUILD_ASSETS_MPQ ON) # Packbrew SDK provides SDL_image, but FindSDL2_image() fails to # pick up its dependencies (with includes libjpeg, libwebp etc). # One way to address this, is to do the following: # # target_link_libraries(${BIN_TARGET} PUBLIC ${PC_SDL2_image_LIBRARIES}) # # or simply use the in-tree copy as follows: set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) # If the executable is stripped, create-fself fails with: # Failed to build FSELF: no symbol section set(DEVILUTIONX_DISABLE_STRIP ON) ================================================ FILE: CMake/platforms/retrofw.cmake ================================================ set(BUILD_ASSETS_MPQ OFF) set(NONET ON) set(USE_SDL1 ON) set(PREFILL_PLAYER_NAME ON) set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3") set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_LCTRL) set(KBCTRL_BUTTON_A SDLK_LALT) set(KBCTRL_BUTTON_Y SDLK_SPACE) set(KBCTRL_BUTTON_X SDLK_LSHIFT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_LEFTSTICK SDLK_END) # Suspend set(KBCTRL_BUTTON_BACK SDLK_ESCAPE) # Select set(KBCTRL_IGNORE_1 SDLK_3) # Backlight ================================================ FILE: CMake/platforms/rg350.cmake ================================================ set(BUILD_ASSETS_MPQ OFF) set(USE_SDL1 ON) # LTO temporarily disabled to work around a compiler bug. # https://github.com/diasurgical/devilutionX/issues/4953 set(DISABLE_LTO ON) set(SDL1_VIDEO_MODE_BPP 8) set(SDL1_VIDEO_MODE_FLAGS SDL_HWSURFACE|SDL_TRIPLEBUF) set(SDL1_FORCE_SVID_VIDEO_MODE ON) set(PREFILL_PLAYER_NAME ON) set(JOY_AXIS_LEFTX 0) set(JOY_AXIS_LEFTY 1) set(JOY_AXIS_RIGHTX 2) set(JOY_AXIS_RIGHTY 3) # OpenDingux Beta does not currently support X-OD-NeedsJoystick, # so we use KBCTRL instead. Unfortunately, this partially breaks # external mouse and keyboard support. set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_LCTRL) set(KBCTRL_BUTTON_A SDLK_LALT) set(KBCTRL_BUTTON_Y SDLK_SPACE) set(KBCTRL_BUTTON_X SDLK_LSHIFT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_BACK SDLK_ESCAPE) # Select set(KBCTRL_BUTTON_TRIGGERLEFT SDLK_PAGEUP) set(KBCTRL_BUTTON_TRIGGERRIGHT SDLK_PAGEDOWN) set(KBCTRL_BUTTON_LEFTSTICK SDLK_KP_DIVIDE) set(KBCTRL_BUTTON_RIGHTSTICK SDLK_KP_PERIOD) # Joystick mappings that have no effect on OpenDingux Beta: set(JOY_HAT_DPAD_UP_HAT 0) set(JOY_HAT_DPAD_UP 1) set(JOY_HAT_DPAD_DOWN_HAT 0) set(JOY_HAT_DPAD_DOWN 4) set(JOY_HAT_DPAD_LEFT_HAT 0) set(JOY_HAT_DPAD_LEFT 8) set(JOY_HAT_DPAD_RIGHT_HAT 0) set(JOY_HAT_DPAD_RIGHT 2) set(JOY_BUTTON_A 0) set(JOY_BUTTON_B 1) set(JOY_BUTTON_Y 2) set(JOY_BUTTON_X 3) set(JOY_BUTTON_RIGHTSHOULDER 5) set(JOY_BUTTON_LEFTSHOULDER 4) set(JOY_BUTTON_TRIGGERLEFT 6) set(JOY_BUTTON_TRIGGERRIGHT 7) set(JOY_BUTTON_START 9) set(JOY_BUTTON_BACK 8) set(JOY_BUTTON_LEFTSTICK 10) set(JOY_BUTTON_RIGHTSTICK 11) # Map Power button to Esc (Menu in-game / Exit in-menu). set(REMAP_KEYBOARD_KEYS "{SDLK_HOME,SDLK_ESCAPE}") ================================================ FILE: CMake/platforms/rg99.cmake ================================================ # RG99 has the same layout as RG300 but only 32 MiB RAM set(BUILD_ASSETS_MPQ OFF) set(UNPACKED_MPQS ON) set(UNPACKED_SAVES ON) set(NONET ON) set(USE_SDL1 ON) # Link `libstdc++` dynamically: ~1.3 MiB. # The OPK is mounted as squashfs and the binary is decompressed, while # the system `libstdc++` resides on disk. set(DEVILUTIONX_STATIC_CXX_STDLIB OFF) # -fmerge-all-constants saves ~4 KiB # -fsection-anchors saves ~4 KiB set(_extra_flags "-fmerge-all-constants -fsection-anchors") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${_extra_flags}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${_extra_flags}") # -Wl,-z-stack-size: the default thread stack size for RG99 is 128 KiB, reduce it. # https://wiki.musl-libc.org/functional-differences-from-glibc.html#Thread-stack-size set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,stack-size=32768") # 128 KiB set(DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT OFF) # Must stream most of the audio due to RAM constraints. set(STREAM_ALL_AUDIO_MIN_FILE_SIZE 4096) # Must use a smaller audio buffer due to RAM constraints. set(DEFAULT_AUDIO_BUFFER_SIZE 768) # Use lower resampling quality for FPS. set(DEFAULT_AUDIO_RESAMPLING_QUALITY 2) # RG-99 hardware scaler can only scale YUV. # The SDL library on RG-99 can convert 8-bit palettized surfaces to YUV automatically. set(SDL1_VIDEO_MODE_BPP 8) set(SDL1_FORCE_SVID_VIDEO_MODE ON) set(SDL1_FORCE_DIRECT_RENDER ON) # Must be an HWSURFACE for the scaler to work. set(SDL1_VIDEO_MODE_FLAGS SDL_HWSURFACE|SDL_FULLSCREEN) # Videos are 320x240, so they fit in video ram double-buffered. set(SDL1_VIDEO_MODE_SVID_FLAGS SDL_HWSURFACE|SDL_FULLSCREEN|SDL_DOUBLEBUF) set(PREFILL_PLAYER_NAME ON) set(HAS_KBCTRL 1) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(KBCTRL_BUTTON_DPAD_LEFT SDLK_LEFT) set(KBCTRL_BUTTON_DPAD_RIGHT SDLK_RIGHT) set(KBCTRL_BUTTON_DPAD_UP SDLK_UP) set(KBCTRL_BUTTON_DPAD_DOWN SDLK_DOWN) set(KBCTRL_BUTTON_B SDLK_LCTRL) set(KBCTRL_BUTTON_A SDLK_LALT) set(KBCTRL_BUTTON_Y SDLK_SPACE) set(KBCTRL_BUTTON_X SDLK_LSHIFT) set(KBCTRL_BUTTON_RIGHTSHOULDER SDLK_BACKSPACE) set(KBCTRL_BUTTON_LEFTSHOULDER SDLK_TAB) set(KBCTRL_BUTTON_START SDLK_RETURN) set(KBCTRL_BUTTON_LEFTSTICK SDLK_END) # Suspend set(KBCTRL_BUTTON_BACK SDLK_ESCAPE) # Select set(KBCTRL_IGNORE_1 SDLK_3) # Backlight ================================================ FILE: CMake/platforms/switch/asio_defs.cmake ================================================ # Enables a number of header file definitions required by ASIO target_compile_definitions(asio INTERFACE _DEFAULT_SOURCE=ON) # Missing headers and declarations provided by DevilutionX target_include_directories(asio BEFORE INTERFACE ${PROJECT_SOURCE_DIR}/Source/platform/switch/asio/include) ================================================ FILE: CMake/platforms/switch.cmake ================================================ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/switch") set(ASAN OFF) set(UBSAN OFF) set(BUILD_TESTING OFF) set(BUILD_ASSETS_MPQ OFF) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DISABLE_ZERO_TIER ON) set(PREFILL_PLAYER_NAME ON) set(DEVILUTIONX_GAMEPAD_TYPE Nintendo) set(NOEXIT ON) set(JOY_BUTTON_DPAD_LEFT 16) set(JOY_BUTTON_DPAD_UP 17) set(JOY_BUTTON_DPAD_RIGHT 18) set(JOY_BUTTON_DPAD_DOWN 19) list(APPEND DEVILUTIONX_PLATFORM_SUBDIRECTORIES platform/switch) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES libdevilutionx_switch) ================================================ FILE: CMake/platforms/threads-stub/FindThreads.cmake ================================================ # Stub out the Threads package. # Some platforms do not have a system threads library but SDL threads are supported. add_library(Threads::Threads INTERFACE IMPORTED GLOBAL) ================================================ FILE: CMake/platforms/uwp_lib.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(BUILD_ASSETS_MPQ OFF) set(BUILD_TESTING OFF) set(DISCORD_INTEGRATION OFF) set(DEVILUTIONX_GAMEPAD_TYPE Xbox) # setting all libs to be built statically and from source set(DEVILUTIONX_SYSTEM_SDL2 OFF) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_SDL_AUDIOLIB OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(DEVILUTIONX_SYSTEM_LIBPNG OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_SYSTEM_ZLIB OFF) set(DEVILUTIONX_STATIC_SDL2 ON) set(DEVILUTIONX_STATIC_SDL_IMAGE ON) set(DEVILUTIONX_STATIC_SDL_AUDIOLIB ON) set(DEVILUTIONX_STATIC_LIBSODIUM ON) set(DEVILUTIONX_STATIC_LIBPNG ON) set(DEVILUTIONX_STATIC_LIBFMT ON) set(DEVILUTIONX_STATIC_BZIP2 ON) set(DEVILUTIONX_STATIC_ZLIB ON) # not really necessary but a good measure for SDL related stuff set(WINDOWS_STORE ON) add_library(uwp_defs INTERFACE) target_compile_definitions(uwp_defs INTERFACE __UWP__=1) set(DEVILUTIONX_PLATFORM_ASSETS_LINK_LIBRARIES uwp_defs) set(DEVILUTIONX_PLATFORM_LINK_LIBRARIES uwp_defs) ================================================ FILE: CMake/platforms/vita.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DEVILUTIONX_STATIC_CXX_STDLIB OFF) set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) set(BUILD_TESTING OFF) set(DISABLE_ZERO_TIER ON) set(PREFILL_PLAYER_NAME ON) set(DEVILUTIONX_GAMEPAD_TYPE PlayStation) set(NOEXIT ON) list(APPEND DEVILUTIONX_PLATFORM_SUBDIRECTORIES platform/vita) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES libdevilutionx_vita) list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS VITA) # The Vita build needs the information set(DEVILUTIONX_DISABLE_STRIP ON) ================================================ FILE: CMake/platforms/windows.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DIST ON) set(DEVILUTIONX_PLATFORM_FILE_UTIL_LINK_LIBRARIES shlwapi) set(DEVILUTIONX_PLATFORM_ASSETS_LINK_LIBRARIES find_steam_game) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES find_steam_game shlwapi wsock32 ws2_32 wininet ) add_definitions(-DWINVER=0x0601 -D_WIN32_WINDOWS=0x0601 -D_WIN32_WINNT=0x0601) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS "/W3" "/Zc:__cplusplus" "/utf-8") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS) else() list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS $<$:-gstabs>) endif() if(MINGW_CROSS) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/mingw") endif() ================================================ FILE: CMake/platforms/windows9x.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DIST ON) set(NONET ON) set(DISABLE_ZERO_TIER ON) set(USE_SDL1 ON) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(DEVILUTIONX_STATIC_LIBSODIUM OFF) # Compatibility with Windows 9x 8-bit mode and improved performance set(SDL1_VIDEO_MODE_BPP 8) set(SDL1_FORCE_DIRECT_RENDER ON) set(DEVILUTIONX_WINDOWS_NO_WCHAR ON) # `WINVER=0x0500` without `_WIN32_WINNT` is Windows 98. # MinGW force-defines `_WIN32_WINNT=0xa00` if it isn't defined, so define it as 0. add_definitions(-DWINVER=0x0500 -D_WIN32_WINDOWS=0x0500 -D_WIN32_WINNT=0) set(DEVILUTIONX_PLATFORM_FILE_UTIL_LINK_LIBRARIES shlwapi) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES shlwapi wsock32 ws2_32 wininet ) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS "/W3" "/Zc:__cplusplus" "/utf-8") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS) else() list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS $<$:-gstabs>) endif() if(MINGW_CROSS) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/mingw") endif() ================================================ FILE: CMake/platforms/windowsXP.cmake ================================================ set(ASAN OFF) set(UBSAN OFF) set(DIST ON) set(DEVILUTIONX_PLATFORM_FILE_UTIL_LINK_LIBRARIES shlwapi) set(DEVILUTIONX_PLATFORM_ASSETS_LINK_LIBRARIES find_steam_game) set(DISABLE_ZERO_TIER ON) set(DISCORD_INTEGRATION OFF) set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_STATIC_LIBSODIUM ON) list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES find_steam_game shlwapi wsock32 ws2_32 wininet ) add_definitions(-DWINVER=0x0501 -D_WIN32_WINDOWS=0x0501 -D_WIN32_WINNT=0x0501) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS "/W3" "/Zc:__cplusplus" "/utf-8") list(APPEND DEVILUTIONX_PLATFORM_COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS) else() list(APPEND DEVILUTIONX_PLATFORM_COMPILE_OPTIONS $<$:-gstabs>) endif() if(MINGW_CROSS) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/mingw") endif() ================================================ FILE: CMake/platforms/xbox_nxdk/finders/FindPNG.cmake ================================================ if(NOT TARGET PNG::PNG) find_package(PkgConfig REQUIRED) pkg_check_modules(PNG REQUIRED IMPORTED_TARGET libpng) add_library(PNG::PNG ALIAS PkgConfig::PNG) endif() ================================================ FILE: CMake/platforms/xbox_nxdk/finders/FindSDL2.cmake ================================================ if(NOT TARGET SDL2::SDL2) find_package(PkgConfig REQUIRED) pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) add_library(SDL2::SDL2 ALIAS PkgConfig::SDL2) add_library(SDL2_nomain INTERFACE) add_library(SDL2::SDL2main ALIAS SDL2_nomain) endif() ================================================ FILE: CMake/platforms/xbox_nxdk/finders/FindZLIB.cmake ================================================ if(NOT TARGET ZLIB::ZLIB) find_package(PkgConfig REQUIRED) pkg_check_modules(ZLIB REQUIRED IMPORTED_TARGET zlib) add_library(ZLIB::ZLIB ALIAS PkgConfig::ZLIB) endif() ================================================ FILE: CMake/platforms/xbox_nxdk.cmake ================================================ set(NONET ON) set(ASAN OFF) set(UBSAN OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/xbox_nxdk/finders") set(DEVILUTIONX_SYSTEM_BZIP2 OFF) set(DEVILUTIONX_SYSTEM_LIBFMT OFF) set(BUILD_ASSETS_MPQ OFF) set(DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/pkg/assets") set(DEVILUTIONX_WINDOWS_NO_WCHAR ON) set(DEVILUTIONX_RESAMPLER_SPEEX OFF) set(DEFAULT_AUDIO_BUFFER_SIZE 5120) set(DEVILUTIONX_GAMEPAD_TYPE Xbox) set(CMAKE_THREAD_LIBS_INIT "-lpthread") set(CMAKE_HAVE_THREADS_LIBRARY 1) set(CMAKE_USE_WIN32_THREADS_INIT 0) set(CMAKE_USE_PTHREADS_INIT 1) ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.22) foreach(_policy CMP0135 CMP0141) if(POLICY ${_policy}) cmake_policy(SET ${_policy} NEW) set(CMAKE_POLICY_DEFAULT_${_policy} NEW) endif() endforeach() # Projects added via `add_subdirectory` or `FetchContent` may have a lower # `cmake_minimum_required` than we set here. Set policies that we require # to their new value so that they still apply. set(CMAKE_POLICY_DEFAULT_CMP0069 NEW) set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/dist") message("-- Detected a source distribution with the required FetchContent dependencies and devilutionx.mpq included") set(SRC_DIST ON) add_subdirectory(dist) endif() if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR}) message(WARNING [[In-source build detected, please eg. create a new directory and use `cmake ..`]]) endif() include(CMakeDependentOption) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake/finders") include(functions/genex) # Options required by `VcPkgManifestFeatures`, which must be included before the `project` call. option(USE_SDL1 "Use SDL1.2 instead of SDL2" OFF) option(USE_SDL3 "Use SDL3 instead of SDL2" OFF) option(NONET "Disable network support" OFF) cmake_dependent_option(PACKET_ENCRYPTION "Encrypt network packets" ON "NOT NONET" OFF) # The gettext[tools] package takes a very long time to install if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg.cmake$") option(USE_GETTEXT_FROM_VCPKG "Add vcpkg dependency for gettext[tools] for compiling translations" OFF) endif() option(BUILD_TESTING "Build tests." ON) # These must be included after the options above but before the `project` call. include(VcPkgManifestFeatures) # Set up the `project` before the rest of the options so that: # # 1. Properties such as `TARGET_SUPPORTS_SHARED_LIBS` are defined. # 2. Toolchain file is evaluated, required for `Platforms.cmake`, # which can override the options. file(STRINGS "VERSION" VERSION_STR) if(NOT "${VERSION_STR}" STREQUAL "") string(REGEX MATCH "([0-9]+\\.[0-9]+\\.[0-9]+).*" VERSION_PREFIX ${VERSION_STR}) set(VERSION_NUM ${CMAKE_MATCH_1}) endif() if(NOT VERSION_SUFFIX) # For versions with a suffix, e.g. "1.5.0-dev", include # the build type and the git hash. set(VERSION_SUFFIX "-$") if(VERSION_PREFIX MATCHES "-") if(NOT GIT_COMMIT_HASH) include(functions/git) get_git_commit_hash(GIT_COMMIT_HASH) endif() if(GIT_COMMIT_HASH) set(VERSION_SUFFIX "${VERSION_SUFFIX}-${GIT_COMMIT_HASH}") endif() else() # For versions without a suffix, e.g. "1.5.0" rather than "1.5.0-dev", # only include the build type if it is a debug build. set(VERSION_SUFFIX "$<$:$>") endif() endif() project(DevilutionX VERSION ${VERSION_NUM} LANGUAGES C CXX) set(PROJECT_VERSION_WITH_SUFFIX "${VERSION_PREFIX}${VERSION_SUFFIX}") # Platform definitions can override options and we want `cmake_dependent_option` to see the effects. # Note that a few options are still defined before this because they're needed by `VcPkgManifestFeatures.cmake`. include(Platforms) # This built-in CMake module adds a BUILD_TESTING option (ON by default). # Must be included in the top-level `CMakeLists.txt` after calling `project`. # Because we must include `VcPkgManifestFeatures` before the `project` call, # we add a BUILD_TESTING option ourselves above as well. include(CTest) # Debugging / profiling options DEBUG_OPTION(ASAN "Enable address sanitizer") DEBUG_OPTION(UBSAN "Enable undefined behaviour sanitizer") option(TSAN "Enable thread sanitizer (not compatible with ASAN=ON)" OFF) DEBUG_OPTION(DEBUG "Enable debug mode in engine") option(GPERF "Build with GPerfTools profiler" OFF) cmake_dependent_option(GPERF_HEAP_FIRST_GAME_ITERATION "Save heap profile of the first game iteration" OFF "GPERF" OFF) option(ENABLE_CODECOVERAGE "Instrument code for code coverage (only enabled with BUILD_TESTING)" OFF) # Packaging options RELEASE_OPTION(CPACK "Configure CPack") option(MACOSX_STANDALONE_APP_BUNDLE "Generate a portable app bundle to use on other devices (requires sudo)" OFF) option(WIN_NSIS "Generate an NSIS installer" OFF) # Network options cmake_dependent_option(DISABLE_TCP "Disable TCP multiplayer option" OFF "NOT NONET" ON) cmake_dependent_option(DISABLE_ZERO_TIER "Disable ZeroTier multiplayer option" OFF "NOT NONET" ON) if(USE_SDL1 AND USE_SDL3) message(FATAL_ERROR "USE_SDL1 and USE_SDL3 cannot be set at the same time") endif() # Graphics options if(NOT USE_SDL1) if(USE_SDL3) set(_texture_format_default "SDL_PIXELFORMAT_XRGB8888") else() set(_texture_format_default "SDL_PIXELFORMAT_RGB888") endif() set(DEVILUTIONX_DISPLAY_TEXTURE_FORMAT "${_texture_format_default}" CACHE STRING "Texture format for DevilutionX textures when using the GPU renderer") mark_as_advanced(DEVILUTIONX_DISPLAY_TEXTURE_FORMAT) endif() if(USE_SDL1) # SDL_image in SDL1 does not support PNG, making PCX the only option. set(DEVILUTIONX_SCREENSHOT_FORMAT "DEVILUTIONX_SCREENSHOT_FORMAT_PCX") else() set(DEVILUTIONX_SCREENSHOT_FORMAT "DEVILUTIONX_SCREENSHOT_FORMAT_PNG" CACHE STRING "Screenshot format") set_property(CACHE DEVILUTIONX_SCREENSHOT_FORMAT PROPERTY STRINGS "DEVILUTIONX_SCREENSHOT_FORMAT_PNG;DEVILUTIONX_SCREENSHOT_FORMAT_PCX") mark_as_advanced(DEVILUTIONX_SCREENSHOT_FORMAT) endif() # Sound options option(NOSOUND "Disable sound support" OFF) option(DEVILUTIONX_RESAMPLER_SPEEX "Build with Speex resampler" ON) cmake_dependent_option(DEVILUTIONX_RESAMPLER_SDL "Build with SDL resampler" ON "NOT USE_SDL1" OFF) if(DEVILUTIONX_RESAMPLER_SPEEX) list(APPEND _resamplers Speex) endif() if(DEVILUTIONX_RESAMPLER_SDL) list(APPEND _resamplers SDL) endif() list(GET _resamplers 0 _default_resampler) set(DEVILUTIONX_DEFAULT_RESAMPLER ${_default_resampler} CACHE STRING "Default resampler") set_property(CACHE DEVILUTIONX_DEFAULT_RESAMPLER PROPERTY STRINGS ${_resamplers}) # Optimization / link options option(DISABLE_LTO "Disable link-time optimization (by default enabled in release mode)" OFF) option(PIE "Generate position-independent code" OFF) cmake_dependent_option(DEVILUTIONX_DISABLE_RTTI "Disable RTTI" ON "NONET" OFF) cmake_dependent_option(DEVILUTIONX_DISABLE_EXCEPTIONS "Disable exceptions" ON "DISABLE_ZERO_TIER" OFF) RELEASE_OPTION(DEVILUTIONX_STATIC_CXX_STDLIB "Link C++ standard library statically (if available)") option(DEVILUTIONX_PROFILE_GENERATE "Build a binary that generates the profile for PGO" OFF) option(DEVILUTIONX_PROFILE_USE "Build with PGO using the given profile file" OFF) set(DEVILUTIONX_PROFILE_DIR "" CACHE STRING "Directory where the profile is stored") include(MoldLinker) # Memory / performance trade-off options option(UNPACKED_MPQS "Expect MPQs to be unpacked and the data converted with devilutionx-mpq-tools" OFF) option(UNPACKED_SAVES "Uses unpacked save files instead of MPQ .sv/.hsv files" OFF) option(DISABLE_STREAMING_MUSIC "Disable streaming music (to work around broken platform implementations)" OFF) mark_as_advanced(DISABLE_STREAMING_MUSIC) option(DISABLE_STREAMING_SOUNDS "Disable streaming sounds (to work around broken platform implementations)" OFF) mark_as_advanced(DISABLE_STREAMING_SOUNDS) set(STREAM_ALL_AUDIO_MIN_FILE_SIZE "" CACHE STRING "If set, stream all the audio files larger than this size") mark_as_advanced(STREAM_ALL_AUDIO_MIN_FILE_SIZE) option(DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT "Whether to use a lookup table for transparency blending with black. This improves performance of blending transparent black overlays, such as quest dialog background, at the cost of 128 KiB of RAM." ON) mark_as_advanced(DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT) # Additional features option(DISABLE_DEMOMODE "Disable demo mode support" OFF) option(DISCORD_INTEGRATION "Build with Discord SDK for rich presence support" OFF) option(SCREEN_READER_INTEGRATION "Build with screen reader support" OFF) mark_as_advanced(SCREEN_READER_INTEGRATION) # If both UNPACKED_MPQS and UNPACKED_SAVES are enabled, we completely remove MPQ support. if(UNPACKED_MPQS AND UNPACKED_SAVES) set(SUPPORTS_MPQ OFF) else() set(SUPPORTS_MPQ ON) endif() # By default, devilutionx.mpq and mods/Hellfire.mpq are built only if smpq is installed and MPQ support is enabled. if(SUPPORTS_MPQ AND NOT UNPACKED_MPQS) if(BUILD_ASSETS_MPQ OR (CPACK STREQUAL "ON" AND (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux"))) find_program(SMPQ smpq REQUIRED) elseif(NOT DEFINED BUILD_ASSETS_MPQ AND NOT SRC_DIST) find_program(SMPQ smpq) endif() if(SMPQ) set(_has_smpq ON) else() set(_has_smpq OFF) endif() option(BUILD_ASSETS_MPQ "If true, assets are packaged into devilutionx.mpq and mods/Hellfire to Hellfire.mpq." ${_has_smpq}) else() set(BUILD_ASSETS_MPQS OFF) endif() # === Option overrides === # TSAN is not compatible with ASAN. if(TSAN) set(ASAN OFF) endif() if(MSVC AND NOT CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT DISABLE_LTO) # Work around MSVC + CMake bug when LTO is enabled. # See https://github.com/diasurgical/devilutionX/issues/3778 # and https://gitlab.kitware.com/cmake/cmake/-/issues/23035 set(BUILD_TESTING OFF) endif() # Note: `CMAKE_CROSSCOMPILING` is only available after the `project` call. if(CMAKE_CROSSCOMPILING) set(BUILD_TESTING OFF) endif() if(DISABLE_DEMOMODE) # Testing requires demomode. set(BUILD_TESTING OFF) endif() if(BUILD_TESTING) # When tests are enabled, we build a shared devilutionx_so library, which needs to be PIC to link. set(PIE ON) endif() # Recalculate the dependent options that are defined before `include(Platforms)`: if(NONET) # PACKET_ENCRYPTION is defined before `Platforms.cmake` is included. # This means that if a `Platforms.cmake` sets NONET to OFF, PACKET_ENCRYPTION will not automatically # reflect that. set(PACKET_ENCRYPTION OFF) endif() # === End of option overrides === if(PIE) set(CMAKE_POSITION_INDEPENDENT_CODE TRUE) endif() find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") endif() if(DEVILUTIONX_DISABLE_RTTI) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti") elseif(MSVC) string(REGEX REPLACE "/GR" "/GR-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") endif() endif() if(DEVILUTIONX_DISABLE_EXCEPTIONS) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions") elseif(MSVC) string(REGEX REPLACE "/EHsc" "/EHs-c-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") endif() endif() # Remove unused symbols in non-debug mode. # This is useful even with LTO (-84 KiB with MinSizeRel). # # PS4 toolchain crashes in `create-fself` when linking with these flags, so we exclude it: # https://github.com/PacBrew/ps4-openorbis/issues/8 if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT PS4) add_compile_options("$<$>:-ffunction-sections;-fdata-sections>") if(APPLE) add_link_options("$<$>:LINKER:-dead_strip>") else() add_link_options("$<$>:LINKER:--gc-sections,--as-needed>") endif() endif() if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") # -fipa-pta and -fdevirtualize-at-ltrans improve performance. add_compile_options("$<$>:-fipa-pta;-fdevirtualize-at-ltrans>") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") add_compile_options("$<$:-fprofile-generate>") add_link_options("$<$:-fprofile-generate>") add_compile_options("$<$:-fprofile-use>") add_link_options("$<$:-fprofile-use>") add_compile_options("$<$:-fprofile-dir=${DEVILUTIONX_PROFILE_DIR};-fprofile-prefix-path=${CMAKE_CURRENT_BINARY_DIR}>") add_link_options("$<$:-fprofile-dir=${DEVILUTIONX_PROFILE_DIR};-fprofile-prefix-path=${CMAKE_CURRENT_BINARY_DIR}>") endif() if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") # u8path() function is deprecated but there is no sensible alternative and it might even get un-deprecated. add_definitions(-D_SILENCE_CXX20_U8PATH_DEPRECATION_WARNING) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.25") # This enables Edit & Continue support, see https://learn.microsoft.com/en-us/cpp/build/cmake-projects-in-visual-studio#edit-and-continue-for-cmake-projects set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<$:EditAndContinue>") # Sets /ZI compiler option, see https://cmake.org/cmake/help/latest/variable/CMAKE_MSVC_DEBUG_INFORMATION_FORMAT.html add_link_options("$<$:/INCREMENTAL>") endif() endif() # Not a genexp because CMake doesn't support it # https://gitlab.kitware.com/cmake/cmake/-/issues/20546 if(NOT DISABLE_LTO) # LTO if supported: include(CheckIPOSupported) check_ipo_supported(RESULT is_ipo_supported OUTPUT lto_error) if(is_ipo_supported) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_MINSIZEREL ON) endif() endif() if(GPERF) set(DEVILUTIONX_STATIC_CXX_STDLIB OFF) if(GPERF_HEAP_FIRST_GAME_ITERATION) set(GPERF_HEAP_MAIN ON) endif() # Compile with information about file and line numbers for everything # even in non-Debug build types. if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") add_compile_options("$<$>:-g2>") elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Use the more size-efficient `-gmlt` option on clang. add_compile_options("$<$>:-gmlt>") endif() endif() set(CMAKE_CXX_STANDARD 20) # On some platforms, such as DJGPP, # `-std=c++20` defines `__STRICT_ANSI__` which disables # all POSIX extensions, so we need to use `-std=gnu++20` instead. set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # for clang-tidy set(CMAKE_THREAD_PREFER_PTHREAD ON) set(THREADS_PREFER_PTHREAD_FLAG ON) if(NOT TARGET_PLATFORM STREQUAL "dos") find_package(Threads REQUIRED) endif() # Dependencies must be included after Platforms. include(Dependencies) add_subdirectory(Source) set(BIN_TARGET devilutionx) if(NINTENDO_3DS) set(BIN_TARGET ${BIN_TARGET}.elf) elseif(TARGET_PLATFORM STREQUAL "dos") set(BIN_TARGET devx) endif() if(ANDROID) add_library(${BIN_TARGET} SHARED Source/main.cpp) elseif(UWP_LIB) set(BIN_TARGET libdevilutionx) else() add_executable(${BIN_TARGET} WIN32 MACOSX_BUNDLE Source/main.cpp Packaging/windows/devilutionx.exe.manifest Packaging/windows/devilutionx.rc Packaging/apple/LaunchScreen.storyboard) if(CMAKE_STRIP AND NOT DEVILUTIONX_DISABLE_STRIP) add_custom_command( TARGET ${BIN_TARGET} POST_BUILD COMMAND $<$,$>:${CMAKE_STRIP}> ARGS $) endif() endif() if(TARGET_PLATFORM STREQUAL "dos") # Allow multiple definitions for math stubs in DJGPP set_target_properties(${BIN_TARGET} PROPERTIES LINK_FLAGS "-Wl,--allow-multiple-definition -static" ) target_link_libraries(${BIN_TARGET} PRIVATE m) endif() if(NOT UWP_LIB) target_link_dependencies(${BIN_TARGET} PRIVATE libdevilutionx) endif() if(GPERF) target_link_libraries(${BIN_TARGET} PUBLIC ${GPERFTOOLS_LIBRARIES}) endif() # Must be included after `BIN_TARGET` and `libdevilutionx` are defined. include(Assets) include(Mods) if(EMSCRIPTEN) target_link_options(${BIN_TARGET} PRIVATE --preload-file assets) endif() if(NOT USE_SDL1 AND NOT UWP_LIB) target_link_libraries(${BIN_TARGET} PUBLIC ${SDL2_MAIN}) endif() if(BUILD_TESTING) include(Tests) endif() include(functions/set_relative_file_macro) set_relative_file_macro(${BIN_TARGET}) if(APPLE) set(MACOSX_BUNDLE_GUI_IDENTIFIER com.diasurgical.devilutionx) set(MACOSX_BUNDLE_COPYRIGHT Unlicense) set(MACOSX_BUNDLE_BUNDLE_NAME devilutionx) set(MACOSX_BUNDLE_DISPLAY_NAME DevilutionX) set(MACOSX_BUNDLE_INFO_STRING ${PROJECT_VERSION}) set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}) set(MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION}) set(MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION}") if(IOS) set(MACOSX_BUNDLE_REQUIRED_PLATFORM IPhoneOS) set_target_properties(${BIN_TARGET} PROPERTIES XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2") set(CMAKE_OSX_DEPLOYMENT_TARGET "9.0") elseif(DARWIN_MAJOR_VERSION GREATER_EQUAL 17) set(MACOSX_BUNDLE_REQUIRED_PLATFORM Carbon) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13.0") endif() if(DARWIN_MAJOR_VERSION VERSION_LESS 9) # Finder on OSX Tiger can only handle icns files with up to 128x128 icons. set(_icon_file AppIcon_128) else() set(_icon_file AppIcon) endif() target_sources(${BIN_TARGET} PRIVATE "Packaging/apple/${_icon_file}.icns") set_source_files_properties("./Packaging/apple/${_icon_file}.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_target_properties(${BIN_TARGET} PROPERTIES MACOSX_BUNDLE_ICON_FILE "${_icon_file}.icns") set_target_properties(${BIN_TARGET} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Packaging/apple/Info.plist") install (TARGETS ${BIN_TARGET} DESTINATION ./) if(MACOSX_STANDALONE_APP_BUNDLE) install(CODE " include(BundleUtilities) fixup_bundle(${CMAKE_BINARY_DIR}/${MACOSX_BUNDLE_BUNDLE_NAME}.app \"\" \"\") " COMPONENT Runtime) endif() find_library(COREFOUNDATION_LIBRARY CoreFoundation) if(COREFOUNDATION_LIBRARY) target_link_libraries(libdevilutionx PUBLIC "${COREFOUNDATION_LIBRARY}") target_compile_definitions(libdevilutionx PRIVATE USE_COREFOUNDATION) endif() set(MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION}") set(CPACK On) endif() if(NINTENDO_SWITCH) nx_generate_nacp (${BIN_TARGET}.nacp NAME "DevilutionX" AUTHOR "Devilution Team" VERSION "${PROJECT_VERSION}" ) file(MAKE_DIRECTORY "${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY}") nx_create_nro(${BIN_TARGET} NACP ${BIN_TARGET}.nacp ICON "${PROJECT_SOURCE_DIR}/Packaging/switch/icon.jpg" ROMFS ${DEVILUTIONX_ASSETS_OUTPUT_DIRECTORY} ) endif() if(VITA) set(VITA_APP_NAME "devilutionX") set(VITA_TITLEID "DVLX00001") set(VITA_VERSION "01.00") set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d PARENTAL_LEVEL=1") set(VITA_MKSFOEX_FLAGS "${VITA_MKSFOEX_FLAGS} -d ATTRIBUTE2=12") vita_create_self(devilutionx.self devilutionx UNSAFE) if(BUILD_ASSETS_MPQ OR SRC_DIST) vita_create_vpk(devilutionx.vpk ${VITA_TITLEID} devilutionx.self VERSION ${VITA_VERSION} NAME ${VITA_APP_NAME} FILE Packaging/vita/sce_sys sce_sys FILE ${DEVILUTIONX_MPQ} devilutionx.mpq FILE ${HELLFIRE_MPQ} mods/Hellfire.mpq ) else() vita_create_vpk(devilutionx.vpk ${VITA_TITLEID} devilutionx.self VERSION ${VITA_VERSION} NAME ${VITA_APP_NAME} FILE Packaging/vita/sce_sys sce_sys FILE assets assets ${VITA_TRANSLATIONS_LIST} ) endif() endif() if(PS4) add_custom_command( TARGET devilutionx_mpq POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${DEVILUTIONX_MPQ} "${PROJECT_SOURCE_DIR}/Packaging/ps4/") add_custom_command( TARGET hellfire_mpq POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_SOURCE_DIR}/Packaging/ps4/mods" COMMAND ${CMAKE_COMMAND} -E copy ${HELLFIRE_MPQ} "${PROJECT_SOURCE_DIR}/Packaging/ps4/mods/") add_self(${BIN_TARGET}) add_pkg(${BIN_TARGET} "${PROJECT_SOURCE_DIR}/Packaging/ps4" "DVLX00001" "DevilutionX" "${PROJECT_VERSION}") endif() if(NINTENDO_3DS) set(APP_TITLE "DevilutionX") set(APP_DESCRIPTION "DevilutionX port for 3DS") set(APP_AUTHOR "Diasurgical Team") set(APP_ICON "${PROJECT_SOURCE_DIR}/Packaging/ctr/icon.png") set(APP_BANNER "${PROJECT_SOURCE_DIR}/Packaging/ctr/banner.png") set(APP_AUDIO "${CMAKE_BINARY_DIR}/banner_audio.wav") set(APP_RSF "${PROJECT_SOURCE_DIR}/Packaging/ctr/template.rsf") set(APP_VERSION ${PROJECT_VERSION}) find_program(FFMPEG ffmpeg) if(FFMPEG) add_custom_command(OUTPUT ${APP_AUDIO} COMMAND ${FFMPEG} -y -ss 3.3 -t 3 -i "${PROJECT_SOURCE_DIR}/Packaging/resources/shareware-startup.wav" -af "afade=t=in:st=0:d=0.1,afade=t=out:st=2.9:d=0.1" ${APP_AUDIO} DEPENDS ${PROJECT_SOURCE_DIR}/Packaging/resources/shareware-startup.wav VERBATIM) else() add_custom_command(OUTPUT ${APP_AUDIO} COMMAND ${CMAKE_COMMAND} -E copy ${PROJECT_SOURCE_DIR}/Packaging/ctr/audio_silent.wav ${APP_AUDIO} DEPENDS ${PROJECT_SOURCE_DIR}/Packaging/ctr/audio_silent.wav VERBATIM) endif() include(Tools3DS) add_3dsx_target(${BIN_TARGET}) add_cia_target(${BIN_TARGET} ${APP_RSF} ${APP_BANNER} ${APP_AUDIO}) endif() if(NXDK) target_link_libraries(${BIN_TARGET} PRIVATE "${NXDK_DIR}/lib/libnxdk_automount_d.lib") target_link_options(${BIN_TARGET} PRIVATE "-include:_automount_d_drive") set(_nxdk_pkg_dir "${CMAKE_BINARY_DIR}/pkg") set(_xbe_path "${_nxdk_pkg_dir}/default.xbe") add_custom_command( OUTPUT "${_xbe_path}" COMMAND "${CMAKE_COMMAND}" -E make_directory "${_nxdk_pkg_dir}" COMMAND "${NXDK_DIR}/tools/cxbe/cxbe" "-OUT:${_xbe_path}" -TITLE:DevilutionX "-Logo:${PROJECT_SOURCE_DIR}/Packaging/xbox_nxdk/xbe_logo.pgm" "${CMAKE_BINARY_DIR}/${BIN_TARGET}.exe" DEPENDS "${BIN_TARGET}" ) add_custom_target(nxdk_xbe DEPENDS "${_xbe_path}") endif() if(CPACK AND (APPLE OR BUILD_ASSETS_MPQ OR SRC_DIST)) if(WIN32) if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") set(SDL2_WIN32_DLLS_DIR "${CMAKE_BINARY_DIR}") else() set(SDL2_WIN32_DLLS_DIR "${SDL2_EXEC_PREFIX}") endif() set(SDL2_WIN32_LICENSES_DIR "${PROJECT_SOURCE_DIR}/Packaging/resources") file(GLOB SDL2_WIN32_ALL_DLLS LIST_DIRECTORIES false "${SDL2_WIN32_DLLS_DIR}/*.dll") file(GLOB SDL2_WIN32_ALL_LICENSES LIST_DIRECTORIES false "${SDL2_WIN32_LICENSES_DIR}/LICENSE*.txt" "${SDL2_WIN32_LICENSES_DIR}/README*.txt") set(CPACK_PACKAGE_NAME ${project_name}) if(WIN_NSIS) set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}_${PROJECT_VERSION}_Installer") set(CPACK_GENERATOR "NSIS") set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") set(CPACK_NSIS_MUI_ICON "${PROJECT_SOURCE_DIR}/Packaging/windows/icon.ico") set(CPACK_NSIS_MUI_UNIICON "${PROJECT_SOURCE_DIR}/Packaging/windows/icon.ico") set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.md") set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") set(CPACK_PACKAGE_EXECUTABLES "devilutionx" "DevilutionX") set(CPACK_NSIS_MUI_FINISHPAGE_RUN "${BIN_TARGET}") else() set(CPACK_PACKAGE_FILE_NAME "devilutionx") set(CPACK_GENERATOR "ZIP") endif() set(CPACK_STRIP_FILES TRUE) install(TARGETS ${BIN_TARGET} DESTINATION .) install(FILES "${PROJECT_SOURCE_DIR}/Packaging/windows/README.txt" DESTINATION "." ) install(FILES "${DEVILUTIONX_MPQ}" DESTINATION "." ) install(FILES "${HELLFIRE_MPQ}" DESTINATION "mods" ) foreach(_SDL2_WIN32_DLL_PATH ${SDL2_WIN32_ALL_DLLS} ${WIN32_INSTALL_DLLS}) install(FILES "${_SDL2_WIN32_DLL_PATH}" DESTINATION "." ) endforeach() foreach(_SDL2_WIN32_LICENSE_PATH ${SDL2_WIN32_ALL_LICENSES}) install(FILES "${_SDL2_WIN32_LICENSE_PATH}" DESTINATION "LICENSE" ) endforeach() if(DISCORD_SHARED_LIB) install(FILES "${DISCORD_SHARED_LIB}" DESTINATION "." ) endif() if(SCREEN_READER_INTEGRATION) install(FILES "${Tolk_BINARY_DIR}/libTolk.dll" DESTINATION "." ) endif() elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") string(TOLOWER ${PROJECT_NAME} project_name) set(CPACK_PACKAGE_NAME ${project_name}) set(CPACK_GENERATOR "7Z") # Common *nix files set(CPACK_STRIP_FILES TRUE) install(TARGETS ${BIN_TARGET} DESTINATION bin) set(desktop_file "${PROJECT_SOURCE_DIR}/Packaging/nix/${project_name}.desktop") set(desktop_file_hellfire "${PROJECT_SOURCE_DIR}/Packaging/nix/${project_name}-hellfire.desktop") find_program(DFI desktop-file-install) if(DFI) execute_process(COMMAND ${DFI} --dir=${CMAKE_BINARY_DIR} ${desktop_file}) set(desktop_file "${CMAKE_BINARY_DIR}/${project_name}.desktop") execute_process(COMMAND ${DFI} --dir=${CMAKE_BINARY_DIR} ${desktop_file_hellfire}) set(desktop_file_hellfire "${CMAKE_BINARY_DIR}/${project_name}-hellfire.desktop") endif() install(FILES "${desktop_file}" DESTINATION "share/applications" ) install(FILES "${desktop_file_hellfire}" DESTINATION "share/applications" ) install(FILES "${PROJECT_SOURCE_DIR}/Packaging/nix/README.txt" DESTINATION "share/diasurgical/${project_name}" ) install(FILES "${DEVILUTIONX_MPQ}" DESTINATION "share/diasurgical/${project_name}" ) install(FILES "${HELLFIRE_MPQ}" DESTINATION "share/diasurgical/${project_name}/mods" ) install(FILES "${PROJECT_SOURCE_DIR}/Packaging/resources/icon_flat.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "${project_name}.png" ) install(FILES "${PROJECT_SOURCE_DIR}/Packaging/resources/hellfire.png" DESTINATION "share/icons/hicolor/512x512/apps" RENAME "${project_name}-hellfire.png" ) install(FILES "${PROJECT_SOURCE_DIR}/Packaging/nix/devilutionx.metainfo.xml" DESTINATION "share/metainfo" RENAME "${project_name}.metainfo.xml" ) if(DISCORD_SHARED_LIB) install(FILES "${DISCORD_SHARED_LIB}" DESTINATION "lib") endif() # -G DEB set(CPACK_PACKAGE_CONTACT "anders@jenbo.dk") if(USE_SDL1) set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl1.2debian") else() set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0, libsdl2-image-2.0-0") endif() set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) # -G RPM set(CPACK_RPM_FILE_NAME RPM-DEFAULT) find_program(RPMBUILD rpmbuild) if(RPMBUILD) list(APPEND CPACK_GENERATOR "RPM") endif() find_program(DPKG dpkg) if(DPKG) list(APPEND CPACK_GENERATOR "DEB") endif() elseif(APPLE) set(CPACK_PACKAGE_FILE_NAME "devilutionx") set(CPACK_DMG_DISABLE_APPLICATIONS_SYMLINK "ON") set(CPACK_STRIP_FILES TRUE) set(CPACK_GENERATOR "DragNDrop") endif() set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) include(CPack) endif() resolve_target_link_dependencies() if(UWP_LIB) get_target_property(_linked_objects libdevilutionx LINKED_OBJECTS) target_sources(libdevilutionx PRIVATE ${_linked_objects}) endif() ================================================ FILE: CMakeSettings.json ================================================ { "configurations": [ { "name": "x64-Debug", "generator": "Ninja", "configurationType": "Debug", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "inheritEnvironments": [ "msvc_x64" ], "intelliSenseMode": "windows-msvc-x64", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x64-Debug-Translations", "generator": "Ninja", "configurationType": "Debug", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "inheritEnvironments": [ "msvc_x64" ], "intelliSenseMode": "windows-msvc-x64", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" }, { "name": "USE_GETTEXT_FROM_VCPKG", "value": "True", "type": "BOOL" } ] }, { "name": "x64-Debug-SDL1", "generator": "Ninja", "configurationType": "Debug", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "inheritEnvironments": [ "msvc_x64" ], "intelliSenseMode": "windows-msvc-x64", "cmakeCommandArgs": "-DUSE_SDL1=ON", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x64-Release", "generator": "Ninja", "configurationType": "Release", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "cmakeCommandArgs": "-DCPACK=ON", "inheritEnvironments": [ "msvc_x64" ], "intelliSenseMode": "windows-msvc-x64", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x64-RelWithDebInfo", "generator": "Ninja", "configurationType": "RelWithDebInfo", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "cmakeCommandArgs": "-DCPACK=ON", "inheritEnvironments": [ "msvc_x64" ], "intelliSenseMode": "windows-msvc-x64", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x86-Debug", "generator": "Ninja", "configurationType": "Debug", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "inheritEnvironments": [ "msvc_x86" ], "intelliSenseMode": "windows-msvc-x86", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x86-Release", "generator": "Ninja", "configurationType": "Release", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "cmakeCommandArgs": "-DCPACK=ON", "inheritEnvironments": [ "msvc_x86" ], "intelliSenseMode": "windows-msvc-x86", "enableClangTidyCodeAnalysis": true, "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] }, { "name": "x64-Debug-WSL-GCC", "generator": "Ninja", "configurationType": "Debug", "buildRoot": "${workspaceRoot}\\build\\${name}", "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", "cmakeExecutable": "cmake", "cmakeCommandArgs": "", "buildCommandArgs": "", "ctestCommandArgs": "", "inheritEnvironments": [ "linux_x64" ], "wslPath": "${defaultWSLPath}", "variables": [ { "name": "DISCORD_INTEGRATION", "value": "True", "type": "BOOL" } ] } ] } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at contact@diasurgical.org. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. ================================================ FILE: LICENSE.md ================================================ # Sustainable Use License Version 1.0 ## Acceptance By using the software, you agree to all of the terms and conditions below. ## Copyright License The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations below. ## Limitations You may use or modify the software only for your own internal business purposes or for non-commercial or personal use. You may distribute the software or provide it to others only if you do so free of charge for non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law. ## Patents The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. ## Notices You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms. If you modify the software, you must include in any modified copies of the software a prominent notice stating that you have modified the software. ## No Other Rights These terms do not imply any licenses other than those expressly granted in these terms. ## Termination If you use the software in violation of these terms, such use is not licensed, and your license will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your license will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your license to terminate automatically and permanently. ## No Liability As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. ## Definitions The “licensor” is the entity offering these terms. The “software” is the software the licensor makes available under these terms, including any portion of it. “You” refers to the individual or entity agreeing to these terms. “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. Control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. “Your license” is the license granted to you for the software under these terms. “Use” means anything you do with the software requiring your license. “Trademark” means trademarks, service marks, and similar rights. ================================================ FILE: Packaging/OpenDingux/.editorconfig ================================================ [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: Packaging/OpenDingux/.gitignore ================================================ /tmp/ ================================================ FILE: Packaging/OpenDingux/README.md ================================================ # DevilutionX OpenDingux Port See docs/building.md for build instructions. ================================================ FILE: Packaging/OpenDingux/build-all.sh ================================================ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." source Packaging/OpenDingux/targets.sh for target in "${VALID_TARGETS[@]}"; do Packaging/OpenDingux/build.sh "$target" done ================================================ FILE: Packaging/OpenDingux/build.sh ================================================ #!/usr/bin/env bash set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." source Packaging/OpenDingux/targets.sh source Packaging/OpenDingux/package-opk.sh usage() { >&2 echo "Usage: build.sh [--profile-generate|--profile-use] [--profile-dir PATH] [target]" >&2 usage_target } declare -rA BUILDROOT_REPOS=( [lepus]=https://github.com/OpenDingux/buildroot.git [retrofw]=https://github.com/retrofw/buildroot.git [rg99]=https://github.com/OpenDingux/buildroot.git [rg350]=https://github.com/OpenDingux/buildroot.git [gkd350h]=https://github.com/tonyjih/RG350_buildroot.git ) declare -rA BUILDROOT_DEFCONFIGS=( [lepus]='od_lepus_defconfig BR2_EXTERNAL=board/opendingux' [retrofw]='RetroFW_defconfig BR2_EXTERNAL=retrofw' [rg99]='od_rs90_defconfig BR2_EXTERNAL=board/opendingux' [rg350]='od_gcw0_defconfig BR2_EXTERNAL=board/opendingux' [gkd350h]='rg350_defconfig BR2_EXTERNAL=board/opendingux' ) declare TARGET declare BUILD_DIR declare BUILDROOT declare BUILDROOT_TARGET declare TOOLCHAIN declare -a CMAKE_CONFIGURE_OPTS=() declare PROFILE_GENERATE=0 declare PROFILE_USE=0 declare PROFILE_DIR="%q{HOME}/devilutionx-profile" declare BUILD_TYPE=release main() { parse_args "$@" BUILDROOT_TARGET="$TARGET" # If a TOOLCHAIN environment variable is set, just use that. if [[ -z ${TOOLCHAIN:-} ]]; then BUILDROOT="${BUILDROOT:-$HOME/devilutionx-buildroots/$BUILDROOT_TARGET}" TOOLCHAIN="${BUILDROOT}/output/host" fi >&2 echo "Building for target ${TARGET} in ${BUILD_DIR}" set -x if [[ -n ${BUILDROOT:-} ]]; then prepare_buildroot make_buildroot fi build package_opk } parse_args() { local -a positional=() while [[ $# -gt 0 ]]; do case "$1" in --profile-generate) PROFILE_GENERATE=1 shift ;; --profile-use) PROFILE_USE=1 shift ;; --profile-dir) shift if [[ $# -eq 0 ]]; then usage exit 64 fi PROFILE_DIR="$1" shift ;; -*|--*) >&2 echo "Error: unknown argument $1" >&2 echo usage exit 64 ;; *) positional+=("$1") shift ;; esac done if [[ ${#positional[@]} -ne 1 ]] || ! check_target "${positional[0]}"; then >&2 echo "Error: target is required" >&2 echo usage exit 64 fi TARGET="${positional[0]}" BUILD_DIR="build-${TARGET}" if (( PROFILE_GENERATE )) && (( PROFILE_USE )); then >&2 echo "Error: at most one of --profile-use and --profile-generate is allowed" exit 64 fi if [[ $TARGET = rg99 ]]; then OPK_EXTRA_FILES+=( Packaging/OpenDingux/devilutionx-from-disk.sh Packaging/OpenDingux/devilutionx-umount-opk-and-run.sh ) OPK_DESKTOP_EXEC="devilutionx-from-disk.sh" fi if (( PROFILE_GENERATE )); then CMAKE_CONFIGURE_OPTS+=( "-DDEVILUTIONX_PROFILE_GENERATE=ON" "-DDEVILUTIONX_PROFILE_DIR=${PROFILE_DIR}" ) OPK_DESKTOP_NAME="DevilutionX PG" OPK_DESKTOP_EXEC="profile-generate.sh" OPK_EXTRA_FILES+=( Packaging/OpenDingux/profile-generate.sh test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo ) local -a demo_saves=( test/fixtures/timedemo/WarriorLevel1to2/demo_0_reference_spawn_0.sv test/fixtures/timedemo/WarriorLevel1to2/spawn_0.sv ) if [[ $TARGET = rg99 ]]; then # We use unpacked saves on RG99. mkdir -p "$BUILD_DIR/demo-saves" unpack_and_minify_mpq --output-dir "$BUILD_DIR/demo-saves" "${demo_saves[@]}" OPK_EXTRA_FILES+=("$BUILD_DIR/demo-saves"/*) else OPK_EXTRA_FILES+=("${demo_saves[@]}") fi fi if (( PROFILE_USE )); then CMAKE_CONFIGURE_OPTS+=( "-DDEVILUTIONX_PROFILE_USE=ON" "-DDEVILUTIONX_PROFILE_DIR=${PROFILE_DIR}" ) fi } prepare_buildroot() { if [[ -d $BUILDROOT ]]; then return fi if [[ "${BUILDROOT_REPOS[$BUILDROOT_TARGET]}" == *.tar.gz ]]; then mkdir -p "$BUILDROOT" curl -L --fail "${BUILDROOT_REPOS[$BUILDROOT_TARGET]}" | \ tar -xz --strip-components 1 -C "$BUILDROOT" else git clone --depth=1 "${BUILDROOT_REPOS[$BUILDROOT_TARGET]}" "$BUILDROOT" fi cd "$BUILDROOT" mkdir -p ../shared-dl ln -s ../shared-dl dl # Work around a BR2_EXTERNAL initialization bug in older buildroots. mkdir -p output touch output/.br-external.mk local -a config_args=(${BUILDROOT_DEFCONFIGS[$BUILDROOT_TARGET]}) local -r config="${config_args[0]}" # If the buildroot uses per-package directories, disable them. # Otherwise, we'd have to buildroot the entire buildroot (up to `host-finalize`) to get # the merged host directory. if grep -q BR2_PER_PACKAGE_DIRECTORIES=y "configs/${config}"; then local -r new_config="${config%_defconfig}_no_ppd_defconfig" sed 's/BR2_PER_PACKAGE_DIRECTORIES=y/# BR2_PER_PACKAGE_DIRECTORIES is not selected/' \ "configs/${config}" > "configs/${new_config}" config_args[0]="$new_config" fi make "${config_args[@]}" cd - } make_buildroot() { cd "$BUILDROOT" local -a env_args=( # Unset client variables that cause issues with buildroot -u PERL_MM_OPT -u CMAKE_GENERATOR -u CMAKE_GENERATOR_PLATFORM -u CMAKE_GENERATOR_TOOLSET -u CMAKE_GENERATOR_INSTANCE # Enable parallelism BR2_JLEVEL=0 ) env "${env_args[@]}" make toolchain sdl cd - } cmake_configure() { # libzt uses `-fstack-protector` GCC flag by default. # We disable `-fstack-protector` because it isn't supported by target libc. cmake -S. -B"$BUILD_DIR" \ -G "Unix Makefiles" \ "-DTARGET_PLATFORM=$TARGET" \ -DCMAKE_TOOLCHAIN_FILE="${TOOLCHAIN}/usr/share/buildroot/toolchainfile.cmake" \ -DBUILD_TESTING=OFF \ -DDEVILUTIONX_SYSTEM_LIBFMT=OFF \ -DDEVILUTIONX_SYSTEM_LIBSODIUM=OFF \ -DDEVILUTIONX_SYSTEM_BZIP2=OFF \ -DSTACK_PROTECTOR=OFF \ "${CMAKE_CONFIGURE_OPTS[@]}" } cmake_build() { BR_CACHE_DIR="${HOME}/.buildroot-ccache" cmake --build "$BUILD_DIR" -j "$(getconf _NPROCESSORS_ONLN)" } strip_bin() { "${TOOLCHAIN}/usr/bin/"*-linux-strip -s -R .comment -R .gnu.version "${BUILD_DIR}/devilutionx" } build_debug() { cmake_configure -DCMAKE_BUILD_TYPE=Debug -DASAN=OFF -DUBSAN=OFF -DCMAKE_CXX_FLAGS_DEBUG="-g -fno-omit-frame-pointer" "$@" cmake_build } build_relwithdebinfo() { cmake_configure -DCMAKE_BUILD_TYPE=RelWithDebInfo "$@" cmake_build } build_minsizerel() { cmake_configure -DCMAKE_BUILD_TYPE=MinSizeRel "$@" cmake_build strip_bin } build_release() { cmake_configure -DCMAKE_BUILD_TYPE=Release "$@" cmake_build strip_bin } build() { rm -f "${BUILD_DIR}/CMakeCache.txt" build_"$BUILD_TYPE" "$@" } main "$@" ================================================ FILE: Packaging/OpenDingux/devilutionx-from-disk.sh ================================================ #!/bin/sh # Unpacks the mounted OPK to disk before running it # in order to avoid the memory overhead of squashfs. OPK_DIR="${PWD}" STORAGE="$(grep mmcblk /proc/mounts | cut -d' ' -f2 || echo /media/data/local/home)" UNPACK_DIR="${STORAGE}/devilutionx-opk-on-disk" set -e set -x DO_COPY=1 if [ -f "${UNPACK_DIR}/devilutionx" ]; then INSTALLED_MD5="$(md5sum "${UNPACK_DIR}/devilutionx" | cut -d' ' -f1)" OPK_MD5="$(md5sum "${PWD}/devilutionx" | cut -d' ' -f1)" if [ "$INSTALLED_MD5" = "$OPK_MD5" ]; then DO_COPY=0 fi fi if [ "$DO_COPY" = "1" ]; then rm -rf "$UNPACK_DIR" mkdir -p "$UNPACK_DIR" cp -rf "$OPK_DIR"/* "$UNPACK_DIR" fi exec "${UNPACK_DIR}/devilutionx-umount-opk-and-run.sh" "${UNPACK_DIR}/devilutionx" "$@" ================================================ FILE: Packaging/OpenDingux/devilutionx-umount-opk-and-run.sh ================================================ #!/bin/sh set -x echo | sudo -S umount -l "$PWD" exec "$@" ================================================ FILE: Packaging/OpenDingux/gkd350h-manual.txt ================================================ Copy diabdat.mpq from your CD (or GoG install folder) to: /usr/local/home/.local/share/diasurgical/devilution/ For Hellfire, also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, and hfvoice.mpq. For Chinese, Japanese, and Korean text support copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq For the Polish voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq For the Russian voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq Game saves and diablo.ini are located at: /usr/local/home/.local/share/diasurgical/devilution/ Controls: - Joystick / D-Pad: move hero - ○: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - ×: select spell, back while in menus - △: pickup items, open nearby chests and doors, use item in the inventory - □: cast spell, delete character while in main menu - L: use health item from belt - R: use mana potion from belt - Select + ↑: game menu - Select + L or ←: character info - Select + R or →: inventory - Select + ↓: map - Select + □: Quest log - Select + ×: Spell book - Start + △○×□: Quick spell hotkeys Known issues/quirks: * Start and Select are swapped in this version because Start + D-Pad controls backlight on the GKD350h. * There is nothing to map mouse emulation to in this version because of this but it isn't necessary to play the game. Unfortunately, this also means that you can't move the automap. * This version looks uglier than on RG350 because it uses software scaling beacuse the IPU on the GKD350h doesn't work. Source: https://github.com/diasurgical/devilutionX/ ================================================ FILE: Packaging/OpenDingux/gkd350h.desktop ================================================ [Desktop Entry] Name=DevilutionX Comment=Diablo 1 for GKD350h Exec=devilutionx Terminal=false Type=Application StartupNotify=true Icon=icon_32 Categories=games; X-OD-Manual=readme.gcw0.txt X-OD-NeedsDownscaling=true ================================================ FILE: Packaging/OpenDingux/lepus-manual.txt ================================================ Copy diabdat.mpq from your CD (or GoG install folder) to: ~/.local/share/diasurgical/devilution/ For Hellfire, also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, and hfvoice.mpq. For Chinese, Japanese, and Korean text support copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq For the Polish voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq For the Russian voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq Game saves and diablo.ini are located at: ~/.local/share/diasurgical/devilution ~ is your home directory, /media/data/home by default. Controls: - D-pad: move hero - A: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - B: select spell, back while in menus - X: pickup items, open nearby chests and doors, use item in the inventory - Y: cast spell, delete character while in main menu - R: use mana potion from belt - L: use health item from belt - Start + Select: game menu (alt: Start + ↑) - Start + L or ←: character info - Start + R or →: inventory - Start + ↓: map - Start + Y: Quest log - Start + B: Spell book - Select + A/B/X/Y: hot spell - Select + D-pad: move map/cursor - Select + L: left mouse click - Select + R: right mouse click - Suspend: map Source: https://github.com/diasurgical/devilutionX/ ================================================ FILE: Packaging/OpenDingux/lepus.desktop ================================================ [Desktop Entry] Name=DevilutionX Comment=Diablo 1 for OpenDingux Exec=devilutionx Terminal=false Type=Application StartupNotify=true Icon=icon_32 Categories=games; X-OD-Manual=readme.lepus.txt X-OD-NeedsDownscaling=true X-OD-NeedsJoystick=true ================================================ FILE: Packaging/OpenDingux/package-opk.sh ================================================ #!/usr/bin/env bash declare OPK_DESKTOP_NAME declare OPK_DESKTOP_EXEC declare -a OPK_EXTRA_FILES package_opk() { local ext if [[ $TARGET == rg350 ]] || [[ $TARGET == gkd350h ]]; then ext=gcw0 else ext="$TARGET" fi local -r tmp="${BUILD_DIR}/opk" set -x rm -rf "$tmp" mkdir -p "$tmp" cp "Packaging/OpenDingux/${TARGET}.desktop" "${tmp}/default.${ext}.desktop" cp "Packaging/OpenDingux/${TARGET}-manual.txt" "${tmp}/readme.${ext}.txt" if [[ -v OPK_DESKTOP_NAME ]]; then sed -i "s/Name=.*/Name=${OPK_DESKTOP_NAME}/" "${tmp}/default.${ext}.desktop" fi if [[ -v OPK_DESKTOP_EXEC ]]; then sed -i "s/Exec=.*/Exec=${OPK_DESKTOP_EXEC}/" "${tmp}/default.${ext}.desktop" fi mksquashfs "${BUILD_DIR}/devilutionx" \ "${tmp}/default.${ext}.desktop" \ "${tmp}/readme.${ext}.txt" Packaging/resources/icon_32.png \ "${BUILD_DIR}/assets/" \ "${OPK_EXTRA_FILES[@]}" \ "${BUILD_DIR}/devilutionx-${TARGET}.opk" \ -all-root -no-xattrs -noappend -no-exports -no-progress } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/../.." source Packaging/OpenDingux/targets.sh usage() { echo "Usage: package-opk.sh [target]" usage_target } if ! check_target "$@"; then usage exit 64 fi declare -r TARGET="$1" declare -r BUILD_DIR="build-${TARGET}" package_opk fi ================================================ FILE: Packaging/OpenDingux/profile-generate.sh ================================================ #!/bin/sh set -x SAVE_DIR="$(mktemp -d)" cp "${PWD}/demo_0_reference_spawn_0_sv" "${SAVE_DIR}/" cp "${PWD}/demo_0.dmo" "${SAVE_DIR}/" cp -r "${PWD}/spawn_0_sv" "${SAVE_DIR}/" rm -rf "${HOME}/devilutionx-profile" mkdir -p "${HOME}/devilutionx-profile" ./devilutionx-from-disk.sh --diablo --spawn --demo 0 --timedemo --save-dir "$SAVE_DIR" --data-dir ~/.local/share/diasurgical/devilution rm -rf "$SAVE_DIR" ================================================ FILE: Packaging/OpenDingux/retrofw-manual.txt ================================================ Copy diabdat.mpq from your CD (or GoG install folder) to: ~/.local/share/diasurgical/devilution For Hellfire, also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, and hfvoice.mpq. For Chinese, Japanese, and Korean text support copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq For the Polish voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq For the Russian voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq Game saves and diablo.ini are located at: ~/.local/share/diasurgical/devilution ~ is your home directory, /home/retrofw by default. Controls: - D-pad: move hero - A: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - B: select spell, back while in menus - X: pickup items, open nearby chests and doors, use item in the inventory - Y: cast spell, delete character while in main menu - R: use mana potion from belt - L: use health item from belt - Start + Select: game menu (alt: Start + ↑) - Start + L or ←: character info - Start + R or →: inventory - Start + ↓: map - Start + Y: Quest log - Start + B: Spell book - Select + A/B/X/Y: hot spell - Select + D-pad: move map/cursor - Select + L: left mouse click - Select + R: right mouse click - Suspend: map ================================================ FILE: Packaging/OpenDingux/retrofw.desktop ================================================ [Desktop Entry] Name=DevilutionX Comment=Diablo 1 for RetroFW Exec=devilutionx Terminal=false Type=Application StartupNotify=true Icon=icon_32 Categories=games; X-OD-Manual=readme.retrofw.txt X-OD-NeedsDownscaling=true X-OD-NeedsJoystick=true ================================================ FILE: Packaging/OpenDingux/rg350-manual.txt ================================================ Copy diabdat.mpq from your CD (or GoG install folder) to: ~/.local/share/diasurgical/devilution/ For Hellfire, also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, and hfvoice.mpq. For Chinese, Japanese, and Korean text support copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq For the Polish voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq For the Russian voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq Game saves and diablo.ini are located at: ~/.local/share/diasurgical/devilution/ ~ is your home directory, /media/data/home by default. Controls: - Left analog or D-Pad: move hero - A: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - B: select spell, back while in menus - X: pickup items, open nearby chests and doors, use item in the inventory - Y: cast spell, delete character while in main menu - L1: use health item from belt - R1: use mana potion from belt - L2: character sheet (alt: Start + L1 or ←) - R2: inventory (alt: Start + R1 or →) - Left analog click: toggle automap (alt: Start + ↓) - Start + Select: game menu (alt: Start + ↑) - Select + A/B/X/Y: Spell hotkeys - Right analog: move automap or simulate mouse - Right analog click: left mouse click (alt: Select + L1) - Select + Right analog click: right mouse click (alt: Select + R1) - Select + L2: quest log (alt: Start + Y) - Select + R2: spell book (alt: Start + B) Source: https://github.com/diasurgical/devilutionX/ ================================================ FILE: Packaging/OpenDingux/rg350.desktop ================================================ [Desktop Entry] Name=DevilutionX Comment=Diablo 1 for RG350 Exec=devilutionx Terminal=false Type=Application StartupNotify=true Icon=icon_32 Categories=games; X-OD-Manual=readme.gcw0.txt X-OD-NeedsDownscaling=true X-OD-NeedsJoystick=true ================================================ FILE: Packaging/OpenDingux/rg99-manual.txt ================================================ Copy diabdat.mpq from your CD (or GoG install folder) to: ~/.local/share/diasurgical/devilution/ For Hellfire, also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, and hfvoice.mpq. For Chinese, Japanese, and Korean text support copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq For the Polish voice pack copy: https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq Game saves and diablo.ini are located at: ~/.local/share/diasurgical/devilution ~ is your home directory, /media/data/home by default. Controls: - D-pad: move hero - A: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - B: select spell, back while in menus - X: pickup items, open nearby chests and doors, use item in the inventory - Y: cast spell, delete character while in main menu - R: use mana potion from belt - L: use health item from belt - Start + Select: game menu (alt: Start + ↑) - Start + L or ←: character info - Start + R or →: inventory - Start + ↓: map - Start + Y: Quest log - Start + B: Spell book - Select + A/B/X/Y: hot spell - Select + D-pad: move map/cursor - Select + L: left mouse click - Select + R: right mouse click - Suspend: map ================================================ FILE: Packaging/OpenDingux/rg99-pgo.md ================================================ # RG99 profile-guided optimization The RG99 build must be PGO'd for reasonable performance. Here are the instructions for producing a PGO'd build. 1. Install 2. Build the OPK for profiling data collection: ```sh TOOLCHAIN=/opt/rs90-toolchain Packaging/OpenDingux/build.sh rg99 --profile-generate ``` 3. Copy the OPK to RG99 (`rg99` is 10.1.1.3): ```sh scp -O build-rg99/devilutionx-rg99.opk rg99:/media/sdcard/apps ``` 4. Now, run the OPK. It will run the timedemo instead of the actual game and will take about 1 hour (due to heavy swapping). 5. Copy the profiling data from RG99: ```sh rm -rf /tmp/devilutionx-profile scp -r -O rg99:/media/data/local/home/devilutionx-profile /tmp/devilutionx-profile ``` 6. Build the OPK use the collected profiling data: ```sh TOOLCHAIN=/opt/rs90-toolchain Packaging/OpenDingux/build.sh rg99 --profile-use --profile-dir /tmp/devilutionx-profile ``` 7. The final package is at `build-rg99/devilutionx-rg99.opk`. ## Remote Debugging with VS Code If the demo crashes and you cannot reproduce this on PC, you can use a remote debugger to diagnose the issue. Unpack the package and copy it to the RG99: ```bash cd build-rg99 rm -rf squashfs-root unsquashfs devilutionx-rg99.opk ssh rg99 'rm -rf /media/data/local/home/squashfs-root' scp -r -O squashfs-root/ rg99:/media/data/local/home/squashfs-root ``` Then, on RG99, prepare the demo files and run `gdbserver`: ```bash mkdir -p demo cp -r squashfs-root/demo_0* demo cp -r squashfs-root/spawn_0_sv demo cd squashfs-root gdbserver 10.1.1.1:8001 devilutionx --diablo --spawn --demo 0 --timedemo \ --save-dir ~/demo --data-dir ~/.local/share/diasurgical/devilution ``` Then, on the PC, add the following VS Code configuration to `.vscode/launch.json`: ```json { "name": "rg99 remote debug", "type": "cppdbg", "request": "launch", "program": "build-rg99/devilutionx", "stopAtEntry": true, "miDebuggerPath": "/opt/rs90-toolchain/bin/mipsel-linux-gdb", "miDebuggerArgs": "-ix /opt/rs90-toolchain/mipsel-rs90-linux-musl/sysroot/usr/share/buildroot/gdbinit", "MIMode": "gdb", "miDebuggerServerAddress": "10.1.1.3:8001", "targetArchitecture": "mips", "additionalSOLibSearchPath": "/opt/rs90-toolchain/mipsel-rs90-linux-musl/sysroot", "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } ], "externalConsole": false, "cwd": "${workspaceFolder}" } ``` Finally, run the configuration from the "Run and Debug" VS Code tab. ================================================ FILE: Packaging/OpenDingux/rg99.desktop ================================================ [Desktop Entry] Name=DevilutionX Comment=Diablo 1 for RG99 Exec=devilutionx Terminal=false Type=Application StartupNotify=true Icon=icon_32 Categories=games; X-OD-Manual=readme.rg99.txt X-OD-NeedsDownscaling=true ================================================ FILE: Packaging/OpenDingux/targets.sh ================================================ declare -ra VALID_TARGETS=( lepus retrofw rg99 rg350 gkd350h ) usage_target() { echo -n " target: target platform:" printf " %s" "${VALID_TARGETS[@]}" echo } check_target() { if [[ $# -eq 0 ]] || [[ -z $1 ]]; then echo "Error: target is missing" return 1 fi for target in "${VALID_TARGETS[@]}"; do if [[ $target == $1 ]]; then return 0 fi done echo "Error: invalid target" return 1 } ================================================ FILE: Packaging/amiga/Dockerfile ================================================ FROM amigadev/crosstools:m68k-amigaos-gcc10 RUN apt-get update && apt-get install --no-install-recommends -y smpq RUN mkdir /devilutionx-deps-build COPY Packaging/amiga/prep.sh /devilutionx-deps-build/prep.sh RUN cd /devilutionx-deps-build && ./prep.sh CMD cmake -S. -Bbuild-amiga -DCPACK=ON \ -DCMAKE_BUILD_TYPE=Release \ -DM68K_CPU=68040 \ -DM68K_FPU=hard \ -DM68K_COMMON="-s -fbbb=- -ffast-math" && \ cmake --build build-amiga -j $(nproc) ================================================ FILE: Packaging/amiga/prep.sh ================================================ #!/usr/bin/env bash # exit when any command fails set -euo pipefail #set compiler params export TARGET='m68k-amigaos' export SYSROOT=/opt/$TARGET export M68K_CPU=68040 export M68K_FPU=hard export M68K_CPU_FPU="-m${M68K_CPU} -m${M68K_FPU}-float" export M68K_COMMON="-s -ffast-math -fomit-frame-pointer -fbbb=-" export M68K_CFLAGS="${M68K_CPU_FPU} ${M68K_COMMON}" export M68K_CXXFLAGS="${M68K_CPU_FPU} ${M68K_COMMON}" PARALLELISM="$(getconf _NPROCESSORS_ONLN)" mkdir -p deps mkdir -p ${SYSROOT}/usr/lib mkdir -p ${SYSROOT}/usr/include cd deps # SDL1.2 wget https://github.com/AmigaPorts/libSDL12/archive/master.tar.gz -O SDL-1.2.tar.gz tar -xvf SDL-1.2.tar.gz cd libSDL12-master make PREFX=${SYSROOT} PREF=${SYSROOT} -j"$PARALLELISM" mkdir -p ${SYSROOT}/usr/lib mkdir -p ${SYSROOT}/usr/include cp -fvr libSDL.a ${SYSROOT}/usr/lib/ cp -fvr include/* ${SYSROOT}/usr/include/ cd .. ================================================ FILE: Packaging/apple/Info.plist ================================================ CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundleDisplayName ${MACOSX_BUNDLE_DISPLAY_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleSignature ???? CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} UILaunchStoryboardName LaunchScreen NSHighResolutionCapable CSResourcesFileMapped LSRequires${MACOSX_BUNDLE_REQUIRED_PLATFORM} NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} SDL_FILESYSTEM_BASE_DIR_TYPE resource NSSupportsAutomaticGraphicsSwitching UIApplicationSupportsIndirectInputEvents LSSupportsOpeningDocumentsInPlace UIFileSharingEnabled CADisableMinimumFrameDurationOnPhone UIDeviceFamily 1 2 UIRequiresFullScreen UIStatusBarHidden UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIApplicationSceneManifest UIApplicationSupportsMultipleScenes CFBundleAllowMixedLocalizations ================================================ FILE: Packaging/apple/LaunchScreen.storyboard ================================================ ================================================ FILE: Packaging/apple/png2icns_tiger/.gitignore ================================================ /build*/ ================================================ FILE: Packaging/apple/png2icns_tiger/CMakeLists.txt ================================================ # On Debian/Ubuntu, this requires libpng-dev and libicns-dev cmake_minimum_required(VERSION 3.22) project(png2icns_tiger LANGUAGES C) add_executable(png2icns_tiger png2icns_tiger.c) find_package(PkgConfig REQUIRED) pkg_check_modules(PNG REQUIRED IMPORTED_TARGET libpng) pkg_check_modules(ICNS REQUIRED IMPORTED_TARGET libicns) target_link_libraries(png2icns_tiger PRIVATE PkgConfig::PNG PkgConfig::ICNS) ================================================ FILE: Packaging/apple/png2icns_tiger/png2icns_tiger.c ================================================ /* * png2icns_tiger - based on png2icns from the pngutils Debian package but * modified for Tiger support. See the section that says MODIFICATION. * * Copyright (C) 2008 Julien BLACHE * Copyright (C) 2012 Mathew Eis * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include #include #include #include #include #include #include #include #include #define FALSE 0 #define TRUE 1 #if PNG_LIBPNG_VER >= 10209 #define PNG2ICNS_EXPAND_GRAY 1 #endif static int read_png(FILE *fp, png_bytepp buffer, int32_t *bpp, int32_t *width, int32_t *height) { png_structp png_ptr; png_infop info; png_uint_32 w; png_uint_32 h; png_bytep *rows; int bit_depth; int32_t color_type; int row; int rowsize; png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (png_ptr == NULL) return FALSE; info = png_create_info_struct(png_ptr); if (info == NULL) { png_destroy_read_struct(&png_ptr, NULL, NULL); return FALSE; } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info, NULL); return FALSE; } png_init_io(png_ptr, fp); png_read_info(png_ptr, info); png_get_IHDR(png_ptr, info, &w, &h, &bit_depth, &color_type, NULL, NULL, NULL); switch (color_type) { case PNG_COLOR_TYPE_GRAY: #ifdef PNG2ICNS_EXPAND_GRAY png_set_expand_gray_1_2_4_to_8(png_ptr); #else png_set_gray_1_2_4_to_8(png_ptr); #endif if (bit_depth == 16) { png_set_strip_16(png_ptr); bit_depth = 8; } png_set_gray_to_rgb(png_ptr); png_set_add_alpha(png_ptr, 0xff, PNG_FILLER_AFTER); break; case PNG_COLOR_TYPE_GRAY_ALPHA: #ifdef PNG2ICNS_EXPAND_GRAY png_set_expand_gray_1_2_4_to_8(png_ptr); #else png_set_gray_1_2_4_to_8(png_ptr); #endif if (bit_depth == 16) { png_set_strip_16(png_ptr); bit_depth = 8; } png_set_gray_to_rgb(png_ptr); break; case PNG_COLOR_TYPE_PALETTE: png_set_palette_to_rgb(png_ptr); if (png_get_valid(png_ptr, info, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png_ptr); else png_set_add_alpha(png_ptr, 0xff, PNG_FILLER_AFTER); break; case PNG_COLOR_TYPE_RGB: if (bit_depth == 16) { png_set_strip_16(png_ptr); bit_depth = 8; } png_set_add_alpha(png_ptr, 0xff, PNG_FILLER_AFTER); break; case PNG_COLOR_TYPE_RGB_ALPHA: if (bit_depth == 16) { png_set_strip_16(png_ptr); bit_depth = 8; } break; } *width = w; *height = h; *bpp = bit_depth * 4; png_set_interlace_handling(png_ptr); png_read_update_info(png_ptr, info); rowsize = png_get_rowbytes(png_ptr, info); rows = malloc(sizeof(png_bytep) * h); *buffer = malloc(rowsize * h + 8); rows[0] = *buffer; for (row = 1; row < h; row++) { rows[row] = rows[row - 1] + rowsize; } png_read_image(png_ptr, rows); png_destroy_read_struct(&png_ptr, &info, NULL); free(rows); return TRUE; } static int add_png_to_family(icns_family_t **iconFamily, char *pngname) { FILE *pngfile; int icnsErr = ICNS_STATUS_OK; icns_image_t icnsImage; icns_image_t icnsMask; icns_type_t iconType; icns_type_t maskType; icns_icon_info_t iconInfo; icns_element_t *iconElement = NULL; icns_element_t *maskElement = NULL; char iconStr[5] = {0, 0, 0, 0, 0}; char maskStr[5] = {0, 0, 0, 0, 0}; int iconDataOffset = 0; int maskDataOffset = 0; png_bytep buffer; int width, height, bpp; pngfile = fopen(pngname, "rb"); if (pngfile == NULL) { fprintf(stderr, "Could not open '%s' for reading: %s\n", pngname, strerror(errno)); return FALSE; } if (!read_png(pngfile, &buffer, &bpp, &width, &height)) { fprintf(stderr, "Failed to read PNG file\n"); fclose(pngfile); return FALSE; } fclose(pngfile); icnsImage.imageWidth = width; icnsImage.imageHeight = height; icnsImage.imageChannels = 4; icnsImage.imagePixelDepth = 8; icnsImage.imageDataSize = width * height * 4; icnsImage.imageData = buffer; iconInfo.isImage = 1; iconInfo.iconWidth = icnsImage.imageWidth; iconInfo.iconHeight = icnsImage.imageHeight; iconInfo.iconBitDepth = bpp; iconInfo.iconChannels = (bpp == 32 ? 4 : 1); iconInfo.iconPixelDepth = bpp / iconInfo.iconChannels; iconType = icns_get_type_from_image_info(iconInfo); maskType = icns_get_mask_type_for_icon_type(iconType); /* MODIFICATION */ if (iconType == ICNS_128x128_32BIT_ARGB_DATA) { /* libicns returns "ic07" for 128x128 icons but that doesn't work on Tiger */ iconType = ICNS_128X128_32BIT_DATA; maskType = ICNS_128X128_8BIT_MASK; } /* END OF MODIFICATION */ icns_type_str(iconType, iconStr); icns_type_str(maskType, maskStr); /* Only convert the icons that match sizes icns supports */ if (iconType == ICNS_NULL_TYPE) { fprintf(stderr, "Bad dimensions: PNG file '%s' is %dx%d\n", pngname, width, height); free(buffer); return FALSE; } if (bpp != 32) { fprintf(stderr, "Bit depth %d unsupported in '%s'\n", bpp, pngname); free(buffer); return FALSE; } icns_set_print_errors(0); if (icns_get_element_from_family(*iconFamily, iconType, &iconElement) == ICNS_STATUS_OK) { icns_set_print_errors(1); fprintf(stderr, "Duplicate icon element of type '%s' detected (%s)\n", iconStr, pngname); free(buffer); return FALSE; } icns_set_print_errors(1); if ((iconType != ICNS_1024x1024_32BIT_ARGB_DATA) && (iconType != ICNS_512x512_32BIT_ARGB_DATA) && (iconType != ICNS_256x256_32BIT_ARGB_DATA) && (iconType != ICNS_128x128_32BIT_ARGB_DATA)) { printf("Using icns type '%s', mask '%s' for '%s'\n", iconStr, maskStr, pngname); } else { printf("Using icns type '%s' (ARGB) for '%s'\n", iconStr, pngname); } icnsErr = icns_new_element_from_image(&icnsImage, iconType, &iconElement); if (iconElement != NULL) { if (icnsErr == ICNS_STATUS_OK) { icns_set_element_in_family(iconFamily, iconElement); } free(iconElement); } if ((iconType != ICNS_1024x1024_32BIT_ARGB_DATA) && (iconType != ICNS_512x512_32BIT_ARGB_DATA) && (iconType != ICNS_256x256_32BIT_ARGB_DATA) && (iconType != ICNS_128x128_32BIT_ARGB_DATA)) { icns_init_image_for_type(maskType, &icnsMask); iconDataOffset = 0; maskDataOffset = 0; while ((iconDataOffset < icnsImage.imageDataSize) && (maskDataOffset < icnsMask.imageDataSize)) { icnsMask.imageData[maskDataOffset] = icnsImage.imageData[iconDataOffset + 3]; iconDataOffset += 4; /* move to the next alpha byte */ maskDataOffset += 1; /* move to the next byte */ } icnsErr = icns_new_element_from_mask(&icnsMask, maskType, &maskElement); if (maskElement != NULL) { if (icnsErr == ICNS_STATUS_OK) { icns_set_element_in_family(iconFamily, maskElement); } free(maskElement); } icns_free_image(&icnsMask); } free(buffer); return TRUE; } int main(int argc, char **argv) { FILE *icnsfile; icns_family_t *iconFamily; int i; if (argc < 3) { printf("Usage: png2icns file.icns file1.png file2.png ... filen.png\n"); exit(1); } icnsfile = fopen(argv[1], "wb+"); if (icnsfile == NULL) { fprintf(stderr, "Could not open '%s' for writing: %s\n", argv[1], strerror(errno)); exit(1); } icns_set_print_errors(1); icns_create_family(&iconFamily); for (i = 2; i < argc; i++) { if (!add_png_to_family(&iconFamily, argv[i])) { fclose(icnsfile); unlink(argv[1]); exit(1); } } if (icns_write_family_to_file(icnsfile, iconFamily) != ICNS_STATUS_OK) { fprintf(stderr, "Failed to write icns file\n"); fclose(icnsfile); exit(1); } fclose(icnsfile); printf("Saved icns file to %s\n", argv[1]); if (iconFamily != NULL) free(iconFamily); return 0; } ================================================ FILE: Packaging/apple/png2icns_tiger/run.sh ================================================ #!/bin/bash set -euo pipefail declare -r INPUT=../../resources/icon.png declare -ra SIZES=(16 32 128) declare -r OUTPUT=../AppIcon_128.icns declare -rA DEPENDENCY_SOURCES=( [convert]=imagemagick [cmake]=cmake ) check_deps() { local -a missing_deps=() local path for dep in "${!DEPENDENCIES[@]}"; do if path="$(which "$dep")"; then echo >&2 "Using $dep from $path" else missing_deps+=("$dep") fi done if (( ${#missing_deps[@]} )); then echo >&2 "Error: Missing dependencies" for dep in "${missing_deps[@]}"; do echo >&2 '* '"Please install \"${dep}\", provided by ${DEPENDENCY_SOURCES[$dep]} on Debian/Ubuntu" done exit 1 fi } main() { cd "$(dirname "$0")" check_deps set -x mkdir -p tmp { set +x; } 2> /dev/null local path local -a FILES=() for s in "${SIZES[@]}"; do path="tmp/output_${s}.png" FILES+=("$path") set -x convert "$INPUT" -resize "${s}x${s}" "$path" { set +x; } 2> /dev/null done set -x cmake -S. -Bbuild-rel -DCMAKE_BUILD_TYPE=Release cmake --build build-rel build-rel/png2icns_tiger "$OUTPUT" "${FILES[@]}" rm -rf tmp } main "$@" ================================================ FILE: Packaging/cpi-gamesh/__init__.py ================================================ import pygame import validators import commands from UI.constants import Width,Height,ICON_TYPES,RUNSYS from UI.simple_name_space import SimpleNamespace from UI.page import Page from UI.label import Label from UI.icon_item import IconItem from UI.keys_def import CurKeys, IsKeyMenuOrB, IsKeyStartOrA from UI.skin_manager import MySkinManager from UI.lang_manager import MyLangManager from libs.DBUS import is_wifi_connected_now,get_wifi_ip from collections import deque from enum import Enum import os import subprocess import pipes class DevilutionPage(Page): _FootMsg = ["Nav","Check","Upgrade","Back","Play"] _GameName = "devilutionX" _GamePath = "/home/cpi/games/devilutionX" _GameExecutable = _GamePath + "/build/devilutionx" _GameExecutableRevision = _GameExecutable + ".rev" _GameBuildScript = _GamePath + "/Packaging/cpi-gamesh/build.sh -t " + pipes.quote(os.path.dirname(os.path.abspath( __file__ ))) _GamePNG = _GamePath + "/Packaging/cpi-gamesh/Devilution.png" _DevilutionDiabdatmpq = "/home/cpi/.local/share/diasurgical/devilution/diabdat.mpq" _DevilutionDiabdatmpqPresent = False _GameInstalled = False _CiteNewUpdate = "Ahh... fresh meat!" _CiteCheckUpdate = "Lets search the books..." _CiteWelcome = "Well, what can I do for ya?" _CiteCompiling = "Stay awhile and listen." _CiteDone = "You must venture through the portal..." _CiteFailed = "Game Over. Better luck next time!" _GitURL = "https://github.com/diasurgical/devilutionX.git" _GitBranch = "master" _GitRevision = "" _GameIcon = None _Process = None _Labels = {} _Coords = {} _ListFontObj = MyLangManager.TrFont("varela13") _URLColor = MySkinManager.GiveColor('URL') _TextColor = MySkinManager.GiveColor('Text') def __init__(self): Page.__init__(self) def InitLabels(self): y = 15 x = 11 yInc = 19 xGitRefLabelWidth = 48 labels = \ [["greeting",self._CiteWelcome, self._TextColor, x, y], ["status", "", self._URLColor, x, y + 72-yInc], ["comment", "", self._TextColor, x, y + 72], ["console_out","",self._URLColor, x, y + 72 + yInc], ["label_rev","GIT Revisions: ", self._TextColor, x, 132], ["label_git_rev","Source: ", self._TextColor, x, 151], ["content_git_rev","", self._URLColor, x + xGitRefLabelWidth, 151], ["label_bin_rev","Bin: ", self._TextColor, x, 170], ["content_bin_rev","", self._URLColor, x + xGitRefLabelWidth, 170] ] for i in labels: l = Label() l.SetCanvasHWND(self._CanvasHWND) l.Init(i[1],self._ListFontObj) l.SetColor(i[2]) self._Labels[ i[0] ] = l c = SimpleNamespace() c.x = i[3] c.y = i[4] self._Coords[ i[0] ] = c def GitGetRevision(self): if not os.path.exists(self._GamePath): return "game not installed" process = subprocess.Popen("cd " + pipes.quote(self._GamePath) + "; git rev-parse HEAD",stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True) self._GitRevision = process.communicate()[0].strip() process.wait() return self._GitRevision def ExectuableGetRevision(self): try: with open(self._GameExecutableRevision, 'r') as file: executableRevsion = file.read().replace('\n', '') return executableRevsion except: return "unknown" def InitGameDirectory(self): try: os.makedirs(self._GamePath) except: pass self.StartShellProcess("cd " + pipes.quote(self._GamePath) + "; git clone --single-branch --branch " + pipes.quote(self._GitBranch) + " " + pipes.quote(self._GitURL) + " .") def CheckDevilutionMPQ(self): self._DevilutionDiabdatmpqPresent = os.path.isfile(self._DevilutionDiabdatmpq) def CheckGameInstalled(self): self._GameInstalled = os.path.isfile(self._GameExecutable) def UpdateFootMsg(self): if not self._GameInstalled: self._FootMsg = ["Nav","","Install","Back",""] self.UpdateLabel("status", "GIT Upgrade") self.UpdateLabel("comment", "Press X to install") elif not self._DevilutionDiabdatmpqPresent: self._FootMsg = ["Nav","","Upgrade","Back","Re-check"] self.UpdateLabel("status", "Gamefile diabdat.mpq missing") self.UpdateLabel("comment", "see readme") else: self._FootMsg = ["Nav","","Upgrade","Back","Play"] self.UpdateLabel("status", "Ready") self.UpdateLabel("comment", self._CiteDone) def Init(self): Page.Init(self) if self._Screen != None: if self._Screen._CanvasHWND != None and self._CanvasHWND == None: self._HWND = self._Screen._CanvasHWND self._CanvasHWND = pygame.Surface( (self._Screen._Width,self._Screen._Height) ) if os.path.isfile(self._GamePNG): self._GameIcon = IconItem() self._GameIcon._ImageName = self._GamePNG self._GameIcon._MyType = ICON_TYPES["STAT"] self._GameIcon._Parent = self self._GameIcon.Adjust(290,70,128,128,0) self.InitLabels() self.CheckDevilutionMPQ() self.CheckGameInstalled() self.UpdateFootMsg() self.UpdateLabel("content_git_rev", self.GitGetRevision(), 24) self.UpdateLabel("content_bin_rev", self.ExectuableGetRevision(), 24) def UpdateLabel(self, label, msg, maxLen=38): print(label + ": " + msg) if len(msg) > maxLen: m = msg[:maxLen] + "..." else: m = msg self._Labels[label].SetText(m) def StartShellProcess(self, cmd): print("StartShellProcess " + cmd) proc = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True) while(True): line = proc.stdout.readline() if line: self.UpdateLabel("console_out", line.strip(), 48) self._Screen.Draw() self._Screen.SwapAndShow() if line == '' and proc.poll() is not None: break self.UpdateLabel("console_out", "") self._Screen.Draw() self._Screen.SwapAndShow() def GitUpgrade(self): self.UpdateLabel("status", "GIT Upgrade") self.UpdateLabel("comment", self._CiteCheckUpdate) curRev = "unset" if not os.path.exists(self._GamePath): self.InitGameDirectory() else: curRev = self.GitGetRevision() self.StartShellProcess("cd " + pipes.quote(self._GamePath) + "; git pull") self._GitRevision = self.GitGetRevision() self.UpdateLabel("content_git_rev", self._GitRevision, 24) if curRev != self._GitRevision: self.UpdateLabel("comment", self._CiteNewUpdate) else: self.UpdateLabel("comment", self._CiteDone) self._Screen.Draw() self._Screen.SwapAndShow() def GitExectuableIsGitRevision(self): return self.GitGetRevision() == self.ExectuableGetRevision() def Build(self): self.UpdateLabel("status", "Building") self.StartShellProcess(self._GameBuildScript) def UpgradeAndBuild(self): self.GitUpgrade() self.UpdateLabel("comment", self._CiteCompiling) self._Screen.Draw() self._Screen.SwapAndShow() if not self.GitExectuableIsGitRevision(): self.Build() self.UpdateLabel("content_git_rev", self.GitGetRevision(), 24) self.UpdateLabel("content_bin_rev", self.ExectuableGetRevision(), 24) self.UpdateLabel("status", "Done") if self.GitExectuableIsGitRevision(): self.UpdateLabel("comment", self._CiteDone) else: self.UpdateLabel("comment", self._CiteFailed) self.CheckDevilutionMPQ() self.CheckGameInstalled() self.UpdateFootMsg() self._Screen.Draw() self._Screen.SwapAndShow() def KeyDown(self,event): if IsKeyMenuOrB(event.key): self.ReturnToUpLevelPage() self._Screen.Draw() self._Screen.SwapAndShow() if self._DevilutionDiabdatmpqPresent and self._GameInstalled: if IsKeyStartOrA(event.key): pygame.event.post( pygame.event.Event(RUNSYS, message=self._GameExecutable)) if event.key == CurKeys["X"]: self.UpgradeAndBuild() elif not self._GameInstalled: if event.key == CurKeys["X"]: self.UpgradeAndBuild() elif not self._DevilutionDiabdatmpqPresent: if IsKeyStartOrA(event.key): self.CheckDevilutionMPQ() self.CheckGameInstalled() self.UpdateFootMsg() self._Screen.Draw() self._Screen.SwapAndShow() def Draw(self): self.ClearCanvas() if self._GameIcon != None: self._GameIcon.Draw() for i in self._Labels: if i in self._Coords: self._Labels[i].NewCoord( self._Coords[i].x, self._Coords[i].y) self._Labels[i].Draw() if self._HWND != None: self._HWND.fill(MySkinManager.GiveColor('White')) self._HWND.blit(self._CanvasHWND,(self._PosX,self._PosY,self._Width, self._Height ) ) class APIOBJ(object): _Page = None def __init__(self): pass def Init(self,main_screen): self._Page = DevilutionPage() self._Page._Screen = main_screen self._Page._Name ="devilutionX" self._Page.Init() def API(self,main_screen): if main_screen !=None: main_screen.PushPage(self._Page) main_screen.Draw() main_screen.SwapAndShow() OBJ = APIOBJ() def Init(main_screen): OBJ.Init(main_screen) def API(main_screen): OBJ.API(main_screen) ================================================ FILE: Packaging/cpi-gamesh/build.sh ================================================ #!/usr/bin/env bash set -euo pipefail echo "Building for target: clockwork pi GameSH" declare -r DIR="$(dirname "${BASH_SOURCE[0]}")" cd "$DIR" declare -r ABSDIR="$(pwd)" usage() { echo "${BASH_SOURCE[0]} [--target /path/to/devliution/in/gameshell/menu] [--usage]" exit 1 } POSITIONAL=() while [[ $# -gt 0 ]] do key="$1" case $key in -t|--target) TARGET="$2" shift # past argument shift # past value ;; --help|-h|--usage|-u) usage shift # past argument ;; *) # unknown option POSITIONAL+=("$1") # save it in an array for later shift # past argument ;; esac done set -- "${POSITIONAL[@]}" # restore positional parameters install_deps() { sudo apt install -y cmake libsdl2-dev libbz2-dev libsodium-dev } main() { install_deps build install } build() { cd ../.. rm -f CMakeCache.txt cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release -DTARGET_PLATFORM=cpigamesh -DDISABLE_LTO=ON cmake --build build -j $(getconf _NPROCESSORS_ONLN) cd - } install() { git rev-parse HEAD > /home/cpi/games/devilutionX/build/devilutionx.rev if [ -z ${TARGET+x} ]; then local target_dir="25_devilutionX" else local target_dir=${TARGET#"/home/cpi/apps/Menu/"} fi local script_dir="/home/cpi/apps/Menu/$target_dir" local target_dir_base=`basename "$target_dir"` local target_dir_dir=`dirname "$target_dir"` local icon_name="${target_dir_dir}/${target_dir_base#*_}" local icon_dir="/home/cpi/launcher/skin/default/Menu/GameShell/${icon_name}.png" echo $target_dir echo $script_dir echo $target_dir_base echo $target_dir_dir echo $icon_name echo $icon_dir mkdir -p $script_dir cp __init__.py $script_dir cp Devilution.png $icon_dir } main ================================================ FILE: Packaging/cpi-gamesh/readme.md ================================================ # devilutionX package for ClockworkPi GameShell For more information about this device see [here](https://www.clockworkpi.com/gameshell). ## Install devilutionX on the CPi GameShell To install devilutionX, just copy the [\_\_init__.py](__init__.py) to a newly created folder under /home/cpi/apps/Menu and run it from the menu. The folder then symbolizes the devilutionX icon. From this menu, you can press 'X' to clone the git repository for devilutionX and compile the code. Dependencies are installed automatically (cmake and SDL development packages). Once installed, 'X' pulls the updated code and does the compiling. Note that any changes made locally to the source are reverted before pulling. When the compile is finished and the diabdat.mpq is in place at '/home/cpi/.local/share/diasurgical/devilution/', you can play the game. - To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. Enjoy! For ClockworkOS v0.5, buster-backports are required to have updated libraries: https://backports.debian.org/Instructions/ ## Play devilutionX on the CPi GameShell When the game is compiled and the mpq files are in place, you can press "A" in the devilutionX menu to play. The following key mapping is in place. Unfortunately, the mapping is not trivial as devilutionX and GameShell use their own not(yet) compatible shift mechanism. The mapping is based on the standard GameShell keyboard layout, devilutionX uses the Nintento mapping. | GameShell Key | Keyboard Key | devilutionX Key | devilutionX Action | | --------------------- | --------------- | ------------------------- | ------------------ | | D-Pad | Arrows | D-Pad | move hero | | Select + D-Pad | Space + Arrows | Back + D-Pad | simulate mouse | | B | K | A | attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu | | A | J | B | select spell, back while in menus | | Y | I | X | pickup items, open nearby chests and doors, use item in the inventory | | X | U | Y | cast spell, delete character while in main menu | | Select | Space | Select | Select | | Start | Enter | Start | game menu, skip movie | | LK1 | H | L1 | use health item from belt | | Start + Left | Enter + Left | Start + Left | character sheet | | Start + Right | Enter + Right | Start + Right | inventory | | LK5 | L | R1 | use mana item from belt | | Start + Down | Enter + Down | Start + Down | automap | | Select + LK1 | Space + H | Select + L1 | left mouse click | | Start + X | Enter + U | Start + Y | quest log | | Start + A | Enter + J | Start + B | spell book | | Select + LK5 | Space + L | Select + R1 | right mouse click | | Select + A/B/X/Y | Space + J/K/U/I | Select + A/B/X/Y | hot spell | ================================================ FILE: Packaging/ctr/template.rsf ================================================ BasicInfo: Title : DevilutionX ProductCode : CTR-P-DIABLO Logo : Homebrew # Nintendo / Licensed / Distributed / iQue / iQueForSystem RomFs: # Specifies the root path of the read only file system to include in the ROM. RootPath : romfs TitleInfo: Category : Application UniqueId : 0x3F395 Option: UseOnSD : true # true if App is to be installed to SD FreeProductCode : true # Removes limitations on ProductCode MediaFootPadding : false # If true CCI files are created with padding EnableCrypt : false # Enables encryption for NCCH and CIA EnableCompress : true # Compresses where applicable (currently only exefs:/.code) AccessControlInfo: CoreVersion : 2 # Exheader Format Version DescVersion : 2 # Minimum Required Kernel Version (below is for 4.5.0) ReleaseKernelMajor : "02" ReleaseKernelMinor : "33" # ExtData UseExtSaveData : false # enables ExtData #ExtSaveDataId : 0x300 # only set this when the ID is different to the UniqueId # FS:USER Archive Access Permissions # Uncomment as required FileSystemAccess: #- CategorySystemApplication #- CategoryHardwareCheck - CategoryFileSystemTool #- Debug #- TwlCardBackup #- TwlNandData #- Boss - DirectSdmc #- Core #- CtrNandRo #- CtrNandRw #- CtrNandRoWrite #- CategorySystemSettings #- CardBoard #- ExportImportIvs #- DirectSdmcWrite #- SwitchCleanup #- SaveDataMove #- Shop #- Shell #- CategoryHomeMenu # Process Settings MemoryType : Application # Application/System/Base SystemMode : 80MB # 64MB(Default)/96MB/80MB/72MB/32MB IdealProcessor : 0 AffinityMask : 1 Priority : 16 MaxCpu : 0x9E # Default HandleTableSize : 0x200 DisableDebug : false EnableForceDebug : false CanWriteSharedPage : true CanUsePrivilegedPriority : false CanUseNonAlphabetAndNumber : true PermitMainFunctionArgument : true CanShareDeviceMemory : true RunnableOnSleep : false SpecialMemoryArrange : true # New3DS Exclusive Process Settings SystemModeExt : 124MB # Legacy(Default)/124MB/178MB Legacy:Use Old3DS SystemMode CpuSpeed : 804MHz # 256MHz(Default)/804MHz EnableL2Cache : true # false(default)/true CanAccessCore2 : true # Virtual Address Mappings IORegisterMapping: - 1ff00000-1ff7ffff # DSP memory MemoryMapping: - 1f000000-1f5fffff:r # VRAM # Accessible SVCs, : SystemCallAccess: ArbitrateAddress: 34 Backdoor: 123 Break: 60 CancelTimer: 28 ClearEvent: 25 ClearTimer: 29 CloseHandle: 35 ConnectToPort: 45 ControlMemory: 1 ControlProcessMemory: 112 CreateAddressArbiter: 33 CreateEvent: 23 CreateMemoryBlock: 30 CreateMutex: 19 CreateSemaphore: 21 CreateThread: 8 CreateTimer: 26 DuplicateHandle: 39 ExitProcess: 3 ExitThread: 9 GetCurrentProcessorNumber: 17 GetHandleInfo: 41 GetProcessId: 53 GetProcessIdOfThread: 54 GetProcessIdealProcessor: 6 GetProcessInfo: 43 GetResourceLimit: 56 GetResourceLimitCurrentValues: 58 GetResourceLimitLimitValues: 57 GetSystemInfo: 42 GetSystemTick: 40 GetThreadContext: 59 GetThreadId: 55 GetThreadIdealProcessor: 15 GetThreadInfo: 44 GetThreadPriority: 11 MapMemoryBlock: 31 OutputDebugString: 61 QueryMemory: 2 ReleaseMutex: 20 ReleaseSemaphore: 22 SendSyncRequest1: 46 SendSyncRequest2: 47 SendSyncRequest3: 48 SendSyncRequest4: 49 SendSyncRequest: 50 SetThreadPriority: 12 SetTimer: 27 SignalEvent: 24 SleepThread: 10 UnmapMemoryBlock: 32 WaitSynchronization1: 36 WaitSynchronizationN: 37 # Service List # Maximum 34 services (32 if firmware is prior to 9.6.0) ServiceAccessControl: - APT:U - ac:u - am:net - boss:U - cam:u - cecd:u - cfg:nor - cfg:u - csnd:SND - dsp::DSP - frd:u - fs:USER - gsp::Gpu - hid:USER - http:C - ir:rst - ir:u - ir:USER - mic:u - ndm:u - news:u - nwm::UDS - ptm:u - pxi:dev - soc:U - ssl:C - y2r:u SystemControlInfo: SaveDataSize: 0KB # Change if the app uses savedata RemasterVersion: 2 StackSize: 0x40000 # Modules that run services listed above should be included below # Maximum 48 dependencies # : Dependency: ac: 0x0004013000002402 act: 0x0004013000003802 am: 0x0004013000001502 boss: 0x0004013000003402 camera: 0x0004013000001602 cecd: 0x0004013000002602 cfg: 0x0004013000001702 codec: 0x0004013000001802 csnd: 0x0004013000002702 dlp: 0x0004013000002802 dsp: 0x0004013000001a02 friends: 0x0004013000003202 gpio: 0x0004013000001b02 gsp: 0x0004013000001c02 hid: 0x0004013000001d02 http: 0x0004013000002902 i2c: 0x0004013000001e02 ir: 0x0004013000003302 mcu: 0x0004013000001f02 mic: 0x0004013000002002 ndm: 0x0004013000002b02 news: 0x0004013000003502 nfc: 0x0004013000004002 nim: 0x0004013000002c02 nwm: 0x0004013000002d02 pdn: 0x0004013000002102 ps: 0x0004013000003102 ptm: 0x0004013000002202 qtm: 0x0004013020004202 ro: 0x0004013000003702 socket: 0x0004013000002e02 spi: 0x0004013000002302 ssl: 0x0004013000002f02 ================================================ FILE: Packaging/emscripten/index.html ================================================ DevilutionX
Downloading...
================================================ FILE: Packaging/haiku/devilutionX.rdef.in ================================================ /* * Resources for Haiku */ resource app_flags B_SINGLE_LAUNCH; resource app_version { major = @MAJOR@, middle = @MIDDLE@, minor = @MINOR@, variety = B_APPV_FINAL, internal = 0, short_info = "devilutionX", long_info = "Diablo build for modern operating systems" }; resource app_signature "application/x-vnd.diasurgical-devilutionX"; resource vector_icon { $"6E6369660A050004016C03400000020106023EF000000000000000401800477F" $"FF49200000FD5252FFFF0606038E020202000602381D74BA3DDC3CAC7E3A85D7" $"474E1B48C45A00FFFFFFFFFF29290381030303EB3939020106023E1000000000" $"0000003E300048600043000000FF7C7CFFCD0505037103030E0204C282C38EC2" $"82C38ECB18CA6ECA21C0BABF67B9BDCA21C0BACA0CC181CA0CC181C0A3BAD1C2" $"75C495CC5CCB38C275C4950204C282C38EC282C38ECB18CA6ECA21C0BABF67B9" $"BDCA21C0BACA0CC181CA0CC181C0A3BAD1C275C495CC5CCB38C275C4950607FE" $"1BBE7CCC84C21ACC84C04BCC84C3E9CC84C76FCB79C5CACB6ACB30CB9CCA5FC9" $"08CCAEC9C1C8FDC899C61FC7E7C72AC8A0C4CBC6FAC21AC6BAC9420A0DB8B2BB" $"E0B8B2C206BA25C379BA25C99FBB97CA58BC50CA58BCADCAE3BE7CCBCAC160C8" $"E6C160C4EBC2D3C379C2D339BBF4B9B50A0BB8B2BBE0B8B2C206BA25C379BA25" $"C99FBB97CA58BC50CA58BCADCAE3BE7CCBCABE7CC5A4BFEEC5A4BFEEBF220A06" $"B8B2BBE0BFEEBF22C2D339BDC3BC9ABFEDBB83BBF4B9B50A08BFEEC5A4BE7CC5" $"A4BE7CCBCAC160C8E6C160C4EBC2D3C379C2D339BFEEBF220A04BE7CC5A4BE7C" $"C7D0C160C4EBBFEEC5A40A03BDC3BC9AC2D339BFEEBB84020ABDC3B617BE1FB6" $"17BD66B617BCB3B657BD09B62EBC5CB67FBB13B48ABB59B444BACEB4CFBB1EB7" $"ECBB46B795BAF5B843BADEB8FCBADEB89FBADEBA6EBDC3BBE0BC50BBE0BF35BB" $"E0C0A7B8FCC0A7BA6EC0A7B89FC068B7ECC090B843C03FB795C144B51CC189B5" $"62C0FEB4D7BED3B657BF29B67FBE7CB62E0A04BB97CA58BC50CA58BC50C548BB" $"97C4EB0805C906C096C8B3C141CAECC2ACCA3FBF61C93EC06C0805C906C096C8" $"B3C141CAECC2ACCA3FBF61C93EC06C020ABDC3B617BE1FB617BD66B617BCB3B6" $"57BD09B62EBC5CB67FBB09B475BB4EB42FBAC3B4BABB1EB7ECBB46B795BAF5B8" $"43BADEB8FCBADEB89FBADEBA6EBDC3BBE0BC50BBE0BF35BBE0C0A7B8FCC0A7BA" $"6EC0A7B89FC068B7ECC090B843C03FB79544B512C19EB557C113B4CDBED3B657" $"BF29B67FBE7CB62E0E0A070100000A0001011001178100040A010102000A0001" $"031001178300040A030104000A050105000A040106000A020107000A09010800" $"0A0001091001178300040A06010A000A07010B000A00010C1001178100040A08" $"010D00" }; ================================================ FILE: Packaging/miyoo_mini/build.sh ================================================ #!/usr/bin/env bash set -euo pipefail declare -r PACKAGING_DIR=`cd -- "$(dirname "$0")" >/dev/null 2>&1; pwd -P` declare -r CFLAGS="-O3 -marm -mtune=cortex-a7 -mfpu=neon-vfpv4 -mfloat-abi=hard -march=armv7ve -Wall" declare -r LDFLAGS="-lSDL -lmi_sys -lmi_gfx -s -lSDL -lSDL_image" declare -r BUILD_DIR="build-miyoo-mini" declare -r MIYOO_CUSTOM_SDL_REPO="https://github.com/Brocky/SDL-1.2-miyoo-mini.git" declare -r MIYOO_CUSTOM_SDL_BRANCH="miniui-miyoomini" main() { # ensure we are in devilutionx root cd "$PACKAGING_DIR/../.." rm -f "$BUILD_DIR/CMakeCache.txt" cmake_configure -DCMAKE_BUILD_TYPE=Release cmake_build package_onion package_miniui } cmake_configure() { cmake -S. -B"$BUILD_DIR" \ -DTARGET_PLATFORM=miyoo_mini \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_TOOLCHAIN_FILE="${PACKAGING_DIR}/toolchainfile.cmake" \ -DBUILD_TESTING=OFF \ -DDISABLE_DEMOMODE=ON \ "$@" } cmake_build() { cmake --build "$BUILD_DIR" -j $(getconf _NPROCESSORS_ONLN) } build_custom_sdl() { # make clean folder for custom SDL build rm -rf $BUILD_DIR/CustomSDL mkdir $BUILD_DIR/CustomSDL # clone the repo and build the lib cd $BUILD_DIR/CustomSDL git clone $MIYOO_CUSTOM_SDL_REPO --branch $MIYOO_CUSTOM_SDL_BRANCH --single-branch . PATH="/opt/miyoomini-toolchain/usr/bin:${PATH}:/opt/miyoomini-toolchain/usr/arm-linux-gnueabihf/sysroot/bin" \ CROSS_COMPILE=/opt/miyoomini-toolchain/usr/bin/arm-linux-gnueabihf- \ PREFIX=/opt/miyoomini-toolchain/usr/arm-linux-gnueabihf/sysroot/usr \ UNION_PLATFORM=miyoomini \ ./make.sh # change back to devilutionx root cd "$PACKAGING_DIR/../.." cp -rfL "$BUILD_DIR/CustomSDL/build/.libs/libSDL-1.2.so.0" "$BUILD_DIR/OnionOS/Roms/PORTS/Games/Diablo (DevilutionX)/lib/libSDL-1.2.so.0" } prepare_onion_skeleton() { mkdir -p $BUILD_DIR/OnionOS # Copy basic skeleton cp -rf Packaging/miyoo_mini/skeleton_OnionOS/* $BUILD_DIR/OnionOS # ensure lib dir for custom SDL mkdir -p "$BUILD_DIR/OnionOS/Roms/PORTS/Games/Diablo (DevilutionX)/lib" } package_onion() { prepare_onion_skeleton build_custom_sdl # copy assets mpq cp -f $BUILD_DIR/devilutionx.mpq "$BUILD_DIR/OnionOS/Roms/PORTS/Games/Diablo (DevilutionX)/devilutionx.mpq" # copy executable cp -f $BUILD_DIR/devilutionx "$BUILD_DIR/OnionOS/Roms/PORTS/Games/Diablo (DevilutionX)/devilutionx" rm -f $BUILD_DIR/onion.zip cd $BUILD_DIR/OnionOS zip -r ../devilutionx-miyoo-mini-onion-os.zip . cd "$PACKAGING_DIR/../.." } prepare_miniui_skeleton() { mkdir -p $BUILD_DIR/MiniUI # copy basic skeleton cp -rf Packaging/miyoo_mini/skeleton_MiniUI/* $BUILD_DIR/MiniUI # ensure devilutionx asset dir mkdir -p $BUILD_DIR/MiniUI/Diablo/assets } package_miniui() { prepare_miniui_skeleton # copy assets cp -rf $BUILD_DIR/assets/* $BUILD_DIR/MiniUI/Diablo/assets # copy executable cp -f $BUILD_DIR/devilutionx $BUILD_DIR/MiniUI/Diablo/devilutionx rm -f $BUILD_DIR/miniui.zip cd $BUILD_DIR/MiniUI zip -r ../devilutionx-miyoo-mini-miniui.zip . cd "$PACKAGING_DIR/../.." } main ================================================ FILE: Packaging/miyoo_mini/setup_toolchain.sh ================================================ #!/bin/sh main() { install_dependencies install_toolchain } install_toolchain() { TOOLCHAIN_VERSION=v0.0.3 TOOLCHAIN_TAR="miyoomini-toolchain.tar.xz" TOOLCHAIN_ARCH=`uname -m` if [ "$TOOLCHAIN_ARCH" = "aarch64" ]; then TOOLCHAIN_REPO=miyoomini-toolchain-buildroot-aarch64 else TOOLCHAIN_REPO=miyoomini-toolchain-buildroot fi TOOLCHAIN_URL="https://github.com/shauninman/$TOOLCHAIN_REPO/releases/download/$TOOLCHAIN_VERSION/$TOOLCHAIN_TAR" cd /opt wget "$TOOLCHAIN_URL" echo "extracting remote toolchain $TOOLCHAIN_VERSION ($TOOLCHAIN_ARCH)" tar xf "./$TOOLCHAIN_TAR" rm -rf "./$TOOLCHAIN_TAR" } install_dependencies() { apt-get -y update && apt-get -y install \ bc \ build-essential \ bzip2 \ bzr \ cmake \ cmake-curses-gui \ cpio \ gettext \ git \ libncurses5-dev \ make \ rsync \ scons \ smpq \ tree \ unzip \ wget \ zip } main ================================================ FILE: Packaging/miyoo_mini/skeleton_MiniUI/Diablo/Diablo.m3u ================================================ launch.sh ================================================ FILE: Packaging/miyoo_mini/skeleton_MiniUI/Diablo/launch.sh ================================================ #!/bin/sh cd "$(dirname "$0")" HOME="$USERDATA_PATH" if [ -f "DIABDAT.MPQ" ] || [ -f "spawn.mpq" ]; then ./devilutionx else show "okay.png" say "Missing DIABDAT.MPQ!"$'\n\n'"Please see readme.txt"$'\n'"in the Diablo folder"$'\n'"on your SD card."$'\n' confirm only fi ================================================ FILE: Packaging/miyoo_mini/skeleton_MiniUI/Diablo/readme.txt ================================================ devilutionx(Diablo port) for MiniUI ==================================== Installation -------- - Copy this folder onto your SD card in the location /Roms/Native Games (SH)/ For the full game: - Get the DIABDAT.MPQ from either the CD or GOG release (https://github.com/diasurgical/devilutionX/wiki/Extracting-the-.MPQs-from-the-GoG-installer) - Copy the DIABDAT.MPQ into this folder - Optional: If you want to also play the Hellfire expansion, copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq into this fodler aswell For the free shareware version: - Get the spawn.mpq (https://github.com/diasurgical/devilutionx-assets/releases/latest/download/spawn.mpq) - Copy the spawn.mpq into this folder Controls -------- - D-Pad: move - A: Attack nearest enemy, talk to NPC, pickup/place in inventory, OK in menus - B: select spell, back in menus - X: pickup items, open chests and doors, use item in inventory - Y: cast spell, delete character in main menu - L: use health item from belt - R: use mana potion from belt - L2: character panel - R2: inventory panel - Menu: game menu - Start show quick acess overlay - Start + Up: game menu - Start + Down: toggle automap - Start + Left: character sheet - Start + Right: inventory - Start + Y: quest log - Start + B: spell book - Select + D-Pad: simulate arrow keys - Select + A;B;X;Y: spell hotkeys ================================================ FILE: Packaging/miyoo_mini/skeleton_OnionOS/Roms/PORTS/Games/Diablo (devilutionX)/_required_files.txt ================================================ Directory tree for "Diablo (DevilutionX)" | | devilutionx | devilutionx.mpq | DIABDAT.MPQ | _required_files.txt | \---lib libSDL-1.2.so.0 ================================================ FILE: Packaging/miyoo_mini/skeleton_OnionOS/Roms/PORTS/Shortcuts/Action/Diablo (DevilutionX).notfound ================================================ #!/bin/sh # Standalone Ports Script Template # main configuration : GameName="Diablo (DevilutionX)" GameDir="Diablo (DevilutionX)" GameExecutable="devilutionx" GameDataFile="DIABDAT.MPQ" # additional configuration : KillAudioserver=0 PerformanceMode=0 # specific to this port : FullGamePath="/mnt/SDCARD/Roms/PORTS/Games/$GameDir" savedir="/mnt/SDCARD/Saves/CurrentProfile/saves/DevilutionX" configdir="/mnt/SDCARD/Saves/CurrentProfile/config/DevilutionX" mkdir -p "$savedir" mkdir -p "$configdir" export SDL_HIDE_BATTERY=1 Arguments="--data-dir \"$FullGamePath\" --save-dir \"$savedir\" --config-dir \"$configdir\" --diablo" # running command line : /mnt/SDCARD/Emu/PORTS/launch_standalone.sh "$GameName" "$GameDir" "$GameExecutable" "$Arguments" "$GameDataFile" "$KillAudioserver" "$PerformanceMode" ================================================ FILE: Packaging/miyoo_mini/skeleton_OnionOS/readme.txt ================================================ DevilutionX (Diablo port) for OnionOS ==================================== Installation -------- - Activate the Ports Collection inside the Onion Installer on your device - Copy the Roms folder from this archive to the root of your sd card For the full game: - Get the DIABDAT.MPQ from the CD, GOG or Battle.net release (https://github.com/diasurgical/devilutionX/wiki/Extracting-the-.MPQs-from-the-GoG-installer) - Copy the DIABDAT.MPQ into Roms/PORTS/Games/Diablo (DevilutionX) - Optional: If you want to also play the Hellfire expansion, copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq into the same folder For the free shareware version: - Get the spawn.mpq (https://github.com/diasurgical/devilutionx-assets/releases/download/v2/spawn.mpq) - Copy the spawn.mpq into Roms/PORTS/Games/Diablo (DevilutionX) Controls -------- - D-Pad: move - A: Attack nearest enemy, talk to NPC, pickup/place in inventory, OK in menus - B: select spell, back in menus - X: pickup items, open chests and doors, use item in inventory - Y: cast spell, delete character in main menu - L: use health item from belt - R: use mana potion from belt - L2: character panel - R2: inventory panel - Menu: game menu - Start show quick acess overlay - Start + Up: game menu - Start + Down: toggle automap - Start + Left: character sheet - Start + Right: inventory - Start + Y: quest log - Start + B: spell book - Select + D-Pad: simulate arrow keys - Select + A;B;X;Y: spell hotkeys ================================================ FILE: Packaging/miyoo_mini/toolchainfile.cmake ================================================ set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_VERSION 1) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_PROGRAM_PATH "/opt/miyoomini-toolchain/bin") set(CMAKE_SYSROOT "/opt/miyoomini-toolchain/arm-linux-gnueabihf/sysroot") set(CMAKE_FIND_ROOT_PATH "/opt/miyoomini-toolchain/arm-linux-gnueabihf/sysroot") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(ENV{PKG_CONFIG_SYSROOT_DIR} "/opt/miyoomini-toolchain/arm-linux-gnueabihf/sysroot") set(CMAKE_C_COMPILER "/opt/miyoomini-toolchain/bin/arm-linux-gnueabihf-gcc") ================================================ FILE: Packaging/nix/AppImage.sh ================================================ #!/usr/bin/env bash set -euo pipefail set -x BUILD_DIR="${1-build}" cmake --install "$BUILD_DIR" --prefix "${BUILD_DIR}/AppDir/usr" mv "$BUILD_DIR"/AppDir/usr/share/diasurgical/devilutionx/devilutionx.mpq "$BUILD_DIR"/AppDir/usr/bin/devilutionx.mpq APPIMAGE_BUILDER="${APPIMAGE_BUILDER:-linuxdeploy-x86_64.AppImage}" if ! which "$APPIMAGE_BUILDER"; then if ! [[ -f linuxdeploy-x86_64.AppImage ]]; then wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage -N chmod +x linuxdeploy-x86_64.AppImage fi APPIMAGE_BUILDER=../linuxdeploy-x86_64.AppImage fi SRC_DIR="${PWD}" cd "$BUILD_DIR" LD_LIBRARY_PATH="${PWD}/AppDir/usr/lib" "$APPIMAGE_BUILDER" --appimage-extract-and-run \ --appdir=AppDir \ --custom-apprun="${SRC_DIR}/Packaging/nix/AppRun" \ -d "${SRC_DIR}/Packaging/nix/devilutionx.desktop" \ -o appimage cd - mv "${BUILD_DIR}/"DevilutionX*.AppImage devilutionx.appimage ================================================ FILE: Packaging/nix/AppRun ================================================ #!/bin/sh exec "$APPDIR/usr/bin/devilutionx" "$@" ================================================ FILE: Packaging/nix/LinuxReleasePackaging.sh ================================================ #!/usr/bin/env bash set -euo pipefail set -x BUILD_DIR="${1-build}" mkdir -p "${BUILD_DIR}/package" PKG_PATH=("${BUILD_DIR}/_CPack_Packages/Linux/7Z/"devilutionx-*/) PKG_PATH="${PKG_PATH[@]}" PKG_PATH="${PKG_PATH%/}" cp "${PKG_PATH}/bin/devilutionx" "${BUILD_DIR}/package/devilutionx" if [[ -f "${PKG_PATH}/lib/discord_game_sdk.so" ]]; then cp "${PKG_PATH}/lib/discord_game_sdk.so" "${BUILD_DIR}/package/" cat <<'SH' > "${BUILD_DIR}/package/devilutionx.sh" #!/bin/sh BASEDIR="$(dirname "$(realpath "$0")")" LD_LIBRARY_PATH="$BASEDIR" "$BASEDIR"/devilutionx "$@" SH chmod +x "${BUILD_DIR}/package/devilutionx.sh" fi cp "${BUILD_DIR}/devilutionx" "${BUILD_DIR}/package/devilutionx" cp "${BUILD_DIR}/devilutionx.mpq" "${BUILD_DIR}/package/devilutionx.mpq" if which dpkg 2>/dev/null; then cp "${BUILD_DIR}/"devilutionx*.deb "${BUILD_DIR}/package/devilutionx.deb" fi if which rpmbuild; then cp "${BUILD_DIR}/"devilutionx*.rpm "${BUILD_DIR}/package/devilutionx.rpm" fi cp ./Packaging/nix/README.txt "${BUILD_DIR}/package/README.txt" cp ./Packaging/resources/LICENSE.CC-BY.txt "${BUILD_DIR}/package/LICENSE.CC-BY.txt" cp ./Packaging/resources/LICENSE.OFL.txt "${BUILD_DIR}/package/LICENSE.OFL.txt" cd "${BUILD_DIR}/package/" && tar -cavf ../../devilutionx.tar.xz * ================================================ FILE: Packaging/nix/README.txt ================================================ # DevilutionX DevilutionX is a source port of Diablo and Hellfire that strives to make it simple to run the game while providing engine improvements, bugfixes, and some optional quality of life features. # Links Discord: https://discord.gg/devilutionx GitHub: https://github.com/diasurgical/devilutionX Check out the manual for what features are available and how best to take advantage of them: https://github.com/diasurgical/devilutionX/wiki For a full list of changes see our changelog: https://github.com/diasurgical/devilutionX/blob/master/docs/CHANGELOG.md # How To Install: - Extract the files in the archive. - Install libsdl2 - Copy DIABDAT.MPQ from the CD or GOG-installation (or extract it from the GoG installer) to the DevilutionX folder. - To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. - For the Russian voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq. - Run ./devilutionx # Multiplayer - TCP/IP requires the host to expose port 6112. All games are encrypted and password protected. # Save Games and configurations The configurations and save games are located in: ~/.local/share/diasurgical/devilution # Credits - See list of contributors https://github.com/diasurgical/devilutionX/graphs/contributors # Legal This software is being released to the Public Domain. No assets of Diablo are being provided. You must own a copy of Diablo and have access to the assets beforehand in order to use this software. Battle.net® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Battle.net and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. This software is in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Packaging/nix/debian-cross-aarch64-prep.sh ================================================ #!/usr/bin/env bash set -euo pipefail set -x FLAVOR="$(lsb_release -sc)" if dpkg-vendor --derives-from Ubuntu; then sudo tee /etc/apt/sources.list.d/arm64.list <\fR Record a demo file. .TP .B \-\-demo \fI<#>\fR Play a demo file. .TP .B \-\-timedemo Disable all frame limiting during demo playback. .SH GAME SELECTION .TP .B \-\-spawn Force Shareware mode. .TP .B \-\-diablo Force Diablo mode. .TP .B \-\-hellfire Force Hellfire mode. .SH MULTI-PLAYER TCP/IP requires the host to expose the port the game is listening on, 6112 by default. Private games are encrypted and password protected. .SH SAVE GAMES AND CONFIGURATIONS By default the configurations and save games are located in: .I ~/.local/share/diasurgical/devilution .SH INSTALLATION To install .B DevilutionX: .IP 1. Extract the files in the archive. .IP 2. Install .B libsdl2. .IP 3. Copy .I DIABDAT.MPQ from the CD or GOG-installation (or extract it from the GOG installer) to the DevilutionX folder. .IP 4. To run the Diablo: Hellfire expansion you will need to also copy .I hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. .IP 5. For Chinese, Japanese, and Korean text support download: .B https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. .RE .IP 6. For the Polish voice pack download: .B https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq .IP 7. For the Russian voice pack download: .B https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq .IP 8. For the Spanish voice pack download: .B https://github.com/diasurgical/devilutionx-assets/releases/latest/download/es.mpq .IP 8. Run .B ./devilutionx .SH REPORTING BUGS Report bugs at .B https://github.com/diasurgical/devilutionX/ .SH SEE ALSO Discord: .B https://discord.gg/devilutionx .RE .PP GitHub: .B https://github.com/diasurgical/devilutionX .RE .PP Manual: .B https://github.com/diasurgical/devilutionX/wiki .RE .PP Changelog: .B https://github.com/diasurgical/devilutionX/blob/master/docs/CHANGELOG.md .RE .SH AUTHOR Written by the DevilutionX community. .SH COPYRIGHT DevilutionX is made publicly available and released under the Sustainable Use License (see LICENSE). .B https://github.com/diasurgical/DevilutionX/blob/master/LICENSE.md .P The source code in this repository is for non-commercial use only. If you use the source code, you may not charge others for access to it or any derivative work thereof. .P Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. .P DevilutionX and any of its maintainers are in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Packaging/nix/devilutionx.desktop ================================================ [Desktop Entry] Name=DevilutionX GenericName=DevilutionX Comment=Play Diablo I Comment[da]=Spil Diablo I Comment[hr]=Igrajte Diablo I Comment[it]=Gioca a Diablo I Comment[ru]=Играть в Diablo I Comment[ua]=Грати в Diablo I Exec=devilutionx --diablo Icon=devilutionx Terminal=false Type=Application X-DCOP-ServiceType=Multi X-KDE-StartupNotify=true Categories=Game;RolePlaying; Keywords=Game;Diablo;Action;RPG;DevilutionX; ================================================ FILE: Packaging/nix/devilutionx.metainfo.xml ================================================ org.diasurgical.DevilutionX Diasurgical team DevilutionX Experience Diablo and Hellfire anew CC-BY-SA-4.0 SUL-1.0 #c86666 #640000 https://github.com/diasurgical/devilutionX https://github.com/diasurgical/devilutionX/issues https://discord.gg/devilutionx https://github.com/diasurgical/devilutionX moderate moderate moderate moderate mild mild intense Game ActionGame RolePlaying pointing keyboard touch gamepad

DevilutionX is a source port of Diablo and Hellfire that strives to make it simple to run the game while providing engine improvements, bugfixes, and some optional quality of life features. This includes support for modern operating systems and higher resolutions, controller and touch control support, translations, stash, optional experience and enemy health bars, etc.

Note: DevilutionX requires data files from original Diablo/Hellfire. By default, a demo version of Diablo is installed.

To install the full version of Diablo, copy DIABDAT.MPQ from the CD or GOG-installation (or extract it from the GoG installer) to ~/.var/app/org.diasurgical.DevilutionX/data/diasurgical/devilution.

To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq.

devilutionx.desktop https://raw.githubusercontent.com/flathub/org.diasurgical.DevilutionX/master/screens/screen1.png Diablo gameplay in widescreen https://raw.githubusercontent.com/flathub/org.diasurgical.DevilutionX/master/screens/screen2.png Diablo gameplay with enemy health bar and XP bar https://raw.githubusercontent.com/flathub/org.diasurgical.DevilutionX/master/screens/screen3.png Diablo gameplay on Android mobile device

This release includes the following updates:

  • App icon on Linux is now aligned with Android
  • Adjusted game speeds for Multiplayer
  • You can now use CTRL+ Mouse Scroll to zoom the map
  • Murphy Shrine has been added to Crippling Shrines
  • Updates to Russian and Polish translations
  • Crash fixes

Please visit the full changelog for more detailed notes

https://github.com/diasurgical/devilutionX/releases/tag/1.5.5

This release includes the following updates:

  • Invalid items are now unusable
  • Improved gamepad controls
  • Updated Italian, Russian and Ukrainian translations
  • Fixed multiple crashes
  • Improved item validation in multiplayer
  • Fixed multiple bugs found in original Diablo and Hellfire

Please visit the full changelog for more detailed notes

https://github.com/diasurgical/devilutionX/releases/tag/1.5.4

This release includes the following updates:

  • Resolved validation multiplayer errors
  • Added Hungarian translation
  • Added Turkish translation
  • Fixed issue where a line is repeated in the info panel
  • Fixed errors when converting Hellfire saved games

Please visit the full changelog for more detailed notes

https://github.com/diasurgical/devilutionX/releases/tag/1.5.3

This release includes the following updates:

  • Fix issues with joining games due to invalid player data
  • Improve rendering of Chinese/Japanese/Korean texts
  • Fix Hellfire items not saving their identified state
  • Fix stash corrupting when converting a hero between Diablo and Hellfire

Please visit the full changelog for more detailed notes

https://github.com/diasurgical/devilutionX/releases/tag/1.5.2

This is a primarily bugfix release, which includes the following updates:

  • Resolve various gameplay and graphical issues
  • Revamped settings menu for better organization
  • Rectification of crashes identified in version 1.5.0
  • Reduced RAM usage for improved performance
  • Updates to PVP arenas
  • Increased reliability in multiplayer functionality
  • Improved translations
  • Fixed gameplay recording playback issues

Please visit the full changelog for more detailed notes

https://github.com/diasurgical/devilutionX/releases/tag/1.5.1

This release includes a lot of features, such as:

  • All quests are playable in multiplayer
  • PVP arenas
  • Controller button remapping
  • Tons of bugfixes, including for original Diablo/Hellfire

And a lot more! Please visit the link for detailed notes.

https://github.com/diasurgical/devilutionX/releases/tag/1.5.0
================================================ FILE: Packaging/pi/README.txt ================================================ # DevilutionX DevilutionX is a source port of Diablo and Hellfire that strives to make it simple to run the game while providing engine improvements, bugfixes, and some optional quality of life features. # Links Discord: https://discord.gg/devilutionx GitHub: https://github.com/diasurgical/devilutionX Check out the manual for what features are available and how best to take advantage of them: https://github.com/diasurgical/devilutionX/wiki For a full list of changes see our changelog: https://github.com/diasurgical/devilutionX/blob/master/docs/CHANGELOG.md # How To Install: - Extract the files in the archive. - Install libsdl2 - Copy DIABDAT.MPQ from the CD or GOG-installation (or extract it from the GoG installer) to the DevilutionX folder. - To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. - For the Russian voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq. - Run ./devilutionx # Raspberry Pi performance - This build is compiled for Raspbian Stretch - For the best experience set upscale=0 in diablo.ini and set the system resolution to 640x480 - Alternately you can enable experimental GL-drivers via raspi-config for upscaling support # Multiplayer - TCP/IP requires the host to expose port 6112. All games are encrypted and password protected. # Save Games and configurations The configurations and save games are located in: ~/.local/share/diasurgical/devilution # Credits - See list of contributors https://github.com/diasurgical/devilutionX/graphs/contributors # Legal This software is being released to the Public Domain. No assets of Diablo are being provided. You must own a copy of Diablo and have access to the assets beforehand in order to use this software. Battle.net® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Battle.net and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. This software is in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Packaging/ps4/README.md ================================================ # devilutionX PS4 port ## Prerequisites - A Playstation 4 capable of running homebrew. - Game assets from the Diablo game (diabdat.mpq), or its [shareware][shareware] (spawn.mpq) ## Installation - Install the devilutionX PS4 pkg - Copy the game assets (e.g., via ftp) to /user/data/diasurgical/devilution/ - Launch the game ## Known limitations - No networking ## Controls - D-pad: move hero - ○: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - ×: select spell, back while in menus - △: pickup items, open nearby chests and doors, use item in the inventory - □: cast spell, delete character while in main menu - L1: use health item from belt - R1: use mana potion from belt - L2: toggle character sheet - R2: toggle inventory - Left stick: move hero - Right stick: move cursor - L3: toggle auto map - R3: click with cursor ## Building from Source Install the [PacBrew openorbis SDK][pacbrew-openorbis], then run the following bash script. ```console devilutionX$ ./Packaging/ps4/build.sh ``` [shareware]: http://ftp.blizzard.com/pub/demos/diablosw.exe [pacbrew-openorbis]: https://github.com/PacBrew/pacbrew-packages ================================================ FILE: Packaging/ps4/build.sh ================================================ #!/usr/bin/env bash set -e SCRIPTDIR="${BASH_SOURCE[0]}" SCRIPTDIR="$(dirname "${SCRIPTDIR}")" cmake -S "${SCRIPTDIR}/../../" \ -B build-ps4 \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_VERBOSE_MAKEFILE=ON \ -DCMAKE_TOOLCHAIN_FILE="/opt/pacbrew/ps4/openorbis/cmake/ps4.cmake" cmake --build build-ps4 -j $(getconf _NPROCESSORS_ONLN) mv build-ps4/IV0001-DVLX00001_00-*.pkg build-ps4/devilutionx.pkg ================================================ FILE: Packaging/ps5/README.md ================================================ # devilutionX PS5 port ## Prerequisites - A Playstation 5 capable of running the [ps5-payload-websrv][websrv] homebrew. - Game assets from the Diablo game (diabdat.mpq), or its [shareware][shareware] (spawn.mpq) ## Installation - Copy the game assets (e.g., via ftp) to /data/homebrew/devilutionX - Launch the [ps5-payload-websrv][websrv] homebrew - Launch the game from your browser at http://PS5-IP:8080, or using the [companion launcher][launcher] from the PS5 menu system ## Controls - D-pad: move hero - ○: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - ×: select spell, back while in menus - △: pickup items, open nearby chests and doors, use item in the inventory - □: cast spell, delete character while in main menu - L1: use health item from belt - R1: use mana potion from belt - L2: toggle character sheet - R2: toggle inventory - Left stick: move hero - Right stick: move cursor - L3: toggle auto map - R3: click with cursor ## Building from Source Install the [PacBrew PS5 SDK][pacbrew], then run the following bash script: ```console devilutionX$ ./Packaging/ps5/build.sh ``` [shareware]: http://ftp.blizzard.com/pub/demos/diablosw.exe [pacbrew]: https://github.com/ps5-payload-dev/pacbrew-repo [websrv]: https://github.com/ps5-payload-dev/websrv [launcher]: https://github.com/ps5-payload-dev/websrv/blob/master/homebrew/IV9999-FAKE00001_00-HOMEBREWLOADER01.pkg?raw=true ================================================ FILE: Packaging/ps5/build.sh ================================================ #!/usr/bin/env bash set -e SCRIPTDIR="${BASH_SOURCE[0]}" SCRIPTDIR="$(dirname "${SCRIPTDIR}")" if [ -z "${PS5_PAYLOAD_SDK}" ]; then export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk fi source "${PS5_PAYLOAD_SDK}/toolchain/prospero.sh" ${CMAKE} -DCMAKE_BUILD_TYPE=Release \ -DDISCORD_INTEGRATION=OFF \ -DBUILD_TESTING=OFF \ -DASAN=OFF \ -DUBSAN=OFF \ -DDISABLE_LTO=ON \ -DNOEXIT=ON \ -DNONET=OFF \ -DBUILD_ASSETS_MPQ=ON \ -DDEVILUTIONX_SYSTEM_SDL_IMAGE=OFF \ -B build-ps5 \ -S "${SCRIPTDIR}/../../" ${MAKE} -C build-ps5 -j $(getconf _NPROCESSORS_ONLN) rm -rf build-ps5/DevilutionX mkdir build-ps5/DevilutionX cp -r "${SCRIPTDIR}/sce_sys" build-ps5/DevilutionX/ cp "${SCRIPTDIR}/homebrew.js" build-ps5/DevilutionX/ cp "${SCRIPTDIR}/README.md" build-ps5/DevilutionX/ cp build-ps5/devilutionx.mpq build-ps5/DevilutionX/ cp build-ps5/devilutionx build-ps5/DevilutionX/devilutionx.elf # Let github actions do this? cd build-ps5 rm -f devilutionx-ps5.zip zip -r devilutionx-ps5.zip DevilutionX ================================================ FILE: Packaging/ps5/homebrew.js ================================================ async function main() { const PAYLOAD = window.workingDir + '/devilutionx.elf'; return { mainText: "DevilutionX", secondaryText: 'Diablo build for modern OSes', onclick: async () => { return { path: PAYLOAD, args: '' }; }, options: [ { text: "Force Shareware mode", onclick: async () => { return { path: PAYLOAD, args: '--spawn' }; } }, { text: "Force Diablo mode", onclick: async () => { return { path: PAYLOAD, args: '--diablo' }; } }, { text: "Force Hellfire mode", onclick: async () => { return { path: PAYLOAD, args: '--hellfire' }; } } ] }; } ================================================ FILE: Packaging/resources/LICENSE.CC-BY.txt ================================================ Attribution 4.0 International ======================================================================= Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. Using Creative Commons Public Licenses Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC- licensed material, or material used under an exception or limitation to copyright. More considerations for licensors: wiki.creativecommons.org/Considerations_for_licensors Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason--for example, because of any applicable exception or limitation to copyright--then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public: wiki.creativecommons.org/Considerations_for_licensees ======================================================================= Creative Commons Attribution 4.0 International Public License By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. Section 1 -- Definitions. a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. Section 2 -- Scope. a. License grant. 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: a. reproduce and Share the Licensed Material, in whole or in part; and b. produce, reproduce, and Share Adapted Material. 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 3. Term. The term of this Public License is specified in Section 6(a). 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a) (4) never produces Adapted Material. 5. Downstream recipients. a. Offer from the Licensor -- Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. b. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). b. Other rights. 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 2. Patent and trademark rights are not licensed under this Public License. 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. Section 3 -- License Conditions. Your exercise of the Licensed Rights is expressly made subject to the following conditions. a. Attribution. 1. If You Share the Licensed Material (including in modified form), You must: a. retain the following if it is supplied by the Licensor with the Licensed Material: i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); ii. a copyright notice; iii. a notice that refers to this Public License; iv. a notice that refers to the disclaimer of warranties; v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; b. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and c. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. Section 4 -- Sui Generis Database Rights. Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. Section 5 -- Disclaimer of Warranties and Limitation of Liability. a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. Section 6 -- Term and Termination. a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 2. upon express reinstatement by the Licensor. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. Section 7 -- Other Terms and Conditions. a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. Section 8 -- Interpretation. a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. ======================================================================= Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. Creative Commons may be contacted at creativecommons.org. ================================================ FILE: Packaging/resources/LICENSE.OFL.txt ================================================ This Font Software is Copyright (c) 1997-2009, SIL International (https://scripts.sil.org/) with Reserved Font Names "Charis" and "SIL". This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: Packaging/resources/LICENSE.zlib.txt ================================================ The source code to this library used with SDL_ttf can be found here: https://github.com/libsdl-org/SDL_image/tree/main/external --- Copyright notice: (C) 1995-2010 Jean-loup Gailly and Mark Adler This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Jean-loup Gailly Mark Adler jloup@gzip.org madler@alumni.caltech.edu If you use the zlib library in a product, we would appreciate *not* receiving lengthy legal documents to sign. The sources are provided for free but without warranty of any kind. The library has been entirely written by Jean-loup Gailly and Mark Adler; it does not include third-party code. ================================================ FILE: Packaging/resources/README-SDL.txt ================================================ Please distribute this file with the SDL runtime environment: The Simple DirectMedia Layer (SDL for short) is a cross-platform library designed to make it easy to write multi-media software, such as games and emulators. The Simple DirectMedia Layer library source code is available from: https://www.libsdl.org/ This library is distributed under the terms of the zlib license: https://www.zlib.net/zlib_license.html ================================================ FILE: Packaging/switch/README.txt ================================================ # Nintendo Switch Port of DevilutionX (Diablo) # How To Install: - Put `devilutionx.nro` into `/switch/devilutionx` - Copy diabdat.mpq from your CD (or GoG install folder) to `/switch/devilutionx`. - To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. - For the Russian voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq. - Launch `devilutionx.nro`. (Do not use album to launch; see the note below.) - *Note:* Hold R on any installed game and launch it. Do not use album to launch. If you use album, the homebrew will only have a small amount memory available, and the touch keyboard won't work. This is true for all homebrew, not just DevilutionX. # Joycon Controls - Left analog or D-Pad: move hero - A: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - B: select spell, back while in menus - X: pickup items, open nearby chests and doors, use item in the inventory - Y: cast spell, delete character while in main menu - L: use health item from belt - R: use mana potion from belt - ZL: character sheet (alt: Start + L1 or ←) - ZR: inventory (alt: Start + L2 or →) - Left analog click: toggle automap (alt: Start + ↓) - Start + Select: game menu (alt: Start + ↑) - Select + A/B/X/Y: Spell hotkeys - Right analog: move automap or simulate mouse - Right analog click or Select + L: left mouse click - Select + Right analog click: right mouse click (alt: Select + R1) - Select + L2: quest log (alt: Start + Y) - Select + R2: spell book (alt: Start + B) # Credits - See list of contributors https://github.com/diasurgical/devilutionX/graphs/contributors # Links Discord: https://discord.gg/devilutionx GitHub: https://github.com/diasurgical/devilutionX Check out the manual for what features are available and how best to take advantage of them: https://github.com/diasurgical/devilutionX/wiki For a full list of changes see our changelog: https://github.com/diasurgical/devilutionX/blob/master/docs/CHANGELOG.md # Legal DevilutionX is released to the Public Domain. The documentation and functionality provided by DevilutionX may only be utilized with assets provided by ownership of Diablo. The source code in this repository is for non-commercial use only. If you use the source code you may not charge others for access to it or any derivative work thereof. Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. DevilutionX and any of its maintainers are in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Packaging/switch/packages.txt ================================================ switch-mesa switch-glad switch-glm switch-libpng switch-sdl2 switch-libsodium libnx devkitA64 general-tools switch-tools switch-bzip2 ================================================ FILE: Packaging/vita/README.txt ================================================ # devilutionX PS Vita port ## How To Play: - Install VPK - Copy diabdat.mpq from your CD or GoG installation (or [extract it from the GoG installer](https://github.com/diasurgical/devilutionX/wiki/Extracting-the-.MPQs-from-the-GoG-installer)) to the `ux0:/data/diasurgical/devilution/`. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. - For the Russian voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq. # Building from Source ``` cd build cmake -DCMAKE_TOOLCHAIN_FILE=${VITASDK}/share/vita.toolchain.cmake -DCMAKE_BUILD_TYPE=Release .. make ``` # Multiplayer - Not supported yet # Controls ## Default - Left analog or D-Pad: move hero - ○: attack nearby enemies, talk to townspeople and merchants, pickup/place items in the inventory, OK while in main menu - ×: select spell, back while in menus - △: pickup items, open nearby chests and doors, use item in the inventory - □: cast spell, delete character while in main menu - L1: use health item from belt - R1: use mana potion from belt - Left back touch panel: character sheet (alt: Start + ←, alt: L2 on ds4) - Right back touch panel: inventory (alt: Start + →, alt: R2 on ds4) - Start + ↓: toggle automap - Start + Select: game menu (alt: Start + ↑) - Select + ×/○/□/△: Spell hotkeys - Right analog: move automap or simulate mouse - Select + L1: left mouse click - Select + R1: right mouse click - Start + □: quest log - Start + △: spell book ## Options There's special section `controls` in diablo.ini file, that allows you to adjust controls: ``` [controls] switch_potions_and_clicks=0 dpad_hotkeys=0 enable_second_touchscreen=1 sdl2_controller_mapping=50535669746120436f6e74726f6c6c65,PSVita Controller,y:b0,b:b1,a:b2,x:b3,leftshoulder:b4,rightshoulder:b5,dpdown:b6,dpleft:b7,dpup:b8,dpright:b9,back:b10,start:b11,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5,leftstick:b14,rightstick:b15, ``` - **dpad_hotkeys:** dpad works as hotkeys without holding Start button - **switch_potions_and_clicks:** L1/R1 works as left/right mouse clicks by debault, and as health/mana potion while holding Select - **sdl2_controller_mapping:** allows you to remap controls. It uses https://github.com/gabomdq/SDL_GameControllerDB syntax - **enable_second_touchscreen:** enable/disable back touch mapping to L2/R2 ================================================ FILE: Packaging/vita/sce_sys/livearea/contents/template.xml ================================================       bg.png startup.png logo0.png devilutionX ================================================ FILE: Packaging/windows/CMakePresets.json ================================================ { "version": 3, "cmakeMinimumRequired": { "major": 3, "minor": 19, "patch": 0 }, "configurePresets": [ { "name": "ninja-vcpkg", "displayName": "Ninja with VcPkg Configure Settings", "description": "Configure with vcpkg toolchain", "binaryDir": "${sourceDir}/build-${presetName}", "generator": "Ninja", "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": { "type": "FILEPATH", "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" } } }, { "name": "ninja-vcpkg-debug", "displayName": "Ninja with VcPkg Configure Settings with CMAKE_BUILD_TYPE=Debug", "inherits": "ninja-vcpkg", "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" } }, { "name": "ninja-vcpkg-relwithdebinfo", "displayName": "Ninja with VcPkg Configure Settings with CMAKE_BUILD_TYPE=RelWithDebInfo", "inherits": "ninja-vcpkg", "cacheVariables": { "CMAKE_BUILD_TYPE": "RelWithDebInfo", "DISABLE_LTO": { "type": "BOOL", "value": "ON" } } }, { "name": "ninja-vcpkg-discord-relwithdebinfo", "displayName": "Ninja with VcPkg Configure Settings with DISCORD_INTEGRATION=ON CMAKE_BUILD_TYPE=RelWithDebInfo", "inherits": "ninja-vcpkg-relwithdebinfo", "cacheVariables": { "DISCORD_INTEGRATION": { "type": "BOOL", "value": "ON" } } } ], "buildPresets": [ { "name": "ninja-vcpkg-debug", "configurePreset": "ninja-vcpkg-debug", "displayName": "Build ninja-vcpkg-debug", "description": "Build ninja-vcpkg-debug Configuration" }, { "name": "ninja-vcpkg-relwithdebinfo", "configurePreset": "ninja-vcpkg-relwithdebinfo", "displayName": "Build ninja-vcpkg-relwithdebinfo", "description": "Build ninja-vcpkg-relwithdebinfo Configuration" }, { "name": "ninja-vcpkg-discord-relwithdebinfo", "configurePreset": "ninja-vcpkg-discord-relwithdebinfo", "displayName": "Build ninja-vcpkg-discord-relwithdebinfo", "description": "Build ninja-vcpkg-discord-relwithdebinfo Configuration" } ], "testPresets": [ { "name": "ninja-vcpkg-debug", "configurePreset": "ninja-vcpkg-debug", "output": {"outputOnFailure": true} }, { "name": "ninja-vcpkg-relwithdebinfo", "configurePreset": "ninja-vcpkg-relwithdebinfo", "output": {"outputOnFailure": true} }, { "name": "ninja-vcpkg-discord-relwithdebinfo", "configurePreset": "ninja-vcpkg-discord-relwithdebinfo", "output": {"outputOnFailure": true} } ] } ================================================ FILE: Packaging/windows/README.txt ================================================ # DevilutionX DevilutionX is a source port of Diablo and Hellfire that strives to make it simple to run the game while providing engine improvements, bugfixes, and some optional quality of life features. # Links Discord: https://discord.gg/devilutionx GitHub: https://github.com/diasurgical/devilutionX Check out the manual for what features are available and how best to take advantage of them: https://github.com/diasurgical/devilutionX/wiki For a full list of changes see our changelog: https://github.com/diasurgical/devilutionX/blob/master/docs/CHANGELOG.md # How To Install: - Extract the files in the zip - Copy DIABDAT.MPQ from the CD or GOG-installation (or extract it from the GoG installer) to the DevilutionX folder. - To run the Diablo: Hellfire expansion you will need to also copy hellfire.mpq, hfmonk.mpq, hfmusic.mpq, hfvoice.mpq. - For Chinese, Japanese, and Korean text support download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/fonts.mpq and add it to the game folder. - For the Polish voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/pl.mpq. - For the Russian voice pack download https://github.com/diasurgical/devilutionx-assets/releases/latest/download/ru.mpq. - Run devilutionx.exe # Multiplayer - TCP/IP requires the host to expose port 6112. All games are encrypted and password protected. # Save Games and configurations The configurations and save games are located in: C:\Users\[username]\AppData\Roaming\diasurgical\devilution # Credits - See list of contributors https://github.com/diasurgical/devilutionX/graphs/contributors # Legal This software is being released to the Public Domain. No assets of Diablo are being provided. You must own a copy of Diablo and have access to the assets beforehand in order to use this software. Battle.net® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Battle.net and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. This software is in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Packaging/windows/devilutionx.exe.manifest ================================================ true/pm ================================================ FILE: Packaging/windows/devilutionx.rc ================================================ #ifdef __MINGW32__ #include "winuser.h" #define MANIFEST_RESOURCE_ID 1 MANIFEST_RESOURCE_ID RT_MANIFEST "devilutionx.exe.manifest" #endif IDI_ICON1 ICON DISCARDABLE "icon.ico" ================================================ FILE: Packaging/windows/dos-prep.sh ================================================ #!/usr/bin/env bash set -euo pipefail INSTALL_PREFIX=/opt/i386-pc-msdosdjgpp-toolchain # only use sudo when necessary if [ `id -u` -ne 0 ]; then SUDO=sudo else SUDO="" fi # Install dependencies on Debian / Ubuntu: install_system_deps() { if which apt-get 2>/dev/null; then set -x $SUDO apt-get update $SUDO apt-get install bison flex curl gcc g++ make texinfo zlib1g-dev tar bzip2 \ gzip xz-utils unzip python3-dev m4 dos2unix nasm cmake { set +x; } 2>/dev/null fi } # Build and install DJGPP build_and_install_djgpp() { git clone https://github.com/jwt27/build-gcc.git cd build-gcc $SUDO ./build-djgpp.sh --prefix="$INSTALL_PREFIX" --batch binutils gcc-14.2.0 djgpp-cvs cd - $SUDO rm -rf build-gcc } main() { set -x mkdir -p tmp/dos-prep cd tmp/dos-prep install_system_deps build_and_install_djgpp } main "$@" ================================================ FILE: Packaging/windows/mingw-prep.sh ================================================ #!/usr/bin/env bash SDLDEV_VERS=2.32.0 SODIUM_VERS=1.0.20 # exit when any command fails set -euo pipefail # detect architecture from script name if echo "$(basename $0)" | grep -q 64; then MINGW_ARCH=x86_64-w64-mingw32 SODIUM_ARCH=win64 else MINGW_ARCH=i686-w64-mingw32 SODIUM_ARCH=win32 fi # set MINGW_PREFIX MINGW_PREFIX=/usr/${MINGW_ARCH} if [ ! -d "${MINGW_PREFIX}" ]; then echo "MinGW prefix not found (${MINGW_PREFIX})" exit 1 else echo "Installing to ${MINGW_PREFIX}" fi # only use sudo when necessary if [ `id -u` -ne 0 ]; then SUDO=sudo else SUDO="" fi rm -rf "tmp-mingw-${MINGW_ARCH}-prep" mkdir -p "tmp-mingw-${MINGW_ARCH}-prep" cd "tmp-mingw-${MINGW_ARCH}-prep" wget -q https://www.libsdl.org/release/SDL2-devel-${SDLDEV_VERS}-mingw.tar.gz -OSDL2-devel-${SDLDEV_VERS}-mingw.tar.gz tar -xzf SDL2-devel-${SDLDEV_VERS}-mingw.tar.gz sed -i '/$(CROSS_PATH)\/cmake/ s/^/#/' SDL2*/Makefile $SUDO make -C SDL2*/ cross CROSS_PATH=/usr ARCHITECTURES=${MINGW_ARCH} wget -q https://github.com/jedisct1/libsodium/releases/download/${SODIUM_VERS}-RELEASE/libsodium-${SODIUM_VERS}-mingw.tar.gz -Olibsodium-${SODIUM_VERS}-mingw.tar.gz tar -xzf libsodium-${SODIUM_VERS}-mingw.tar.gz --no-same-owner $SUDO cp -r libsodium-${SODIUM_ARCH}/* ${MINGW_PREFIX} # Fixup pkgconfig prefix: find "${MINGW_PREFIX}/lib/pkgconfig/" -name '*.pc' -exec \ $SUDO sed -i "s|^prefix=.*|prefix=${MINGW_PREFIX}|" '{}' \; # Fixup CMake prefix: find "${MINGW_PREFIX}" -name '*.cmake' -exec \ $SUDO sed -i "s|/opt/local/${MINGW_ARCH}|${MINGW_PREFIX}|" '{}' \; # Fixup zlib linking: $SUDO mv "${MINGW_PREFIX}/lib/libz.dll.a" "${MINGW_PREFIX}/lib/libz.dll.a.bak" ================================================ FILE: Packaging/windows/mingw9x-prep.sh ================================================ #!/usr/bin/env bash SDLDEV_VERS=1.2.15 SODIUM_VERS=1.0.20 # exit when any command fails set -euo pipefail MINGW_ARCH=i686-w64-mingw32 SODIUM_ARCH=win32 # set MINGW_PREFIX MINGW_PREFIX=/usr/${MINGW_ARCH} if [ ! -d "${MINGW_PREFIX}" ]; then echo "MinGW prefix not found (${MINGW_PREFIX})" exit 1 else echo "Installing to ${MINGW_PREFIX}" fi # only use sudo when necessary if [ `id -u` -ne 0 ]; then SUDO=sudo else SUDO="" fi rm -rf tmp-mingw9x-prep mkdir -p tmp-mingw9x-prep cd tmp-mingw9x-prep curl --no-progress-meter -OL https://www.libsdl.org/release/SDL-devel-${SDLDEV_VERS}-mingw32.tar.gz tar -xzf SDL-devel-${SDLDEV_VERS}-mingw32.tar.gz $SUDO cp -r SDL-*/include/* ${MINGW_PREFIX}/include $SUDO cp -r SDL-*/lib/* ${MINGW_PREFIX}/lib $SUDO cp -r SDL-*/bin/* ${MINGW_PREFIX}/bin wget -q https://github.com/jedisct1/libsodium/releases/download/${SODIUM_VERS}-RELEASE/libsodium-${SODIUM_VERS}-mingw.tar.gz -Olibsodium-${SODIUM_VERS}-mingw.tar.gz tar -xzf libsodium-${SODIUM_VERS}-mingw.tar.gz --no-same-owner $SUDO cp -r libsodium-${SODIUM_ARCH}/* ${MINGW_PREFIX} # Fixup pkgconfig prefix: find "${MINGW_PREFIX}/lib/pkgconfig/" -name '*.pc' -exec \ $SUDO sed -i "s|^prefix=.*|prefix=${MINGW_PREFIX}|" '{}' \; # Fixup CMake prefix: find "${MINGW_PREFIX}" -name '*.cmake' -exec \ $SUDO sed -i "s|/opt/local/${MINGW_ARCH}|${MINGW_PREFIX}|" '{}' \; ================================================ FILE: Packaging/xbox-one/build.bat ================================================ call VsDevCmd.bat mkdir ..\..\build cd ..\..\build git clone --branch SDL2 https://github.com/libsdl-org/SDL.git git -C SDL reset --hard 10135b2d7bbed6ea0cba24410ebc12887d92968d msbuild /p:PlatformToolset=v143;TargetPlatformVersion=10.0.26100.0;TargetPlatformMinVersion=10.0.14393.0;ConfigurationType=StaticLibrary;Configuration=Release;Platform=x64 SDL\VisualC-WinRT\SDL-UWP.vcxproj cmake -DUWP_LIB=1 -DUWP_SDL2_DIR="%CD%/SDL" -DCMAKE_BUILD_TYPE=x64-Release .. msbuild /p:Configuration=Release;Platform=x64 /m DevilutionX.sln powershell "Get-Content ..\uwp-project\Package.appxmanifest.template | %% {$_ -replace '__PROJECT_VERSION__',$(Select-String -Path ..\VERSION -Pattern \d+\.\d+\.\d+).Matches[0].Value} | Out-File -FilePath ..\uwp-project\Package.appxmanifest -encoding ASCII" msbuild /p:Configuration=Release;Platform=x64;AppxBundle=Always;AppxBundlePlatforms=x64 /m ..\uwp-project\devilutionx.sln powershell "Get-Childitem -Path uwp-project\AppxPackages, uwp-project\Release -Include Microsoft.VCLibs.x64.*.appx, devilutionX_*_x64.appx -File -Recurse | Compress-Archive -DestinationPath ..\devilutionx.zip" ================================================ FILE: README.md ================================================

image

--- [![Discord Channel](https://img.shields.io/discord/518540764754608128?color=%237289DA&logo=discord&logoColor=%23FFFFFF)](https://discord.gg/devilutionx) [![Downloads](https://img.shields.io/github/downloads/diasurgical/devilutionX/total.svg)](https://github.com/diasurgical/devilutionX/releases/latest) [![Codecov](https://codecov.io/gh/diasurgical/devilutionX/branch/master/graph/badge.svg)](https://codecov.io/gh/diasurgical/devilutionX)

image

*(The health-bar and XP-bar are off by default but can be enabled in the [game settings](https://github.com/diasurgical/DevilutionX/wiki/Config-File). Widescreen can also be disabled if preferred.)* # What is DevilutionX DevilutionX is a port of Diablo and Hellfire that strives to make it simple to run the game while providing engine improvements, bug fixes, and some optional quality of life features. Check out the [manual](https://github.com/diasurgical/devilutionX/wiki) for available features and how to take advantage of them. For a full list of changes, see our [changelog](docs/CHANGELOG.md). # How to Install Note: You'll need access to the data from the original game. If you don't have an original CD, you can [buy Diablo from GoG.com](https://www.gog.com/game/diablo) or Battle.net. Alternatively, you can use `spawn.mpq` from the [shareware](https://github.com/diasurgical/devilutionx-assets/releases/latest/download/spawn.mpq) [[2]](http://ftp.blizzard.com/pub/demos/diablosw.exe) version, in place of `DIABDAT.MPQ`, to play the shareware portion of the game. Download the latest [DevilutionX release](https://github.com/diasurgical/devilutionX/releases/latest) and extract the contents to a location of your choosing or [build from source](#building-from-source). - Copy `DIABDAT.MPQ` from the CD or Diablo installation (or [extract it from the GoG installer](https://github.com/diasurgical/devilutionX/wiki/Extracting-MPQs-from-the-GoG-installer)) to the DevilutionX folder. - To run the Diablo: Hellfire expansion, you will also need to copy `hellfire.mpq`, `hfmonk.mpq`, `hfmusic.mpq`, and `hfvoice.mpq`. For more detailed instructions: [Installation Instructions](./docs/installing.md). # Contributing We are always looking for more people to help with [coding](docs/CONTRIBUTING.md), [documentation](https://github.com/diasurgical/devilutionX/wiki), [testing the latest builds](#test-builds), spreading the word, or simply just hanging out on our [Discord server](https://discord.gg/devilutionx). # Mods We hope to provide a good starting point for mods. In addition to the full Devilution source code, we also provide modding tools. Check out the list of known [mods based on DevilutionX](https://github.com/diasurgical/devilutionX/wiki/Mods). # Test Builds If you want to help test the latest development version (make sure to back up your files, as these may contain bugs), you can fetch the test build artifact from one of the build servers: *Note: You must be logged into GitHub to download the attachments!* [![Linux x86_64](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64.yml?query=branch%3Amaster) [![Linux AArch64](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_aarch64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_aarch64.yml?query=branch%3Amaster) [![Linux x86](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86.yml?query=branch%3Amaster) [![Linux x86_64 SDL1](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64_SDL1.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Linux_x86_64_SDL1.yml?query=branch%3Amaster) [![macOS x86_64](https://github.com/diasurgical/devilutionX/actions/workflows/macOS_x86_64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/macOS_x86_64.yml?query=branch%3Amaster) [![Windows MinGW x64](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x64.yml?query=branch%3Amaster) [![Windows MinGW x86](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x86.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MinGW_x86.yml?query=branch%3Amaster) [![Windows MSVC x64](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Windows_MSVC_x64.yml?query=branch%3Amaster) [![Android](https://github.com/diasurgical/devilutionX/actions/workflows/Android.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/Android.yml?query=branch%3Amaster) [![iOS](https://github.com/diasurgical/devilutionX/actions/workflows/iOS.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/iOS.yml?query=branch%3Amaster) [![PS4](https://github.com/diasurgical/devilutionX/actions/workflows/PS4.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/PS4.yml?query=branch%3Amaster) [![Original Xbox](https://github.com/diasurgical/devilutionX/actions/workflows/xbox_nxdk.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/xbox_nxdk.yml?query=branch%3Amaster) [![Xbox One/Series](https://github.com/diasurgical/devilutionX/actions/workflows/xbox_one.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/xbox_one.yml?query=branch%3Amaster) [![Nintendo Switch](https://github.com/diasurgical/devilutionX/actions/workflows/switch.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/switch.yml) [![Sony PlayStation Vita](https://github.com/diasurgical/devilutionX/actions/workflows/vita.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/vita.yml) [![Nintendo 3DS](https://github.com/diasurgical/devilutionX/actions/workflows/3ds.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/3ds.yml) [![Amiga M68K](https://github.com/diasurgical/devilutionX/actions/workflows/amiga-m68k.yml/badge.svg)](https://github.com/diasurgical/devilutionX/actions/workflows/amiga-m68k.yml) # Building from Source Want to compile the program by yourself? Great! Simply follow the [build instructions](./docs/building.md). # Credits - The original Devilution project: [Devilution](https://github.com/diasurgical/devilution#credits) - [Everyone](https://github.com/diasurgical/devilutionX/graphs/contributors) who worked on Devilution/DevilutionX - [Nikolay Popov](https://www.instagram.com/nikolaypopovz/) for UI and graphics - [WiAParker](https://wiaparker.pl/projekty/diablo-hellfire/) for the Polish voice pack - And thanks to all who support the project, report bugs, and help spread the word ❤️ # Legal DevilutionX is made publicly available and released under the Sustainable Use License (see [LICENSE](LICENSE.md)). The source code in this repository is for non-commercial use only. If you use the source code, you may not charge others for access to it or any derivative work thereof. Diablo® - Copyright © 1996 Blizzard Entertainment, Inc. All rights reserved. Diablo and Blizzard Entertainment are trademarks or registered trademarks of Blizzard Entertainment, Inc. in the U.S. and/or other countries. DevilutionX and any of its maintainers are in no way associated with or endorsed by Blizzard Entertainment®. ================================================ FILE: Source/.clang-format ================================================ BasedOnStyle: webkit AlignTrailingComments: true AllowShortBlocksOnASingleLine: true AllowShortCaseLabelsOnASingleLine: true AllowShortFunctionsOnASingleLine: All AllowShortIfStatementsOnASingleLine: WithoutElse PointerAlignment: Right TabWidth: 4 UseTab: ForIndentation SortIncludes: true NamespaceIndentation: None FixNamespaceComments: true QualifierAlignment: Left ================================================ FILE: Source/.clang-tidy ================================================ --- # clang-tidy configuration # # clang-tidy can be run manually like this: # # run-clang-tidy -p build 'Source.*' # # To apply fixes suggested by clang-tidy, run: # # run-clang-tidy -p build -fix -format 'Source.*' # # To limit the run to certain checks: # # run-clang-tidy -checks='-*,modernize-use-nullptr' -p build 'Source.*' # # clang-tidy also has several IDE integrations listed here: # https://clang.llvm.org/extra/clang-tidy/Integrations.html # Enable most checks. # # Built-in checks: # https://clang.llvm.org/extra/clang-tidy/checks/list.html # # Exclusions: # # -modernize-avoid-c-arrays # We use C-arrays throughout, e.g. for `constexpr char []`. # `std::array` is not a replacement because its length is # not deduced. # # -modernize-use-trailing-return-type # A purely stylistic change that we do not want. # # -modernize-concat-nested-namespaces # -modernize-avoid-bind # Compatibility with older compilers. Checks: > -*, bugprone-*, cppcoreguidelines-pro-type-cstyle-cast, google-runtime-int, llvm-include-order, llvm-namespace-comment, misc-*, modernize-*, performance-*, portability-*, readability-*, -readability-identifier-length, -bugprone-easily-swappable-parameters, -readability-magic-numbers, -misc-non-private-member-variables-in-classes, -modernize-avoid-c-arrays, -modernize-use-trailing-return-type, -modernize-concat-nested-namespaces, -modernize-avoid-bind, -modernize-use-constraints HeaderFilterRegex: "^(Source|test)\\.h$" CheckOptions: - { key: readability-identifier-naming.NamespaceCase, value: lower_case } - { key: readability-identifier-naming.ClassCase, value: CamelCase } - { key: readability-identifier-naming.StructCase, value: CamelCase } - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } - { key: readability-identifier-naming.MethodCase, value: camelBack } - { key: readability-identifier-naming.FunctionCase, value: CamelCase } - { key: readability-identifier-naming.ParameterCase, value: camelBack } - { key: readability-identifier-naming.MemberCase, value: camelBack } - { key: readability-identifier-naming.VariableCase, value: camelBack } - { key: readability-identifier-naming.ClassMemberCase, value: lower_case } - { key: readability-identifier-naming.GlobalVariableCase, value: aNy_CasE } - { key: readability-identifier-naming.GlobalFunctionCase, value: aNy_CasE } - { key: readability-identifier-naming.ClassMemberSuffix, value: _ } - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } # Allow short if-statements without braces - { key: readability-braces-around-statements.ShortStatementLines, value: 3 } # Use fixed-width integer types instead of short, long and long long - { key: google-runtime-int.UnsignedTypePrefix, value: "std::uint" } - { key: google-runtime-int.SignedTypePrefix, value: "std::int" } - { key: google-runtime-int.TypeSuffix, value: "_t" } # `int8_t` aren't used as chars, disable misleading warning. - { key: bugprone-signed-char-misuse.CharTypdefsToIgnore, value: "std::int8_t" } - { key: readability-identifier-length.MinimumLoopCounterNameLength, value: 1 } # Ignore warnings for individual SDL headers, e.g. SDL_video.h, SDL_render.h - { key: misc-include-cleaner.IgnoreHeaders, value: 'SDL_.*\\.h' } ================================================ FILE: Source/CMakeLists.txt ================================================ include(Definitions) include(functions/devilutionx_library) include(functions/genex) set(libdevilutionx_SRCS appfat.cpp automap.cpp capture.cpp cursor.cpp dead.cpp debug.cpp diablo.cpp diablo_msg.cpp doom.cpp gamemenu.cpp gmenu.cpp help.cpp hwcursor.cpp interfac.cpp inv.cpp loadsave.cpp menu.cpp minitext.cpp missiles.cpp movie.cpp msg.cpp nthread.cpp pfile.cpp plrmsg.cpp portal.cpp restrict.cpp sync.cpp tmsg.cpp towners.cpp track.cpp control/control_chat.cpp control/control_chat_commands.cpp control/control_flasks.cpp control/control_gold.cpp control/control_infobox.cpp control/control_panel.cpp controls/axis_direction.cpp controls/controller_motion.cpp controls/controller.cpp controls/devices/joystick.cpp controls/devices/kbcontroller.cpp controls/game_controls.cpp controls/keymapper.cpp controls/menu_controls.cpp controls/modifier_hints.cpp controls/plrctrls.cpp DiabloUI/button.cpp DiabloUI/credits.cpp DiabloUI/credits_lines.cpp DiabloUI/diabloui.cpp DiabloUI/dialogs.cpp DiabloUI/hero/selhero.cpp DiabloUI/mainmenu.cpp DiabloUI/multi/selconn.cpp DiabloUI/multi/selgame.cpp DiabloUI/progress.cpp DiabloUI/scrollbar.cpp DiabloUI/selok.cpp DiabloUI/selstart.cpp DiabloUI/selyesno.cpp DiabloUI/settingsmenu.cpp DiabloUI/support_lines.cpp DiabloUI/title.cpp DiabloUI/text_input.cpp dvlnet/abstract_net.cpp dvlnet/base.cpp dvlnet/cdwrap.cpp dvlnet/frame_queue.cpp dvlnet/loopback.cpp dvlnet/packet.cpp engine/actor_position.cpp engine/animationinfo.cpp engine/backbuffer_state.cpp engine/dx.cpp engine/events.cpp engine/palette.cpp engine/sound_position.cpp engine/trn.cpp engine/render/automap_render.cpp engine/render/scrollrt.cpp items/validation.cpp levels/reencode_dun_cels.cpp levels/setmaps.cpp levels/themes.cpp levels/tile_properties.cpp levels/town.cpp levels/trigs.cpp lua/autocomplete.cpp lua/lua_event.cpp lua/lua_global.cpp lua/modules/audio.cpp lua/modules/hellfire.cpp lua/modules/dev.cpp lua/modules/dev/display.cpp lua/modules/dev/items.cpp lua/modules/dev/level.cpp lua/modules/dev/level/map.cpp lua/modules/dev/level/warp.cpp lua/modules/dev/monsters.cpp lua/modules/dev/player.cpp lua/modules/dev/player/gold.cpp lua/modules/dev/player/spells.cpp lua/modules/dev/player/stats.cpp lua/modules/dev/quests.cpp lua/modules/dev/search.cpp lua/modules/dev/towners.cpp lua/modules/floatingnumbers.cpp lua/modules/i18n.cpp lua/modules/items.cpp lua/modules/log.cpp lua/modules/monsters.cpp lua/modules/player.cpp lua/modules/render.cpp lua/modules/system.cpp lua/modules/towners.cpp lua/repl.cpp monsters/validation.cpp panels/charpanel.cpp panels/console.cpp panels/info_box.cpp panels/mainpanel.cpp panels/partypanel.cpp panels/spell_book.cpp panels/spell_icons.cpp panels/spell_list.cpp platform/locale.cpp portals/validation.cpp qol/autopickup.cpp qol/chatlog.cpp qol/floatingnumbers.cpp qol/itemlabels.cpp qol/monhealthbar.cpp qol/stash.cpp qol/xpbar.cpp quests/validation.cpp storm/storm_net.cpp storm/storm_svid.cpp tables/misdat.cpp tables/textdat.cpp tables/townerdat.cpp utils/display.cpp utils/language.cpp utils/sdl_bilinear_scale.cpp utils/sdl_thread.cpp utils/surface_to_clx.cpp utils/timer.cpp) # These files are responsible for most of the runtime in Debug mode. # Apply some optimizations to them even in Debug mode to get reasonable performance. # # They also perform better with -O2 rather than -O3 even in Release mode. set(_optimize_in_debug_srcs engine/render/clx_render.cpp engine/render/dun_render.cpp engine/render/text_render.cpp utils/cel_to_clx.cpp utils/cl2_to_clx.cpp utils/pcx_to_clx.cpp) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set_source_files_properties(${_optimize_in_debug_srcs} PROPERTIES COMPILE_OPTIONS "-O2;--param=max-vartrack-size=900000000") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set_source_files_properties(${_optimize_in_debug_srcs} PROPERTIES COMPILE_OPTIONS "-O2") endif() # We need to define all the object libraries first # because our workaround for transitive dependency support # requires targets to exist when calling `target_link_dependencies` # (see object_libraries.cmake). add_devilutionx_object_library(libdevilutionx_assets engine/assets.cpp ) target_link_dependencies(libdevilutionx_assets PUBLIC DevilutionX::SDL fmt::fmt tl libdevilutionx_headless_mode libdevilutionx_game_mode libdevilutionx_mpq libdevilutionx_paths libdevilutionx_sdl2_to_1_2_backports libdevilutionx_strings ${DEVILUTIONX_PLATFORM_ASSETS_LINK_LIBRARIES} ) add_devilutionx_object_library(libdevilutionx_cel_to_clx utils/cel_to_clx.cpp ) target_link_dependencies(libdevilutionx_cel_to_clx PRIVATE libdevilutionx_endian_write ) add_devilutionx_object_library(libdevilutionx_cl2_to_clx utils/cl2_to_clx.cpp ) target_link_dependencies(libdevilutionx_cl2_to_clx PRIVATE libdevilutionx_endian_write ) add_devilutionx_object_library(libdevilutionx_clx_render engine/render/clx_render.cpp ) target_link_dependencies(libdevilutionx_clx_render PUBLIC DevilutionX::SDL fmt::fmt libdevilutionx_light_render libdevilutionx_palette_blending libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_codec codec.cpp sha.cpp ) target_link_dependencies(libdevilutionx_codec PRIVATE DevilutionX::SDL libdevilutionx_log ) add_devilutionx_object_library(libdevilutionx_controller_buttons controls/controller_buttons.cpp ) target_link_dependencies(libdevilutionx_controller_buttons DevilutionX::SDL ) add_devilutionx_object_library(libdevilutionx_control_mode controls/control_mode.cpp ) target_link_dependencies(libdevilutionx_control_mode PUBLIC libdevilutionx_controller_buttons ) add_devilutionx_object_library(libdevilutionx_padmapper controls/padmapper.cpp ) target_link_dependencies(libdevilutionx_padmapper PUBLIC libdevilutionx_controller_buttons libdevilutionx_options ) add_devilutionx_object_library(libdevilutionx_palette_kd_tree utils/palette_kd_tree.cpp ) target_link_dependencies(libdevilutionx_palette_kd_tree PUBLIC DevilutionX::SDL libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_paths utils/paths.cpp ) target_link_dependencies(libdevilutionx_paths PUBLIC DevilutionX::SDL libdevilutionx_file_util libdevilutionx_log libdevilutionx_sdl2_to_1_2_backports ) add_devilutionx_object_library(libdevilutionx_pcx_to_clx utils/pcx_to_clx.cpp ) target_link_dependencies(libdevilutionx_pcx_to_clx PUBLIC DevilutionX::SDL libdevilutionx_assets libdevilutionx_endian_write ) add_devilutionx_object_library(libdevilutionx_primitive_render engine/render/primitive_render.cpp ) target_link_dependencies(libdevilutionx_primitive_render PUBLIC libdevilutionx_palette_blending libdevilutionx_surface ) add_devilutionx_object_library(libdevilutionx_crawl crawl.cpp ) target_link_dependencies(libdevilutionx_crawl PUBLIC tl ) add_devilutionx_object_library(libdevilutionx_direction engine/direction.cpp ) add_devilutionx_object_library(libdevilutionx_dun_render engine/render/dun_render.cpp ) target_link_libraries(libdevilutionx_dun_render PUBLIC DevilutionX::SDL libdevilutionx_light_render libdevilutionx_surface PRIVATE libdevilutionx_options ) add_library(libdevilutionx_endian_write INTERFACE) target_link_libraries(libdevilutionx_endian_write INTERFACE DevilutionX::SDL ) add_devilutionx_object_library(libdevilutionx_surface engine/surface.cpp ) target_link_dependencies(libdevilutionx_surface PUBLIC DevilutionX::SDL ) add_devilutionx_object_library(libdevilutionx_file_util utils/file_util.cpp ) target_link_dependencies(libdevilutionx_file_util PRIVATE DevilutionX::SDL libdevilutionx_log ${DEVILUTIONX_PLATFORM_FILE_UTIL_LINK_LIBRARIES} ) add_devilutionx_object_library(libdevilutionx_format_int utils/format_int.cpp ) target_link_dependencies(libdevilutionx_format_int PUBLIC libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_game_mode game_mode.cpp ) target_link_dependencies(libdevilutionx_game_mode PRIVATE tl magic_enum::magic_enum libdevilutionx_options ) add_devilutionx_object_library(libdevilutionx_gendung levels/crypt.cpp levels/drlg_l1.cpp levels/drlg_l2.cpp levels/drlg_l3.cpp levels/drlg_l4.cpp levels/gendung.cpp ) target_link_dependencies(libdevilutionx_gendung PUBLIC DevilutionX::SDL fmt::fmt tl libdevilutionx_assets libdevilutionx_items libdevilutionx_monster libdevilutionx_random ) add_devilutionx_object_library(libdevilutionx_headless_mode headless_mode.cpp ) add_devilutionx_object_library(libdevilutionx_init init.cpp ) target_link_dependencies(libdevilutionx_init PUBLIC magic_enum::magic_enum libdevilutionx_assets libdevilutionx_config libdevilutionx_mpq libdevilutionx_options ) add_devilutionx_object_library(libdevilutionx_load_cel engine/load_cel.cpp ) target_link_dependencies(libdevilutionx_load_cel PUBLIC tl PRIVATE libdevilutionx_strings ) if(SUPPORTS_MPQ) target_link_dependencies(libdevilutionx_load_cel PRIVATE libdevilutionx_mpq libdevilutionx_cel_to_clx ) else() target_link_dependencies(libdevilutionx_load_cel PRIVATE libdevilutionx_load_clx ) endif() add_devilutionx_object_library(libdevilutionx_load_cl2 engine/load_cl2.cpp ) target_link_dependencies(libdevilutionx_load_cl2 PUBLIC tl libdevilutionx_endian_write PRIVATE libdevilutionx_strings ) if(SUPPORTS_MPQ) target_link_dependencies(libdevilutionx_load_cl2 PUBLIC libdevilutionx_mpq libdevilutionx_cl2_to_clx ) else() target_link_dependencies(libdevilutionx_load_cl2 PRIVATE libdevilutionx_load_clx ) endif() add_devilutionx_object_library(libdevilutionx_load_clx engine/load_clx.cpp ) target_link_dependencies(libdevilutionx_load_clx PUBLIC tl PRIVATE libdevilutionx_assets ) add_devilutionx_object_library(libdevilutionx_load_pcx engine/load_pcx.cpp ) target_link_dependencies(libdevilutionx_load_pcx PRIVATE DevilutionX::SDL libdevilutionx_sdl2_to_1_2_backports libdevilutionx_log libdevilutionx_strings ) if(SUPPORTS_MPQ) target_link_dependencies(libdevilutionx_load_pcx PUBLIC libdevilutionx_assets libdevilutionx_pcx_to_clx ) else() target_link_dependencies(libdevilutionx_load_pcx PRIVATE libdevilutionx_load_clx ) endif() add_devilutionx_object_library(libdevilutionx_light_render engine/render/light_render.cpp ) add_devilutionx_object_library(libdevilutionx_lighting lighting.cpp ) target_link_dependencies(libdevilutionx_lighting PUBLIC DevilutionX::SDL fmt::fmt magic_enum::magic_enum tl unordered_dense::unordered_dense libdevilutionx_vision ) add_devilutionx_object_library(libdevilutionx_logged_fstream utils/logged_fstream.cpp ) target_link_dependencies(libdevilutionx_logged_fstream PUBLIC libdevilutionx_file_util libdevilutionx_log ) add_devilutionx_object_library(libdevilutionx_items tables/itemdat.cpp items.cpp ) target_link_dependencies(libdevilutionx_items PUBLIC DevilutionX::SDL sol2::sol2 tl libdevilutionx_headless_mode libdevilutionx_sound libdevilutionx_spells libdevilutionx_stores libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_ini utils/ini.cpp ) target_link_dependencies(libdevilutionx_ini PUBLIC fmt::fmt tl unordered_dense::unordered_dense libdevilutionx_strings libdevilutionx_utf8 ) # We use an INTERFACE library rather than an OBJECT library # because `libdevilutionx_log` does not have any sources. add_library(libdevilutionx_log INTERFACE) target_include_directories(libdevilutionx_log INTERFACE ${PROJECT_SOURCE_DIR}/Source) target_link_libraries(libdevilutionx_log INTERFACE DevilutionX::SDL fmt::fmt ) target_sources(libdevilutionx_log INTERFACE $) add_devilutionx_object_library(libdevilutionx_level_objects tables/objdat.cpp objects.cpp ) target_link_dependencies(libdevilutionx_level_objects PUBLIC DevilutionX::SDL unordered_dense::unordered_dense tl libdevilutionx_direction libdevilutionx_headless_mode libdevilutionx_monster libdevilutionx_options libdevilutionx_player libdevilutionx_random libdevilutionx_txtdata ) add_devilutionx_object_library(libdevilutionx_monster tables/monstdat.cpp monster.cpp ) target_link_dependencies(libdevilutionx_monster PUBLIC DevilutionX::SDL magic_enum::magic_enum sol2::sol2 tl unordered_dense::unordered_dense libdevilutionx_game_mode libdevilutionx_headless_mode libdevilutionx_sound libdevilutionx_txtdata PRIVATE libdevilutionx_cl2_to_clx ) add_devilutionx_object_library(libdevilutionx_palette_blending utils/palette_blending.cpp ) target_link_dependencies(libdevilutionx_palette_blending PUBLIC DevilutionX::SDL libdevilutionx_palette_kd_tree libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_parse_int utils/parse_int.cpp ) target_link_dependencies(libdevilutionx_parse_int PUBLIC tl ) if(SUPPORTS_MPQ) add_devilutionx_object_library(libdevilutionx_mpq mpq/mpq_common.cpp mpq/mpq_reader.cpp mpq/mpq_sdl_rwops.cpp mpq/mpq_writer.cpp ) target_link_dependencies(libdevilutionx_mpq PUBLIC DevilutionX::SDL fmt::fmt tl libmpq libdevilutionx_file_util libdevilutionx_logged_fstream libdevilutionx_pkware_encrypt libdevilutionx_strings ) else() add_library(libdevilutionx_mpq INTERFACE) endif() add_devilutionx_object_library(libdevilutionx_multiplayer multi.cpp pack.cpp ) target_link_dependencies(libdevilutionx_multiplayer PUBLIC libdevilutionx_config libdevilutionx_items ) add_devilutionx_object_library(libdevilutionx_options options.cpp ) target_link_dependencies(libdevilutionx_options PUBLIC DevilutionX::SDL fmt::fmt magic_enum::magic_enum tl unordered_dense::unordered_dense libdevilutionx_controller_buttons libdevilutionx_control_mode libdevilutionx_logged_fstream libdevilutionx_quick_messages libdevilutionx_strings libdevilutionx_ini ) add_devilutionx_object_library(libdevilutionx_pathfinding engine/path.cpp ) target_link_dependencies(libdevilutionx_pathfinding PUBLIC tl libdevilutionx_crawl libdevilutionx_direction ) if(SUPPORTS_MPQ OR NOT NONET) add_devilutionx_object_library(libdevilutionx_pkware_encrypt encrypt.cpp ) target_link_dependencies(libdevilutionx_pkware_encrypt PUBLIC DevilutionX::SDL PKWare ) else() add_library(libdevilutionx_pkware_encrypt INTERFACE) endif() add_devilutionx_object_library(libdevilutionx_player player.cpp tables/playerdat.cpp ) target_link_dependencies(libdevilutionx_player PUBLIC DevilutionX::SDL fmt::fmt magic_enum::magic_enum sol2::sol2 tl unordered_dense::unordered_dense libdevilutionx_game_mode PRIVATE libdevilutionx_load_cl2 libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_quests quests.cpp ) target_link_dependencies(libdevilutionx_quests PUBLIC libdevilutionx_surface libdevilutionx_gendung ) add_devilutionx_object_library(libdevilutionx_random engine/random.cpp ) add_devilutionx_object_library(libdevilutionx_quick_messages quick_messages.cpp ) add_devilutionx_object_library(libdevilutionx_spells tables/spelldat.cpp spells.cpp ) target_link_dependencies(libdevilutionx_spells PUBLIC tl libdevilutionx_player libdevilutionx_txtdata ) add_devilutionx_object_library(libdevilutionx_text_render engine/render/text_render.cpp ) target_link_dependencies(libdevilutionx_text_render PUBLIC libdevilutionx_surface PRIVATE fmt::fmt unordered_dense::unordered_dense libdevilutionx_clx_render libdevilutionx_game_mode libdevilutionx_load_cel libdevilutionx_load_clx libdevilutionx_load_pcx libdevilutionx_log libdevilutionx_primitive_render libdevilutionx_ticks libdevilutionx_utf8 ) add_devilutionx_object_library(libdevilutionx_ticks engine/ticks.cpp ) target_link_dependencies(libdevilutionx_ticks PRIVATE DevilutionX::SDL ) add_devilutionx_object_library(libdevilutionx_txtdata data/file.cpp data/parser.cpp data/record_reader.cpp data/value_reader.cpp ) target_link_dependencies(libdevilutionx_txtdata PUBLIC fmt::fmt tl libdevilutionx_assets libdevilutionx_parse_int libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_utf8 utils/utf8.cpp ) target_link_dependencies(libdevilutionx_utf8 PRIVATE SheenBidi::SheenBidi ) if(NOSOUND) add_devilutionx_object_library(libdevilutionx_sound effects_stubs.cpp engine/sound_stubs.cpp ) target_link_dependencies(libdevilutionx_sound PUBLIC DevilutionX::SDL fmt::fmt magic_enum::magic_enum tl unordered_dense::unordered_dense libdevilutionx_options libdevilutionx_random libdevilutionx_sdl2_to_1_2_backports ) else() add_devilutionx_object_library(libdevilutionx_sound effects.cpp engine/sound.cpp utils/soundsample.cpp ) if(USE_SDL3) target_link_dependencies(libdevilutionx_sound PUBLIC SDL3_mixer::SDL3_mixer ) else() target_sources(libdevilutionx_sound PRIVATE utils/push_aulib_decoder.cpp) target_link_dependencies(libdevilutionx_sound PUBLIC SDL_audiolib::SDL_audiolib ) endif() target_link_dependencies(libdevilutionx_sound PUBLIC DevilutionX::SDL fmt::fmt magic_enum::magic_enum tl unordered_dense::unordered_dense libdevilutionx_options libdevilutionx_random libdevilutionx_sdl2_to_1_2_backports ) endif() add_devilutionx_object_library(libdevilutionx_stores stores.cpp ) target_link_dependencies(libdevilutionx_stores PUBLIC DevilutionX::SDL fmt::fmt tl libdevilutionx_clx_render libdevilutionx_options libdevilutionx_sound libdevilutionx_strings ) add_devilutionx_object_library(libdevilutionx_strings utils/str_cat.cpp utils/str_case.cpp ) target_link_dependencies(libdevilutionx_strings PRIVATE fmt::fmt) add_devilutionx_object_library(libdevilutionx_utils_console utils/console.cpp ) add_devilutionx_object_library(libdevilutionx_vision vision.cpp ) target_link_dependencies(libdevilutionx_vision PUBLIC tl ) if(USE_SDL1) add_devilutionx_library(libdevilutionx_sdl2_to_1_2_backports STATIC utils/sdl2_to_1_2_backports.cpp ) target_link_dependencies(libdevilutionx_sdl2_to_1_2_backports PRIVATE libdevilutionx_strings libdevilutionx_utils_console ) target_link_libraries(DevilutionX::SDL INTERFACE libdevilutionx_sdl2_to_1_2_backports ) if(APPLE) enable_language(OBJC) target_sources(libdevilutionx_sdl2_to_1_2_backports PRIVATE platform/macos_sdl1/SDL_filesystem.m) target_link_libraries(libdevilutionx_sdl2_to_1_2_backports PRIVATE "-framework Foundation") endif() else() add_library(libdevilutionx_sdl2_to_1_2_backports INTERFACE) endif() if(IOS) list(APPEND libdevilutionx_SRCS platform/ios/ios_paths.m) endif() if(NOT DISABLE_DEMOMODE) list(APPEND libdevilutionx_SRCS engine/demomode.cpp) endif() if(NOT NONET) if(NOT DISABLE_TCP) list(APPEND libdevilutionx_SRCS dvlnet/tcp_client.cpp dvlnet/tcp_server.cpp) endif() if(NOT DISABLE_ZERO_TIER) list(APPEND libdevilutionx_SRCS dvlnet/protocol_zt.cpp dvlnet/zerotier_native.cpp dvlnet/zerotier_lwip.cpp) endif() endif() if(NOT USE_SDL1) list(APPEND libdevilutionx_SRCS controls/devices/game_controller.cpp controls/touch/event_handlers.cpp controls/touch/gamepad.cpp controls/touch/renderers.cpp) endif() if(DISCORD_INTEGRATION) list(APPEND libdevilutionx_SRCS discord/discord.cpp ) endif() if(SCREEN_READER_INTEGRATION) list(APPEND libdevilutionx_SRCS utils/screen_reader.cpp ) endif() if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PCX) list(APPEND libdevilutionx_SRCS utils/surface_to_pcx.cpp ) endif() if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG) add_devilutionx_object_library(libdevilutionx_surface_to_png utils/surface_to_png.cpp ) target_link_dependencies(libdevilutionx_surface_to_png PUBLIC DevilutionX::SDL tl libdevilutionx_surface ) if(USE_SDL3) target_link_dependencies(libdevilutionx_surface_to_png PUBLIC SDL3_image::SDL3_image) target_compile_definitions(libdevilutionx_surface_to_png INTERFACE USE_SDL3) else() target_link_dependencies(libdevilutionx_surface_to_png PUBLIC SDL2::SDL2_image) endif() endif() add_devilutionx_object_library(libdevilutionx ${libdevilutionx_SRCS}) target_include_directories(libdevilutionx PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_link_dependencies(libdevilutionx PUBLIC DevilutionX::SDL fmt::fmt libsmackerdec ${LUA_LIBRARIES} magic_enum::magic_enum sol2::sol2 tl unordered_dense::unordered_dense libdevilutionx_assets libdevilutionx_clx_render libdevilutionx_codec libdevilutionx_config libdevilutionx_controller_buttons libdevilutionx_control_mode libdevilutionx_crawl libdevilutionx_direction libdevilutionx_dun_render libdevilutionx_surface libdevilutionx_file_util libdevilutionx_format_int libdevilutionx_game_mode libdevilutionx_gendung libdevilutionx_headless_mode libdevilutionx_ini libdevilutionx_init libdevilutionx_items libdevilutionx_level_objects libdevilutionx_light_render libdevilutionx_lighting libdevilutionx_monster libdevilutionx_mpq libdevilutionx_multiplayer libdevilutionx_options libdevilutionx_padmapper libdevilutionx_palette_blending libdevilutionx_parse_int libdevilutionx_pathfinding libdevilutionx_pkware_encrypt libdevilutionx_player libdevilutionx_primitive_render libdevilutionx_quests libdevilutionx_quick_messages libdevilutionx_random libdevilutionx_sound libdevilutionx_spells libdevilutionx_stores libdevilutionx_strings libdevilutionx_text_render libdevilutionx_txtdata libdevilutionx_ticks libdevilutionx_utf8 libdevilutionx_utils_console ) if(NOT TARGET_PLATFORM STREQUAL "dos") target_link_dependencies(libdevilutionx PUBLIC Threads::Threads) endif() if(DEVILUTIONX_SCREENSHOT_FORMAT STREQUAL DEVILUTIONX_SCREENSHOT_FORMAT_PNG) target_link_dependencies(libdevilutionx PUBLIC libdevilutionx_surface_to_png) endif() # Use file GENERATE instead of configure_file because configure_file # does not support generator expressions. get_property(is_multi_config GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) add_library(libdevilutionx_config INTERFACE) if(is_multi_config) set(CONFIG_PATH $/config.h) target_include_directories(libdevilutionx_config INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/$) else() set(CONFIG_PATH config.h) target_include_directories(libdevilutionx_config INTERFACE ${CMAKE_CURRENT_BINARY_DIR}) endif() file(GENERATE OUTPUT ${CONFIG_PATH} CONTENT "#pragma once #define PROJECT_NAME \"${PROJECT_NAME}\" #define PROJECT_VERSION \"${PROJECT_VERSION_WITH_SUFFIX}\" #define PROJECT_VERSION_MAJOR ${PROJECT_VERSION_MAJOR} #define PROJECT_VERSION_MINOR ${PROJECT_VERSION_MINOR} #define PROJECT_VERSION_PATCH ${PROJECT_VERSION_PATCH} ") if(DISCORD_INTEGRATION) target_compile_definitions(libdevilutionx PRIVATE DISCORD) target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk) endif() if(SCREEN_READER_INTEGRATION) if(WIN32) target_compile_definitions(libdevilutionx PRIVATE Tolk) target_link_libraries(libdevilutionx PUBLIC Tolk) else() target_include_directories(libdevilutionx PUBLIC ${Speechd_INCLUDE_DIRS}) target_link_libraries(libdevilutionx PUBLIC speechd) endif() endif() if(USE_SDL1) # No need for SDL_image elseif(USE_SDL3) target_link_libraries(libdevilutionx PUBLIC SDL3_image::SDL3_image) else() target_link_libraries(libdevilutionx PUBLIC SDL2::SDL2_image) endif() if(NOT NONET) if(NOT DISABLE_TCP) target_link_libraries(libdevilutionx PUBLIC asio) endif() if(PACKET_ENCRYPTION) target_link_libraries(libdevilutionx PUBLIC sodium) endif() endif() if(NOT NOSOUND AND NOT USE_SDL3) target_link_libraries(libdevilutionx PUBLIC SDL_audiolib::SDL_audiolib) endif() if(NOT NONET AND NOT DISABLE_ZERO_TIER) if(NOT ANDROID) target_link_libraries(libdevilutionx PUBLIC zt-static) else() target_link_libraries(libdevilutionx PUBLIC zt-shared) endif() endif() foreach(path ${DEVILUTIONX_PLATFORM_SUBDIRECTORIES}) add_subdirectory(${path}) endforeach() target_link_dependencies(libdevilutionx PUBLIC ${DEVILUTIONX_PLATFORM_LINK_LIBRARIES}) if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9 AND NOT AMIGA) target_link_libraries(libdevilutionx PUBLIC stdc++fs) endif() elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") # Assumes libc++ (clang) is used rather than libstdc++ (gcc). # This is not always true but these are ancient clang versions anyway. if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 7) target_link_libraries(libdevilutionx PUBLIC c++experimental) elseif(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9) target_link_libraries(libdevilutionx PUBLIC c++fs) endif() endif() ================================================ FILE: Source/DiabloUI/button.cpp ================================================ #include "DiabloUI/button.h" #include #ifdef USE_SDL3 #include #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/load_pcx.hpp" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/surface.hpp" #include "utils/sdl_compat.h" namespace devilution { namespace { OptionalOwnedClxSpriteList ButtonSprites; } // namespace void LoadDialogButtonGraphics() { ButtonSprites = LoadOptionalClx("ui_art\\dvl_but_sml.clx"); if (!ButtonSprites) { ButtonSprites = LoadPcxSpriteList("ui_art\\but_sml", 15); } } void FreeDialogButtonGraphics() { ButtonSprites = std::nullopt; } ClxSprite ButtonSprite(bool pressed) { return (*ButtonSprites)[pressed ? 1 : 0]; } void RenderButton(const UiButton &button) { const Surface &out = Surface(DiabloUiSurface()).subregion(button.m_rect.x, button.m_rect.y, button.m_rect.w, button.m_rect.h); RenderClxSprite(out, ButtonSprite(button.IsPressed()), { 0, 0 }); Rectangle textRect { { 0, 0 }, { button.m_rect.w, button.m_rect.h } }; if (!button.IsPressed()) { --textRect.position.y; } DrawString(out, button.GetText(), textRect, { .flags = UiFlags::AlignCenter | UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite }); } bool HandleMouseEventButton(const SDL_Event &event, UiButton *button) { if (event.button.button != SDL_BUTTON_LEFT) return false; switch (event.type) { case SDL_EVENT_MOUSE_BUTTON_UP: if (button->IsPressed()) { button->Activate(); return true; } return false; case SDL_EVENT_MOUSE_BUTTON_DOWN: button->Press(); return true; default: return false; } } void HandleGlobalMouseUpButton(UiButton *button) { button->Release(); } } // namespace devilution ================================================ FILE: Source/DiabloUI/button.h ================================================ #pragma once #include "DiabloUI/ui_item.h" #include "engine/clx_sprite.hpp" namespace devilution { const Uint16 DialogButtonWidth = 110; const Uint16 DialogButtonHeight = 28; void LoadDialogButtonGraphics(); void FreeDialogButtonGraphics(); ClxSprite ButtonSprite(bool pressed); void RenderButton(const UiButton &button); bool HandleMouseEventButton(const SDL_Event &event, UiButton *button); void HandleGlobalMouseUpButton(UiButton *button); } // namespace devilution ================================================ FILE: Source/DiabloUI/credits.cpp ================================================ #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #else #include #endif #include "DiabloUI/credits_lines.h" #include "DiabloUI/diabloui.h" #include "DiabloUI/support_lines.h" #include "DiabloUI/ui_flags.hpp" #include "controls/input.h" #include "controls/menu_controls.h" #include "engine/load_clx.hpp" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/surface.hpp" #include "hwcursor.hpp" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/ui_fwd.h" namespace devilution { namespace { const SDL_Rect VIEWPORT = { 0, 114, 640, 251 }; const int LINE_H = 22; // The maximum number of visible lines is the number of whole lines // (VIEWPORT.h / LINE_H) rounded up, plus one extra line for when // a line is leaving the screen while another one is entering. #define MAX_VISIBLE_LINES ((VIEWPORT.h - 1) / LINE_H + 2) class CreditsRenderer { public: CreditsRenderer(const char *const *text, std::size_t textLines) { for (size_t i = 0; i < textLines; i++) { const std::string_view orgText = _(text[i]); uint16_t offset = 0; size_t indexFirstNotTab = 0; while (indexFirstNotTab < orgText.size() && orgText[indexFirstNotTab] == '\t') { offset += 40; indexFirstNotTab++; } const std::string paragraphs = WordWrapString(orgText.substr(indexFirstNotTab), 580 - offset, FontSizeDialog); size_t previous = 0; while (true) { const size_t next = paragraphs.find('\n', previous); linesToRender.emplace_back(LineContent { offset, paragraphs.substr(previous, next - previous) }); if (next == std::string::npos) break; previous = next + 1; } } ticks_begin_ = SDL_GetTicks(); prev_offset_y_ = 0; finished_ = false; } ~CreditsRenderer() { ArtBackgroundWidescreen = std::nullopt; ArtBackground = std::nullopt; } void Render(); [[nodiscard]] bool Finished() const { return finished_; } private: struct LineContent { uint16_t offset; std::string text; }; std::vector linesToRender; bool finished_; Uint32 ticks_begin_; int prev_offset_y_; }; void CreditsRenderer::Render() { const int offsetY = -VIEWPORT.h + ((SDL_GetTicks() - ticks_begin_) / 40); if (offsetY == prev_offset_y_) return; prev_offset_y_ = offsetY; SDL_FillSurfaceRect(DiabloUiSurface(), nullptr, 0); const Point uiPosition = GetUIRectangle().position; if (ArtBackgroundWidescreen) RenderClxSprite(Surface(DiabloUiSurface()), (*ArtBackgroundWidescreen)[0], uiPosition - Displacement { 320, 0 }); RenderClxSprite(Surface(DiabloUiSurface()), (*ArtBackground)[0], uiPosition); const std::size_t linesBegin = std::max(offsetY / LINE_H, 0); const std::size_t linesEnd = std::min(linesBegin + MAX_VISIBLE_LINES, linesToRender.size()); if (linesBegin >= linesEnd) { if (linesEnd == linesToRender.size()) finished_ = true; return; } SDL_Rect viewport = VIEWPORT; viewport.x += uiPosition.x; viewport.y += uiPosition.y; ScaleOutputRect(&viewport); // We use unscaled coordinates for calculation throughout. Sint16 destY = static_cast(uiPosition.y + VIEWPORT.y - (offsetY - linesBegin * LINE_H)); for (std::size_t i = linesBegin; i < linesEnd; ++i, destY += LINE_H) { const Sint16 destX = uiPosition.x + VIEWPORT.x + 31; auto &lineContent = linesToRender[i]; SDL_Rect dstRect = MakeSdlRect(destX + lineContent.offset, destY, 0, 0); ScaleOutputRect(&dstRect); dstRect.x -= viewport.x; dstRect.y -= viewport.y; const Surface &out = Surface(DiabloUiSurface(), viewport); DrawString(out, lineContent.text, Point { dstRect.x, dstRect.y }, { .flags = UiFlags::FontSizeDialog | UiFlags::ColorDialogWhite, .spacing = -1 }); } } bool TextDialog(const char *const *text, std::size_t textLines) { CreditsRenderer creditsRenderer(text, textLines); bool endMenu = false; if (IsHardwareCursor()) SetHardwareCursorVisible(false); SDL_Event event; do { creditsRenderer.Render(); UiFadeIn(); while (PollEvent(&event)) { switch (event.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: endMenu = true; break; default: for (const MenuAction menuAction : GetMenuActions(event)) { if (IsNoneOf(menuAction, MenuAction_BACK, MenuAction_SELECT)) continue; endMenu = true; break; } break; } UiHandleEvents(&event); } } while (!endMenu && !creditsRenderer.Finished()); return true; } } // namespace bool UiCreditsDialog() { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\creditsw.clx"); LoadBackgroundArt("ui_art\\credits"); return TextDialog(CreditLines, CreditLinesSize); } bool UiSupportDialog() { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\supportw.clx"); if (ArtBackgroundWidescreen.has_value()) { LoadBackgroundArt("ui_art\\support"); } else { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\creditsw.clx"); LoadBackgroundArt("ui_art\\credits"); } return TextDialog(SupportLines, SupportLinesSize); } } // namespace devilution ================================================ FILE: Source/DiabloUI/credits_lines.cpp ================================================ #include #include "DiabloUI/credits_lines.h" #include "utils/language.h" namespace devilution { const char *const CreditLines[] = { N_("Game Design"), " Blizzard North", "", N_("Senior Designers"), " Erich Schaefer, David Brevik", "", N_("Additional Design"), " Max Schaefer, Eric Sexton, Kenneth Williams", "", N_("Lead Programmer"), " David Brevik", "", N_("Senior Programmers"), " Richard Seis, Doron Gartner,", " Patrick Wyatt, Mike O'Brien", "", N_("Programming"), " Peter Brevik, Robin van der Wel, Jon Morin", "", N_("Special Guest Programmers"), " Collin Murray, Frank Pearce", "", N_("Battle.net Programming"), " Mike O'Brien, Mike Morhaime, Dan Liebgold", "", N_("Serial Communications Programming"), " Jeff Strain, Mike O'Brien", "", N_("Installer Programming"), " Bob Fitch, Patrick Wyatt", "", N_("Art Directors"), " Erich Schaefer, Samwise Didier", "", N_("Artwork"), " Michio Okamura, Eric Sexton, Ben Haas, Patrick Tougas,", " Kelly Johnson, Erich Schaefer, Max Schaefer, Hugh Tom", " Byrne, Roman Kenney, Samwise Didier, Dave Berggren,", " Micky Neilson, Eric Flannum, Justin Thavirat, Brian", " Sousa, Stu Rose, Nick Carpenter, Joeyray Hall", "", N_("Technical Artwork"), " Christopher Root, Mark Sutherland,", " Robert Djordjevich, Jeffrey Vaughn", "", N_("Cinematic Art Directors"), " Duane Stinnett, Matt Samia", "", N_("3D Cinematic Artwork"), " Duane Stinnett, Matt Samia, Rob McNaughton,", " Eric Flannum, Dave Pursley, Micky Neilson,", " Maxx Marshall, Trevor Jacobs, Brian Sousa,", " Samwise Didier, Ron Smorynski, Ben Haas, Patrick Tougas", "", N_("Cinematic Technical Artwork"), " Joeyray Hall ", "", N_("Executive Producer"), " Allen Adham", "", N_("Producer"), " Bill Roper", "", N_("Associate Producer"), " James Phinney", "", N_(/* TRANSLATORS: Keep Strike Team as Name */ "Diablo Strike Team"), " Allen Adham, Ron Millar, Mike O'Brien,", " James Phinney, Bill Roper, Patrick Wyatt", "", N_("Music"), " Matt Uelmen", "", N_("Sound Design"), " Matt Uelmen, Glenn Stafford", "", N_("Cinematic Music & Sound"), " Glenn Stafford, Matt Uelmen", "", N_("Voice Production, Direction & Casting"), " Mark Schwarz, Glenn Stafford, Bill Roper", "", N_("Script & Story"), " Erich Schaefer, Eric Sexton,", " Chris Metzen, Bill Roper", "", N_("Voice Editing"), " Glenn Stafford", "", N_("Voices"), " Michael Haley, Glynnis Talken, Michael Gough,", " Paul Eiding, Lani Minella, Steve Brodie, Bill Roper,", " Mark Schwarz, Glenn Stafford, Chris Metzen, Max Schaefer", "", N_("Recording Engineer"), " Robert Koenigsberg", "", N_("Manual Design & Layout"), " Peter Underwood, Jeffrey Vaughn,", " Chris Metzen, Bill Roper", "", N_("Manual Artwork"), " Samwise Didier, Chris Metzen,", " Roman Kenney, Maxx Marshall", "", N_("Provisional Director of QA (Lead Tester)"), " Shane Dabiri", "", N_("QA Assault Team (Testers)"), " Christopher Cross, Mike Givens, Dave Hale,", " Brian \"Doc\" Love, Hung Nguyen, Dean Shipley,", " Chris Sigaty, Ian Welke, Jamie Wiggs", "", N_("QA Special Ops Team (Compatibility Testers)"), " Adam Maxwell, Russell \"Rug\" Miller", "", N_("QA Artillery Support (Additional Testers) "), " Rich Alcazar, Mike Jones, Rev. Dan Moore, Matt Morris,", " Mark Pfeiffer, Harish \"Pepe the Enforcer\" Rao, Chris Millar", "", N_("QA Counterintelligence"), " Alan Dabiri, Jesse McReynolds, Walter Takata", "", N_(/* TRANSLATORS: A group of people */ "Order of Network Information Services"), " Robert Bridenbecker, Jason \"Brother Shaggy\" Schmit,", " Jamie \"Brother Gug\" Steigerwald, Richard Seis ", "", N_("Customer Support"), " John Schwartz, Vic Larson, Chad Chaudry, Mick Yanko,", " Brandon Riseling, Kirk Mahony, Tony Hardy, Richard", " Amtower, Collin Smith, Andrew Edwards, Roger Eberhart", "", N_("Sales"), " Jorge Rivero, Rob Beatie", "", N_("Dunsel"), " Alan Dabiri", "", N_("Mr. Dabiri's Background Vocalists"), " Karin Colenzo, Christina Cade,", " Kim Farrington, Melissa Edwards", "", N_("Public Relations"), " Linda Duttenhaver, Susan Wooley", "", N_("Marketing"), " John Patrick, Steve Huot, Neal Hubbard, Kathy Carter", "", N_("International Sales"), " Ralph Becker, Chris Yoshimura", "", N_("U.S. Sales"), " Todd Coyle, Danny Kearns", "", N_("Manufacturing"), " John Goodman, Tom Bryan", "", N_("Legal & Business"), " Paula Duffy, Eric Roeder, Paul Sams, Kenneth Williams", "", N_("Special Thanks To"), " Primenet, Earthlink, AOL, Compuserve, ATI, Matrox, STB, ", " Creative Labs, Logitech, U.S. Robotics, Gravis, Cyrix", "", N_("Thanks To"), " Ian Colquhoun, Rod Shean, Brian Fitzgerald, Sharon Schafer, Todd ", " Schafer, Richard and Sandra Schaefer, Rick Bowles, Greg Bogden, ", " Cindy Sievert, Brad Mason, Chuck Goldman, Karin Colenzo, Wendy ", " Brevik, Jennie Brevik, Hanna Carroll, Katie Newell, Karen Weiss, ", " Kurt Beaver, Stan McDonald, Brian Sexton, Michael Wan, Megan ", " Williams, Jessica Gensley, Beth Ann Parks, Brian Piltin, Johnathan ", " Root, Brett Supernaw, Sabeer Bhatia, Mark Rearick, Brad Mason, ", " Diane Kodama, Bernadette Sexton, Barbara Uhlmann, Patricia", " McDonald, Kris Kelley, Alissa Vaage, Denise Hernandez, Paula ", " White, Rachel Marie Hawley, Laura Gaber, Isaac Matarasso,", " Susan Stafford, Gretchen Witte, Jason Sailor, Eufemia Palomino, ", " Nathalie Didier, Nicole Welke, Dawn Caddel, Anna-Marcelle ", " Okamura, Amber Okamura, Megan Okamura Tracey McNaughton, ", " Trisha Ann Ochoa, Annie Strain, Dr. Gregory T. Street, Ray the Soda ", " Guy, Sam Raimi, A. Guinness Brewing Co., John Woo, Chow Yun Fat, ", " Jackie Chan,Proposition 215, Rumiko Takahashi, Bunchy & Mason, ", " The Friends of Stan, The Zoo Crew, Team Hamro, Brazil 2001, RUSH, ", " Cornholio, THE BROS., Dar, Emeril Lagasse, Tom Waits, Ice-Cube, ", " The Strike Team Deflectors, Tony Levin, Big Jim and the Twins, ", " Jag`rmeister, The Megasphere, Condor, The Allower, The Sunday ", " Night Group, Gravitar, Steinway Pianos, Round Table Pizza, The Poxy ", " Boggards, Urban Mystic & Co., Caffeine, Hunter Rose, Marly ", " mediums in the box, sweet Poteen, Dave Scarpitti, TheByter, Bim ", " Man, Krissann for color, Patty at Avalon Tattoo, James, Whaleboy, ", " Dunsel Training Institute, monkeys, Dob Bole, Rootes Group, Porshe, ", " Bongo, Scarlett, Apollo, The House of Mediocrity, Amelias, The King, ", " Saag and all the folks at Davidson", "", N_("In memory of"), " Lawrence and Barbara Williams", " David A. Hargrave", " Tony \"Xatre\" Collin", " Thomas H Sexton", "", N_("Very Special Thanks to"), " Bob and Jan Davidson", " Walter Forbes", "", " Synergistic Software", "", N_("General Manager"), " Bob Clardy", "", N_("Lead Programmer"), " Donald Tsang", "", N_("Software Engineering"), " Jim Edwards, Gary Powell, D'Andre Pritchett", "", N_("Art Director"), " Kirt Lemons", "", N_("Artists"), " Darren Lamb, Mike McMillen,", " Jason Robertson, Peter Watje", "", N_("Design"), " Kirt Lemons, Donald Tsang, Peter Watje", " Jim Edwards, Mike McMillen", "", N_("Additional Design"), " Michael Ormsby", "", N_("Sound Design, SFX & Audio Engineering"), " Craig Utterback", "", N_("Quality Assurance Lead"), " Mona Branham", "", N_("Testers"), " Chris Barker, Steve Chin, Derek Clardy, Conor Durdy,", " Amy Edwards, Greg Hightower, Tim Jensen, Jeremy Jones,", " John Lang, Daudi Pritchett, Doug Van Horne, Gordon Wilcox", "", N_("Manual"), " Suzanne David, Linda Westerfield,", " Ann Dickens Clardy, Kirt Lemons", "", N_("Voices"), " Jim Edwards, Paul Eiding, Eve Forward,", " Mackenzie Powell, Rodney Sherwood, Craig Utterback", "", "", N_(" Additional Work"), "", N_("Quest Text Writing"), " Eve Forward", "", N_("Thanks to"), " Trisha Durdy, Dustin Freeman, D'Andre Pritchett", "", "", "", N_(" Special Thanks to Blizzard Entertainment"), "", "", "", "", N_(" Sierra On-Line Inc. Northwest"), "", N_("Quality Assurance Manager"), " Gary Stevens", "", N_("Quality Assurance Lead Tester"), " Bernadette Pryor", "", N_("Main Testers"), " Erik Johnson, David Lee, Kristofer Hall", "", N_("Additional Testers"), " Cade Myers, Jeff Ingham, Phil Kuhlmey,", " Eric Gelfand, Mark Wyman, Torsten Reinl,", " Erinn Hamilton", "", N_("Product Marketing Manager"), " Eddie Ranchigoda", "", N_("Public Relations Manager"), " Eric Twelker", "", N_("Associate Product Manager"), " Joe Roth", "", N_("Thanks to"), " Cindy Vanous, Joshua Bentley, Robert Zimmerman, Robert Jerauld,", " Dennis Ham, Andrea Stone, Beth Quintana, Jennifer Keenan, Gary Brown,", " Ken Eaton, Matt Eslick, Kate Powell, Bryon Mcpherson, David Beetlestone", "", "", N_("The Ring of One Thousand"), " Andrew Abernathy, Christopher Abramo, David Adams, David ", " Adcox, Marko Agterberg, Cory Aiken, Judah Altaras, John ", " Alvarez, Jose Alvarez, Richard Amable, Alexander Amaral, ", " Scott Amis, Vincent Amoroso, Mark An, David Andersen, Jason ", " Andrew Abernathy, Christopher Abramo, David Adams, David ", " Adcox, Marko Agterberg, Cory Aiken, Judah Altaras, John ", " Alvarez, Jose Alvarez, Richard Amable, Alexander Amaral, ", " Scott Amis, Vincent Amoroso, Mark An, David Andersen, Jason ", " Andersen, Aaron Anderson, George Anderson, Matthew ", " Anderson, Michael Anderson, Sean Anderson, Robert Andrade, ", " Cerny Andre, Michael Andreev, Devin Angle, Brian Apple, ", " Brian Arbuthnot, Billy Arden, Dorian Arnold, Andre Arsenault, ", " Erik Asplund, Mark Assam, John Athey, Jason Attard, Jeff ", " Atwood, Ricky Au, Scott Avery, Rand Babcock, Steve Babel, ", " Raymond Bacalso, Ed Bachta, Steven Back, Scott Baeder, Alex ", " Baevski, Scott Bailey, Kenneth Baird, Thomas Baker, Todd ", " Bala, Jan Ball, Greg Baltz, Blake Baltzley, Doug Bambrick, Wes ", " Bangerter, Paul Barfuss, Chris Barghout, Dave Barnebey, Jon ", " Barnhart, Terje Barth, Nicole Baskin, Bernard Baylen, Ryan ", " Bear, Phil Bedard, Todd Belcher, Chip Bell, Erez Ben-Aharon, ", " Jonathan Bender, Nick Bennett, Ireney Berezniak, Ted Berg, ", " Gunnar Bergem, Russell Beuker, Ed Bickford, Stephen Biles, ", " John Billdt, Gerald Binder, John Bird, Hannah Blackerby, Tom ", " Blackerby, Clayton Blackwell, Thomas Blake, Shawn Blaszak, ", " Daniel Bliss, Fred Bliss, Jeff Bliss, Jon Blum, Rune Boersjoe, ", " Andrew Boggs, Dave Boisvenu, Joe Bolt, John Bonds, Jeff ", " Borenstein, Dorian Borin, Ed Boris, Bartholomew Botta, ", " Michael Boutin, Michael Boyd, Charles Boyer, Mike ", " Boyersmith, Michael Boyko, Eric Bradberry, John Brandstetter, ", " Ryan Breding, Paul Brinkmann, Patrick Briscoe, Scott Brisko, ", " Jeremy Britton, Adrian Broadhead, Glenn Brouwer, Joe Brown, ", " Sébastien Brulotte, Darrell Brunsch, William Bryan, Jason ", " Bucher, Chris Buchinger, Clayton Buckingham, John Buckles, ", " David Bugay, Ed Bujone, Erik Burgess, Gabriel Burkett, Chris ", " Burnes, Richard Butler, Jeffrey Bye, Dougall Campbell, Donnie ", " Cannon, Shane Cantrell, Connor Caple, Daniel Carey, James ", " Carlton, Michael Carmel, Mike Carpenter, Ronald Carruthers, ", " Phil Carter, Bryce Cartmill, Eric Cartwright, Anthony Caruso, ", " Lon Casey, Tim Caster, Aaron Chan, Otto Chan, Gene Chang, ", " Hsiao-Lung Chang, William Chang, George Chappel, Larry ", " Charbonneau, Troy Chase, Bruce Chen, Chun Hsien Chen, Sam ", " Chen, Tzu-Mainn Chen, Mike Cheng, Chris Chiapusio, Damien ", " Chiesa, Nick Chin, Nim Ching, Jonathan Chinn, Michael Chinn, ", " Philander Chiu, Thayne Christiansen, Philip Chui, Steve ", " Cintamani, Richard Ciordia, Colin Clark, Steve Clark, Samuel ", " Clements, Meredith Clifton, Jeff Cohen, Dale Colton, Dax ", " Combe, Matt Compton, Jacob Conklin, Richard Conn, Zac ", " Cook, Tim Coolong, Gregory Coomer, Dennis Cosgrove, Kelly ", " Couch, Andrew Coulter, Eric Coutinho, David Coutts, James ", " Craig, John Craig, Kazial Craig, John Crawford, Marcelo ", " Crespo, Orville Crews, Tim Cristy, Elmer Crosby III, Russell ", " Cullison, Ryan Cupples, Andrew Dagley, Steve Dallaire, Richard ", " Dalton, David Dandar, Pro Daulo, Rob Dautermann, Mike ", " Davies, Kalieb Davis, Marshall Davis, Ronald Davis, Danny De ", " Bie, Marc De Filippis, Myles Deighton, Kent Dejarnett, Anthony ", " Delarosa, Neil Dempster, Peter Denitto, Joaquim Dentz, Scott ", " Dewar, Anish Dhingra, Philip Dhingra, David Diaz, Stewart ", " Dicks, Brad Dietz, Josh Dietz, Colleen Diggins, Mike Ditchburn, ", " Eric Dittman, Allen Do, Huy Doan, Matthew Dolman, Antoine ", " Dongois, Eamonn Donohoe, Bill Dorell, Mark Dorison, Dan ", " Dorsett, Chris Dorsey, Jim Dosé, Willis Doss, Chin Du, William ", " Dubis, Timothy Duewell, Brandon Dunn, Andrew Durham, Don ", " Duvall, Kevin Dvojack, Daniel Eaton, Paul Eberting, Eric ", " Echelbarger, Lance Eddleman, Ben Eggers, David Eggum, John ", " Ehde, Brian Eikenberry, Patrick Elven, Peter Engdahl, Philip ", " Engdahl, Michael Ennis, Darren Eslinger, Eric Ezell, Darren ", " Falslev, Stephen Feather, Tony Fenn, Ben Ferguson, Mike ", " Fernandez, Gwendal Feuillet, Guy Fietz, Julian Figueroa, Dale ", " Fillpot, Stan Finchem, Michael Finley, Nick Fisher, William ", " Fisher, Mark Fitlin, Dave Flatt, Joel J. Flores, John Folkers, ", " Steven Forgie, Tom Forsythe, Matthew Foster, Scott Francis, ", " Jim Frank, Paulo Fraser, Glenn French, Kurt Frerichs, Chris ", " Frey, Mark Friedman, Charles Friedmann, Dan Friend, Kirk ", " Fry, Aaron Fu, Erik Gaalema, Brandon Gabbard, Phil Gagner, ", " Tommy Gannon, David Gappmayer, Chris Garrison, Tony ", " Garrison, David Gasca, Jeremy Gasser, Michael Geist, Michael ", " Genereux, Daniel Genovese, Josh Gerwin, Paul Gibson, William ", " Gilchrist, Gabriel Gils Carbo, Chad Glendenin, Ryan Glinski, ", " Dean Gobrecht, Andrew Goldfinch, David Goodman, Mark ", " Goodson, Matt Gordon, Frank Gorgenyi, Sean Gould, Perry ", " Goutsos, Ed Govednik, Michael Grayson, Chris Green, Justin ", " Grenier, Jeff Greulich, Don Grey, Rob Griesbeck, Don Griffes, ", " Kimberly Griffeth, Jay Grizzard, Don Gronlund, Joe Gross, ", " Troy Growden, Greg Guilford, David Gusovsky, Jeremy ", " Guthrie, Adam Gutierrez, James Guzicki, Matthew Haas, Matt ", " Hadley, Ryan Hagelstrom, Bobby Hagen, Ben Hall, Brian Hall, ", " Kris Hall, Calvin Hamilton, Kris Hamilton, Bo Hammil, Dave ", " Hans, Rick Hansen, Robert Harlan, Travis Harlan, Seth ", " Harman, Jeff Harris, Shawn Hartford, Adam Hartsell, Neil ", " Harvey, Ray Hayes, John Hein, Chris Heinonen, Christer ", " Helsing, Chris Hempel, Dustin Hempel, Mathieu Henaire, Matt ", " Henry, Chuck Herb, Michael Herron, Sage Herron, Thomas ", " Herschbach, Cliff Hicks, Nelson Hicks, Paul Hierling, William ", " Hiers, Mike Higdon, Tim Hildebrand, Casey Hinkle, Ryan ", " Hitchings, Wes Hix, Alan Ho, Jenson Ho, Alan Hoffman, Jeff ", " Hoffman, Eleanor Hoffmann, Steve Hogg, Richard Holler, Brian ", " Homolya, Wade Hone, Joe Horvath, Jeff Howe, Eric Hudson, ", " Glen Huey, Chris Hufnagel, Joshua Hughes, Melissa Hughes, ", " Arief Hujaya, Thomas Hulen, Ryan Hupp, Justin Hurst, Rick ", " Hutchins, Steve Iams, Mike Iarossi, Bjorn Idren, Johan Idrén, ", " Micah Imparato, Joe Ingersoll, David Ingram, Greg Ipp, Rodney ", " Irvin, Darin Isola, Justin Itoh, Mario Ivan, Fredrik Ivarsson, ", " Dax Jacobson, Michael Jacques, Stevens Jacques, Duane Jahnke, ", " William Jambrosek, Daniel Janick, Narciso Jaramillo, Neil ", " Jariwala, Harvie Jarriell, Scott Javadi, Joe Jenkins, Bart ", " Jennings, Paul Jennings, Julien Jenny, Jason Jensen, Martin ", " Jeremy, Mark Jeschke, Andy Johnson, James Johnson, Leigh ", " Johnson, Mark Johnson, Rupert Johnson, Clyde Jones, Michael ", " Jones, Tim Jordan, Ben Judy, Michael Kaae, Steve Kaczkowski, ", " Neville Kadwa, Brian Kaisner, Yoshihisa Kameyama, Michael ", " Kanemura, Daniel Kao, Eric Karabin, Ben Katz, Christopher ", " Kawamura, Erick Kayser, Craig Keddie, Kevin Kelley, Bryan ", " Kemp, Michael Kendrigan, Dan Kerber, Timothy Kerber, Tomi ", " Keski-Heikkilä, Greg Kettering, Nathan Kilber, Howard Kim, ", " Orrin Kinion, Jon Kirst, David Kitch, John Klingbeil, Neil ", " Klopfenstein, Kerry Knouse, David Knox, Said Kobeissi, Jeff ", " Koches, Hades Kong, Jeff Kong, Kevin Konkle, Steve Koon, ", " David Koontz, Dan Koopmann, Steve Koskela, Kuan Kou, ", " Cameron Kracke, Jensen Krage, York Kramer, Cedar Kraus, ", " Jason Kraus, Bobby Krimen, Melissa Krispli, Steven Krispli, ", " James Kruger, Charles Kubasta, Kimmo Kulonen, Frank ", " Lackaff, Michael Lacour, Matt Lake, Jason Landry, Hans Erik ", " Lange, Michael Laramee, Brad Lascelle, Pat Laschinger, Alan ", " Lau, Sean Laurence, Anthony Lavey, Jr., Gary Le, Huey Le, ", " Stephane Le Roy Audy, Lim Leandro, Charles Lee, Conroy Lee, ", " Mike Lee, Shih-Hang Lee, Jonathan Leipert, Jason Lemann, ", " Ron Lenzi, Mitchell Leon, Stephanie Lesniewski, Brendan ", " Lewis, Robert Lewis, Sam Liao, Tom Liem, Adam Ligas, Steven ", " Liggett, Roger Lilley, Benjamin Lim, Jeff Lindholm, Johnson ", " Linwood, David Litchman, Bruce Lithimane, William Liu, ", " Wilson Liu, Robert Lobdell, Chris Logan, Razvan Loghin, Jack ", " Loh, George Loo, Russell Love, Juan Loyola, Ricardo Lozano, ", " Mike Luban, Tim Luc, Henry Luciano, Dianne Ludwig, Charles ", " Lueras, Derek Lung, Phong Ly, Scott MacGillivray, Dave Mack, ", " Alec Mak, Steve Mamayek, Michael Mancini, Daniel Mann, ", " Michael Mann, Chris Manofsky, Abdullah Marafie, Nicholas ", " Marcy, Piompino Mariano, Bob Marius, Trey Marshall, Dane ", " Martin, Gregg Martin, Renard Martin, Rich Martin, Scott ", " Martin, Thomas Martin, Jon Masters, Christopher Mathews, ", " Jay Mathis, Marc Matthews, Chris Mazur, Doug McBride, ", " Mackey McCandlish, Robin McCollum, Steven McCombie, ", " Andy McConnell, Michael McCourt, Bill McCoy, Doug ", " McCracken, Michael McDeed, Robert McDonald, Steve ", " McEachron, Craig McGee, Ryan McKenzie, Michael McKeown, ", " Daniel McMahon, Colin McMillan, Ian McWilliam, Mark ", " McWilliams, Khann Mean, Bryan Meason, Kenneth Medley, ", " Jeff Meek, John Mehr, Christopher Mende, Brian Mendenhall, ", " Peter Mengel, Michael Mersic, Mike Messom, Don Metcalf, ", " Gary Metzker, Scott Meyer, Joseph Michaud, Andrew Mielke, ", " Travis Mikalson, Troy Milburn, Ike Miller, Ronnie Miller, Sean ", " Miller, Steve Miller, Arthur Min, David Minniti, Brenda ", " Mirsberger, Bill Misek, David Mitchell, Joseph Mobley, Robert ", " Mollard, Will Mooar, Curtis Moore, Matthew Moore, Al ", " Morales, Ryan Moran, Lance Mortensen, Karel Mrazek, Ward ", " Mullee, William Munoz, Kirk Munro, Craig Murray, Shawn P. ", " Murray, Travis Murray, Michael Mushrush, Tom Mustaine, ", " David Myers, Joseph Myett, Morgan Najar, Kenta Nakamura, ", " Damian Nastri, Joshua Naumann, Nick Navarro, Douglas ", " Neitzel, Arnold Ng, Anthony Nguyen, Steve Nguyen, Joseph ", " Nicholas, Charles Nickolaus, Jon Nisbet, Patrick Nomee, David ", " Norling-Christensen, Bobby Norton, Joseph Nottingham, Frank ", " O'Connor, Jon Oden, David Oester, Lavern Ogden, Zach ", " Oglesby, Lucas Oldfield, Toby Olsson, Aaron Ondek, Sean ", " O'Neill, John Orlando, Samuel Orlando, Donovan Orloski, ", " David Pai, Nikolas Paldan, David Palek, John Palmieri, Anthony ", " Palmisano, Sanjay Pandit, Jesse Park, Alex Parker, Jimmy ", " Pasher, Lukasz Paszek, Andy Patterson, William Pelletier, ", " Duane Pemberton, Ivan Pemic, Kelly Pendergast, Mike ", " Penezich, Jon Penk, Willie Penley, Ron Penna, Matthew ", " Pennington, Kevin Pereira, Ross Perez, Ken Perkins, Brian ", " Peterik, Kelly Peterson, Chris Phillips, Rod Pickett, Cameron ", " Pierce, Reuben Pierce, Tim Pilger, Billy Pippin, Brad Plank, ", " Brian Plant, Craig Platt, David Plunkett, Michael Politi, Albert ", " Portillo, Brian Powell, David Powell, Franklin Powers Jr., Alan ", " Precourt, Michael Pronchick, Julian Quintana, Justin Radziej, ", " Steven Rajewski, Shawn Rawles, Ian Reardon, Marc Reed, Ric ", " Reichelt, Judd Reiffin, David Reilly, Garry Reisky, Drew ", " Ressler, Robert Reynolds, Walter Reynolds, Michael Rice, Ian ", " Richards, James Richards, Raymond Richmond, Dustin Riggs, ", " Keith Riskey, Brian Ro, Scott Roberts, Jorge Rodriguez, Chad ", " Rogers, Clint Rogers, Robert Rogers, Steve Rogers, Ethan Roots, ", " Ron Roque, William Ross, Sebastian Rossi, Jeff Rostis, Ben ", " Roth, Demar Roth, Rich Rouse, Oleg Rovner, Jonathan Roy, ", " Drew Rozema, Mike Ruggles, Mathias Russ, James Russell, Jim ", " Rutledge, James Rutter, Dave Ryder, Chris Salvadras, Anders ", " Samnerud, Nick Sanders, Jakob Sandgren, Joe Sapinsky, Tyler ", " Sargent, Jonas Saunders, Mark Savage, Scott Sawyer, Robert ", " Scanlon, Trevor Schaben, Aaron Schmidt, Chris Schmidt, Greg ", " Schultz, Nicholas Schumacher, Scott Schumacher, Kevin Scott, ", " Rastislav Seffer, Robert Seidler, Corey Sellers, Justin Sellers, ", " Marc Senecal, George Shannon, Ian Sheffield, Anoop Shekar, ", " Sandeep Shekar, Kevin Shelton, Leon Shephard, Eric Shepperd, ", " Jeffrey Shneidman, Samuel Shockey, Mark Shoemaker, Mike ", " Shupe, Sean Sibbet, Brian Sidharta, Jimmy Sieben, Eric ", " Siemens, William Silva, Jody Simpson, Jatinder Singh, Sonia ", " Siu, Omar Skarsvaag, Tom Skiba, Carl Skow, David Skuse, ", " Robert Slifka, Brent Smith, C. Eric Smith, Jared Smith, Jeffrey ", " Smith, Owen Smith, Shannon Smith, Steven Smith, Edward Smola, ", " Matthew Sneep, Eric Snow, Brad Sobel, Jean-Pierre Solignac, Rasmus ", " Sørensen, Andrew Sorg, Poppy Southcott, Ross Specter, Erik Spencer, ", " Keith Spencer, Chris Springer, Erikson Squier, Dean St. Onge, Stewart ", " Stanfield, John Stanley, Terrence Staton, Benjamin Stein, James ", " Steiner, David Steyer, William Stickney, Chris Stiff, James Stofer, ", " Norm Storch, Patrick Stovall, Brandon Sturgeon, Sean Stutler, Anne ", " Sukprasert, Jamal Sullivan, Bruce Sully, Jay Sung, Park Sung ", " Joon, Stein Sunnarvik, Nathan Surginer, Robert Swaringen, Lee ", " Sweeney, David Szabo, Scott Taft, Christian Takvam, Patrick ", " Talaska, Tadashi Tamaki, Simon Tan, Mark Taraba, Khon-", " Whey Tay, John Taylor, Keith Taylor, Donald Temean, Phillip ", " Tesar, Pete Thao, Brian Thomas, Keith Thomas, Speed Thomas, ", " Miles Thorpe, Paul Thurrott, Mike Timbol, Nicholas Timmins, ", " Tom Tobin, Robert Towster, Hien Tran, Timothy Traviss, Toby ", " Traylor, Tony Treadwell, George Tremoulis, Paul Trinh, Thanh ", " Trinh, Chris Tristan, Brad Truswell, Jason Tryon, Mike Tu, ", " Gernel Tuazon, Eric Tuggle, Mike Turnbull, Lyle Ubben, ", " Amilcar Ubiera, Robert Ulozas, Arie Upton, Mark Van Noy, ", " Matthew Van Sickler, Jake Vantlin, Tony Vasquez, Brady ", " Vauclin, Gianpiero Vecchi, Chad Verrall, Chris Vicente, Brett ", " Vickers, David Vickery, Jonathan Vilante, Simon Vince, Ben ", " Vinson, David Voelkert, Paul Vogt, Nicholas Voorhies, Robert ", " Vreeland, Mike Vrooman, Rick Vuong, Brian Wachhaus, Todd ", " Wachhaus, Caine Wade, Mathew Wadstein, Kenneth Wagenius, ", " Trevor Wagner, John Wagstaff, Asad Wahid, Richard ", " Wahlberg, Helio Wakasugui, Richard Walker, Wilkins Walker, ", " Matthew Wallace, Daniel Walsh, Joel Walters, Andrew Waltz, ", " Tom Wang, Tony Wang, Jay Ward, Jonathan Ward, John ", " Warner, Mark Warren, Matt Washer, Mike Watson, Sean ", " Wattles, Mike Wayne, Christian Wehba, Benjamin Wei, Richard ", " Weight, Cary Wells, David Wenck, Bill Werring, Leonard ", " Wesley, Marlon West, Mikael Westerbacka, Brian Wharry, ", " Chris White, Chris White, Jeremy White, Greg Whitlock, Gary ", " Widener, Marty Wilfried, Israel Wilkinson, Michael Willams, ", " Derek Williams, Sean Willson, Nitzan Wilnai, Jim Wilson, ", " Karsten Wilson, William Wilt, Tim Winn, Brian Winzeler, Matt ", " Wise, Lee Wissmiller, Brendan Wolfe, Daniel Wolpert, Felix ", " Wong, Power Wong, Tony Wong, Tim Wood, Timothy Wood, ", " Jeremy Woods, Michael Woods, Bill Wright, Keith Wright, ", " Patrick Wu, Gang Xie, Robert Yao, Sun Lim Yap, Stephen Yau, ", " Erek Yedwabnick, Christopher Yee, Nick Yee, Juan Yip, David ", " Young, Rob Young, Seth Young, Alex Yu, Terry Zahn, Jia-Ning ", " Zhang, Eric Zieg, Jordan Zielin, Clint Zimmerman, Matt Zinke ", "", "", "", "", N_(" No souls were sold in the making of this game."), "", "", " ", " ", " " }; const std::size_t CreditLinesSize = sizeof(CreditLines) / sizeof(CreditLines[0]); } // namespace devilution ================================================ FILE: Source/DiabloUI/credits_lines.h ================================================ #include namespace devilution { extern const char *const CreditLines[]; extern const std::size_t CreditLinesSize; } // namespace devilution ================================================ FILE: Source/DiabloUI/diabloui.cpp ================================================ #include "DiabloUI/diabloui.h" #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #else #include #endif #include #include "DiabloUI/button.h" #include "DiabloUI/scrollbar.h" #include "DiabloUI/text_input.hpp" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "appfat.h" #include "controls/control_mode.hpp" #include "controls/controller.h" #include "controls/input.h" #include "controls/menu_controls.h" #include "diablo.h" #include "discord/discord.h" #include "effects.h" #include "engine/clx_sprite.hpp" #include "engine/dx.h" #include "engine/load_pcx.hpp" #include "engine/palette.h" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/sound.h" #include "engine/surface.hpp" #include "engine/ticks.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "init.hpp" #include "options.h" #include "player.h" #include "sound_effect_enums.h" #include "tables/playerdat.hpp" #include "utils/algorithm/container.hpp" #include "utils/display.h" #include "utils/enum_traits.h" #include "utils/is_of.hpp" #include "utils/screen_reader.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/str_cat.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" #ifdef __SWITCH__ // for virtual keyboard on Switch #include "platform/switch/keyboard.h" #endif #ifdef __vita__ // for virtual keyboard on Vita #include "platform/vita/keyboard.h" #endif #ifdef __3DS__ // for virtual keyboard on 3DS #include "platform/ctr/keyboard.h" #endif namespace devilution { OptionalOwnedClxSpriteList ArtLogo; OptionalOwnedClxSpriteList DifficultyIndicator; std::array ArtFocus; OptionalOwnedClxSpriteList ArtBackgroundWidescreen; OptionalOwnedClxSpriteList ArtBackground; OptionalOwnedClxSpriteList ArtCursor; std::size_t SelectedItem = 0; namespace { OptionalOwnedClxSpriteList ArtHero; std::vector ArtHeroPortraitOrder; std::vector ArtHeroOverrides; std::size_t SelectedItemMax; std::size_t ListViewportSize = 1; std::size_t listOffset = 0; void (*gfnListFocus)(size_t value); void (*gfnListSelect)(size_t value); void (*gfnListEsc)(); void (*gfnFullscreen)(); bool (*gfnListYesNo)(); std::vector gUiItems; UiList *gUiList = nullptr; bool UiItemsWraps; std::optional UiTextInputState; bool allowEmptyTextInput = false; constexpr Uint32 ListDoubleClickTimeMs = 500; std::size_t lastListClickIndex = static_cast(-1); Uint32 lastListClickTicks = 0; struct ScrollBarState { bool upArrowPressed; bool downArrowPressed; ScrollBarState() { upArrowPressed = false; downArrowPressed = false; } } scrollBarState; void AdjustListOffset(std::size_t itemIndex) { if (itemIndex >= listOffset + ListViewportSize) listOffset = itemIndex - (ListViewportSize - 1); if (itemIndex < listOffset) listOffset = itemIndex; } uint32_t fadeTc; int fadeValue; void StartUiFadeIn() { fadeValue = 0; fadeTc = 0; } void UiUpdateFadePalette() { if (fadeValue == 256) return; if (fadeValue == 0 && fadeTc == 0) { // Start the fade-in. fadeTc = SDL_GetTicks(); fadeValue = 0; BlackPalette(); // We can skip hardware cursor update for fade level 0 (everything is black). return; } const int prevFadeValue = fadeValue; fadeValue = static_cast((SDL_GetTicks() - fadeTc) / 2.083); // 32 frames @ 60hz if (fadeValue == prevFadeValue) return; if (fadeValue >= 256) { // Finish the fade-in: fadeValue = 256; fadeTc = 0; ApplyGlobalBrightness(system_palette.data(), logical_palette.data()); SystemPaletteUpdated(); if (IsHardwareCursor()) ReinitializeHardwareCursor(); return; } SDL_Color palette[256]; ApplyGlobalBrightness(palette, logical_palette.data()); ApplyFadeLevel(fadeValue, system_palette.data(), palette); SystemPaletteUpdated(); if (IsHardwareCursor()) ReinitializeHardwareCursor(); } } // namespace bool IsTextInputActive() { return UiTextInputState.has_value(); } void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector> &items, bool itemsWraps, void (*fnFullscreen)(), bool (*fnYesNo)(), size_t selectedItem /*= 0*/) { SelectedItem = selectedItem; SelectedItemMax = 0; ListViewportSize = 0; gfnListFocus = fnFocus; gfnListSelect = fnSelect; gfnListEsc = fnEsc; gfnFullscreen = fnFullscreen; gfnListYesNo = fnYesNo; gUiItems.clear(); for (const auto &item : items) gUiItems.push_back(item.get()); UiItemsWraps = itemsWraps; listOffset = 0; if (fnFocus != nullptr) fnFocus(selectedItem); #ifndef __SWITCH__ SDLC_StopTextInput(ghMainWnd); // input is enabled by default #endif UiScrollbar *uiScrollbar = nullptr; for (const auto &item : items) { if (item->IsType(UiType::Edit)) { auto *pItemUIEdit = static_cast(item.get()); SDL_SetTextInputArea(ghMainWnd, &item->m_rect, 0); allowEmptyTextInput = pItemUIEdit->m_allowEmpty; #ifdef __SWITCH__ switch_start_text_input(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_max_length); #elif defined(__vita__) vita_start_text_input(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_max_length); #elif defined(__3DS__) ctr_vkbdInput(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_value, pItemUIEdit->m_max_length); #else SDLC_StartTextInput(ghMainWnd); #endif UiTextInputState.emplace(TextInputState::Options { .value = pItemUIEdit->m_value, .cursor = &pItemUIEdit->m_cursor, .maxLength = pItemUIEdit->m_max_length, }); } else if (item->IsType(UiType::List)) { auto *uiList = static_cast(item.get()); SelectedItemMax = std::max(uiList->m_vecItems.size() - 1, static_cast(0)); ListViewportSize = uiList->viewportSize; gUiList = uiList; if (selectedItem <= SelectedItemMax && HasAnyOf(uiList->GetItem(selectedItem)->uiFlags, UiFlags::NeedsNextElement)) AdjustListOffset(selectedItem + 1); SpeakText(uiList->GetItem(selectedItem)->m_text); } else if (item->IsType(UiType::Scrollbar)) { uiScrollbar = static_cast(item.get()); } } AdjustListOffset(selectedItem); if (uiScrollbar != nullptr) { if (ListViewportSize >= static_cast(SelectedItemMax + 1)) { uiScrollbar->Hide(); } else { uiScrollbar->Show(); } } } void UiRenderListItems() { UiRenderItems(gUiItems); } void UiInitList_clear() { SelectedItem = 0; SelectedItemMax = 0; ListViewportSize = 1; gfnListFocus = nullptr; gfnListSelect = nullptr; gfnListEsc = nullptr; gfnFullscreen = nullptr; gfnListYesNo = nullptr; gUiList = nullptr; gUiItems.clear(); UiItemsWraps = false; } void UiPlayMoveSound() { effects_play_sound(SfxID::MenuMove); } void UiPlaySelectSound() { effects_play_sound(SfxID::MenuSelect); } namespace { void UiFocus(std::size_t itemIndex, bool checkUp, bool ignoreItemsWraps = false) { if (SelectedItem == itemIndex) return; AdjustListOffset(itemIndex); const auto *pItem = gUiList->GetItem(itemIndex); while (HasAnyOf(pItem->uiFlags, UiFlags::ElementHidden | UiFlags::ElementDisabled)) { if (checkUp) { if (itemIndex > 0) itemIndex -= 1; else if (UiItemsWraps && !ignoreItemsWraps) itemIndex = SelectedItemMax; else checkUp = false; } else { if (itemIndex < SelectedItemMax) itemIndex += 1; else if (UiItemsWraps && !ignoreItemsWraps) itemIndex = 0; else checkUp = true; } pItem = gUiList->GetItem(itemIndex); } SpeakText(pItem->m_text); if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement)) AdjustListOffset(itemIndex + 1); AdjustListOffset(itemIndex); SelectedItem = itemIndex; UiPlayMoveSound(); if (gfnListFocus != nullptr) gfnListFocus(itemIndex); } void UiFocusUp() { if (SelectedItem > 0) UiFocus(SelectedItem - 1, true); else if (UiItemsWraps) UiFocus(SelectedItemMax, true); } void UiFocusDown() { if (SelectedItem < SelectedItemMax) UiFocus(SelectedItem + 1, false); else if (UiItemsWraps) UiFocus(0, false); } // UiFocusPageUp/Down mimics the slightly weird behaviour of actual Diablo. void UiFocusPageUp() { if (listOffset == 0) { UiFocus(0, true, true); } else { const std::size_t relpos = SelectedItem - listOffset; std::size_t prevPageStart = SelectedItem - relpos; if (prevPageStart >= ListViewportSize) prevPageStart -= ListViewportSize; else prevPageStart = 0; AdjustListOffset(prevPageStart); UiFocus(listOffset + relpos, true, true); } } void UiFocusPageDown() { if (listOffset + ListViewportSize > static_cast(SelectedItemMax)) { UiFocus(SelectedItemMax, false, true); } else { const std::size_t relpos = SelectedItem - listOffset; std::size_t nextPageEnd = SelectedItem + (ListViewportSize - relpos - 1); if (nextPageEnd + ListViewportSize <= static_cast(SelectedItemMax)) nextPageEnd += ListViewportSize; else nextPageEnd = SelectedItemMax; AdjustListOffset(nextPageEnd); UiFocus(listOffset + relpos, false, true); } } bool HandleMenuAction(MenuAction menuAction) { switch (menuAction) { case MenuAction_SELECT: UiFocusNavigationSelect(); return true; case MenuAction_UP: UiFocusUp(); return true; case MenuAction_DOWN: UiFocusDown(); return true; case MenuAction_PAGE_UP: UiFocusPageUp(); return true; case MenuAction_PAGE_DOWN: UiFocusPageDown(); return true; case MenuAction_DELETE: UiFocusNavigationYesNo(); return true; case MenuAction_BACK: if (gfnListEsc == nullptr) return false; UiFocusNavigationEsc(); return true; default: return false; } } void UiOnBackgroundChange() { StartUiFadeIn(); if (IsHardwareCursorEnabled() && ArtCursor && ControlDevice == ControlTypes::KeyboardAndMouse && GetCurrentCursorInfo().type() != CursorType::UserInterface) { SetHardwareCursor(CursorInfo::UserInterfaceCursor()); } // It may take some time to get to the first `UiFadeIn()` call from here // if there is non-trivial initialization work, such as loading the list // of single-player characters. // // Black out the screen immediately to make it appear more smooth. SDL_FillSurfaceRect(DiabloUiSurface(), nullptr, 0); if (DiabloUiSurface() == PalSurface) BltFast(nullptr, nullptr); RenderPresent(); } void UiFocusNavigation(SDL_Event *event) { switch (event->type) { case SDL_EVENT_KEY_UP: case SDL_EVENT_MOUSE_BUTTON_UP: case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_JOYSTICK_BUTTON_UP: case SDL_EVENT_JOYSTICK_AXIS_MOTION: case SDL_EVENT_JOYSTICK_BALL_MOTION: case SDL_EVENT_JOYSTICK_HAT_MOTION: #ifndef USE_SDL1 case SDL_EVENT_MOUSE_WHEEL: case SDL_EVENT_FINGER_UP: case SDL_EVENT_FINGER_MOTION: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_AXIS_MOTION: #endif #ifdef USE_SDL3 case SDL_EVENT_WINDOW_EXPOSED: #else #ifndef USE_SDL1 case SDL_WINDOWEVENT: #endif case SDL_SYSWMEVENT: #endif mainmenu_restart_repintro(); break; default: break; } bool menuActionHandled = false; for (const MenuAction menuAction : GetMenuActions(*event)) menuActionHandled |= HandleMenuAction(menuAction); if (menuActionHandled) return; #ifndef USE_SDL1 if (event->type == SDL_EVENT_MOUSE_WHEEL) { if (SDLC_EventWheelIntY(*event) > 0) { UiFocusUp(); } else if (SDLC_EventWheelIntY(*event) < 0) { UiFocusDown(); } return; } #else if (event->type == SDL_MOUSEBUTTONDOWN) { switch (event->button.button) { case SDL_BUTTON_WHEELUP: UiFocusUp(); return; case SDL_BUTTON_WHEELDOWN: UiFocusDown(); return; } } #endif if (UiTextInputState.has_value() && HandleTextInputEvent(*event, *UiTextInputState)) { return; } if (IsAnyOf(event->type, SDL_EVENT_MOUSE_BUTTON_DOWN, SDL_EVENT_MOUSE_BUTTON_UP) && UiItemMouseEvents(event, gUiItems)) { return; } } } // namespace void UiHandleEvents(SDL_Event *event) { if (event->type == SDL_EVENT_MOUSE_MOTION) { MousePosition = { SDLC_EventMotionIntX(*event), SDLC_EventMotionIntY(*event) }; return; } if (event->type == SDL_EVENT_KEY_DOWN && SDLC_EventKey(*event) == SDLK_RETURN) { const auto *state = SDLC_GetKeyState(); if (state[SDLC_KEYSTATE_LALT] != 0 || state[SDLC_KEYSTATE_RALT] != 0) { GetOptions().Graphics.fullscreen.SetValue(!IsFullScreen()); SaveOptions(); if (gfnFullscreen != nullptr) gfnFullscreen(); return; } } if (event->type == SDL_EVENT_QUIT) { diablo_quit(0); } #ifndef USE_SDL1 HandleControllerAddedOrRemovedEvent(*event); #ifdef USE_SDL3 switch (event->type) { case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_EXPOSED: case SDL_EVENT_WINDOW_RESTORED: gbActive = true; break; case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_MINIMIZED: gbActive = false; break; case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: DoReinitializeHardwareCursor(); break; case SDL_EVENT_WINDOW_FOCUS_LOST: if (*GetOptions().Gameplay.pauseOnFocusLoss) music_mute(); break; case SDL_EVENT_WINDOW_FOCUS_GAINED: if (*GetOptions().Gameplay.pauseOnFocusLoss) diablo_focus_unpause(); break; default: break; } #else if (event->type == SDL_WINDOWEVENT) { if (IsAnyOf(event->window.event, SDL_WINDOWEVENT_SHOWN, SDL_WINDOWEVENT_EXPOSED, SDL_WINDOWEVENT_RESTORED)) { gbActive = true; } else if (IsAnyOf(event->window.event, SDL_WINDOWEVENT_HIDDEN, SDL_WINDOWEVENT_MINIMIZED)) { gbActive = false; } else if (event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { // We reinitialize immediately (by calling `DoReinitializeHardwareCursor` instead of `ReinitializeHardwareCursor`) // because the cursor's Enabled state may have changed, resulting in changes to visibility. // // For example, if the previous size was too large for a hardware cursor then it was invisible // but may now become visible. DoReinitializeHardwareCursor(); } else if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST && *GetOptions().Gameplay.pauseOnFocusLoss) { music_mute(); } else if (event->window.event == SDL_WINDOWEVENT_FOCUS_GAINED && *GetOptions().Gameplay.pauseOnFocusLoss) { diablo_focus_unpause(); } } #endif #else if (event->type == SDL_ACTIVEEVENT && (event->active.state & SDL_APPINPUTFOCUS) != 0) { if (event->active.gain == 0) music_mute(); else diablo_focus_unpause(); } #endif } void UiFocusNavigationSelect() { UiPlaySelectSound(); if (UiTextInputState.has_value()) { if (!allowEmptyTextInput && UiTextInputState->empty()) { return; } #ifndef __SWITCH__ SDLC_StopTextInput(ghMainWnd); #endif UiTextInputState = std::nullopt; } if (gfnListSelect != nullptr) gfnListSelect(SelectedItem); } void UiFocusNavigationEsc() { UiPlaySelectSound(); if (UiTextInputState.has_value()) { #ifndef __SWITCH__ SDLC_StopTextInput(ghMainWnd); #endif UiTextInputState = std::nullopt; } if (gfnListEsc != nullptr) gfnListEsc(); } void UiFocusNavigationYesNo() { if (gfnListYesNo == nullptr) return; if (gfnListYesNo()) UiPlaySelectSound(); } namespace { bool IsInsideRect(const SDL_Event &event, const SDL_Rect &rect) { const SDL_Point point = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; return SDLC_PointInRect(&point, &rect); } void LoadHeros() { constexpr unsigned PortraitHeight = 76; ArtHero = LoadPcxSpriteList("ui_art\\heros", -static_cast(PortraitHeight)); if (!ArtHero) return; const uint16_t numPortraits = ClxSpriteList { *ArtHero }.numSprites(); ArtHeroPortraitOrder.resize(GetNumPlayerClasses() + 1); for (size_t i = 0; i < GetNumPlayerClasses(); ++i) { const PlayerData &playerClassData = GetPlayerDataForClass(static_cast(i)); ArtHeroPortraitOrder[i] = playerClassData.portrait; } ArtHeroPortraitOrder.back() = 3; if (numPortraits >= 6) { ArtHeroPortraitOrder.back() = 5; } ArtHeroOverrides.resize(GetNumPlayerClasses() + 1); for (size_t i = 0; i <= GetNumPlayerClasses(); ++i) { char portraitPath[18]; *BufCopy(portraitPath, "ui_art\\hero", i) = '\0'; ArtHeroOverrides[i] = LoadPcx(portraitPath, /*transparentColor=*/std::nullopt, /*outPalette=*/nullptr, /*logError=*/false); } } void LoadUiGFX() { ArtLogo = LoadPcxSpriteList("ui_art\\hf_logo2", /*numFrames=*/16, /*transparentColor=*/0, nullptr, false); if (!ArtLogo.has_value()) { ArtLogo = LoadPcxSpriteList("ui_art\\smlogo", /*numFrames=*/15, /*transparentColor=*/250); } DifficultyIndicator = LoadPcx("ui_art\\r1_gry", /*transparentColor=*/0); ArtFocus[FOCUS_SMALL] = LoadPcxSpriteList("ui_art\\focus16", /*numFrames=*/8, /*transparentColor=*/250); ArtFocus[FOCUS_MED] = LoadPcxSpriteList("ui_art\\focus", /*numFrames=*/8, /*transparentColor=*/250); ArtFocus[FOCUS_BIG] = LoadPcxSpriteList("ui_art\\focus42", /*numFrames=*/8, /*transparentColor=*/250); ArtCursor = LoadPcx("ui_art\\cursor", /*transparentColor=*/0); LoadHeros(); } } // namespace ClxSprite UiGetHeroDialogSprite(size_t heroClassIndex) { return ArtHeroOverrides[heroClassIndex] ? (*ArtHeroOverrides[heroClassIndex])[0] : (*ArtHero)[ArtHeroPortraitOrder[heroClassIndex]]; } void UnloadUiGFX() { ArtHero = std::nullopt; for (OptionalOwnedClxSpriteList &override : ArtHeroOverrides) override = std::nullopt; ArtCursor = std::nullopt; for (auto &art : ArtFocus) art = std::nullopt; ArtLogo = std::nullopt; DifficultyIndicator = std::nullopt; } void UiInitialize() { LoadUiGFX(); if (ArtCursor) { if (!SDLC_HideCursor()) ErrSdl(); } } void UiDestroy() { UnloadFonts(); UnloadUiGFX(); } bool UiValidPlayerName(std::string_view name) { if (name.empty()) return false; // Currently only allow saving PlayerNameLength bytes as a player name, so if the name is too long we'd have to truncate it. // That said the input buffer is only 16 bytes long... if (name.size() > PlayerNameLength) return false; if (name.find_first_of(",<>%&\\\"?*#/: ") != name.npos) return false; // Only basic latin alphabet is supported for multiplayer characters to avoid rendering issues for players who do // not have fonts.mpq installed if (!c_all_of(name, IsBasicLatin)) return false; const std::string_view bannedNames[] = { "gvdl", "dvou", "tiju", "cjudi", "bttipmf", "ojhhfs", "cmj{{bse", "benjo", }; std::string buffer { name }; for (char &character : buffer) character++; const std::string_view tempName { buffer }; for (const std::string_view bannedName : bannedNames) { if (tempName.find(bannedName) != tempName.npos) return false; } return true; } Sint16 GetCenterOffset(Sint16 w, Sint16 bw) { if (bw == 0) { bw = gnScreenWidth; } return (bw - w) / 2; } void UiLoadDefaultPalette() { LoadPalette("ui_art\\diablo.pal"); UpdateSystemPalette(logical_palette); } bool UiLoadBlackBackground() { ArtBackground = std::nullopt; UiLoadDefaultPalette(); UiOnBackgroundChange(); return true; } void LoadBackgroundArt(const char *pszFile, int frames) { ArtBackground = std::nullopt; ArtBackground = LoadPcxSpriteList(pszFile, static_cast(frames), /*transparentColor=*/std::nullopt, logical_palette.data()); if (!ArtBackground) return; UpdateSystemPalette(logical_palette); UiOnBackgroundChange(); } void UiAddBackground(std::vector> *vecDialog) { const SDL_Rect rect = MakeSdlRect(0, GetUIRectangle().position.y, 0, 0); if (ArtBackgroundWidescreen) { vecDialog->push_back(std::make_unique((*ArtBackgroundWidescreen)[0], rect, UiFlags::AlignCenter)); } if (ArtBackground) { vecDialog->push_back(std::make_unique((*ArtBackground)[0], rect, UiFlags::AlignCenter)); } } void UiAddLogo(std::vector> *vecDialog, int y) { vecDialog->push_back(std::make_unique( *ArtLogo, MakeSdlRect(0, y, 0, 0), UiFlags::AlignCenter)); } void UiFadeIn() { if (HeadlessMode) return; UiUpdateFadePalette(); if (DiabloUiSurface() == PalSurface) { BltFast(nullptr, nullptr); } RenderPresent(); } namespace { ClxSpriteList GetListSelectorSprites(int itemHeight) { int size; if (itemHeight >= 42) { size = FOCUS_BIG; } else if (itemHeight >= 30) { size = FOCUS_MED; } else { size = FOCUS_SMALL; } return *ArtFocus[size]; } } // namespace void DrawSelector(const SDL_Rect &rect) { const ClxSpriteList sprites = GetListSelectorSprites(rect.h); const ClxSprite sprite = sprites[GetAnimationFrame(sprites.numSprites())]; // TODO FOCUS_MED appears higher than the box const int y = rect.y + ((rect.h - static_cast(sprite.height())) / 2); const Surface &out = Surface(DiabloUiSurface()); RenderClxSprite(out, sprite, { rect.x, y }); RenderClxSprite(out, sprite, { rect.x + rect.w - sprite.width(), y }); } void UiClearScreen() { if (!ArtBackground || gnScreenWidth > (*ArtBackground)[0].width() || gnScreenHeight > (*ArtBackground)[0].height()) { SDL_FillSurfaceRect(DiabloUiSurface(), nullptr, 0); } } void UiPollAndRender(std::optional> eventHandler) { SDL_Event event; while (PollEvent(&event)) { if (eventHandler && (*eventHandler)(event)) continue; if (!SDLC_ConvertEventToRenderCoordinates(renderer, &event)) { LogWarn(LogCategory::Application, "SDL_ConvertEventToRenderCoordinates: {}", SDL_GetError()); SDL_ClearError(); } UiFocusNavigation(&event); UiHandleEvents(&event); } HandleMenuAction(GetMenuHeldUpDownAction()); UiRenderListItems(); DrawMouse(); UiFadeIn(); // Must happen after at least one call to `UiFadeIn` with non-zero fadeValue. // `UiFadeIn` reinitializes the hardware cursor only for fadeValue > 0. if (IsHardwareCursor() && fadeValue != 0) SetHardwareCursorVisible(ControlDevice == ControlTypes::KeyboardAndMouse); #ifdef __3DS__ // Keyboard blocks until input is finished // so defer until after render and fade-in ctr_vkbdFlush(); #endif discord_manager::UpdateMenu(); } namespace { void Render(const UiText &uiText) { const Surface &out = Surface(DiabloUiSurface()); DrawString(out, uiText.GetText(), MakeRectangle(uiText.m_rect), { .flags = uiText.GetFlags() | UiFlags::FontSizeDialog }); } void Render(const UiArtText &uiArtText) { const Surface &out = Surface(DiabloUiSurface()); DrawString(out, uiArtText.GetText(), MakeRectangle(uiArtText.m_rect), { .flags = uiArtText.GetFlags(), .spacing = uiArtText.GetSpacing(), .lineHeight = uiArtText.GetLineHeight() }); } void Render(const UiImageClx &uiImage) { const ClxSprite sprite = uiImage.sprite(); int x = uiImage.m_rect.x; if (uiImage.isCentered()) { x += GetCenterOffset(sprite.width(), uiImage.m_rect.w); } RenderClxSprite(Surface(DiabloUiSurface()), sprite, { x, uiImage.m_rect.y }); } void Render(const UiImageAnimatedClx &uiImage) { const ClxSprite sprite = uiImage.sprite(GetAnimationFrame(uiImage.numFrames())); int x = uiImage.m_rect.x; if (uiImage.isCentered()) { x += GetCenterOffset(sprite.width(), uiImage.m_rect.w); } RenderClxSprite(Surface(DiabloUiSurface()), sprite, { x, uiImage.m_rect.y }); } void Render(const UiArtTextButton &uiButton) { const Surface &out = Surface(DiabloUiSurface()); DrawString(out, uiButton.GetText(), MakeRectangle(uiButton.m_rect), { .flags = uiButton.GetFlags() }); } void Render(const UiList &uiList) { const Surface &out = Surface(DiabloUiSurface()); for (std::size_t i = listOffset; i < uiList.m_vecItems.size() && (i - listOffset) < ListViewportSize; ++i) { const SDL_Rect rect = uiList.itemRect(static_cast(i - listOffset)); const UiListItem &item = *uiList.GetItem(i); if (i == SelectedItem) DrawSelector(rect); const Rectangle rectangle = MakeRectangle(rect).inset( Displacement(GetListSelectorSprites(rect.h)[0].width(), 0)); const UiFlags uiFlags = uiList.GetFlags() | item.uiFlags; const GameFontTables fontSize = GetFontSizeFromUiFlags(uiFlags); std::string_view text = item.m_text.str(); while (GetLineWidth(text, fontSize, 1) > rectangle.size.width) { text = std::string_view(text.data(), FindLastUtf8Symbols(text)); } if (item.args.empty()) { DrawString(out, text, rectangle, { .flags = uiFlags, .spacing = uiList.GetSpacing() }); } else { DrawStringWithColors(out, text, item.args, rectangle, { .flags = uiFlags, .spacing = uiList.GetSpacing() }); } } } void Render(const UiScrollbar &uiSb) { const Surface out = Surface(DiabloUiSurface()); // Bar background (tiled): { const int bgY = uiSb.m_rect.y + uiSb.m_arrow[0].height(); const int bgH = DownArrowRect(uiSb).y - bgY; const Surface backgroundOut = out.subregion(uiSb.m_rect.x, bgY, ScrollBarBgWidth, bgH); int y = 0; while (y < bgH) { RenderClxSprite(backgroundOut, uiSb.m_bg, { 0, y }); y += uiSb.m_bg.height(); } } // Arrows: { const SDL_Rect rect = UpArrowRect(uiSb); const auto frame = static_cast(scrollBarState.upArrowPressed ? ScrollBarArrowFrame_UP_ACTIVE : ScrollBarArrowFrame_UP); RenderClxSprite(out.subregion(rect.x, 0, ScrollBarArrowWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); } { const SDL_Rect rect = DownArrowRect(uiSb); const auto frame = static_cast(scrollBarState.downArrowPressed ? ScrollBarArrowFrame_DOWN_ACTIVE : ScrollBarArrowFrame_DOWN); RenderClxSprite(out.subregion(rect.x, 0, ScrollBarArrowWidth, out.h()), uiSb.m_arrow[frame], { 0, rect.y }); } // Thumb: if (SelectedItemMax > 0) { const SDL_Rect rect = ThumbRect(uiSb, SelectedItem, SelectedItemMax + 1); RenderClxSprite(out, uiSb.m_thumb, { rect.x, rect.y }); } } void Render(const UiEdit &uiEdit) { DrawSelector(uiEdit.m_rect); // To simulate padding we inset the region used to draw text in an edit control const Rectangle rect = MakeRectangle(uiEdit.m_rect).inset({ 43, 1 }); const Surface &out = Surface(DiabloUiSurface()); DrawString(out, uiEdit.m_value, rect, { .flags = uiEdit.GetFlags(), .cursorPosition = static_cast(uiEdit.m_cursor.position), .highlightRange = { static_cast(uiEdit.m_cursor.selection.begin), static_cast(uiEdit.m_cursor.selection.end) }, .highlightColor = 126, }); } bool HandleMouseEventArtTextButton(const SDL_Event &event, const UiArtTextButton *uiButton) { if (event.type != SDL_EVENT_MOUSE_BUTTON_UP || event.button.button != SDL_BUTTON_LEFT) { return false; } uiButton->Activate(); return true; } bool HandleMouseEventList(const SDL_Event &event, UiList *uiList) { if (event.button.button != SDL_BUTTON_LEFT) return false; if (!IsAnyOf(event.type, SDL_EVENT_MOUSE_BUTTON_UP, SDL_EVENT_MOUSE_BUTTON_DOWN)) { return false; } std::size_t index = uiList->indexAt(event.button.y); if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { uiList->Press(index); return true; } if (event.type == SDL_EVENT_MOUSE_BUTTON_UP && !uiList->IsPressed(index)) { return false; } index += listOffset; const bool hasFocusCallback = gfnListFocus != nullptr; const Uint32 ticksNow = SDL_GetTicks(); const bool recentlyClickedSameItem = hasFocusCallback && lastListClickIndex == index && ticksNow - lastListClickTicks <= ListDoubleClickTimeMs; #ifndef USE_SDL1 const bool sdlReportedDoubleClick = event.button.clicks >= 2; #else const bool sdlReportedDoubleClick = false; #endif const bool doubleClicked = recentlyClickedSameItem || sdlReportedDoubleClick; lastListClickIndex = index; lastListClickTicks = ticksNow; if (hasFocusCallback && SelectedItem != index) { UiFocus(index, true, false); return true; } if (hasFocusCallback && !doubleClicked) { return true; } if (HasAnyOf(uiList->GetItem(index)->uiFlags, UiFlags::ElementHidden | UiFlags::ElementDisabled)) return false; SelectedItem = index; UiFocusNavigationSelect(); return true; } bool HandleMouseEventScrollBar(const SDL_Event &event, const UiScrollbar *uiSb) { if (event.button.button != SDL_BUTTON_LEFT) return false; if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { if (scrollBarState.upArrowPressed && IsInsideRect(event, UpArrowRect(*uiSb))) { UiFocusUp(); return true; } if (scrollBarState.downArrowPressed && IsInsideRect(event, DownArrowRect(*uiSb))) { UiFocusDown(); return true; } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { if (IsInsideRect(event, BarRect(*uiSb))) { // Scroll up or down based on thumb position. const SDL_Rect thumbRect = ThumbRect(*uiSb, SelectedItem, SelectedItemMax + 1); if (event.button.y < thumbRect.y) { UiFocusPageUp(); } else if (event.button.y > thumbRect.y + thumbRect.h) { UiFocusPageDown(); } return true; } if (IsInsideRect(event, UpArrowRect(*uiSb))) { scrollBarState.upArrowPressed = true; return true; } if (IsInsideRect(event, DownArrowRect(*uiSb))) { scrollBarState.downArrowPressed = true; return true; } } return false; } bool HandleMouseEvent(const SDL_Event &event, UiItemBase *item) { if (item->IsNotInteractive() || !IsInsideRect(event, item->m_rect)) return false; switch (item->GetType()) { case UiType::ArtTextButton: return HandleMouseEventArtTextButton(event, static_cast(item)); case UiType::Button: return HandleMouseEventButton(event, static_cast(item)); case UiType::List: return HandleMouseEventList(event, static_cast(item)); case UiType::Scrollbar: return HandleMouseEventScrollBar(event, static_cast(item)); default: return false; } } } // namespace void UiRenderItem(const UiItemBase &item) { if (item.IsHidden()) return; switch (item.GetType()) { case UiType::Text: Render(static_cast(item)); break; case UiType::ArtText: Render(static_cast(item)); break; case UiType::ImageClx: Render(static_cast(item)); break; case UiType::ImageAnimatedClx: Render(static_cast(item)); break; case UiType::ArtTextButton: Render(static_cast(item)); break; case UiType::Button: RenderButton(static_cast(item)); break; case UiType::List: Render(static_cast(item)); break; case UiType::Scrollbar: Render(static_cast(item)); break; case UiType::Edit: Render(static_cast(item)); break; } } void UiRenderItems(const std::vector &items) { for (const UiItemBase *item : items) UiRenderItem(*item); } void UiRenderItems(const std::vector> &items) { for (const std::unique_ptr &item : items) UiRenderItem(*item); } bool UiItemMouseEvents(SDL_Event *event, const std::vector &items) { if (items.empty()) { return false; } bool handled = false; for (const auto &item : items) { if (HandleMouseEvent(*event, item)) { handled = true; break; } } if (event->type == SDL_EVENT_MOUSE_BUTTON_UP && event->button.button == SDL_BUTTON_LEFT) { scrollBarState.downArrowPressed = scrollBarState.upArrowPressed = false; for (const auto &item : items) { if (item->IsType(UiType::Button)) { HandleGlobalMouseUpButton(static_cast(item)); } else if (item->IsType(UiType::List)) { static_cast(item)->Release(); } } } return handled; } bool UiItemMouseEvents(SDL_Event *event, const std::vector> &items) { if (items.empty()) { return false; } bool handled = false; for (const auto &item : items) { if (HandleMouseEvent(*event, item.get())) { handled = true; break; } } if (event->type == SDL_EVENT_MOUSE_BUTTON_UP && event->button.button == SDL_BUTTON_LEFT) { scrollBarState.downArrowPressed = scrollBarState.upArrowPressed = false; for (const auto &item : items) { if (item->IsType(UiType::Button)) { HandleGlobalMouseUpButton(static_cast(item.get())); } else if (item->IsType(UiType::List)) { static_cast(item.get())->Release(); } } } return handled; } void DrawMouse() { if (ControlDevice != ControlTypes::KeyboardAndMouse || IsHardwareCursor() || !ArtCursor) return; RenderClxSprite(Surface(DiabloUiSurface()), (*ArtCursor)[0], MousePosition); } } // namespace devilution ================================================ FILE: Source/DiabloUI/diabloui.h ================================================ #pragma once #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "DiabloUI/ui_item.h" #include "engine/clx_sprite.hpp" #include "engine/load_pcx.hpp" // IWYU pragma: export #include "player.h" #include "utils/display.h" namespace devilution { extern std::size_t SelectedItem; bool IsTextInputActive(); enum _artFocus : uint8_t { FOCUS_SMALL, FOCUS_MED, FOCUS_BIG, }; enum _mainmenu_selections : uint8_t { MAINMENU_NONE, MAINMENU_SINGLE_PLAYER, MAINMENU_MULTIPLAYER, MAINMENU_SHOW_SUPPORT, MAINMENU_SETTINGS, MAINMENU_SHOW_CREDITS, MAINMENU_EXIT_DIABLO, MAINMENU_ATTRACT_MODE, }; enum _selhero_selections : uint8_t { SELHERO_NEW_DUNGEON, SELHERO_CONTINUE, SELHERO_CONNECT, SELHERO_PREVIOUS, }; struct _uidefaultstats { uint16_t strength; uint16_t magic; uint16_t dexterity; uint16_t vitality; }; struct _uiheroinfo { uint32_t saveNumber; char name[16]; uint8_t level; HeroClass heroclass; uint8_t herorank; uint16_t strength; uint16_t magic; uint16_t dexterity; uint16_t vitality; bool hassaved; bool spawned; }; extern OptionalOwnedClxSpriteList ArtLogo; extern OptionalOwnedClxSpriteList DifficultyIndicator; extern std::array ArtFocus; extern OptionalOwnedClxSpriteList ArtBackgroundWidescreen; extern OptionalOwnedClxSpriteList ArtBackground; extern OptionalOwnedClxSpriteList ArtCursor; extern bool (*gfnHeroInfo)(bool (*fninfofunc)(_uiheroinfo *)); inline SDL_Surface *DiabloUiSurface() { return PalSurface; } void UiDestroy(); void UiTitleDialog(); void UnloadUiGFX(); void UiInitialize(); bool UiValidPlayerName(std::string_view name); /* check */ void UiSelHeroMultDialog(bool (*fninfo)(bool (*fninfofunc)(_uiheroinfo *)), bool (*fncreate)(_uiheroinfo *), bool (*fnremove)(_uiheroinfo *), void (*fnstats)(HeroClass, _uidefaultstats *), _selhero_selections *dlgresult, uint32_t *saveNumber); void UiSelHeroSingDialog(bool (*fninfo)(bool (*fninfofunc)(_uiheroinfo *)), bool (*fncreate)(_uiheroinfo *), bool (*fnremove)(_uiheroinfo *), void (*fnstats)(HeroClass, _uidefaultstats *), _selhero_selections *dlgresult, uint32_t *saveNumber, _difficulty *difficulty); bool UiCreditsDialog(); bool UiSupportDialog(); bool UiMainMenuDialog(const char *name, _mainmenu_selections *pdwResult, int attractTimeOut); bool UiProgressDialog(int (*fnfunc)()); bool UiSelectGame(GameData *gameData, int *playerId); bool UiSelectProvider(GameData *gameData); void UiFadeIn(); void UiHandleEvents(SDL_Event *event); bool UiItemMouseEvents(SDL_Event *event, const std::vector &items); bool UiItemMouseEvents(SDL_Event *event, const std::vector> &items); Sint16 GetCenterOffset(Sint16 w, Sint16 bw = 0); void DrawMouse(); void UiLoadDefaultPalette(); bool UiLoadBlackBackground(); void LoadBackgroundArt(const char *pszFile, int frames = 1); void UiAddBackground(std::vector> *vecDialog); void UiAddLogo(std::vector> *vecDialog, int y = GetUIRectangle().position.y); void UiFocusNavigationSelect(); void UiFocusNavigationEsc(); void UiFocusNavigationYesNo(); void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector> &items, bool wraps = false, void (*fnFullscreen)() = nullptr, bool (*fnYesNo)() = nullptr, size_t selectedItem = 0); void UiRenderListItems(); void UiInitList_clear(); void UiClearScreen(); void UiPollAndRender(std::optional> eventHandler = std::nullopt); void UiRenderItem(const UiItemBase &item); void UiRenderItems(const std::vector &items); void UiRenderItems(const std::vector> &items); ClxSprite UiGetHeroDialogSprite(size_t heroClassIndex); void mainmenu_restart_repintro(); } // namespace devilution ================================================ FILE: Source/DiabloUI/dialogs.cpp ================================================ #include "DiabloUI/dialogs.h" #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #else #include #endif #include "DiabloUI/button.h" #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "controls/input.h" #include "controls/menu_controls.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/load_pcx.hpp" #include "engine/point.hpp" #include "engine/render/text_render.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "init.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/ui_fwd.h" namespace devilution { namespace { OptionalOwnedClxSpriteList ownedDialogSprite; std::string wrappedText; bool dialogEnd; void DialogActionOK() { dialogEnd = true; } std::vector> vecNULL; std::vector> vecOkDialog; OptionalClxSprite LoadDialogSprite(bool hasCaption, bool isError) { constexpr uint8_t TransparentColor = 255; if (!hasCaption) { ownedDialogSprite = LoadPcx(isError ? "ui_art\\srpopup" : "ui_art\\spopup", TransparentColor); } else if (isError) { ownedDialogSprite = LoadOptionalClx("ui_art\\dvl_lrpopup.clx"); if (!ownedDialogSprite) { ownedDialogSprite = LoadPcx("ui_art\\lrpopup", TransparentColor); } } else { ownedDialogSprite = LoadPcx("ui_art\\lpopup", TransparentColor); } if (!ownedDialogSprite) return std::nullopt; return (*ownedDialogSprite)[0]; } bool Init(std::string_view caption, std::string_view text, bool error, bool renderBehind) { if (!renderBehind) { if (!UiLoadBlackBackground()) { if (!SDLC_ShowCursor()) { LogError("{}", SDL_GetError()); } } } if (!IsHardwareCursor() && !ArtCursor) { ArtCursor = LoadPcx("ui_art\\cursor", /*transparentColor=*/0); } LoadDialogButtonGraphics(); OptionalClxSprite dialogSprite = LoadDialogSprite(!caption.empty(), error); if (!dialogSprite) return false; const int dialogWidth = dialogSprite->width(); const int textWidth = dialogWidth - 40; wrappedText = WordWrapString(text, textWidth, FontSizeDialog); const Point uiPosition = GetUIRectangle().position; if (caption.empty()) { const SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 180, uiPosition.y + 168, dialogSprite->width(), dialogSprite->height()); vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); const SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 200, uiPosition.y + 211, textWidth, 80); vecOkDialog.push_back(std::make_unique(wrappedText, rect2, UiFlags::AlignCenter | UiFlags::ColorDialogWhite)); const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 265, uiPosition.y + 265, DialogButtonWidth, DialogButtonHeight); vecOkDialog.push_back(std::make_unique(_("OK"), &DialogActionOK, rect3)); } else { const SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 127, uiPosition.y + 100, dialogSprite->width(), dialogSprite->height()); vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); const SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 147, uiPosition.y + 110, textWidth, 20); vecOkDialog.push_back(std::make_unique(caption, rect2, UiFlags::AlignCenter | UiFlags::ColorYellow)); const SDL_Rect rect3 = MakeSdlRect(uiPosition.x + 147, uiPosition.y + 141, textWidth, 190); vecOkDialog.push_back(std::make_unique(wrappedText, rect3, UiFlags::AlignCenter | UiFlags::ColorDialogWhite)); const SDL_Rect rect4 = MakeSdlRect(uiPosition.x + 264, uiPosition.y + 335, DialogButtonWidth, DialogButtonHeight); vecOkDialog.push_back(std::make_unique(_("OK"), &DialogActionOK, rect4)); } return true; } void Deinit() { ownedDialogSprite = std::nullopt; vecOkDialog.clear(); FreeDialogButtonGraphics(); } void DialogLoop(const std::vector> &items, const std::vector> &renderBehind) { SDL_Event event; dialogEnd = false; do { while (PollEvent(&event)) { switch (event.type) { case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: UiItemMouseEvents(&event, items); break; default: for (const MenuAction menuAction : GetMenuActions(event)) { if (IsNoneOf(menuAction, MenuAction_BACK, MenuAction_SELECT)) continue; dialogEnd = true; break; } break; } UiHandleEvents(&event); } UiClearScreen(); UiRenderItems(renderBehind); UiRenderListItems(); UiRenderItems(items); DrawMouse(); UiFadeIn(); } while (!dialogEnd); } void UiOkDialog(std::string_view caption, std::string_view text, bool error, const std::vector> &renderBehind) { static bool inDialog = false; if (!caption.empty()) { LogError("{}\n{}", caption, text); } else { LogError("{}", text); } if (!gbActive || inDialog) { if (!HeadlessMode) { if (!SDLC_ShowCursor()) { LogError("{}", SDL_GetError()); } const std::string captionStr = std::string(caption); const std::string textStr = std::string(text); if (!SDLC_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, captionStr.c_str(), textStr.c_str(), nullptr)) { LogError("{}", SDL_GetError()); } } return; } if (IsHardwareCursor()) { if (!SDLC_ShowCursor()) { LogError("{}", SDL_GetError()); } } if (!Init(caption, text, error, !renderBehind.empty())) { LogError("{}\n{}", caption, text); const std::string captionStr = std::string(caption); const std::string textStr = std::string(text); if (!SDLC_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, captionStr.c_str(), textStr.c_str(), nullptr)) { LogError("{}", SDL_GetError()); } } inDialog = true; SDL_SetSurfaceClipRect(DiabloUiSurface(), nullptr); DialogLoop(vecOkDialog, renderBehind); Deinit(); inDialog = false; } } // namespace void UiErrorOkDialog(std::string_view caption, std::string_view text, const std::vector> &renderBehind) { UiOkDialog(caption, text, /*error=*/true, renderBehind); } void UiErrorOkDialog(std::string_view caption, std::string_view text, bool error) { UiOkDialog(caption, text, error, vecNULL); } void UiErrorOkDialog(std::string_view text, const std::vector> &renderBehind) { UiErrorOkDialog({}, text, renderBehind); } } // namespace devilution ================================================ FILE: Source/DiabloUI/dialogs.h ================================================ #pragma once #include #include #include "DiabloUI/ui_item.h" namespace devilution { void UiErrorOkDialog(std::string_view text, const std::vector> &renderBehind); void UiErrorOkDialog(std::string_view caption, std::string_view text, const std::vector> &renderBehind); } // namespace devilution ================================================ FILE: Source/DiabloUI/hero/selhero.cpp ================================================ #include "DiabloUI/hero/selhero.h" #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "DiabloUI/dialogs.h" #include "DiabloUI/multi/selgame.h" #include "DiabloUI/scrollbar.h" #include "DiabloUI/selok.h" #include "DiabloUI/selyesno.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "controls/control_mode.hpp" #include "engine/assets.hpp" #include "engine/point.hpp" #include "game_mode.hpp" #include "levels/gendung.h" #include "options.h" #include "pfile.h" #include "tables/playerdat.hpp" #include "utils/enum_traits.h" #include "utils/language.h" #include "utils/sdl_geometry.h" #include "utils/str_cat.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { bool selhero_endMenu; bool selhero_isMultiPlayer; bool (*gfnHeroInfo)(bool (*fninfofunc)(_uiheroinfo *)); bool (*gfnHeroCreate)(_uiheroinfo *); void (*gfnHeroStats)(HeroClass, _uidefaultstats *); namespace { std::size_t selhero_SaveCount = 0; _uiheroinfo selhero_heros[MAX_CHARACTERS]; _uiheroinfo selhero_heroInfo; char textStats[6][4]; const char *title = ""; _selhero_selections selhero_result; bool selhero_navigateYesNo; bool selhero_isSavegame; std::vector> vecSelHeroDialog; std::vector> vecSelHeroDlgItems; std::vector> vecSelDlgItems; UiImageClx *SELHERO_DIALOG_HERO_IMG; void SelheroListFocus(size_t value); void SelheroListSelect(size_t value); void SelheroListEsc(); void SelheroLoadFocus(size_t value); void SelheroLoadSelect(size_t value); void SelheroNameSelect(size_t value); void SelheroNameEsc(); void SelheroClassSelectorFocus(size_t value); void SelheroClassSelectorSelect(size_t value); void SelheroClassSelectorEsc(); const char *SelheroGenerateName(HeroClass heroClass); void SelheroUiFocusNavigationYesNo() { if (selhero_isSavegame) UiFocusNavigationYesNo(); } void SelheroFree() { ArtBackground = std::nullopt; vecSelHeroDialog.clear(); vecSelDlgItems.clear(); vecSelHeroDlgItems.clear(); UnloadScrollBar(); } void SelheroSetStats() { SELHERO_DIALOG_HERO_IMG->setSprite(UiGetHeroDialogSprite(static_cast(selhero_heroInfo.heroclass))); CopyUtf8(textStats[0], StrCat(selhero_heroInfo.level), sizeof(textStats[0])); CopyUtf8(textStats[1], StrCat(selhero_heroInfo.strength), sizeof(textStats[1])); CopyUtf8(textStats[2], StrCat(selhero_heroInfo.magic), sizeof(textStats[2])); CopyUtf8(textStats[3], StrCat(selhero_heroInfo.dexterity), sizeof(textStats[3])); CopyUtf8(textStats[4], StrCat(selhero_heroInfo.vitality), sizeof(textStats[4])); CopyUtf8(textStats[5], StrCat(selhero_heroInfo.saveNumber), sizeof(textStats[5])); } void RenderDifficultyIndicators() { if (!selhero_isSavegame) return; const uint16_t width = (*DifficultyIndicator)[0].width(); const uint16_t height = (*DifficultyIndicator)[0].height(); SDL_Rect rect = MakeSdlRect( SELHERO_DIALOG_HERO_IMG->m_rect.x + 1, SELHERO_DIALOG_HERO_IMG->m_rect.y + SELHERO_DIALOG_HERO_IMG->m_rect.h - height - 1, width, height); for (int i = 0; i <= DIFF_LAST; i++) { if (i >= selhero_heroInfo.herorank) break; UiRenderItem(UiImageClx((*DifficultyIndicator)[0], rect, UiFlags::None)); rect.x += width; } } UiArtTextButton *SELLIST_DIALOG_DELETE_BUTTON; bool SelHeroGetHeroInfo(_uiheroinfo *pInfo) { selhero_heros[selhero_SaveCount] = *pInfo; selhero_SaveCount++; return true; } void SelheroListFocus(size_t value) { const UiFlags baseFlags = UiFlags::AlignCenter | UiFlags::FontSize30; if (selhero_SaveCount != 0 && value < selhero_SaveCount) { memcpy(&selhero_heroInfo, &selhero_heros[value], sizeof(selhero_heroInfo)); SelheroSetStats(); SELLIST_DIALOG_DELETE_BUTTON->SetFlags(baseFlags | UiFlags::ColorUiGold); selhero_isSavegame = true; return; } SELHERO_DIALOG_HERO_IMG->setSprite(UiGetHeroDialogSprite(GetNumPlayerClasses())); for (char *textStat : textStats) strcpy(textStat, "--"); SELLIST_DIALOG_DELETE_BUTTON->SetFlags(baseFlags | UiFlags::ColorUiSilver | UiFlags::ElementDisabled); selhero_isSavegame = false; } bool SelheroListDeleteYesNo() { selhero_navigateYesNo = selhero_isSavegame; return selhero_navigateYesNo; } void SelheroListSelect(size_t value) { const Point uiPosition = GetUIRectangle().position; if (static_cast(value) == selhero_SaveCount) { vecSelDlgItems.clear(); const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 242), (Sint16)(uiPosition.y + 211), 365, 33 }; vecSelDlgItems.push_back(std::make_unique(_("Choose Class").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); vecSelHeroDlgItems.clear(); int itemH = 33; for (size_t i = 0; i < GetNumPlayerClasses(); ++i) { const HeroClass heroClass = static_cast(i); if (heroClass == HeroClass::Monk && !gbIsHellfire) { continue; } if (heroClass == HeroClass::Bard && !HaveBardAssets() && !(*GetOptions().Gameplay.testBard)) { continue; } if (heroClass == HeroClass::Barbarian && !HaveBarbarianAssets() && !(*GetOptions().Gameplay.testBarbarian)) { continue; } const PlayerData &playerData = GetPlayerDataForClass(heroClass); vecSelHeroDlgItems.push_back(std::make_unique(_(playerData.className), static_cast(heroClass))); } if (vecSelHeroDlgItems.size() > 4) itemH = 26; const int itemY = static_cast(246 + (176 - std::min(vecSelHeroDlgItems.size(), 6) * itemH) / 2); vecSelDlgItems.push_back(std::make_unique(vecSelHeroDlgItems, std::min(vecSelHeroDlgItems.size(), 6), uiPosition.x + 264, (uiPosition.y + itemY), 320, itemH, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rectScrollBar = { (Sint16)(uiPosition.x + 585), (Sint16)(uiPosition.y + 244), 25, 178 }; vecSelDlgItems.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollBar)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 279), (Sint16)(uiPosition.y + 429), 140, 35 }; vecSelDlgItems.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect2, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 429), (Sint16)(uiPosition.y + 429), 144, 35 }; vecSelDlgItems.push_back(std::make_unique(_("Cancel"), &UiFocusNavigationEsc, rect3, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(SelheroClassSelectorFocus, SelheroClassSelectorSelect, SelheroClassSelectorEsc, vecSelDlgItems, true); memset(&selhero_heroInfo.name, 0, sizeof(selhero_heroInfo.name)); selhero_heroInfo.saveNumber = pfile_ui_get_first_unused_save_num(); SelheroSetStats(); title = selhero_isMultiPlayer ? _("New Multi Player Hero").data() : _("New Single Player Hero").data(); selhero_isSavegame = false; return; } if (selhero_heroInfo.hassaved) { vecSelDlgItems.clear(); const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 242), (Sint16)(uiPosition.y + 211), 365, 33 }; vecSelDlgItems.push_back(std::make_unique(_("Save File Exists").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); vecSelHeroDlgItems.clear(); vecSelHeroDlgItems.push_back(std::make_unique(_("Load Game"), 0)); vecSelHeroDlgItems.push_back(std::make_unique(_("New Game"), 1)); vecSelDlgItems.push_back(std::make_unique(vecSelHeroDlgItems, vecSelHeroDlgItems.size(), uiPosition.x + 265, (uiPosition.y + 285), 320, 33, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 279), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelDlgItems.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect2, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 429), (Sint16)(uiPosition.y + 427), 144, 35 }; vecSelDlgItems.push_back(std::make_unique(_("Cancel"), &UiFocusNavigationEsc, rect3, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(SelheroLoadFocus, SelheroLoadSelect, selhero_List_Init, vecSelDlgItems, true); title = _("Single Player Characters").data(); return; } SelheroLoadSelect(1); } void SelheroListEsc() { UiInitList_clear(); selhero_endMenu = true; selhero_result = SELHERO_PREVIOUS; } void SelheroClassSelectorFocus(size_t value) { const auto heroClass = static_cast(vecSelHeroDlgItems[value]->m_value); _uidefaultstats defaults; gfnHeroStats(heroClass, &defaults); selhero_heroInfo.level = 1; selhero_heroInfo.heroclass = heroClass; selhero_heroInfo.strength = defaults.strength; selhero_heroInfo.magic = defaults.magic; selhero_heroInfo.dexterity = defaults.dexterity; selhero_heroInfo.vitality = defaults.vitality; SelheroSetStats(); } bool ShouldPrefillHeroName() { #if defined(PREFILL_PLAYER_NAME) return true; #else return ControlMode != ControlTypes::KeyboardAndMouse; #endif } void RemoveSelHeroBackground() { vecSelHeroDialog.erase(vecSelHeroDialog.begin()); ArtBackground = std::nullopt; } void AddSelHeroBackground() { LoadBackgroundArt("ui_art\\selhero"); vecSelHeroDialog.insert(vecSelHeroDialog.begin(), std::make_unique((*ArtBackground)[0], MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter)); } void SelheroClassSelectorSelect(size_t value) { auto hClass = static_cast(vecSelHeroDlgItems[value]->m_value); if (gbIsSpawn && (hClass == HeroClass::Rogue || hClass == HeroClass::Sorcerer || (hClass == HeroClass::Bard && !HaveBardAssets()))) { RemoveSelHeroBackground(); UiSelOkDialog(nullptr, _("The Rogue and Sorcerer are only available in the full retail version of Diablo. Visit https://www.gog.com/game/diablo to purchase.").data(), false); AddSelHeroBackground(); SelheroListSelect(selhero_SaveCount); return; } const Point uiPosition = GetUIRectangle().position; title = selhero_isMultiPlayer ? _("New Multi Player Hero").data() : _("New Single Player Hero").data(); memset(selhero_heroInfo.name, '\0', sizeof(selhero_heroInfo.name)); if (ShouldPrefillHeroName()) strcpy(selhero_heroInfo.name, SelheroGenerateName(selhero_heroInfo.heroclass)); vecSelDlgItems.clear(); const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 242), (Sint16)(uiPosition.y + 211), 365, 33 }; vecSelDlgItems.push_back(std::make_unique(_("Enter Name").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 265), (Sint16)(uiPosition.y + 317), 320, 33 }; vecSelDlgItems.push_back(std::make_unique(_("Enter Name"), selhero_heroInfo.name, 15, false, rect2, UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 279), (Sint16)(uiPosition.y + 429), 140, 35 }; vecSelDlgItems.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect3, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 429), (Sint16)(uiPosition.y + 429), 144, 35 }; vecSelDlgItems.push_back(std::make_unique(_("Cancel"), &UiFocusNavigationEsc, rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(nullptr, SelheroNameSelect, SelheroNameEsc, vecSelDlgItems); } void SelheroClassSelectorEsc() { vecSelDlgItems.clear(); vecSelHeroDlgItems.clear(); if (selhero_SaveCount != 0) { selhero_List_Init(); return; } SelheroListEsc(); } void SelheroNameSelect(size_t /*value*/) { // only check names in multiplayer, we don't care about them in single if (selhero_isMultiPlayer && !UiValidPlayerName(selhero_heroInfo.name)) { RemoveSelHeroBackground(); UiSelOkDialog(title, _("Invalid name. A name cannot contain spaces, reserved characters, or reserved words.\n").data(), false); AddSelHeroBackground(); } else { if (gfnHeroCreate(&selhero_heroInfo)) { SelheroLoadSelect(1); return; } UiErrorOkDialog(_(/* TRANSLATORS: Error Message */ "Unable to create character."), vecSelHeroDialog); } memset(selhero_heroInfo.name, '\0', sizeof(selhero_heroInfo.name)); SelheroClassSelectorSelect(0); } void SelheroNameEsc() { SelheroListSelect(selhero_SaveCount); } void SelheroLoadFocus(size_t value) { } void SelheroLoadSelect(size_t value) { UiInitList_clear(); selhero_endMenu = true; if (vecSelHeroDlgItems[value]->m_value == 0) { selhero_result = SELHERO_CONTINUE; return; } if (!selhero_isMultiPlayer) { // This is part of a dangerous hack to enable difficulty selection in single-player. // FIXME: Dialogs should not refer to each other's variables. // We disable `selhero_endMenu` and replace the background and art // and the item list with the difficulty selection ones. // // This means selhero's render loop will render selgame's items, // which happens to work because the render loops are similar. selhero_endMenu = false; // Set this to false so that we do not attempt to render difficulty indicators. selhero_isSavegame = false; SelheroFree(); LoadBackgroundArt("ui_art\\selgame"); selgame_GameSelection_Select(0); } selhero_result = SELHERO_NEW_DUNGEON; } const char *SelheroGenerateName(HeroClass heroClass) { static const char *const Names[6][10] = { { // Warrior "Aidan", "Qarak", "Born", "Cathan", "Halbu", "Lenalas", "Maximus", "Vane", "Myrdgar", "Rothat", }, { // Rogue "Moreina", "Akara", "Kashya", "Flavie", "Divo", "Oriana", "Iantha", "Shikha", "Basanti", "Elexa", }, { // Sorcerer "Jazreth", "Drognan", "Armin", "Fauztin", "Jere", "Kazzulk", "Ranslor", "Sarnakyle", "Valthek", "Horazon", }, { // Monk "Akyev", "Dvorak", "Kekegi", "Kharazim", "Mikulov", "Shenlong", "Vedenin", "Vhalit", "Vylnas", "Zhota", }, { // Bard (uses Rogue names) "Moreina", "Akara", "Kashya", "Flavie", "Divo", "Oriana", "Iantha", "Shikha", "Basanti", "Elexa", }, { // Barbarian "Alaric", "Barloc", "Egtheow", "Guthlaf", "Heorogar", "Hrothgar", "Oslaf", "Qual-Kehk", "Ragnar", "Ulf", }, }; const int iRand = rand() % 10; return Names[static_cast(heroClass) % 6][iRand]; } } // namespace void selhero_Init() { AddSelHeroBackground(); UiAddLogo(&vecSelHeroDialog); LoadScrollBar(); selhero_SaveCount = 0; gfnHeroInfo(SelHeroGetHeroInfo); std::reverse(selhero_heros, selhero_heros + selhero_SaveCount); const Point uiPosition = GetUIRectangle().position; vecSelDlgItems.clear(); SDL_Rect rect = MakeSdlRect(uiPosition.x + 24, uiPosition.y + 161, 590, 35); vecSelHeroDialog.push_back(std::make_unique(&title, rect, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); rect = MakeSdlRect(uiPosition.x + 30, uiPosition.y + 211, 180, 76); auto heroImg = std::make_unique(UiGetHeroDialogSprite(0), rect, UiFlags::None); SELHERO_DIALOG_HERO_IMG = heroImg.get(); vecSelHeroDialog.push_back(std::move(heroImg)); const UiFlags labelFlags = UiFlags::FontSize12 | UiFlags::ColorUiSilverDark | UiFlags::AlignRight; const UiFlags valueFlags = UiFlags::FontSize12 | UiFlags::ColorUiSilverDark | UiFlags::AlignCenter; const int labelX = uiPosition.x + 39; const int valueX = uiPosition.x + 159; const int labelWidth = 110; const int valueWidth = 40; const int statHeight = 21; vecSelHeroDialog.push_back(std::make_unique(_("Level:").data(), MakeSdlRect(labelX, uiPosition.y + 323, labelWidth, statHeight), labelFlags)); vecSelHeroDialog.push_back(std::make_unique(textStats[0], MakeSdlRect(valueX, uiPosition.y + 323, valueWidth, statHeight), valueFlags)); const char *statLabels[] { _("Strength:").data(), _("Magic:").data(), _("Dexterity:").data(), _("Vitality:").data(), #ifdef _DEBUG _("Savegame:").data() #endif }; int statY = uiPosition.y + 358; for (size_t i = 0; i < sizeof(statLabels) / sizeof(statLabels[0]); ++i) { vecSelHeroDialog.push_back(std::make_unique(statLabels[i], MakeSdlRect(labelX, statY, labelWidth, statHeight), labelFlags)); vecSelHeroDialog.push_back(std::make_unique(textStats[i + 1], MakeSdlRect(valueX, statY, valueWidth, statHeight), valueFlags)); statY += statHeight; } } void selhero_List_Init() { const Point uiPosition = GetUIRectangle().position; size_t selectedItem = 0; vecSelDlgItems.clear(); const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 242), (Sint16)(uiPosition.y + 211), 365, 33 }; vecSelDlgItems.push_back(std::make_unique(_("Select Hero").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); vecSelHeroDlgItems.clear(); for (std::size_t i = 0; i < selhero_SaveCount; i++) { vecSelHeroDlgItems.push_back(std::make_unique(std::string_view(selhero_heros[i].name), static_cast(i))); if (selhero_heros[i].saveNumber == selhero_heroInfo.saveNumber) selectedItem = i; } vecSelHeroDlgItems.push_back(std::make_unique(_("New Hero"), static_cast(selhero_SaveCount))); vecSelDlgItems.push_back(std::make_unique(vecSelHeroDlgItems, 6, uiPosition.x + 265, (uiPosition.y + 256), 320, 26, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 585), (Sint16)(uiPosition.y + 244), 25, 178 }; vecSelDlgItems.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rect2)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 239), (Sint16)(uiPosition.y + 429), 120, 35 }; vecSelDlgItems.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect3, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 364), (Sint16)(uiPosition.y + 429), 120, 35 }; auto setlistDialogDeleteButton = std::make_unique(_("Delete"), &SelheroUiFocusNavigationYesNo, rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver | UiFlags::ElementDisabled); SELLIST_DIALOG_DELETE_BUTTON = setlistDialogDeleteButton.get(); vecSelDlgItems.push_back(std::move(setlistDialogDeleteButton)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 489), (Sint16)(uiPosition.y + 429), 144, 35 }; vecSelDlgItems.push_back(std::make_unique(_("Cancel"), &UiFocusNavigationEsc, rect5, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(SelheroListFocus, SelheroListSelect, SelheroListEsc, vecSelDlgItems, false, nullptr, SelheroListDeleteYesNo, selectedItem); if (selhero_isMultiPlayer) { title = _("Multi Player Characters").data(); } else { title = _("Single Player Characters").data(); } } static void UiSelHeroDialog( bool (*fninfo)(bool (*fninfofunc)(_uiheroinfo *)), bool (*fncreate)(_uiheroinfo *), void (*fnstats)(HeroClass, _uidefaultstats *), bool (*fnremove)(_uiheroinfo *), _selhero_selections *dlgresult, uint32_t *saveNumber) { do { gfnHeroInfo = fninfo; gfnHeroCreate = fncreate; gfnHeroStats = fnstats; selhero_result = *dlgresult; selhero_navigateYesNo = false; selhero_Init(); if (selhero_SaveCount != 0) { selhero_heroInfo = {}; // Search last used save and remember it as selected item for (size_t i = 0; i < selhero_SaveCount; i++) { if (selhero_heros[i].saveNumber == *saveNumber) { memcpy(&selhero_heroInfo, &selhero_heros[i], sizeof(selhero_heroInfo)); break; } } selhero_List_Init(); } else { SelheroListSelect(selhero_SaveCount); } selhero_endMenu = false; while (!selhero_endMenu && !selhero_navigateYesNo) { UiClearScreen(); UiRenderItems(vecSelHeroDialog); RenderDifficultyIndicators(); UiPollAndRender(); } SelheroFree(); if (selhero_navigateYesNo) { char dialogTitle[128]; char dialogText[256]; if (selhero_isMultiPlayer) { CopyUtf8(dialogTitle, _("Delete Multi Player Hero"), sizeof(dialogTitle)); } else { CopyUtf8(dialogTitle, _("Delete Single Player Hero"), sizeof(dialogTitle)); } strcpy(dialogText, fmt::format(fmt::runtime(_("Are you sure you want to delete the character \"{:s}\"?")), selhero_heroInfo.name).c_str()); if (UiSelHeroYesNoDialog(dialogTitle, dialogText)) fnremove(&selhero_heroInfo); } } while (selhero_navigateYesNo); *dlgresult = selhero_result; *saveNumber = selhero_heroInfo.saveNumber; } void UiSelHeroSingDialog( bool (*fninfo)(bool (*fninfofunc)(_uiheroinfo *)), bool (*fncreate)(_uiheroinfo *), bool (*fnremove)(_uiheroinfo *), void (*fnstats)(HeroClass, _uidefaultstats *), _selhero_selections *dlgresult, uint32_t *saveNumber, _difficulty *difficulty) { selhero_isMultiPlayer = false; UiSelHeroDialog(fninfo, fncreate, fnstats, fnremove, dlgresult, saveNumber); *difficulty = nDifficulty; } void UiSelHeroMultDialog( bool (*fninfo)(bool (*fninfofunc)(_uiheroinfo *)), bool (*fncreate)(_uiheroinfo *), bool (*fnremove)(_uiheroinfo *), void (*fnstats)(HeroClass, _uidefaultstats *), _selhero_selections *dlgresult, uint32_t *saveNumber) { selhero_isMultiPlayer = true; UiSelHeroDialog(fninfo, fncreate, fnstats, fnremove, dlgresult, saveNumber); } } // namespace devilution ================================================ FILE: Source/DiabloUI/hero/selhero.h ================================================ #pragma once #include namespace devilution { extern bool selhero_isMultiPlayer; extern bool selhero_endMenu; void selhero_Init(); void selhero_List_Init(); } // namespace devilution ================================================ FILE: Source/DiabloUI/mainmenu.cpp ================================================ #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/assets.hpp" #include "engine/load_clx.hpp" #include "engine/point.hpp" #include "game_mode.hpp" #include "utils/language.h" #include "utils/ui_fwd.h" namespace devilution { namespace { int mainmenu_attract_time_out; // seconds uint32_t dwAttractTicks; std::vector> vecMainMenuDialog; std::vector> vecMenuItems; _mainmenu_selections MainMenuResult; void UiMainMenuSelect(size_t value) { MainMenuResult = (_mainmenu_selections)vecMenuItems[value]->m_value; } #ifndef NOEXIT void MainmenuEsc() { const std::size_t last = vecMenuItems.size() - 1; if (SelectedItem == last) { UiMainMenuSelect(last); } else { SelectedItem = last; } } #endif void MainmenuLoad(const char *name) { vecMenuItems.push_back(std::make_unique(_("Single Player"), MAINMENU_SINGLE_PLAYER)); vecMenuItems.push_back(std::make_unique(_("Multi Player"), MAINMENU_MULTIPLAYER)); vecMenuItems.push_back(std::make_unique(_("Settings"), MAINMENU_SETTINGS)); vecMenuItems.push_back(std::make_unique(_("Support"), MAINMENU_SHOW_SUPPORT)); vecMenuItems.push_back(std::make_unique(_("Show Credits"), MAINMENU_SHOW_CREDITS)); #ifndef NOEXIT vecMenuItems.push_back(std::make_unique(gbIsHellfire ? _("Exit Hellfire") : _("Exit Diablo"), MAINMENU_EXIT_DIABLO)); #endif if (!gbIsSpawn || gbIsHellfire) { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\mainmenuw.clx"); LoadBackgroundArt("ui_art\\mainmenu"); } else { LoadBackgroundArt("ui_art\\swmmenu"); } UiAddBackground(&vecMainMenuDialog); UiAddLogo(&vecMainMenuDialog); const Point uiPosition = GetUIRectangle().position; if (gbIsSpawn && gbIsHellfire) { const SDL_Rect rect1 = { (Sint16)(uiPosition.x), (Sint16)(uiPosition.y + 145), 640, 30 }; vecMainMenuDialog.push_back(std::make_unique(_("Shareware").data(), rect1, UiFlags::FontSize30 | UiFlags::ColorUiSilver | UiFlags::AlignCenter, 8)); } vecMainMenuDialog.push_back(std::make_unique(vecMenuItems, vecMenuItems.size(), uiPosition.x + 64, (uiPosition.y + 192), 510, 43, UiFlags::FontSize42 | UiFlags::ColorUiGold | UiFlags::AlignCenter, 5)); const SDL_Rect rect2 = { 17, (Sint16)(gnScreenHeight - 36), 605, 21 }; vecMainMenuDialog.push_back(std::make_unique(name, rect2, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark)); #ifndef NOEXIT UiInitList(nullptr, UiMainMenuSelect, MainmenuEsc, vecMainMenuDialog, true); #else UiInitList(nullptr, UiMainMenuSelect, nullptr, vecMainMenuDialog, true); #endif } void MainmenuFree() { ArtBackgroundWidescreen = std::nullopt; ArtBackground = std::nullopt; vecMainMenuDialog.clear(); vecMenuItems.clear(); } } // namespace void mainmenu_restart_repintro() { dwAttractTicks = SDL_GetTicks() + mainmenu_attract_time_out * 1000; } bool UiMainMenuDialog(const char *name, _mainmenu_selections *pdwResult, int attractTimeOut) { MainMenuResult = MAINMENU_NONE; while (MainMenuResult == MAINMENU_NONE) { mainmenu_attract_time_out = attractTimeOut; MainmenuLoad(name); mainmenu_restart_repintro(); // for automatic starts while (MainMenuResult == MAINMENU_NONE) { UiClearScreen(); UiPollAndRender(); if (SDL_GetTicks() >= dwAttractTicks && (HaveIntro() || gbIsHellfire)) { MainMenuResult = MAINMENU_ATTRACT_MODE; } } MainmenuFree(); } *pdwResult = MainMenuResult; return true; } } // namespace devilution ================================================ FILE: Source/DiabloUI/multi/selconn.cpp ================================================ #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/point.hpp" #include "engine/render/text_render.hpp" #include "multi.h" #include "storm/storm_net.hpp" #include "utils/language.h" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { int provider; const char *ConnectionNames[] { "ZeroTier", N_("Client-Server (TCP)"), N_("Offline"), }; namespace { char selconn_MaxPlayers[64]; char selconn_Description[256]; char selconn_Gateway[129]; bool selconn_ReturnValue = false; bool selconn_EndMenu = false; GameData *selconn_GameData; std::vector> vecConnItems; std::vector> vecSelConnDlg; #define DESCRIPTION_WIDTH 205 void SelconnEsc(); void SelconnFocus(size_t value); void SelconnSelect(size_t value); void SelconnLoad() { LoadBackgroundArt("ui_art\\selconn"); #ifndef NONET #ifndef DISABLE_ZERO_TIER vecConnItems.push_back(std::make_unique(std::string_view(ConnectionNames[SELCONN_ZT]), SELCONN_ZT)); #endif #ifndef DISABLE_TCP vecConnItems.push_back(std::make_unique(_(ConnectionNames[SELCONN_TCP]), SELCONN_TCP)); #endif #endif vecConnItems.push_back(std::make_unique(_(ConnectionNames[SELCONN_LOOPBACK]), SELCONN_LOOPBACK)); UiAddBackground(&vecSelConnDlg); UiAddLogo(&vecSelConnDlg); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(Sint16)(uiPosition.y + 161), 590, 35 }; vecSelConnDlg.push_back(std::make_unique(_("Multi Player Game").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 218), DESCRIPTION_WIDTH, 21 }; vecSelConnDlg.push_back(std::make_unique(selconn_MaxPlayers, rect2, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 256), DESCRIPTION_WIDTH, 21 }; vecSelConnDlg.push_back(std::make_unique(_("Requirements:").data(), rect3, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 275), DESCRIPTION_WIDTH, 66 }; vecSelConnDlg.push_back(std::make_unique(selconn_Description, rect4, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark, 1, 16)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 30), (Sint16)(uiPosition.y + 356), 220, 31 }; vecSelConnDlg.push_back(std::make_unique(_("no gateway needed").data(), rect5, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiSilver, 0)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 393), DESCRIPTION_WIDTH, 21 }; vecSelConnDlg.push_back(std::make_unique(selconn_Gateway, rect6, UiFlags::AlignCenter | UiFlags::FontSize12 | UiFlags::ColorUiSilverDark)); const SDL_Rect rect7 = { (Sint16)(uiPosition.x + 300), (Sint16)(uiPosition.y + 211), 295, 33 }; vecSelConnDlg.push_back(std::make_unique(_("Select Connection").data(), rect7, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect8 = { (Sint16)(uiPosition.x + 16), (Sint16)(uiPosition.y + 427), 250, 35 }; vecSelConnDlg.push_back(std::make_unique(_("Change Gateway"), nullptr, rect8, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold | UiFlags::ElementHidden)); vecSelConnDlg.push_back(std::make_unique(vecConnItems, vecConnItems.size(), uiPosition.x + 305, (uiPosition.y + 256), 285, 26, UiFlags::AlignCenter | UiFlags::FontSize12 | UiFlags::VerticalCenter | UiFlags::ColorUiGoldDark)); const SDL_Rect rect9 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelConnDlg.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect9, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect10 = { (Sint16)(uiPosition.x + 454), (Sint16)(uiPosition.y + 427), 144, 35 }; vecSelConnDlg.push_back(std::make_unique(_("Cancel"), &UiFocusNavigationEsc, rect10, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(SelconnFocus, SelconnSelect, SelconnEsc, vecSelConnDlg, true); } void SelconnFree() { ArtBackground = std::nullopt; vecConnItems.clear(); vecSelConnDlg.clear(); } void SelconnEsc() { selconn_ReturnValue = false; selconn_EndMenu = true; } void SelconnFocus(size_t value) { int players = MAX_PLRS; switch (vecConnItems[value]->m_value) { case SELCONN_TCP: CopyUtf8(selconn_Description, _("All computers must be connected to a TCP-compatible network."), sizeof(selconn_Description)); players = MAX_PLRS; break; case SELCONN_ZT: CopyUtf8(selconn_Description, _("All computers must be connected to the internet."), sizeof(selconn_Description)); players = MAX_PLRS; break; case SELCONN_LOOPBACK: CopyUtf8(selconn_Description, _("Play by yourself with no network exposure."), sizeof(selconn_Description)); players = 1; break; } CopyUtf8(selconn_MaxPlayers, fmt::format(fmt::runtime(_("Players Supported: {:d}")), players), sizeof(selconn_MaxPlayers)); CopyUtf8(selconn_Description, WordWrapString(selconn_Description, DESCRIPTION_WIDTH), sizeof(selconn_Description)); } void SelconnSelect(size_t value) { provider = vecConnItems[value]->m_value; SelconnFree(); selconn_EndMenu = SNetInitializeProvider(provider, selconn_GameData); SelconnLoad(); } } // namespace bool UiSelectProvider(GameData *gameData) { selconn_GameData = gameData; SelconnLoad(); selconn_ReturnValue = true; selconn_EndMenu = false; while (!selconn_EndMenu) { UiClearScreen(); UiPollAndRender(); } SelconnFree(); return selconn_ReturnValue; } } // namespace devilution ================================================ FILE: Source/DiabloUI/multi/selgame.cpp ================================================ #include "DiabloUI/multi/selgame.h" #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "DiabloUI/hero/selhero.h" #include "DiabloUI/scrollbar.h" #include "DiabloUI/selok.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "config.h" #include "diablo.h" #include "engine/point.hpp" #include "engine/render/text_render.hpp" #include "levels/gendung.h" #include "menu.h" #include "multi.h" #include "options.h" #include "storm/storm_net.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { char selgame_Label[32]; char selgame_Ip[129] = ""; char selgame_Password[16] = ""; char selgame_Description[512]; std::string selgame_Title; bool selgame_enteringGame; size_t selgame_selectedGame; bool selgame_endMenu; int *gdwPlayerId; _difficulty nDifficulty; int nTickRate; int heroLevel; static GameData *m_game_data; extern int provider; #define DESCRIPTION_WIDTH 205 namespace { const char *title = ""; std::vector> vecSelGameDlgItems; std::vector> vecSelGameDialog; std::vector Gamelist; uint32_t firstPublicGameInfoRequestSend = 0; size_t HighlightedItem; void selgame_FreeVectors() { vecSelGameDlgItems.clear(); vecSelGameDialog.clear(); } void selgame_Init() { LoadBackgroundArt("ui_art\\selgame"); LoadScrollBar(); } void selgame_Free() { ArtBackground = std::nullopt; UnloadScrollBar(); selgame_FreeVectors(); } bool IsGameCompatible(const GameData &data) { return (data.versionMajor == PROJECT_VERSION_MAJOR && data.versionMinor == PROJECT_VERSION_MINOR && data.versionPatch == PROJECT_VERSION_PATCH && data.programid == GAME_ID); return false; } static std::string GetErrorMessageIncompatibility(const GameData &data) { if (data.programid != GAME_ID) { std::string_view gameMode; switch (data.programid) { case GameIdDiabloFull: gameMode = _("Diablo"); break; case GameIdDiabloSpawn: gameMode = _("Diablo Shareware"); break; case GameIdHellfireFull: gameMode = _("Hellfire"); break; case GameIdHellfireSpawn: gameMode = _("Hellfire Shareware"); break; default: return std::string(_("The host is running a different game than you.")); } return fmt::format(fmt::runtime(_("The host is running a different game mode ({:s}) than you.")), gameMode); } else { return fmt::format(fmt::runtime(_(/* TRANSLATORS: Error message when somebody tries to join a game running another version. */ "Your version {:s} does not match the host {:d}.{:d}.{:d}.")), PROJECT_VERSION, data.versionMajor, data.versionMinor, data.versionPatch); } } void UiInitGameSelectionList(std::string_view search) { selgame_enteringGame = false; selgame_selectedGame = 0; if (provider == SELCONN_LOOPBACK) { selgame_enteringGame = true; selgame_GameSelection_Select(0); return; } if (provider == SELCONN_ZT) { CopyUtf8(selgame_Ip, GetOptions().Network.szPreviousZTGame, sizeof(selgame_Ip)); } else { CopyUtf8(selgame_Ip, GetOptions().Network.szPreviousHost, sizeof(selgame_Ip)); } selgame_FreeVectors(); selgame_Label[0] = '\0'; UiAddBackground(&vecSelGameDialog); UiAddLogo(&vecSelGameDialog); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rectScrollbar = { (Sint16)(uiPosition.x + 590), (Sint16)(uiPosition.y + 244), 25, 178 }; vecSelGameDialog.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, rectScrollbar)); const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelGameDialog.push_back(std::make_unique(_(ConnectionNames[provider]).data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 211), 205, 192 }; vecSelGameDialog.push_back(std::make_unique(_("Description:").data(), rect2, UiFlags::FontSize24 | UiFlags::ColorUiSilver)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 256), DESCRIPTION_WIDTH, 192 }; vecSelGameDialog.push_back(std::make_unique(selgame_Description, rect3, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark, 1, 16)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 300), (Sint16)(uiPosition.y + 211), 295, 33 }; vecSelGameDialog.push_back(std::make_unique(_("Select Action").data(), rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); #ifdef PACKET_ENCRYPTION vecSelGameDlgItems.push_back(std::make_unique(_("Create Game"), 0, UiFlags::ColorUiGold)); #endif vecSelGameDlgItems.push_back(std::make_unique(_("Create Public Game"), 1, UiFlags::ColorUiGold)); vecSelGameDlgItems.push_back(std::make_unique(_("Join Game"), 2, UiFlags::ColorUiGold)); if (provider == SELCONN_ZT) { vecSelGameDlgItems.push_back(std::make_unique(std::string_view {}, -1, UiFlags::ElementDisabled)); vecSelGameDlgItems.push_back(std::make_unique(_("Public Games"), -1, UiFlags::ElementDisabled | UiFlags::ColorWhitegold)); if (Gamelist.empty()) { // We expect the game list to be received after 3 seconds if (firstPublicGameInfoRequestSend == 0 || (SDL_GetTicks() - firstPublicGameInfoRequestSend) < 2000) vecSelGameDlgItems.push_back(std::make_unique(_("Loading..."), -1, UiFlags::ElementDisabled | UiFlags::ColorUiSilver)); else vecSelGameDlgItems.push_back(std::make_unique(_("None"), -1, UiFlags::ElementDisabled | UiFlags::ColorUiSilver)); } else { for (unsigned i = 0; i < Gamelist.size(); i++) { vecSelGameDlgItems.push_back(std::make_unique(std::string_view(Gamelist[i].name), i + 3, UiFlags::ColorUiGold)); } } } vecSelGameDialog.push_back(std::make_unique(vecSelGameDlgItems, 6, uiPosition.x + 305, (uiPosition.y + 255), 285, 26, UiFlags::AlignCenter | UiFlags::FontSize24)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect5, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 449), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("CANCEL"), &UiFocusNavigationEsc, rect6, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); auto selectFn = [](size_t index) { // UiListItem::m_value could be different from // the index if packet encryption is disabled const int itemValue = vecSelGameDlgItems[index]->m_value; selgame_GameSelection_Select(itemValue); }; if (!search.empty()) { for (size_t i = 0; i < vecSelGameDlgItems.size(); i++) { const int gameIndex = vecSelGameDlgItems[i]->m_value - 3; if (gameIndex < 0) continue; if (search == Gamelist[gameIndex].name) HighlightedItem = i; } } if (HighlightedItem >= vecSelGameDlgItems.size()) { HighlightedItem = vecSelGameDlgItems.size() - 1; } UiInitList(selgame_GameSelection_Focus, selectFn, selgame_GameSelection_Esc, vecSelGameDialog, true, nullptr, nullptr, HighlightedItem); } } // namespace void selgame_GameSelection_Init() { UiInitGameSelectionList(""); } void selgame_GameSelection_Focus(size_t value) { HighlightedItem = value; const UiListItem &item = *vecSelGameDlgItems[value]; switch (item.m_value) { case 0: CopyUtf8(selgame_Description, _("Create a new game with a difficulty setting of your choice."), sizeof(selgame_Description)); break; case 1: CopyUtf8(selgame_Description, _("Create a new public game that anyone can join with a difficulty setting of your choice."), sizeof(selgame_Description)); break; case 2: if (provider == SELCONN_ZT) { CopyUtf8(selgame_Description, _("Enter Game ID to join a game already in progress."), sizeof(selgame_Description)); } else { CopyUtf8(selgame_Description, _("Enter an IP or a hostname to join a game already in progress."), sizeof(selgame_Description)); } break; default: const GameInfo &gameInfo = Gamelist[item.m_value - 3]; std::string infoString = std::string(_("Join the public game already in progress.")); infoString.append("\n\n"); if (IsGameCompatible(gameInfo.gameData)) { std::string_view difficulty; switch (gameInfo.gameData.nDifficulty) { case DIFF_NORMAL: difficulty = _("Normal"); break; case DIFF_NIGHTMARE: difficulty = _("Nightmare"); break; case DIFF_HELL: difficulty = _("Hell"); break; } infoString.append(fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Game Difficulty. */ "Difficulty: {:s}")), difficulty)); infoString += '\n'; switch (gameInfo.gameData.nTickRate) { case 20: infoString.append(_("Speed: Normal")); break; case 25: infoString.append(_("Speed: Fast")); break; case 30: infoString.append(_("Speed: Faster")); break; case 35: infoString.append(_("Speed: Fastest")); break; default: // This should not occur, so no translation is needed infoString.append(StrCat("Speed: ", gameInfo.gameData.nTickRate)); break; } infoString += '\n'; infoString.append(_("Players: ")); for (const auto &playerName : gameInfo.players) { infoString.append(playerName); infoString += ' '; } infoString += '\n'; if (gameInfo.peerIsRelayed.value_or(false)) infoString.append(fmt::format(fmt::runtime(_("Ping: {:d} ms (RELAYED)")), gameInfo.latency.value_or(0))); else infoString.append(fmt::format(fmt::runtime(_("Ping: {:d} ms")), gameInfo.latency.value_or(0))); } else { infoString.append(GetErrorMessageIncompatibility(gameInfo.gameData)); } CopyUtf8(selgame_Description, infoString, sizeof(selgame_Description)); break; } CopyUtf8(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH), sizeof(selgame_Description)); } /** * @brief Load the current hero level from save file * @param pInfo Hero info * @return always true */ bool UpdateHeroLevel(_uiheroinfo *pInfo) { if (pInfo->saveNumber == gSaveNumber) heroLevel = pInfo->level; return true; } void selgame_GameSelection_Select(size_t value) { selgame_enteringGame = true; selgame_selectedGame = value; gfnHeroInfo(UpdateHeroLevel); selgame_FreeVectors(); if (value > 2) { CopyUtf8(selgame_Ip, Gamelist[value - 3].name, sizeof(selgame_Ip)); selgame_Password_Select(value); return; } UiAddBackground(&vecSelGameDialog); UiAddLogo(&vecSelGameDialog); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelGameDialog.push_back(std::make_unique(&title, rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 34), (Sint16)(uiPosition.y + 211), 205, 33 }; vecSelGameDialog.push_back(std::make_unique(selgame_Label, rect2, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 256), DESCRIPTION_WIDTH, 192 }; vecSelGameDialog.push_back(std::make_unique(selgame_Description, rect3, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark, 1, 16)); switch (value) { case 0: case 1: { title = _("Create Game").data(); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 211), 295, 35 }; vecSelGameDialog.push_back(std::make_unique(_("Select Difficulty").data(), rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); vecSelGameDlgItems.push_back(std::make_unique(_("Normal"), DIFF_NORMAL)); vecSelGameDlgItems.push_back(std::make_unique(_("Nightmare"), DIFF_NIGHTMARE)); vecSelGameDlgItems.push_back(std::make_unique(_("Hell"), DIFF_HELL)); vecSelGameDialog.push_back(std::make_unique(vecSelGameDlgItems, vecSelGameDlgItems.size(), uiPosition.x + 300, (uiPosition.y + 282), 295, 26, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect5, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 449), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("CANCEL"), &UiFocusNavigationEsc, rect6, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(selgame_Diff_Focus, selgame_Diff_Select, selgame_Diff_Esc, vecSelGameDialog, true); break; } case 2: { selgame_Title = fmt::format(fmt::runtime(_("Join {:s} Games")), _(ConnectionNames[provider])); title = selgame_Title.c_str(); const char *inputHint; if (provider == SELCONN_ZT) { inputHint = _("Enter Game ID").data(); } else { inputHint = _("Enter address").data(); } const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 305), (Sint16)(uiPosition.y + 211), 285, 33 }; vecSelGameDialog.push_back(std::make_unique(inputHint, rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 305), (Sint16)(uiPosition.y + 314), 285, 33 }; vecSelGameDialog.push_back(std::make_unique(inputHint, selgame_Ip, 128, false, rect5, UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect6, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect7 = { (Sint16)(uiPosition.x + 449), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("CANCEL"), &UiFocusNavigationEsc, rect7, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); HighlightedItem = 0; #ifdef PACKET_ENCRYPTION UiInitList(nullptr, selgame_Password_Init, selgame_GameSelection_Init, vecSelGameDialog); #else UiInitList(nullptr, selgame_Password_Select, selgame_GameSelection_Init, vecSelGameDialog); #endif break; } } } void selgame_GameSelection_Esc() { UiInitList_clear(); selgame_enteringGame = false; selgame_endMenu = true; } void selgame_Diff_Focus(size_t value) { switch (vecSelGameDlgItems[value]->m_value) { case DIFF_NORMAL: CopyUtf8(selgame_Label, _("Normal"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Normal Difficulty\nThis is where a starting character should begin the quest to defeat Diablo."), sizeof(selgame_Description)); break; case DIFF_NIGHTMARE: CopyUtf8(selgame_Label, _("Nightmare"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Nightmare Difficulty\nThe denizens of the Labyrinth have been bolstered and will prove to be a greater challenge. This is recommended for experienced characters only."), sizeof(selgame_Description)); break; case DIFF_HELL: CopyUtf8(selgame_Label, _("Hell"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."), sizeof(selgame_Description)); break; } CopyUtf8(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH), sizeof(selgame_Description)); } bool IsDifficultyAllowed(int value) { if (value == 0 || (value == 1 && heroLevel >= 20) || (value == 2 && heroLevel >= 30)) { return true; } selgame_Free(); if (value == 1) UiSelOkDialog(title, _("Your character must reach level 20 before you can enter a multiplayer game of Nightmare difficulty.").data(), false); if (value == 2) UiSelOkDialog(title, _("Your character must reach level 30 before you can enter a multiplayer game of Hell difficulty.").data(), false); selgame_Init(); return false; } void selgame_Diff_Select(size_t value) { if (selhero_isMultiPlayer && !IsDifficultyAllowed(vecSelGameDlgItems[value]->m_value)) { selgame_GameSelection_Select(selgame_selectedGame); return; } nDifficulty = (_difficulty)vecSelGameDlgItems[value]->m_value; if (!selhero_isMultiPlayer) { // This is part of a dangerous hack to enable difficulty selection in single-player. // FIXME: Dialogs should not refer to each other's variables. // We're in the selhero loop instead of the selgame one. // Free the selgame data and flag the end of the selhero loop. selhero_endMenu = true; // We only call FreeVectors because ArtBackground.Unload() // will be called by selheroFree(). selgame_FreeVectors(); // We must clear the InitList because selhero's loop will perform // one more iteration after this function exits. UiInitList_clear(); return; } selgame_GameSpeedSelection(); } void selgame_Diff_Esc() { if (!selhero_isMultiPlayer) { selgame_Free(); selhero_Init(); selhero_List_Init(); return; } if (provider == SELCONN_LOOPBACK) { selgame_GameSelection_Esc(); return; } HighlightedItem = 0; selgame_GameSelection_Init(); } void selgame_GameSpeedSelection() { gfnHeroInfo(UpdateHeroLevel); selgame_FreeVectors(); UiAddBackground(&vecSelGameDialog); UiAddLogo(&vecSelGameDialog); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelGameDialog.push_back(std::make_unique(_("Create Game").data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 34), (Sint16)(uiPosition.y + 211), 205, 33 }; vecSelGameDialog.push_back(std::make_unique(selgame_Label, rect2, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 256), DESCRIPTION_WIDTH, 192 }; vecSelGameDialog.push_back(std::make_unique(selgame_Description, rect3, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark, 1, 16)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 211), 295, 35 }; vecSelGameDialog.push_back(std::make_unique(_("Select Game Speed").data(), rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); vecSelGameDlgItems.push_back(std::make_unique(_("Normal"), 20)); vecSelGameDlgItems.push_back(std::make_unique(_("Fast"), 30)); vecSelGameDlgItems.push_back(std::make_unique(_("Faster"), 40)); vecSelGameDlgItems.push_back(std::make_unique(_("Fastest"), 50)); vecSelGameDialog.push_back(std::make_unique(vecSelGameDlgItems, vecSelGameDlgItems.size(), uiPosition.x + 300, (uiPosition.y + 279), 295, 26, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect5, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 449), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("CANCEL"), &UiFocusNavigationEsc, rect6, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(selgame_Speed_Focus, selgame_Speed_Select, selgame_Speed_Esc, vecSelGameDialog, true); } void selgame_Speed_Focus(size_t value) { switch (vecSelGameDlgItems[value]->m_value) { case 20: CopyUtf8(selgame_Label, _("Normal"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Normal Speed\nThis is where a starting character should begin the quest to defeat Diablo."), sizeof(selgame_Description)); break; case 30: CopyUtf8(selgame_Label, _("Fast"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Fast Speed\nThe denizens of the Labyrinth have been hastened and will prove to be a greater challenge. This is recommended for experienced characters only."), sizeof(selgame_Description)); break; case 40: CopyUtf8(selgame_Label, _("Faster"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Faster Speed\nMost monsters of the dungeon will seek you out quicker than ever before. Only an experienced champion should try their luck at this speed."), sizeof(selgame_Description)); break; case 50: CopyUtf8(selgame_Label, _("Fastest"), sizeof(selgame_Label)); CopyUtf8(selgame_Description, _("Fastest Speed\nThe minions of the underworld will rush to attack without hesitation. Only a true speed demon should enter at this pace."), sizeof(selgame_Description)); break; } CopyUtf8(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH), sizeof(selgame_Description)); } void selgame_Speed_Esc() { selgame_GameSelection_Select(selgame_selectedGame); } void selgame_Speed_Select(size_t value) { nTickRate = vecSelGameDlgItems[value]->m_value; if (provider == SELCONN_LOOPBACK || selgame_selectedGame == 1) { selgame_Password_Select(0); return; } selgame_Password_Init(0); } void selgame_Password_Init(size_t /*value*/) { memset(&selgame_Password, 0, sizeof(selgame_Password)); selgame_FreeVectors(); UiAddBackground(&vecSelGameDialog); UiAddLogo(&vecSelGameDialog); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelGameDialog.push_back(std::make_unique(_(ConnectionNames[provider]).data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 211), 205, 192 }; vecSelGameDialog.push_back(std::make_unique(_("Description:").data(), rect2, UiFlags::FontSize24 | UiFlags::ColorUiSilver)); const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 35), (Sint16)(uiPosition.y + 256), DESCRIPTION_WIDTH, 192 }; vecSelGameDialog.push_back(std::make_unique(selgame_Description, rect3, UiFlags::FontSize12 | UiFlags::ColorUiSilverDark, 1, 16)); const SDL_Rect rect4 = { (Sint16)(uiPosition.x + 305), (Sint16)(uiPosition.y + 211), 285, 33 }; vecSelGameDialog.push_back(std::make_unique(_("Enter Password").data(), rect4, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); // Allow password to be empty only when joining games const bool allowEmpty = selgame_selectedGame == 2; const SDL_Rect rect5 = { (Sint16)(uiPosition.x + 305), (Sint16)(uiPosition.y + 314), 285, 33 }; vecSelGameDialog.push_back(std::make_unique(_("Enter Password"), selgame_Password, 15, allowEmpty, rect5, UiFlags::FontSize24 | UiFlags::ColorUiGold)); const SDL_Rect rect6 = { (Sint16)(uiPosition.x + 299), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect6, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); const SDL_Rect rect7 = { (Sint16)(uiPosition.x + 449), (Sint16)(uiPosition.y + 427), 140, 35 }; vecSelGameDialog.push_back(std::make_unique(_("CANCEL"), &UiFocusNavigationEsc, rect7, UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); UiInitList(nullptr, selgame_Password_Select, selgame_Password_Esc, vecSelGameDialog); } static bool IsGameCompatibleWithErrorMessage(const GameData &data) { if (IsGameCompatible(data)) return IsDifficultyAllowed(data.nDifficulty); selgame_Free(); const std::string errorMessage = GetErrorMessageIncompatibility(data); UiSelOkDialog(title, errorMessage.c_str(), false); selgame_Init(); return false; } void selgame_Password_Select(size_t /*value*/) { char *gamePassword = nullptr; if (selgame_selectedGame == 0) gamePassword = selgame_Password; if (selgame_selectedGame == 2 && strlen(selgame_Password) > 0) gamePassword = selgame_Password; // If there is an error, the error message won't necessarily be set. // Clear the error so that we display "Unknown network error" // instead of an arbitrary message in that case. SDL_ClearError(); if (selgame_selectedGame > 1) { bool allowJoin = true; if (selgame_selectedGame > 2) allowJoin = IsGameCompatible(Gamelist[selgame_selectedGame - 3].gameData); if (provider == SELCONN_ZT) { for (unsigned int i = 0; i < (sizeof(selgame_Ip) / sizeof(selgame_Ip[0])); i++) { selgame_Ip[i] = (selgame_Ip[i] >= 'A' && selgame_Ip[i] <= 'Z') ? selgame_Ip[i] + 'a' - 'A' : selgame_Ip[i]; } strcpy(GetOptions().Network.szPreviousZTGame, selgame_Ip); } else { strcpy(GetOptions().Network.szPreviousHost, selgame_Ip); } if (allowJoin && SNetJoinGame(selgame_Ip, gamePassword, gdwPlayerId)) { if (!IsGameCompatibleWithErrorMessage(*m_game_data)) { InitGameInfo(); selgame_GameSelection_Select(1); return; } UiInitList_clear(); selgame_endMenu = true; } else { InitGameInfo(); selgame_Free(); std::string error; if (!allowJoin) error = GetErrorMessageIncompatibility(Gamelist[selgame_selectedGame - 3].gameData); else error = SDL_GetError(); if (error.empty()) error = "Unknown network error"; UiSelOkDialog(_("Multi Player Game").data(), error.c_str(), false); selgame_Init(); if (selgame_selectedGame == 2) selgame_Password_Init(selgame_selectedGame); else UiInitGameSelectionList(""); } return; } m_game_data->nDifficulty = nDifficulty; m_game_data->nTickRate = nTickRate; m_game_data->bRunInTown = *GetOptions().Gameplay.runInTown ? 1 : 0; m_game_data->bTheoQuest = *GetOptions().Gameplay.theoQuest ? 1 : 0; m_game_data->bCowQuest = *GetOptions().Gameplay.cowQuest ? 1 : 0; GameData gameInitInfo = *m_game_data; gameInitInfo.swapLE(); if (SNetCreateGame(nullptr, gamePassword, reinterpret_cast(&gameInitInfo), sizeof(gameInitInfo), gdwPlayerId)) { UiInitList_clear(); selgame_endMenu = true; } else { selgame_Free(); std::string error = SDL_GetError(); if (error.empty()) error = "Unknown network error"; UiSelOkDialog(_("Multi Player Game").data(), error.c_str(), false); selgame_Init(); selgame_Password_Init(0); } } void selgame_Password_Esc() { if (selgame_selectedGame == 2) selgame_GameSelection_Select(2); else selgame_GameSpeedSelection(); } void RefreshGameList() { static uint32_t lastRequest = 0; static uint32_t lastUpdate = 0; if (selgame_enteringGame) return; const uint32_t currentTime = SDL_GetTicks(); if ((lastRequest == 0 || currentTime - lastRequest > 30000) && DvlNet_SendInfoRequest()) { lastRequest = currentTime; lastUpdate = currentTime - 3000; // Give 2 sec for responses, but don't wait 5 if (firstPublicGameInfoRequestSend == 0) firstPublicGameInfoRequestSend = currentTime; } if (lastUpdate == 0 || currentTime - lastUpdate > 5000) { const int gameIndex = vecSelGameDlgItems[HighlightedItem]->m_value - 3; const std::string gameSearch = gameIndex >= 0 ? Gamelist[gameIndex].name : ""; std::vector gamelist = DvlNet_GetGamelist(); Gamelist.clear(); for (unsigned i = 0; i < gamelist.size(); i++) { Gamelist.push_back(gamelist[i]); } UiInitGameSelectionList(gameSearch); lastUpdate = currentTime; } } bool UiSelectGame(GameData *gameData, int *playerId) { firstPublicGameInfoRequestSend = 0; gdwPlayerId = playerId; m_game_data = gameData; selgame_Init(); HighlightedItem = 0; selgame_GameSelection_Init(); selgame_endMenu = false; DvlNet_ClearPassword(); DvlNet_ClearGamelist(); while (!selgame_endMenu) { UiClearScreen(); UiPollAndRender(); if (provider == SELCONN_ZT) RefreshGameList(); } selgame_Free(); return selgame_enteringGame; } } // namespace devilution ================================================ FILE: Source/DiabloUI/multi/selgame.h ================================================ #pragma once #include "levels/gendung.h" namespace devilution { extern _difficulty nDifficulty; void selgame_GameSelection_Init(); void selgame_GameSelection_Focus(size_t value); void selgame_GameSelection_Select(size_t value); void selgame_GameSelection_Esc(); void selgame_Diff_Focus(size_t value); void selgame_Diff_Select(size_t value); void selgame_Diff_Esc(); void selgame_GameSpeedSelection(); void selgame_Speed_Focus(size_t value); void selgame_Speed_Select(size_t value); void selgame_Speed_Esc(); void selgame_Password_Init(size_t value); void selgame_Password_Select(size_t value); void selgame_Password_Esc(); } // namespace devilution ================================================ FILE: Source/DiabloUI/progress.cpp ================================================ #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include "DiabloUI/button.h" #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_item.h" #include "controls/input.h" #include "controls/menu_controls.h" #include "engine/clx_sprite.hpp" #include "engine/dx.h" #include "engine/load_pcx.hpp" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" #include "engine/surface.hpp" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/ui_fwd.h" namespace devilution { namespace { OptionalOwnedClxSpriteList ArtPopupSm; OptionalOwnedClxSpriteList ArtProgBG; OptionalOwnedClxSpriteList ProgFil; std::vector> vecProgress; bool endMenu; void DialogActionCancel() { endMenu = true; } void ProgressLoadBackground() { UiLoadBlackBackground(); ArtPopupSm = LoadPcx("ui_art\\spopup"); ArtProgBG = LoadPcx("ui_art\\prog_bg"); } void ProgressLoadForeground() { LoadDialogButtonGraphics(); ProgFil = LoadPcx("ui_art\\prog_fil"); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect3 = { (Sint16)(uiPosition.x + 265), (Sint16)(uiPosition.y + 267), DialogButtonWidth, DialogButtonHeight }; vecProgress.push_back(std::make_unique(_("Cancel"), &DialogActionCancel, rect3)); } void ProgressFreeBackground() { ArtBackground = std::nullopt; ArtPopupSm = std::nullopt; ArtProgBG = std::nullopt; } void ProgressFreeForeground() { vecProgress.clear(); ProgFil = std::nullopt; FreeDialogButtonGraphics(); } Point GetPosition() { return { GetCenterOffset(280), GetCenterOffset(144, gnScreenHeight) }; } void ProgressRenderBackground() { SDL_FillSurfaceRect(DiabloUiSurface(), nullptr, 0); const Surface &out = Surface(DiabloUiSurface()); const Point position = GetPosition(); RenderClxSprite(out.subregion(position.x, position.y, 280, 140), (*ArtPopupSm)[0], { 0, 0 }); RenderClxSprite(out.subregion(GetCenterOffset(227), 0, 227, out.h()), (*ArtProgBG)[0], { 0, position.y + 52 }); } void ProgressRenderForeground(int progress) { const Surface &out = Surface(DiabloUiSurface()); const Point position = GetPosition(); if (progress != 0) { const int x = GetCenterOffset(227); const int w = 227 * progress / 100; RenderClxSprite(out.subregion(x, 0, w, out.h()), (*ProgFil)[0], { 0, position.y + 52 }); } // Not rendering an actual button, only the top 2 rows of its graphics. RenderClxSprite( out.subregion(GetCenterOffset(110), position.y + 99, DialogButtonWidth, 2), ButtonSprite(/*pressed=*/false), { 0, 0 }); } } // namespace bool UiProgressDialog(int (*fnfunc)()) { // Blit the background once and then free it. ProgressLoadBackground(); ProgressRenderBackground(); if (RenderDirectlyToOutputSurface && PalSurface != nullptr) { // Render into all the backbuffers if there are multiple. const void *initialPixels = PalSurface->pixels; UiFadeIn(); while (PalSurface->pixels != initialPixels) { ProgressRenderBackground(); UiFadeIn(); } } ProgressFreeBackground(); ProgressLoadForeground(); endMenu = false; int progress = 0; SDL_Event event; while (!endMenu && progress < 100) { progress = fnfunc(); ProgressRenderForeground(progress); UiRenderItems(vecProgress); DrawMouse(); UiFadeIn(); while (PollEvent(&event)) { switch (event.type) { case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: UiItemMouseEvents(&event, vecProgress); break; case SDL_EVENT_KEY_DOWN: switch (SDLC_EventKey(event)) { #ifndef USE_SDL1 case SDLK_KP_ENTER: #endif case SDLK_ESCAPE: case SDLK_RETURN: case SDLK_SPACE: endMenu = true; break; default: break; } break; default: for (const MenuAction menuAction : GetMenuActions(event)) { if (IsNoneOf(menuAction, MenuAction_BACK, MenuAction_SELECT)) continue; endMenu = true; break; } break; } UiHandleEvents(&event); } } ProgressFreeForeground(); return progress == 100; } } // namespace devilution ================================================ FILE: Source/DiabloUI/scrollbar.cpp ================================================ #include "scrollbar.h" #include #include "engine/clx_sprite.hpp" #include "engine/load_pcx.hpp" namespace devilution { OptionalOwnedClxSpriteList ArtScrollBarBackground; OptionalOwnedClxSpriteList ArtScrollBarThumb; OptionalOwnedClxSpriteList ArtScrollBarArrow; void LoadScrollBar() { ArtScrollBarBackground = LoadPcx("ui_art\\sb_bg"); ArtScrollBarThumb = LoadPcx("ui_art\\sb_thumb"); ArtScrollBarArrow = LoadPcxSpriteList("ui_art\\sb_arrow", 4); } void UnloadScrollBar() { ArtScrollBarArrow = std::nullopt; ArtScrollBarThumb = std::nullopt; ArtScrollBarBackground = std::nullopt; } } // namespace devilution ================================================ FILE: Source/DiabloUI/scrollbar.h ================================================ #pragma once #include #include "DiabloUI/ui_item.h" #include "engine/clx_sprite.hpp" #include "utils/sdl_geometry.h" namespace devilution { extern OptionalOwnedClxSpriteList ArtScrollBarBackground; extern OptionalOwnedClxSpriteList ArtScrollBarThumb; extern OptionalOwnedClxSpriteList ArtScrollBarArrow; constexpr Uint16 ScrollBarBgWidth = 25; enum ScrollBarArrowFrame : uint8_t { ScrollBarArrowFrame_UP_ACTIVE, ScrollBarArrowFrame_UP, ScrollBarArrowFrame_DOWN_ACTIVE, ScrollBarArrowFrame_DOWN, }; constexpr Uint16 ScrollBarArrowWidth = 25; inline SDL_Rect UpArrowRect(const UiScrollbar &bar) { return MakeSdlRect( bar.m_rect.x, bar.m_rect.y, ScrollBarArrowWidth, bar.m_arrow[0].height()); } inline SDL_Rect DownArrowRect(const UiScrollbar &bar) { return MakeSdlRect( bar.m_rect.x, bar.m_rect.y + bar.m_rect.h - bar.m_arrow[0].height(), ScrollBarArrowWidth, bar.m_arrow[0].height()); } inline Uint16 BarHeight(const UiScrollbar &bar) { return bar.m_rect.h - 2 * bar.m_arrow[0].height(); } inline SDL_Rect BarRect(const UiScrollbar &bar) { return MakeSdlRect( bar.m_rect.x, bar.m_rect.y + bar.m_arrow[0].height(), ScrollBarArrowWidth, BarHeight(bar)); } inline SDL_Rect ThumbRect(const UiScrollbar &bar, size_t selectedIndex, size_t numItems) { constexpr int ThumbOffsetX = 3; const int thumbMaxY = BarHeight(bar) - bar.m_thumb.height(); const int thumbY = static_cast(selectedIndex * thumbMaxY / (numItems - 1)); return MakeSdlRect( bar.m_rect.x + ThumbOffsetX, bar.m_rect.y + bar.m_arrow[0].height() + thumbY, bar.m_rect.w - ThumbOffsetX, bar.m_thumb.height()); } void LoadScrollBar(); void UnloadScrollBar(); } // namespace devilution ================================================ FILE: Source/DiabloUI/selok.cpp ================================================ #include "DiabloUI/selok.h" #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/point.hpp" #include "engine/render/text_render.hpp" #include "game_mode.hpp" #include "utils/language.h" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { namespace { char dialogText[256]; } // namespace bool selok_endMenu; std::vector> vecSelOkDialogItems; std::vector> vecSelOkDialog; #define MESSAGE_WIDTH 400 void selok_Free() { ArtBackground = std::nullopt; vecSelOkDialogItems.clear(); vecSelOkDialog.clear(); } void selok_Select(size_t /*value*/) { selok_endMenu = true; } void selok_Esc() { selok_endMenu = true; } void UiSelOkDialog(const char *title, const char *body, bool background) { if (!background) { UiLoadBlackBackground(); } else { if (!gbIsSpawn) { LoadBackgroundArt("ui_art\\mainmenu"); } else { LoadBackgroundArt("ui_art\\swmmenu"); } } UiAddBackground(&vecSelOkDialog); UiAddLogo(&vecSelOkDialog); const Point uiPosition = GetUIRectangle().position; if (title != nullptr) { const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelOkDialog.push_back(std::make_unique(title, rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 140), (Sint16)(uiPosition.y + 210), 560, 168 }; vecSelOkDialog.push_back(std::make_unique(dialogText, rect2, UiFlags::FontSize24 | UiFlags::ColorUiSilver)); } else { const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 140), (Sint16)(uiPosition.y + 197), 560, 168 }; vecSelOkDialog.push_back(std::make_unique(dialogText, rect1, UiFlags::FontSize24 | UiFlags::ColorUiSilver)); } vecSelOkDialogItems.push_back(std::make_unique(_("OK"), 0)); vecSelOkDialog.push_back(std::make_unique(vecSelOkDialogItems, 1, uiPosition.x + 230, (uiPosition.y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); CopyUtf8(dialogText, WordWrapString(body, MESSAGE_WIDTH, GameFont24), sizeof(dialogText)); UiInitList(nullptr, selok_Select, selok_Esc, vecSelOkDialog, false); selok_endMenu = false; while (!selok_endMenu) { UiClearScreen(); UiRenderItems(vecSelOkDialog); UiPollAndRender(); } selok_Free(); } } // namespace devilution ================================================ FILE: Source/DiabloUI/selok.h ================================================ #pragma once namespace devilution { void UiSelOkDialog(const char *title, const char *body, bool background); void selok_Free(); void selok_Select(int value); void selok_Esc(); } // namespace devilution ================================================ FILE: Source/DiabloUI/selstart.cpp ================================================ #include "selstart.h" #include #include #include #include #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/load_clx.hpp" #include "engine/point.hpp" #include "options.h" #include "utils/language.h" #include "utils/ui_fwd.h" namespace devilution { namespace { bool endMenu; std::vector> vecDialogItems; std::vector> vecDialog; void ItemSelected(size_t value) { auto option = static_cast(vecDialogItems[value]->m_value); GetOptions().GameMode.gameMode.SetValue(option); GetOptions().Mods.SetHellfireEnabled(option == StartUpGameMode::Hellfire); SaveOptions(); endMenu = true; } void EscPressed() { endMenu = true; } } // namespace void UiSelStartUpGameOption() { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\mainmenuw.clx"); LoadBackgroundArt("ui_art\\mainmenu"); UiAddBackground(&vecDialog); UiAddLogo(&vecDialog); const Point uiPosition = GetUIRectangle().position; vecDialogItems.push_back(std::make_unique(_("Enter Hellfire"), static_cast(StartUpGameMode::Hellfire))); vecDialogItems.push_back(std::make_unique(_("Switch to Diablo"), static_cast(StartUpGameMode::Diablo))); vecDialog.push_back(std::make_unique(vecDialogItems, vecDialogItems.size(), uiPosition.x + 64, uiPosition.y + 240, 510, 43, UiFlags::AlignCenter | UiFlags::FontSize42 | UiFlags::ColorUiGold, 5)); UiInitList(nullptr, ItemSelected, EscPressed, vecDialog, true); endMenu = false; while (!endMenu) { UiClearScreen(); UiRenderItems(vecDialog); UiPollAndRender(); } ArtBackground = std::nullopt; ArtBackgroundWidescreen = std::nullopt; vecDialogItems.clear(); vecDialog.clear(); } } // namespace devilution ================================================ FILE: Source/DiabloUI/selstart.h ================================================ #pragma once namespace devilution { void UiSelStartUpGameOption(); } // namespace devilution ================================================ FILE: Source/DiabloUI/selyesno.cpp ================================================ #include "selyesno.h" #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "engine/point.hpp" #include "engine/render/text_render.hpp" #include "utils/language.h" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { namespace { bool selyesno_endMenu; bool selyesno_value; char selyesno_confirmationMessage[256]; std::vector> vecSelYesNoDialogItems; std::vector> vecSelYesNoDialog; #define MESSAGE_WIDTH 400 void SelyesnoFree() { ArtBackground = std::nullopt; vecSelYesNoDialogItems.clear(); vecSelYesNoDialog.clear(); } void SelyesnoSelect(size_t value) { selyesno_value = vecSelYesNoDialogItems[value]->m_value == 0; selyesno_endMenu = true; } void SelyesnoEsc() { selyesno_value = false; selyesno_endMenu = true; } } // namespace bool UiSelHeroYesNoDialog(const char *title, const char *body) { UiLoadBlackBackground(); UiAddBackground(&vecSelYesNoDialog); UiAddLogo(&vecSelYesNoDialog); const Point uiPosition = GetUIRectangle().position; const SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelYesNoDialog.push_back(std::make_unique(title, rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); const SDL_Rect rect2 = { (Sint16)(uiPosition.x + 120), (Sint16)(uiPosition.y + 236), MESSAGE_WIDTH, 168 }; vecSelYesNoDialog.push_back(std::make_unique(selyesno_confirmationMessage, rect2, UiFlags::FontSize24 | UiFlags::ColorUiSilver)); vecSelYesNoDialogItems.push_back(std::make_unique(_("Yes"), 0)); vecSelYesNoDialogItems.push_back(std::make_unique(_("No"), 1)); vecSelYesNoDialog.push_back(std::make_unique(vecSelYesNoDialogItems, vecSelYesNoDialogItems.size(), uiPosition.x + 230, (uiPosition.y + 390), 180, 35, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); CopyUtf8(selyesno_confirmationMessage, WordWrapString(body, MESSAGE_WIDTH, GameFont24), sizeof(selyesno_confirmationMessage)); UiInitList(nullptr, SelyesnoSelect, SelyesnoEsc, vecSelYesNoDialog, true); selyesno_value = true; selyesno_endMenu = false; while (!selyesno_endMenu) { UiClearScreen(); UiRenderItems(vecSelYesNoDialog); UiPollAndRender(); } SelyesnoFree(); return selyesno_value; } } // namespace devilution ================================================ FILE: Source/DiabloUI/selyesno.h ================================================ #pragma once namespace devilution { bool UiSelHeroYesNoDialog(const char *title, const char *body); } // namespace devilution ================================================ FILE: Source/DiabloUI/settingsmenu.cpp ================================================ #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "DiabloUI/scrollbar.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "controls/controller.h" #include "controls/controller_buttons.h" #include "controls/controller_motion.h" #include "controls/plrctrls.h" #include "controls/remap_keyboard.h" #include "engine/assets.hpp" #include "engine/rectangle.hpp" #include "engine/render/text_render.hpp" #include "game_mode.hpp" #include "hwcursor.hpp" #include "items.h" #include "options.h" #include "utils/enum_traits.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" namespace devilution { namespace { constexpr size_t IndexKeyOrPadInput = 1; constexpr size_t IndexPadTimerText = 2; bool endMenu = false; bool backToMain = false; std::vector> vecDialogItems; std::vector> vecDialog; std::vector vecOptions; OptionCategoryBase *selectedCategory = nullptr; OptionEntryBase *selectedOption = nullptr; enum class ShownMenuType : uint8_t { Categories, Settings, ListOption, KeyInput, PadInput, }; ShownMenuType shownMenu; char optionDescription[512]; Rectangle rectList; Rectangle rectDescription; enum class SpecialMenuEntry : int8_t { None = -1, PreviousMenu = -2, UnbindKey = -3, BindPadButton = -4, UnbindPadButton = -5, }; ControllerButtonCombo padEntryCombo {}; Uint32 padEntryStartTime = 0; std::string padEntryTimerText; bool IsValidEntry(OptionEntryBase *pOptionEntry) { auto flags = pOptionEntry->GetFlags(); if (HasAnyOf(flags, OptionEntryFlags::NeedDiabloMpq) && !HaveIntro()) return false; return HasNoneOf(flags, OptionEntryFlags::Invisible | (gbIsHellfire ? OptionEntryFlags::OnlyDiablo : OptionEntryFlags::OnlyHellfire)); } std::vector CreateDrawStringFormatArgForEntry(OptionEntryBase *pEntry) { return std::vector { { pEntry->GetName(), UiFlags::ColorUiGold }, { pEntry->GetValueDescription(), UiFlags::ColorUiSilver } }; } /** @brief Check if the option text can't fit in one list line (list width minus drawn selector) */ bool NeedsTwoLinesToDisplayOption(std::vector &formatArgs) { return GetLineWidth("{}: {}", formatArgs.data(), formatArgs.size(), 0, GameFontTables::GameFont24, 1) >= (rectList.size.width - 90); } void CleanUpSettingsUI() { UiInitList_clear(); vecDialogItems.clear(); vecDialog.clear(); vecOptions.clear(); ArtBackground = std::nullopt; ArtBackgroundWidescreen = std::nullopt; UnloadScrollBar(); } void GoBackOneMenuLevel() { endMenu = true; switch (shownMenu) { case ShownMenuType::Categories: backToMain = true; break; case ShownMenuType::Settings: shownMenu = ShownMenuType::Categories; break; default: shownMenu = ShownMenuType::Settings; break; } } void StartPadEntryTimer() { padEntryCombo = ControllerButton_NONE; padEntryStartTime = SDL_GetTicks(); if (padEntryStartTime == 0) padEntryStartTime++; // Removes access to these dialog items while entering bindings for (size_t i = IndexPadTimerText + 1; i < vecDialogItems.size(); i++) vecDialogItems[i]->uiFlags |= UiFlags::ElementHidden; } void StopPadEntryTimer() { padEntryCombo = ControllerButton_NONE; padEntryStartTime = 0; padEntryTimerText = ""; vecDialogItems[IndexPadTimerText]->m_text = padEntryTimerText; // Restores access to these dialog items after binding is complete for (size_t i = IndexPadTimerText + 1; i < vecDialogItems.size(); i++) vecDialogItems[i]->uiFlags &= ~UiFlags::ElementHidden; } void UpdatePadEntryTimerText() { if (shownMenu != ShownMenuType::PadInput) return; const Uint32 elapsed = SDL_GetTicks() - padEntryStartTime; if (padEntryStartTime == 0 || elapsed > 10000) { StopPadEntryTimer(); return; } padEntryTimerText = StrCat(_("Press gamepad buttons to change."), " ", 10 - elapsed / 1000); vecDialogItems[IndexPadTimerText]->m_text = padEntryTimerText; } void UpdateDescription(const OptionEntryBase &option) { auto paragraphs = WordWrapString(option.GetDescription(), rectDescription.size.width, GameFont12, 1); CopyUtf8(optionDescription, paragraphs, sizeof(optionDescription)); } void UpdateDescription(const OptionCategoryBase &category) { auto paragraphs = WordWrapString(category.GetDescription(), rectDescription.size.width, GameFont12, 1); CopyUtf8(optionDescription, paragraphs, sizeof(optionDescription)); } void ItemFocused(size_t value) { switch (shownMenu) { case ShownMenuType::Categories: { auto &vecItem = vecDialogItems[value]; optionDescription[0] = '\0'; if (vecItem->m_value < 0) return; auto *pCategory = GetOptions().GetCategories()[vecItem->m_value]; UpdateDescription(*pCategory); } break; case ShownMenuType::Settings: { auto &vecItem = vecDialogItems[value]; optionDescription[0] = '\0'; if (vecItem->m_value < 0) return; auto *pOption = vecOptions[vecItem->m_value]; UpdateDescription(*pOption); } break; default: break; } } bool ChangeOptionValue(OptionEntryBase *pOption, size_t listIndex) { if (HasAnyOf(pOption->GetFlags(), OptionEntryFlags::RecreateUI)) { endMenu = true; // Clean up all UI related Data CleanUpSettingsUI(); UnloadUiGFX(); FreeItemGFX(); selectedOption = pOption; } switch (pOption->GetType()) { case OptionEntryType::Boolean: { auto *pOptionBoolean = static_cast(pOption); pOptionBoolean->SetValue(!**pOptionBoolean); } break; case OptionEntryType::List: { auto *pOptionList = static_cast(pOption); pOptionList->SetActiveListIndex(listIndex); } break; case OptionEntryType::Key: case OptionEntryType::PadButton: break; } if (HasAnyOf(pOption->GetFlags(), OptionEntryFlags::RecreateUI)) { // Reinitialize UI with changed settings (for example game mode, language or resolution) UiInitialize(); InitItemGFX(); SetHardwareCursor(CursorInfo::UnknownCursor()); return false; } return true; } void ItemSelected(size_t value) { auto &vecItem = vecDialogItems[value]; const int vecItemValue = vecItem->m_value; if (vecItemValue < 0) { auto specialMenuEntry = static_cast(vecItemValue); switch (specialMenuEntry) { case SpecialMenuEntry::None: break; case SpecialMenuEntry::PreviousMenu: GoBackOneMenuLevel(); break; case SpecialMenuEntry::UnbindKey: { auto *pOptionKey = static_cast(selectedOption); pOptionKey->SetValue(SDLK_UNKNOWN); vecDialogItems[IndexKeyOrPadInput]->m_text = selectedOption->GetValueDescription(); break; } case SpecialMenuEntry::BindPadButton: StartPadEntryTimer(); break; case SpecialMenuEntry::UnbindPadButton: auto *pOptionPad = static_cast(selectedOption); pOptionPad->SetValue(ControllerButton_NONE); vecDialogItems[IndexKeyOrPadInput]->m_text = selectedOption->GetValueDescription(); break; } return; } switch (shownMenu) { case ShownMenuType::Categories: { selectedCategory = GetOptions().GetCategories()[vecItemValue]; endMenu = true; shownMenu = ShownMenuType::Settings; } break; case ShownMenuType::Settings: { auto *pOption = vecOptions[vecItemValue]; bool updateValueDescription = false; if (pOption->GetType() == OptionEntryType::List) { auto *pOptionList = static_cast(pOption); if (pOptionList->GetListSize() > 2) { selectedOption = pOption; endMenu = true; shownMenu = ShownMenuType::ListOption; } else { // If the list contains only two items, we don't show a submenu and instead change the option value instantly size_t nextIndex = pOptionList->GetActiveListIndex() + 1; if (nextIndex >= pOptionList->GetListSize()) nextIndex = 0; updateValueDescription = ChangeOptionValue(pOption, nextIndex); } } else if (pOption->GetType() == OptionEntryType::Key) { selectedOption = pOption; endMenu = true; shownMenu = ShownMenuType::KeyInput; } else if (pOption->GetType() == OptionEntryType::PadButton) { selectedOption = pOption; endMenu = true; shownMenu = ShownMenuType::PadInput; } else { updateValueDescription = ChangeOptionValue(pOption, 0); } if (updateValueDescription) { auto args = CreateDrawStringFormatArgForEntry(pOption); const bool optionUsesTwoLines = ((value + 1) < vecDialogItems.size() && vecDialogItems[value]->m_value == vecDialogItems[value + 1]->m_value); if (NeedsTwoLinesToDisplayOption(args) != optionUsesTwoLines) { selectedOption = pOption; endMenu = true; } else { vecItem->args.clear(); for (auto &arg : args) vecItem->args.push_back(arg); if (optionUsesTwoLines) { vecDialogItems[value + 1]->m_text = std::string(pOption->GetValueDescription()); } } } } break; case ShownMenuType::ListOption: { ChangeOptionValue(selectedOption, vecItemValue); GoBackOneMenuLevel(); } break; case ShownMenuType::KeyInput: case ShownMenuType::PadInput: break; } } void EscPressed() { GoBackOneMenuLevel(); } void FullscreenChanged() { auto *fullscreenOption = &GetOptions().Graphics.fullscreen; for (auto &vecItem : vecDialogItems) { const int vecItemValue = vecItem->m_value; if (vecItemValue < 0 || static_cast(vecItemValue) >= vecOptions.size()) continue; auto *pOption = vecOptions[vecItemValue]; if (pOption != fullscreenOption) continue; vecItem->args.clear(); for (auto &arg : CreateDrawStringFormatArgForEntry(pOption)) vecItem->args.push_back(arg); break; } } } // namespace void UiSettingsMenu() { backToMain = false; shownMenu = ShownMenuType::Categories; selectedCategory = nullptr; selectedOption = nullptr; do { endMenu = false; // For the settings menu, we use the full height and allow some more width. const int uiWidth = std::clamp(gnScreenWidth, 640, 720); const Rectangle uiRectangle = { { (gnScreenWidth - uiWidth) / 2, 0 }, { uiWidth, gnScreenHeight } }; UiLoadBlackBackground(); LoadScrollBar(); UiAddBackground(&vecDialog); UiAddLogo(&vecDialog, uiRectangle.position.y); const int descriptionLineHeight = IsSmallFontTall() ? 20 : 18; const int descriptionMarginTop = IsSmallFontTall() ? 10 : 16; optionDescription[0] = '\0'; std::string_view titleText; switch (shownMenu) { case ShownMenuType::Categories: titleText = _("Settings"); break; case ShownMenuType::Settings: titleText = selectedCategory->GetName(); break; default: titleText = selectedOption->GetName(); break; } vecDialog.push_back(std::make_unique(titleText.data(), MakeSdlRect(uiRectangle.position.x, uiRectangle.position.y + 161, uiRectangle.size.width, 35), UiFlags::FontSize30 | UiFlags::ColorUiSilver | UiFlags::AlignCenter, 8)); size_t itemToSelect = 0; std::optional> eventHandler; switch (shownMenu) { case ShownMenuType::Categories: { size_t catIndex = 0; for (OptionCategoryBase *pCategory : GetOptions().GetCategories()) { for (OptionEntryBase *pEntry : pCategory->GetEntries()) { if (!IsValidEntry(pEntry)) continue; if (selectedCategory == pCategory) itemToSelect = vecDialogItems.size(); vecDialogItems.push_back(std::make_unique(pCategory->GetName(), static_cast(catIndex), UiFlags::ColorUiGold)); break; } catIndex++; } } break; case ShownMenuType::Settings: { for (OptionEntryBase *pEntry : selectedCategory->GetEntries()) { if (!IsValidEntry(pEntry)) continue; if (selectedOption == pEntry) itemToSelect = vecDialogItems.size(); auto formatArgs = CreateDrawStringFormatArgForEntry(pEntry); const int optionId = static_cast(vecOptions.size()); if (NeedsTwoLinesToDisplayOption(formatArgs)) { vecDialogItems.push_back(std::make_unique(std::string_view("{}:"), formatArgs, optionId, UiFlags::ColorUiGold | UiFlags::NeedsNextElement)); vecDialogItems.push_back(std::make_unique(std::string(pEntry->GetValueDescription()), optionId, UiFlags::ColorUiSilver | UiFlags::ElementDisabled)); } else { vecDialogItems.push_back(std::make_unique(std::string_view("{}: {}"), formatArgs, optionId, UiFlags::ColorUiGold)); } vecOptions.push_back(pEntry); } } break; case ShownMenuType::ListOption: { auto *pOptionList = static_cast(selectedOption); for (size_t i = 0; i < pOptionList->GetListSize(); i++) { vecDialogItems.push_back(std::make_unique(pOptionList->GetListDescription(i), static_cast(i), UiFlags::ColorUiGold)); } itemToSelect = pOptionList->GetActiveListIndex(); UpdateDescription(*pOptionList); } break; case ShownMenuType::KeyInput: { vecDialogItems.push_back(std::make_unique(_("Bound key:"), static_cast(SpecialMenuEntry::None), UiFlags::ColorWhitegold | UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(std::string(selectedOption->GetValueDescription()), static_cast(SpecialMenuEntry::None), UiFlags::ColorUiGold)); assert(IndexKeyOrPadInput == vecDialogItems.size() - 1); itemToSelect = IndexKeyOrPadInput; eventHandler = [](SDL_Event &event) { if (SelectedItem != IndexKeyOrPadInput) return false; uint32_t key = SDLK_UNKNOWN; switch (event.type) { case SDL_EVENT_KEY_DOWN: { SDL_Keycode keycode = SDLC_EventKey(event); remap_keyboard_key(&keycode); key = static_cast(keycode); if (key >= SDLK_A && key <= SDLK_Z) { key -= 'a' - 'A'; } } break; case SDL_EVENT_MOUSE_BUTTON_DOWN: switch (event.button.button) { case SDL_BUTTON_MIDDLE: case SDL_BUTTON_X1: case SDL_BUTTON_X2: key = event.button.button | KeymapperMouseButtonMask; break; } break; #if SDL_VERSION_ATLEAST(2, 0, 0) case SDL_EVENT_MOUSE_WHEEL: if (SDLC_EventWheelIntY(event) > 0) { key = MouseScrollUpButton; } else if (SDLC_EventWheelIntY(event) < 0) { key = MouseScrollDownButton; } else if (SDLC_EventWheelIntX(event) > 0) { key = MouseScrollLeftButton; } else if (SDLC_EventWheelIntX(event) < 0) { key = MouseScrollRightButton; } break; #endif } // Ignore unknown keys if (key == SDLK_UNKNOWN) return false; auto *pOptionKey = static_cast(selectedOption); if (!pOptionKey->SetValue(key)) return false; vecDialogItems[IndexKeyOrPadInput]->m_text = selectedOption->GetValueDescription(); return true; }; vecDialogItems.push_back(std::make_unique(_("Press any key to change."), static_cast(SpecialMenuEntry::None), UiFlags::ColorUiSilver | UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(std::string_view {}, static_cast(SpecialMenuEntry::None), UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(_("Unbind key"), static_cast(SpecialMenuEntry::UnbindKey), UiFlags::ColorUiGold)); UpdateDescription(*selectedOption); } break; case ShownMenuType::PadInput: { vecDialogItems.push_back(std::make_unique(_("Bound button combo:"), static_cast(SpecialMenuEntry::None), UiFlags::ColorWhitegold | UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(selectedOption->GetValueDescription(), static_cast(SpecialMenuEntry::BindPadButton), UiFlags::ColorUiGold)); assert(IndexKeyOrPadInput == vecDialogItems.size() - 1); itemToSelect = IndexKeyOrPadInput; vecDialogItems.push_back(std::make_unique(std::string_view(padEntryTimerText), static_cast(SpecialMenuEntry::None), UiFlags::ColorUiSilver | UiFlags::ElementDisabled)); assert(IndexPadTimerText == vecDialogItems.size() - 1); vecDialogItems.push_back(std::make_unique(std::string_view {}, static_cast(SpecialMenuEntry::None), UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(_("Unbind button combo"), static_cast(SpecialMenuEntry::UnbindPadButton), UiFlags::ColorUiGold)); padEntryStartTime = 0; eventHandler = [](SDL_Event &event) { if (padEntryStartTime == 0) return false; const StaticVector ctrlEvents = ToControllerButtonEvents(event); for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { const bool isGamepadMotion = IsControllerMotion(event); DetectInputMethod(event, ctrlEvent); if (event.type == SDL_EVENT_KEY_UP && SDLC_EventKey(event) == SDLK_ESCAPE) { StopPadEntryTimer(); return true; } if (isGamepadMotion || IsAnyOf(ctrlEvent.button, ControllerButton_NONE, ControllerButton_IGNORE)) { continue; } const bool modifierPressed = padEntryCombo.modifier != ControllerButton_NONE && IsControllerButtonPressed(padEntryCombo.modifier); const bool buttonPressed = padEntryCombo.button != ControllerButton_NONE && IsControllerButtonPressed(padEntryCombo.button); if (ctrlEvent.up) { // When the player has released all relevant inputs, assume the binding is finished and stop the timer if (padEntryCombo.button != ControllerButton_NONE && !modifierPressed && !buttonPressed) { StopPadEntryTimer(); return true; } continue; } auto *pOptionPad = static_cast(selectedOption); if (!modifierPressed && buttonPressed) padEntryCombo.modifier = padEntryCombo.button; padEntryCombo.button = ctrlEvent.button; if (pOptionPad->SetValue(padEntryCombo)) vecDialogItems[IndexKeyOrPadInput]->m_text = selectedOption->GetValueDescription(); } return true; }; UpdateDescription(*selectedOption); } break; } vecDialogItems.push_back(std::make_unique(std::string_view {}, static_cast(SpecialMenuEntry::None), UiFlags::ElementDisabled)); vecDialogItems.push_back(std::make_unique(_("Previous Menu"), static_cast(SpecialMenuEntry::PreviousMenu), UiFlags::ColorUiGold)); constexpr int ListItemHeight = 26; rectList = { uiRectangle.position + Displacement { 50, 204 }, Size { uiRectangle.size.width - 100, std::min(static_cast(vecDialogItems.size()) * ListItemHeight, uiRectangle.size.height - 272) } }; rectDescription = { rectList.position + Displacement { -26, rectList.size.height + descriptionMarginTop }, Size { uiRectangle.size.width - 50, 80 - descriptionMarginTop } }; vecDialog.push_back(std::make_unique((*ArtScrollBarBackground)[0], (*ArtScrollBarThumb)[0], *ArtScrollBarArrow, MakeSdlRect(rectList.position.x + rectList.size.width + 5, rectList.position.y, 25, rectList.size.height))); vecDialog.push_back(std::make_unique(optionDescription, MakeSdlRect(rectDescription), UiFlags::FontSize12 | UiFlags::ColorUiSilverDark | UiFlags::AlignCenter, 1, descriptionLineHeight)); vecDialog.push_back(std::make_unique(vecDialogItems, rectList.size.height / ListItemHeight, rectList.position.x, rectList.position.y, rectList.size.width, ListItemHeight, UiFlags::FontSize24 | UiFlags::AlignCenter)); UiInitList(ItemFocused, ItemSelected, EscPressed, vecDialog, true, FullscreenChanged, nullptr, itemToSelect); while (!endMenu) { UiClearScreen(); UpdatePadEntryTimerText(); UiPollAndRender(eventHandler); } CleanUpSettingsUI(); } while (!backToMain); SaveOptions(); } } // namespace devilution ================================================ FILE: Source/DiabloUI/settingsmenu.h ================================================ #pragma once #include "DiabloUI/diabloui.h" namespace devilution { void UiSettingsMenu(); } // namespace devilution ================================================ FILE: Source/DiabloUI/support_lines.cpp ================================================ #include #include "DiabloUI/support_lines.h" #include "utils/language.h" namespace devilution { const char *const SupportLines[] = { "", N_("We maintain a chat server at Discord.gg/devilutionx Follow the links to join our community where we talk about things related to Diablo, and the Hellfire expansion."), "", N_("DevilutionX is maintained by Diasurgical, issues and bugs can be reported at this address: https://github.com/diasurgical/devilutionX To help us better serve you, please be sure to include the version number, operating system, and the nature of the problem."), "", "", N_("Disclaimer:"), N_(" DevilutionX is not supported or maintained by Blizzard Entertainment, nor GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified the quality or compatibility of DevilutionX. All inquiries regarding DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment or GOG.com."), "", "", N_(" This port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto which are licensed under the SIL Open Font License, as well as Twitmoji which is licensed under CC-BY 4.0. The port also makes use of SDL which is licensed under the zlib-license. See the ReadMe for further details."), "", "", }; const std::size_t SupportLinesSize = sizeof(SupportLines) / sizeof(SupportLines[0]); } // namespace devilution ================================================ FILE: Source/DiabloUI/support_lines.h ================================================ #include namespace devilution { extern const char *const SupportLines[]; extern const std::size_t SupportLinesSize; } // namespace devilution ================================================ FILE: Source/DiabloUI/text_input.cpp ================================================ #include "DiabloUI/text_input.hpp" #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "utils/log.hpp" #include "utils/parse_int.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_ptrs.h" #include "utils/str_cat.hpp" namespace devilution { namespace { bool HandleInputEvent(const SDL_Event &event, TextInputState &state, tl::function_ref typeFn, [[maybe_unused]] tl::function_ref assignFn) { const auto modState = SDL_GetModState(); const bool isCtrl = (modState & SDL_KMOD_CTRL) != 0; const bool isAlt = (modState & SDL_KMOD_ALT) != 0; const bool isShift = (modState & SDL_KMOD_SHIFT) != 0; switch (event.type) { case SDL_EVENT_KEY_DOWN: { switch (SDLC_EventKey(event)) { #ifndef USE_SDL1 case SDLK_A: if (isCtrl) { state.setCursorToStart(); state.setSelectCursorToEnd(); } return true; case SDLK_C: if (isCtrl) { const std::string selectedText { state.selectedText() }; if (SDLC_SetClipboardText(selectedText.c_str())) { LogError("Failed to set clipboard text: {}", SDL_GetError()); SDL_ClearError(); } } return true; case SDLK_X: if (isCtrl) { const std::string selectedText { state.selectedText() }; if (SDLC_SetClipboardText(selectedText.c_str())) { LogError("Failed to set clipboard text: {}", SDL_GetError()); SDL_ClearError(); } else { state.eraseSelection(); } } return true; case SDLK_V: if (isCtrl) { if (SDLC_HasClipboardText()) { const std::unique_ptr> clipboard { SDL_GetClipboardText() }; if (clipboard == nullptr) { LogError("Failed to get clipboard text: {}", SDL_GetError()); SDL_ClearError(); } else if (*clipboard != '\0') { typeFn(clipboard.get()); } } } return true; #endif case SDLK_BACKSPACE: state.backspace(/*word=*/isCtrl || isAlt); return true; case SDLK_DELETE: state.del(/*word=*/isCtrl || isAlt); return true; case SDLK_LEFT: isShift ? state.moveSelectCursorLeft(/*word=*/isCtrl || isAlt) : state.moveCursorLeft(/*word=*/isCtrl || isAlt); return true; case SDLK_RIGHT: isShift ? state.moveSelectCursorRight(/*word=*/isCtrl || isAlt) : state.moveCursorRight(/*word=*/isCtrl || isAlt); return true; case SDLK_HOME: isShift ? state.setSelectCursorToStart() : state.setCursorToStart(); return true; case SDLK_END: isShift ? state.setSelectCursorToEnd() : state.setCursorToEnd(); return true; default: break; } #ifdef USE_SDL1 if ((event.key.keysym.mod & KMOD_CTRL) == 0 && event.key.keysym.unicode >= ' ') { std::string utf8; AppendUtf8(event.key.keysym.unicode, utf8); typeFn(utf8); return true; } #else // Mark events that will also trigger SDL_TEXTINPUT as handled. return !isCtrl && !isAlt && SDLC_EventKey(event) >= SDLK_SPACE && SDLC_EventKey(event) <= SDLK_Z; #endif } break; #ifndef USE_SDL1 case SDL_EVENT_TEXT_INPUT: #ifdef __vita__ assignFn(event.text.text); #else typeFn(event.text.text); #endif return true; #ifdef USE_SDL3 case SDL_EVENT_TEXT_EDITING: case SDL_EVENT_TEXT_EDITING_CANDIDATES: #else case SDL_TEXTEDITING: #endif return true; #endif default: return false; } return false; } } // namespace bool HandleTextInputEvent(const SDL_Event &event, TextInputState &state) { return HandleInputEvent( event, state, [&](std::string_view str) { state.type(str); return true; }, [&](std::string_view str) { state.assign(str); return true; }); } [[nodiscard]] int NumberInputState::value(int defaultValue) const { return ParseInt(textInput_.value()).value_or(defaultValue); } std::string NumberInputState::filterStr(std::string_view str, bool allowMinus) { std::string result; if (allowMinus && !str.empty() && str[0] == '-') { str.remove_prefix(1); result += '-'; } for (const char c : str) { if (c >= '0' && c <= '9') { result += c; } } return result; } void NumberInputState::type(std::string_view str) { const std::string filtered = filterStr( str, /*allowMinus=*/min_ < 0 && textInput_.cursorPosition() == 0); if (filtered.empty()) return; textInput_.type(filtered); enforceRange(); } void NumberInputState::assign(std::string_view str) { const std::string filtered = filterStr(str, /*allowMinus=*/min_ < 0); if (filtered.empty()) { textInput_.clear(); return; } textInput_.assign(filtered); enforceRange(); } void NumberInputState::enforceRange() { if (textInput_.empty()) return; ParseIntResult parsed = ParseInt(textInput_.value()); if (parsed.has_value()) { if (*parsed > max_) { textInput_.assign(StrCat(max_)); } else if (*parsed < min_) { textInput_.assign(StrCat(min_)); } } } bool HandleNumberInputEvent(const SDL_Event &event, NumberInputState &state) { return HandleInputEvent( event, state.textInput(), [&](std::string_view str) { state.type(str); return true; }, [&](std::string_view str) { state.assign(str); return true; }); } } // namespace devilution ================================================ FILE: Source/DiabloUI/text_input.hpp ================================================ #pragma once #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "utils/utf8.hpp" namespace devilution { /** @brief A range of bytes in text. */ struct TextRange { size_t begin = 0; size_t end = 0; [[nodiscard]] size_t size() const { return end - begin; } [[nodiscard]] bool empty() const { return begin == end; } void clear() { begin = end = 0; } }; /** * @brief Current state of the cursor and the selection range. */ struct TextInputCursorState { size_t position = 0; TextRange selection; }; /** * @brief Manages state for a single-line text input with a cursor. * * The text value and the cursor position are stored externally. */ class TextInputState { /** * @brief Manages an unowned fixed size char array. */ struct Buffer { Buffer(char *begin, size_t maxLength) : buf_(begin) , maxLength_(maxLength) { std::string_view str(begin); str = TruncateUtf8(str, maxLength); len_ = str.size(); buf_[len_] = '\0'; } [[nodiscard]] size_t size() const { return len_; } [[nodiscard]] bool empty() const { return len_ == 0; } Buffer &operator=(std::string_view value) { value = TruncateUtf8(value, maxLength_); CopyUtf8(buf_, value, maxLength_); len_ = value.size(); return *this; } void insert(size_t pos, std::string_view value) { value = truncateForInsertion(value); std::memmove(&buf_[pos + value.size()], &buf_[pos], len_ - pos); std::memcpy(&buf_[pos], value.data(), value.size()); len_ += value.size(); buf_[len_] = '\0'; } void erase(size_t pos, size_t len) { std::memmove(&buf_[pos], &buf_[pos + len], len_ - (pos + len)); len_ -= len; buf_[len_] = '\0'; } void clear() { len_ = 0; buf_[0] = '\0'; } explicit operator std::string_view() const { return { buf_, len_ }; } private: /** * @brief Truncates `text` so that it would fit when inserted, * respecting UTF-8 code point boundaries. */ [[nodiscard]] std::string_view truncateForInsertion(std::string_view text) const { return TruncateUtf8(text, maxLength_ - len_); } char *buf_; // unowned size_t maxLength_; size_t len_; }; public: struct Options { char *value; // unowned TextInputCursorState *cursor; // unowned size_t maxLength = 0; }; TextInputState(const Options &options) : value_(options.value, options.maxLength) , cursor_(options.cursor) { cursor_->position = value_.size(); } [[nodiscard]] std::string_view value() const { return std::string_view(value_); } [[nodiscard]] std::string_view selectedText() const { return value().substr(cursor_->selection.begin, cursor_->selection.size()); } [[nodiscard]] bool empty() const { return value_.empty(); } [[nodiscard]] size_t cursorPosition() const { return cursor_->position; } /** * @brief Overwrites the value with the given text and moves cursor to the end. */ void assign(std::string_view text) { value_ = text; cursor_->position = value_.size(); } void clear() { value_.clear(); cursor_->position = 0; } /** * @brief Truncate to precisely `length` bytes. */ void truncate(size_t length) { if (length >= value().size()) return; value_ = value().substr(0, length); cursor_->position = std::min(cursor_->position, value_.size()); } /** * @brief Erases the currently selected text and sets the cursor to selection start. */ void eraseSelection() { value_.erase(cursor_->selection.begin, cursor_->selection.size()); cursor_->position = cursor_->selection.begin; cursor_->selection.clear(); } /** * @brief Inserts the text at the current cursor position. */ void type(std::string_view text) { if (!cursor_->selection.empty()) eraseSelection(); const size_t prevSize = value_.size(); value_.insert(cursor_->position, text); cursor_->position += value_.size() - prevSize; } void backspace(bool word) { if (cursor_->selection.empty()) { if (cursor_->position == 0) return; cursor_->selection.begin = prevPosition(word); cursor_->selection.end = cursor_->position; } eraseSelection(); } void del(bool word) { if (cursor_->selection.empty()) { if (cursor_->position == value_.size()) return; cursor_->selection.begin = cursor_->position; cursor_->selection.end = nextPosition(word); } eraseSelection(); } void setCursorToStart() { cursor_->position = 0; cursor_->selection.clear(); } void setSelectCursorToStart() { if (cursor_->selection.empty()) { cursor_->selection.end = cursor_->position; } else if (cursor_->selection.end == cursor_->position) { cursor_->selection.end = cursor_->selection.begin; } cursor_->selection.begin = cursor_->position = 0; } void setCursorToEnd() { cursor_->position = value_.size(); cursor_->selection.clear(); } void setSelectCursorToEnd() { if (cursor_->selection.empty()) { cursor_->selection.begin = cursor_->position; } else if (cursor_->selection.begin == cursor_->position) { cursor_->selection.begin = cursor_->selection.end; } cursor_->selection.end = cursor_->position = value_.size(); } void moveCursorLeft(bool word) { cursor_->selection.clear(); if (cursor_->position == 0) return; const size_t newPosition = prevPosition(word); cursor_->position = newPosition; } void moveSelectCursorLeft(bool word) { if (cursor_->position == 0) return; const size_t newPosition = prevPosition(word); if (cursor_->selection.empty()) { cursor_->selection.begin = newPosition; cursor_->selection.end = cursor_->position; } else if (cursor_->selection.end == cursor_->position) { cursor_->selection.end = newPosition; } else { cursor_->selection.begin = newPosition; } cursor_->position = newPosition; } void moveCursorRight(bool word) { cursor_->selection.clear(); if (cursor_->position == value_.size()) return; const size_t newPosition = nextPosition(word); cursor_->position = newPosition; } void moveSelectCursorRight(bool word) { if (cursor_->position == value_.size()) return; const size_t newPosition = nextPosition(word); if (cursor_->selection.empty()) { cursor_->selection.begin = cursor_->position; cursor_->selection.end = newPosition; } else if (cursor_->selection.begin == cursor_->position) { cursor_->selection.begin = newPosition; } else { cursor_->selection.end = newPosition; } cursor_->position = newPosition; } private: [[nodiscard]] static bool isWordSeparator(unsigned char c) { const bool isAsciiWordChar = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_'; return c <= '\x7E' && !isAsciiWordChar; } [[nodiscard]] size_t prevPosition(bool word) const { const std::string_view str = beforeCursor(); size_t pos = FindLastUtf8Symbols(str); if (!word) return pos; while (pos > 0 && isWordSeparator(str[pos])) { pos = FindLastUtf8Symbols({ str.data(), pos }); } while (pos > 0) { const size_t prevPos = FindLastUtf8Symbols({ str.data(), pos }); if (isWordSeparator(str[prevPos])) break; pos = prevPos; } return pos; } [[nodiscard]] size_t nextPosition(bool word) const { const std::string_view str = afterCursor(); size_t pos = Utf8CodePointLen(str.data()); if (!word) return cursor_->position + pos; while (pos < str.size() && isWordSeparator(str[pos])) { pos += Utf8CodePointLen(str.data() + pos); } while (pos < str.size()) { pos += Utf8CodePointLen(str.data() + pos); if (isWordSeparator(str[pos])) break; } return cursor_->position + pos; } [[nodiscard]] std::string_view beforeCursor() const { return value().substr(0, cursor_->position); } [[nodiscard]] std::string_view afterCursor() const { return value().substr(cursor_->position); } Buffer value_; TextInputCursorState *cursor_; // unowned }; /** * @brief Manages state for a number input with a cursor. */ class NumberInputState { public: struct Options { TextInputState::Options textOptions; int min; int max; }; NumberInputState(const Options &options) : textInput_(options.textOptions) , min_(options.min) , max_(options.max) { } [[nodiscard]] bool empty() const { return textInput_.empty(); } [[nodiscard]] int value(int defaultValue = 0) const; [[nodiscard]] int max() const { return max_; } /** * @brief Inserts the text at the current cursor position. * * Ignores non-numeric characters. */ void type(std::string_view str); /** * @brief Sets the text of the input. * * Ignores non-numeric characters. */ void assign(std::string_view str); TextInputState &textInput() { return textInput_; } private: void enforceRange(); std::string filterStr(std::string_view str, bool allowMinus); TextInputState textInput_; int min_; int max_; }; bool HandleTextInputEvent(const SDL_Event &event, TextInputState &state); bool HandleNumberInputEvent(const SDL_Event &event, NumberInputState &state); } // namespace devilution ================================================ FILE: Source/DiabloUI/title.cpp ================================================ #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/ui_flags.hpp" #include "DiabloUI/ui_item.h" #include "controls/input.h" #include "controls/menu_controls.h" #include "discord/discord.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/load_pcx.hpp" #include "engine/point.hpp" #include "utils/algorithm/container.hpp" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/ui_fwd.h" namespace devilution { namespace { OptionalOwnedClxSpriteList DiabloTitleLogo; std::vector> vecTitleScreen; void TitleLoad() { ArtBackgroundWidescreen = LoadOptionalClx("ui_art\\hf_titlew.clx"); if (ArtBackgroundWidescreen.has_value()) { LoadBackgroundArt("ui_art\\hf_logo1", 16); } else { LoadBackgroundArt("ui_art\\title"); DiabloTitleLogo = LoadPcxSpriteList("ui_art\\logo", /*numFrames=*/15, /*transparentColor=*/250); } } void TitleFree() { ArtBackground = std::nullopt; ArtBackgroundWidescreen = std::nullopt; DiabloTitleLogo = std::nullopt; vecTitleScreen.clear(); } } // namespace void UiTitleDialog() { TitleLoad(); const Point uiPosition = GetUIRectangle().position; if (ArtBackgroundWidescreen.has_value()) { const SDL_Rect rect = MakeSdlRect(0, uiPosition.y, 0, 0); if (ArtBackgroundWidescreen) vecTitleScreen.push_back(std::make_unique((*ArtBackgroundWidescreen)[0], rect, UiFlags::AlignCenter)); vecTitleScreen.push_back(std::make_unique(*ArtBackground, rect, UiFlags::AlignCenter)); } else { UiAddBackground(&vecTitleScreen); vecTitleScreen.push_back(std::make_unique( *DiabloTitleLogo, MakeSdlRect(0, uiPosition.y + 182, 0, 0), UiFlags::AlignCenter)); const SDL_Rect rect = MakeSdlRect(uiPosition.x, uiPosition.y + 410, 640, 26); vecTitleScreen.push_back(std::make_unique(_("Copyright © 1996-2001 Blizzard Entertainment").data(), rect, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiSilver)); } bool endMenu = false; const Uint32 timeOut = SDL_GetTicks() + 7000; SDL_Event event; while (!endMenu && SDL_GetTicks() < timeOut) { UiRenderItems(vecTitleScreen); UiFadeIn(); discord_manager::UpdateMenu(); while (PollEvent(&event)) { if (c_any_of(GetMenuActions(event), [](MenuAction menuAction) { return menuAction != MenuAction_NONE; })) { endMenu = true; break; } switch (event.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: endMenu = true; break; } UiHandleEvents(&event); } } TitleFree(); } } // namespace devilution ================================================ FILE: Source/DiabloUI/ui_flags.hpp ================================================ #pragma once #include #include "utils/enum_traits.h" namespace devilution { enum class UiFlags : uint32_t { // clang-format off None = 0, FontSize12 = 1 << 0, FontSize24 = 1 << 1, FontSize30 = 1 << 2, FontSize42 = 1 << 3, FontSize46 = 1 << 4, FontSizeDialog = 1 << 5, ColorUiGold = 1 << 6, ColorUiSilver = 1 << 7, ColorUiGoldDark = 1 << 8, ColorUiSilverDark = 1 << 9, ColorDialogWhite = 1 << 10, ColorDialogYellow = 1 << 11, ColorDialogRed = 1 << 12, ColorYellow = 1 << 13, ColorGold = 1 << 14, ColorBlack = 1 << 15, ColorWhite = 1 << 16, ColorWhitegold = 1 << 17, ColorRed = 1 << 18, ColorBlue = 1 << 19, ColorOrange = 1 << 20, ColorButtonface = 1 << 21, ColorButtonpushed = 1 << 22, AlignCenter = 1 << 23, AlignRight = 1 << 24, VerticalCenter = 1 << 25, KerningFitSpacing = 1 << 26, ElementDisabled = 1 << 27, ElementHidden = 1 << 28, PentaCursor = 1 << 29, Outlined = 1 << 30, /** @brief Ensures that the if current element is active that the next element is also visible. */ NeedsNextElement = 1U << 31U, // clang-format on }; use_enum_as_flags(UiFlags); } // namespace devilution ================================================ FILE: Source/DiabloUI/ui_item.h ================================================ #pragma once #include #include #include #include #include "DiabloUI/text_input.hpp" #include "DiabloUI/ui_flags.hpp" #include "engine/clx_sprite.hpp" #include "engine/render/text_render.hpp" #include "utils/enum_traits.h" #include "utils/string_or_view.hpp" #include "utils/stubs.h" namespace devilution { enum class UiType : uint8_t { Text, ArtText, ArtTextButton, ImageClx, ImageAnimatedClx, Button, List, Scrollbar, Edit, }; class UiItemBase { public: virtual ~UiItemBase() = default; [[nodiscard]] UiType GetType() const { return type_; } [[nodiscard]] bool IsType(UiType testType) const { return type_ == testType; } [[nodiscard]] UiFlags GetFlags() const { return uiFlags_; } [[nodiscard]] bool IsHidden() const { return HasAnyOf(uiFlags_, UiFlags::ElementHidden); } [[nodiscard]] bool IsNotInteractive() const { return HasAnyOf(uiFlags_, UiFlags::ElementHidden | UiFlags::ElementDisabled); } void Hide() { uiFlags_ |= UiFlags::ElementHidden; } void Show() { uiFlags_ &= ~UiFlags::ElementHidden; } protected: UiItemBase(UiType type, SDL_Rect rect, UiFlags flags) : type_(type) , m_rect(rect) , uiFlags_(flags) { } void SetFlags(UiFlags flags) { uiFlags_ = flags; } private: UiType type_; public: SDL_Rect m_rect; private: UiFlags uiFlags_; }; //============================================================================= class UiImageClx : public UiItemBase { public: UiImageClx(ClxSprite sprite, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::ImageClx, rect, flags) , sprite_(sprite) { } [[nodiscard]] bool isCentered() const { return HasAnyOf(GetFlags(), UiFlags::AlignCenter); } [[nodiscard]] ClxSprite sprite() const { return sprite_; } void setSprite(ClxSprite sprite) { sprite_ = sprite; } private: ClxSprite sprite_; }; //============================================================================= class UiImageAnimatedClx : public UiItemBase { public: UiImageAnimatedClx(ClxSpriteList list, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::ImageAnimatedClx, rect, flags) , list_(list) { } [[nodiscard]] bool isCentered() const { return HasAnyOf(GetFlags(), UiFlags::AlignCenter); } [[nodiscard]] ClxSprite sprite(uint16_t frame) const { return list_[frame]; } [[nodiscard]] uint16_t numFrames() const { return list_.numSprites(); } private: ClxSpriteList list_; }; //============================================================================= class UiArtText : public UiItemBase { public: /** * @brief Constructs a UI element containing a (presumed to be) static line of text * @param text Pointer to the first character of a c-string * @param rect screen region defining the area to draw the text * @param flags UiFlags controlling color/alignment/size * @param spacing Spacing between characters * @param lineHeight Vertical distance between text lines */ UiArtText(const char *text, SDL_Rect rect, UiFlags flags = UiFlags::None, int spacing = 1, int lineHeight = -1) : UiItemBase(UiType::ArtText, rect, flags) , text_(text) , spacing_(spacing) , lineHeight_(lineHeight) { } /** * @brief Constructs a UI element containing a line of text that may change between frames * @param ptext Pointer to a c-string (pointer to a pointer to the first character) * @param rect screen region defining the area to draw the text * @param flags UiFlags controlling color/alignment/size * @param spacing Spacing between characters * @param lineHeight Vertical distance between text lines */ UiArtText(const char **ptext, SDL_Rect rect, UiFlags flags = UiFlags::None, int spacing = 1, int lineHeight = -1) : UiItemBase(UiType::ArtText, rect, flags) , textPointer_(ptext) , spacing_(spacing) , lineHeight_(lineHeight) { } [[nodiscard]] std::string_view GetText() const { if (text_ != nullptr) return text_; return *textPointer_; } [[nodiscard]] int GetSpacing() const { return spacing_; } [[nodiscard]] int GetLineHeight() const { return lineHeight_; } private: const char *text_ = nullptr; const char **textPointer_ = nullptr; int spacing_; int lineHeight_; }; //============================================================================= class UiScrollbar : public UiItemBase { public: UiScrollbar(ClxSprite bg, ClxSprite thumb, ClxSpriteList arrow, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::Scrollbar, rect, flags) , m_bg(bg) , m_thumb(thumb) , m_arrow(arrow) { } // private: ClxSprite m_bg; ClxSprite m_thumb; ClxSpriteList m_arrow; }; //============================================================================= class UiArtTextButton : public UiItemBase { public: using Callback = void (*)(); UiArtTextButton(std::string_view text, Callback action, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::ArtTextButton, rect, flags) , text_(text) , action_(action) { } void SetFlags(UiFlags flags) { UiItemBase::SetFlags(flags); } [[nodiscard]] std::string_view GetText() const { return text_; } void Activate() const { action_(); } private: std::string_view text_; Callback action_; }; //============================================================================= class UiEdit : public UiItemBase { public: UiEdit(std::string_view hint, char *value, std::size_t maxLength, bool allowEmpty, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::Edit, rect, flags) , m_hint(hint) , m_value(value) , m_max_length(maxLength) , m_allowEmpty(allowEmpty) { } // private: std::string_view m_hint; char *m_value; std::size_t m_max_length; TextInputCursorState m_cursor; bool m_allowEmpty; }; //============================================================================= // Plain text class UiText : public UiItemBase { public: UiText(std::string_view text, SDL_Rect rect, UiFlags flags = UiFlags::ColorDialogWhite) : UiItemBase(UiType::Text, rect, flags) , text_(text) { } [[nodiscard]] std::string_view GetText() const { return text_; } private: std::string_view text_; }; //============================================================================= // A button (uses Diablo sprites) class UiButton : public UiItemBase { public: using Callback = void (*)(); UiButton(std::string_view text, Callback action, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::Button, rect, flags) , text_(text) , action_(action) , pressed_(false) { } [[nodiscard]] std::string_view GetText() const { return text_; } void Activate() const { action_(); } [[nodiscard]] bool IsPressed() const { return pressed_; } void Press() { pressed_ = true; } void Release() { pressed_ = false; } private: std::string_view text_; Callback action_; // State bool pressed_; }; //============================================================================= class UiListItem { public: UiListItem(StringOrView &&text = {}, int value = 0, UiFlags uiFlags = UiFlags::None) : m_text(std::move(text)) , m_value(value) , uiFlags(uiFlags) { } UiListItem(StringOrView &&text, std::vector &args, int value = 0, UiFlags uiFlags = UiFlags::None) : m_text(std::move(text)) , args(args) , m_value(value) , uiFlags(uiFlags) { } // private: StringOrView m_text; std::vector args; int m_value; UiFlags uiFlags; }; class UiList : public UiItemBase { public: using vUiListItem = std::vector>; UiList(const vUiListItem &vItems, size_t viewportMaxSize, Sint16 x, Sint16 y, Uint16 item_width, Uint16 item_height, UiFlags flags = UiFlags::None, int spacing = 1) : UiList(PrivateConstructor {}, vItems, std::min(viewportMaxSize, vItems.size()), x, y, item_width, item_height, flags, spacing) { } [[nodiscard]] SDL_Rect itemRect(int i) const { SDL_Rect tmp; tmp.x = m_x; tmp.y = m_y + m_height * i; tmp.w = m_width; tmp.h = m_height; return tmp; } [[nodiscard]] size_t indexAt(Sint16 y) const { ASSERT(y >= m_rect.y); const size_t index = (y - m_rect.y) / m_height; ASSERT(index < m_vecItems.size()); return index; } [[nodiscard]] UiListItem *GetItem(std::size_t i) const { return m_vecItems[i]; } [[nodiscard]] int GetSpacing() const { return spacing_; } [[nodiscard]] bool IsPressed(size_t index) const { return pressed_item_index_ == index; } void Press(size_t index) { pressed_item_index_ = index; } void Release() { pressed_item_index_ = -1; } // private: size_t viewportSize; Sint16 m_x, m_y; Uint16 m_width, m_height; std::vector m_vecItems; private: struct PrivateConstructor final { }; UiList(PrivateConstructor tag, const vUiListItem &vItems, size_t viewportSize, Sint16 x, Sint16 y, Uint16 item_width, Uint16 item_height, UiFlags flags, int spacing) : UiItemBase(UiType::List, { x, y, item_width, static_cast(item_height * viewportSize) }, flags) , viewportSize(viewportSize) , m_x(x) , m_y(y) , m_width(item_width) , m_height(item_height) , spacing_(spacing) { for (const auto &item : vItems) m_vecItems.push_back(item.get()); pressed_item_index_ = -1; } int spacing_; // State size_t pressed_item_index_; }; } // namespace devilution ================================================ FILE: Source/appfat.cpp ================================================ /** * @file appfat.cpp * * Implementation of error dialogs. */ #include #ifdef USE_SDL3 #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #include "utils/sdl_compat.h" #endif #include #include "diablo.h" #include "dvlnet/leaveinfo.hpp" #include "multi.h" #include "storm/storm_net.hpp" #include "utils/language.h" #include "utils/sdl_thread.h" #include "utils/str_cat.hpp" #include "utils/ui_fwd.h" namespace devilution { namespace { /** Set to true when a fatal error is encountered and the application should shut down. */ bool Terminating = false; /** Thread id of the last callee to FreeDlg(). */ SDL_ThreadID CleanupThreadId; /** * @brief Cleans up after a fatal application error. */ void FreeDlg() { if (Terminating && CleanupThreadId != this_sdl_thread::get_id()) SDL_Delay(20000); Terminating = true; CleanupThreadId = this_sdl_thread::get_id(); if (gbIsMultiplayer) { if (SNetLeaveGame(leaveinfo_t::LEAVE_EXIT)) SDL_Delay(2000); } SNetDestroy(); } } // namespace void DisplayFatalErrorAndExit(std::string_view title, std::string_view body) { FreeDlg(); UiErrorOkDialog(title, body); diablo_quit(1); } void app_fatal(std::string_view str) { DisplayFatalErrorAndExit(_("Error"), str); } #ifdef _DEBUG void assert_fail(int nLineNo, const char *pszFile, const char *pszFail) { app_fatal(StrCat("assertion failed (", pszFile, ":", nLineNo, ")\n", pszFail)); } #endif void ErrDlg(const char *title, std::string_view error, std::string_view logFilePath, int logLineNr) { DisplayFatalErrorAndExit( title, fmt::format(fmt::runtime(_(/* TRANSLATORS: Error message that displays relevant information for bug report */ "{:s}\n\nThe error occurred at: {:s} line {:d}")), error, logFilePath, logLineNr)); } void InsertCDDlg(std::string_view archiveName) { DisplayFatalErrorAndExit(_("Data File Error"), fmt::format(fmt::runtime(_("Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder.")), archiveName)); } void DirErrorDlg(std::string_view error) { DisplayFatalErrorAndExit( _("Read-Only Directory Error"), fmt::format(fmt::runtime(_(/* TRANSLATORS: Error when Program is not allowed to write data */ "Unable to write to location:\n{:s}")), error)); } } // namespace devilution ================================================ FILE: Source/appfat.h ================================================ /** * @file appfat.h * * Interface of error dialogs. */ #pragma once #include #include "utils/attributes.h" namespace devilution { #define ErrSdl() ErrDlg("SDL Error", SDL_GetError(), __FILE__, __LINE__) #undef assert #ifndef _DEBUG #define assert(exp) #else #define assert(exp) (void)((exp) || (assert_fail(__LINE__, __FILE__, #exp), 0)) #endif /** * @brief Terminates the game and displays an error message box. * @param str Message box title. * @param str Error message. */ [[noreturn]] void DisplayFatalErrorAndExit(std::string_view title, std::string_view body); /** * @brief Terminates the game and displays an error message box. * @param str Error message. */ [[noreturn]] void app_fatal(std::string_view str); #ifdef _DEBUG /** * @brief Show an error and exit the application. * @param nLineNo The line number of the assertion * @param pszFile File name where the assertion is located * @param pszFail Fail message */ [[noreturn]] void assert_fail(int nLineNo, const char *pszFile, const char *pszFail); #endif /** * @brief Terminates the game and displays an error dialog box based on the given dialog_id. */ [[noreturn]] void ErrDlg(const char *title, std::string_view error, std::string_view logFilePath, int logLineNr); /** * @brief Terminates the game with an insert CD error dialog. */ [[noreturn]] void InsertCDDlg(std::string_view archiveName); /** * @brief Terminates the game with a read-only directory error dialog. */ [[noreturn]] void DirErrorDlg(std::string_view error); } // namespace devilution ================================================ FILE: Source/automap.cpp ================================================ /** * @file automap.cpp * * Implementation of the in-game map overlay. */ #include "automap.h" #include #include #include #include "control/control.hpp" #include "engine/load_file.hpp" #include "engine/palette.h" #include "engine/render/automap_render.hpp" #include "engine/render/primitive_render.hpp" #include "levels/gendung.h" #include "levels/setmaps.h" #include "options.h" #include "player.h" #include "utils/attributes.h" #include "utils/enum_traits.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" #ifdef _DEBUG #include "debug.h" #include "lighting.h" #endif namespace devilution { namespace { Point Automap; enum MapColors : uint8_t { /** color used to draw the player's arrow */ MapColorsPlayer1 = (PAL8_ORANGE + 1), MapColorsPlayer2 = (PAL8_YELLOW + 1), MapColorsPlayer3 = (PAL8_RED + 1), MapColorsPlayer4 = (PAL8_BLUE + 1), /** color for bright map lines (doors, stairs etc.) */ MapColorsBright = PAL8_YELLOW, /** color for dim map lines/dots */ MapColorsDim = (PAL16_YELLOW + 8), /** color for items on automap */ MapColorsItem = (PAL8_BLUE + 1), /** color for activated pentragram on automap */ MapColorsPentagramOpen = (PAL8_RED + 2), /** color for cave lava on automap */ MapColorsLava = (PAL8_ORANGE + 2), /** color for cave water on automap */ MapColorsWater = (PAL8_BLUE + 2), /** color for hive acid on automap */ MapColorsAcid = (PAL8_YELLOW + 4), }; struct AutomapTile { /** The general shape of the tile */ enum class Types : uint8_t { None, Diamond, Vertical, Horizontal, Cross, FenceVertical, FenceHorizontal, Corner, CaveHorizontalCross, CaveVerticalCross, CaveHorizontal, CaveVertical, CaveCross, Bridge, River, RiverCornerEast, RiverCornerNorth, RiverCornerSouth, RiverCornerWest, RiverForkIn, RiverForkOut, RiverLeftIn, RiverLeftOut, RiverRightIn, RiverRightOut, CaveHorizontalWoodCross, CaveVerticalWoodCross, CaveLeftCorner, CaveRightCorner, CaveBottomCorner, CaveHorizontalWood, CaveVerticalWood, CaveWoodCross, CaveRightWoodCross, CaveLeftWoodCross, HorizontalLavaThin, VerticalLavaThin, BendSouthLavaThin, BendWestLavaThin, BendEastLavaThin, BendNorthLavaThin, VerticalWallLava, HorizontalWallLava, SELava, SWLava, NELava, NWLava, SLava, WLava, ELava, NLava, Lava, CaveHorizontalWallLava, CaveVerticalWallLava, HorizontalBridgeLava, VerticalBridgeLava, VerticalDiamond, HorizontalDiamond, PentagramClosed, PentagramOpen, }; Types type; /** Additional details about the given tile */ enum class Flags : uint8_t { // clang-format off VerticalDoor = 1 << 0, HorizontalDoor = 1 << 1, VerticalArch = 1 << 2, HorizontalArch = 1 << 3, VerticalGrate = 1 << 4, HorizontalGrate = 1 << 5, VerticalPassage = VerticalDoor | VerticalArch | VerticalGrate, HorizontalPassage = HorizontalDoor | HorizontalArch | HorizontalGrate, Dirt = 1 << 6, Stairs = 1 << 7, // clang-format on }; Flags flags = {}; [[nodiscard]] DVL_ALWAYS_INLINE constexpr bool hasFlag(Flags test) const { return (static_cast(flags) & static_cast(test)) != 0; } template [[nodiscard]] DVL_ALWAYS_INLINE constexpr bool hasAnyFlag(Flags flag, Args... testFlags) { return (static_cast(this->flags) & (static_cast(flag) | ... | static_cast(testFlags))) != 0; } }; /** * Maps from tile_id to automap type. */ std::array AutomapTypeTiles; /** * @brief Draw a diamond on top tile. */ void DrawDiamond(const Surface &out, Point center, uint8_t color) { DrawMapLineNE(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::FullTile), color); DrawMapLineSE(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::FullTile), color); DrawMapLineSE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileUp), AmLine(AmLineLength::FullTile), color); DrawMapLineNE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::None), AmLine(AmLineLength::FullTile), color); } /** * @brief Draws a bright diamond and a line, orientation depending on the tileset. */ void DrawMapVerticalDoor(const Surface &out, Point center, AutomapTile neTile, uint8_t colorBright, uint8_t colorDim) { AmWidthOffset lWidthOffset; AmHeightOffset lHeightOffset; AmWidthOffset dWidthOffset; AmHeightOffset dHeightOffset; AmLineLength length; switch (leveltype) { case DTYPE_CATHEDRAL: case DTYPE_CRYPT: lWidthOffset = AmWidthOffset::QuarterTileLeft; lHeightOffset = AmHeightOffset::QuarterTileUp; dWidthOffset = AmWidthOffset::HalfTileLeft; dHeightOffset = AmHeightOffset::HalfTileDown; length = AmLineLength::HalfTile; break; case DTYPE_CATACOMBS: lWidthOffset = AmWidthOffset::ThreeQuartersTileLeft; lHeightOffset = AmHeightOffset::QuarterTileDown; dWidthOffset = AmWidthOffset::None; dHeightOffset = AmHeightOffset::None; length = AmLineLength::FullTile; break; case DTYPE_CAVES: lWidthOffset = AmWidthOffset::QuarterTileLeft; lHeightOffset = AmHeightOffset::ThreeQuartersTileDown; dWidthOffset = AmWidthOffset::HalfTileRight; dHeightOffset = AmHeightOffset::HalfTileDown; length = AmLineLength::FullTile; break; default: app_fatal("Invalid leveltype"); } if (!(neTile.hasFlag(AutomapTile::Flags::VerticalPassage) && leveltype == DTYPE_CATHEDRAL)) DrawMapLineNE(out, center + AmOffset(lWidthOffset, lHeightOffset), AmLine(length), colorDim); DrawDiamond(out, center + AmOffset(dWidthOffset, dHeightOffset), colorBright); } /** * @brief Draws a bright diamond and a line, orientation depending on the tileset. */ void DrawMapHorizontalDoor(const Surface &out, Point center, AutomapTile nwTile, uint8_t colorBright, uint8_t colorDim) { AmWidthOffset lWidthOffset; AmHeightOffset lHeightOffset; AmWidthOffset dWidthOffset; AmHeightOffset dHeightOffset; AmLineLength length; switch (leveltype) { case DTYPE_CATHEDRAL: case DTYPE_CRYPT: lWidthOffset = AmWidthOffset::None; lHeightOffset = AmHeightOffset::HalfTileUp; dWidthOffset = AmWidthOffset::HalfTileRight; dHeightOffset = AmHeightOffset::HalfTileDown; length = AmLineLength::HalfTile; break; case DTYPE_CATACOMBS: lWidthOffset = AmWidthOffset::QuarterTileRight; lHeightOffset = AmHeightOffset::QuarterTileUp; dWidthOffset = AmWidthOffset::None; dHeightOffset = AmHeightOffset::None; length = AmLineLength::FullTile; break; case DTYPE_CAVES: lWidthOffset = AmWidthOffset::QuarterTileLeft; lHeightOffset = AmHeightOffset::QuarterTileDown; dWidthOffset = AmWidthOffset::HalfTileLeft; dHeightOffset = AmHeightOffset::HalfTileDown; length = AmLineLength::FullTile; break; break; default: app_fatal("Invalid leveltype"); } if (!(nwTile.hasFlag(AutomapTile::Flags::HorizontalPassage) && leveltype == DTYPE_CATHEDRAL)) DrawMapLineSE(out, center + AmOffset(lWidthOffset, lHeightOffset), AmLine(length), colorDim); DrawDiamond(out, center + AmOffset(dWidthOffset, dHeightOffset), colorBright); } /** * @brief Draw 16 individual pixels equally spaced apart, used to communicate OOB area to the player. */ void DrawDirt(const Surface &out, Point center, AutomapTile nwTile, AutomapTile neTile, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); // Prevent the top dirt pixel from appearing inside arch diamonds if (!nwTile.hasAnyFlag(AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::HorizontalGrate) && !neTile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::VerticalGrate)) SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawBridge(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverRightIn(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverCornerSouth(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); } void DrawRiverCornerNorth(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); } void DrawRiverLeftOut(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverLeftIn(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); } void DrawRiverCornerWest(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); } void DrawRiverCornerEast(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverRightOut(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiver(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverForkIn(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), color); SetMapPixel(out, center, color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); } void DrawRiverForkOut(const Surface &out, Point center, uint8_t color) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); } template void DrawLavaRiver(const Surface &out, Point center, uint8_t color, bool hasBridge) { // First row (y = 0) if constexpr (IsAnyOf(Direction::NorthWest, TDir1, TDir2)) { if (!(hasBridge && IsAnyOf(TDir1, Direction::NorthWest))) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); } } // Second row (y = 1) if constexpr (IsAnyOf(Direction::NorthEast, TDir1, TDir2)) { if (!(hasBridge && IsAnyOf(Direction::NorthEast, TDir1, TDir2))) SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); } if constexpr (IsAnyOf(Direction::NorthWest, TDir1, TDir2) || IsAnyOf(Direction::NorthEast, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::None), color); } if constexpr (IsAnyOf(Direction::SouthWest, TDir1, TDir2) || IsAnyOf(Direction::NorthWest, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); } if constexpr (IsAnyOf(Direction::SouthWest, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); } // Third row (y = 2) if constexpr (IsAnyOf(Direction::NorthEast, TDir1, TDir2)) { if (!(hasBridge && IsAnyOf(Direction::NorthEast, TDir1, TDir2))) SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); } if constexpr (IsAnyOf(Direction::NorthEast, TDir1, TDir2) || IsAnyOf(Direction::SouthEast, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); } if constexpr (IsAnyOf(Direction::SouthWest, TDir1, TDir2) || IsAnyOf(Direction::SouthEast, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); } if constexpr (IsAnyOf(Direction::SouthWest, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); } // Fourth row (y = 3) if constexpr (IsAnyOf(Direction::SouthEast, TDir1, TDir2)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); } } template void DrawLava(const Surface &out, Point center, uint8_t color) { if constexpr (IsAnyOf(TDir, Direction::NorthWest, Direction::North, Direction::NorthEast, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), color); // north corner } if constexpr (IsNoneOf(TDir, Direction::South, Direction::SouthEast, Direction::East)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileUp), color); // northwest edge SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), color); // northwest edge } if constexpr (IsAnyOf(TDir, Direction::SouthWest, Direction::West, Direction::NorthWest, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), color); // west corner } if constexpr (IsAnyOf(TDir, Direction::South, Direction::SouthWest, Direction::West, Direction::NorthWest, Direction::SouthEast, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), color); // southwest edge SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), color); // southwest edge } if constexpr (IsAnyOf(TDir, Direction::South, Direction::SouthWest, Direction::SouthEast, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), color); // south corner } if constexpr (IsAnyOf(TDir, Direction::South, Direction::SouthWest, Direction::NorthEast, Direction::East, Direction::SouthEast, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), color); // southeast edge SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), color); // southeast edge } if constexpr (IsAnyOf(TDir, Direction::NorthEast, Direction::East, Direction::SouthEast, Direction::NoDirection)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), color); // east corner } if constexpr (IsNoneOf(TDir, Direction::South, Direction::SouthWest, Direction::West)) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileUp), color); // northeast edge SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), color); // northeast edge } if constexpr (TDir != Direction::South) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::None), color); // north center } if constexpr (TDir != Direction::East) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown), color); // west center } if constexpr (TDir != Direction::West) { SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::QuarterTileDown), color); // east center } if constexpr (TDir != Direction::North) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), color); // south center } } /** * @brief Draw 4 south-east facing lines, used to communicate trigger locations to the player. */ void DrawStairs(const Surface &out, Point center, uint8_t color) { constexpr int NumStairSteps = 4; const Displacement offset = AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::QuarterTileDown); AmWidthOffset w = AmWidthOffset::QuarterTileLeft; AmHeightOffset h = AmHeightOffset::QuarterTileUp; if (IsAnyOf(leveltype, DTYPE_CATACOMBS, DTYPE_HELL)) { w = AmWidthOffset::QuarterTileLeft; h = AmHeightOffset::ThreeQuartersTileUp; } // Initial point based on the 'center' position. Point p = center + AmOffset(w, h); for (int i = 0; i < NumStairSteps; ++i) { DrawMapLineSE(out, p, AmLine(AmLineLength::DoubleTile), color); p += offset; } } /** * @brief Redraws the bright line of the door diamond that gets overwritten by later drawn lines. */ void FixHorizontalDoor(const Surface &out, Point center, AutomapTile nwTile, uint8_t colorBright) { if (leveltype != DTYPE_CATACOMBS && nwTile.hasFlag(AutomapTile::Flags::HorizontalDoor)) { DrawMapLineNE(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::FullTile), colorBright); } } /** * @brief Redraws the bright line of the door diamond that gets overwritten by later drawn lines. */ void FixVerticalDoor(const Surface &out, Point center, AutomapTile neTile, uint8_t colorBright) { if (leveltype != DTYPE_CATACOMBS && neTile.hasFlag(AutomapTile::Flags::VerticalDoor)) { DrawMapLineSE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileUp), AmLine(AmLineLength::FullTile), colorBright); } } /** * @brief Draw half-tile length lines to connect walls to any walls to the north-west and/or north-east */ void DrawWallConnections(const Surface &out, Point center, AutomapTile tile, AutomapTile nwTile, AutomapTile neTile, uint8_t colorBright, uint8_t colorDim) { if (tile.hasFlag(AutomapTile::Flags::HorizontalDoor) && nwTile.hasFlag(AutomapTile::Flags::HorizontalDoor)) { // fix missing lower half of the line connecting door pairs in Lazarus' level DrawMapLineSE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::HalfTile), colorDim); } if (IsAnyOf(nwTile.type, AutomapTile::Types::HorizontalWallLava, AutomapTile::Types::Horizontal, AutomapTile::Types::HorizontalDiamond, AutomapTile::Types::FenceHorizontal, AutomapTile::Types::Cross, AutomapTile::Types::CaveVerticalWoodCross, AutomapTile::Types::CaveRightCorner)) { DrawMapLineSE(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileUp), AmLine(AmLineLength::HalfTile), colorDim); FixHorizontalDoor(out, center, nwTile, colorBright); } if (IsAnyOf(neTile.type, AutomapTile::Types::VerticalWallLava, AutomapTile::Types::Vertical, AutomapTile::Types::VerticalDiamond, AutomapTile::Types::FenceVertical, AutomapTile::Types::Cross, AutomapTile::Types::CaveHorizontalWoodCross, AutomapTile::Types::CaveLeftCorner)) { DrawMapLineNE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::HalfTile), colorDim); FixVerticalDoor(out, center, neTile, colorBright); } } /** * @brief Draws a dotted line to represent a wall grate. */ void DrawMapVerticalGrate(const Surface &out, Point center, uint8_t colorDim) { const Point pos1 = center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None) + AmOffset(AmWidthOffset::EighthTileRight, AmHeightOffset::EighthTileUp); const Point pos2 = center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None); const Point pos3 = center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None) + AmOffset(AmWidthOffset::EighthTileLeft, AmHeightOffset::EighthTileDown); SetMapPixel(out, pos1 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos2 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos3 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos1, colorDim); SetMapPixel(out, pos2, colorDim); SetMapPixel(out, pos3, colorDim); } /** * @brief Draws a dotted line to represent a wall grate. */ void DrawMapHorizontalGrate(const Surface &out, Point center, uint8_t colorDim) { const Point pos1 = center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None) + AmOffset(AmWidthOffset::EighthTileLeft, AmHeightOffset::EighthTileUp); const Point pos2 = center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None); const Point pos3 = center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None) + AmOffset(AmWidthOffset::EighthTileRight, AmHeightOffset::EighthTileDown); SetMapPixel(out, pos1 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos2 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos3 + Displacement { 0, 1 }, 0); SetMapPixel(out, pos1, colorDim); SetMapPixel(out, pos2, colorDim); SetMapPixel(out, pos3, colorDim); } /** * Left-facing obstacle */ void DrawHorizontal(const Surface &out, Point center, AutomapTile tile, AutomapTile nwTile, AutomapTile neTile, AutomapTile seTile, uint8_t colorBright, uint8_t colorDim) { AmWidthOffset w = AmWidthOffset::None; AmHeightOffset h = AmHeightOffset::HalfTileUp; AmLineLength l = AmLineLength::FullAndHalfTile; // Draw a diamond in the top tile if (neTile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::VerticalGrate) // NE tile has an arch, so add a diamond for visual consistency || nwTile.hasAnyFlag(AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::HorizontalGrate) // NW tile has an arch, so add a diamond for visual consistency || tile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::VerticalGrate, AutomapTile::Flags::HorizontalGrate) // Current tile has an arch, add a diamond || tile.type == AutomapTile::Types::HorizontalDiamond) { // wall ending in hell that should end with a diamond w = AmWidthOffset::QuarterTileRight; h = AmHeightOffset::QuarterTileUp; l = AmLineLength::FullTile; // shorten line to avoid overdraw DrawDiamond(out, center, colorDim); FixHorizontalDoor(out, center, nwTile, colorBright); FixVerticalDoor(out, center, neTile, colorBright); } // Shorten line to avoid overdraw if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST) && IsAnyOf(tile.type, AutomapTile::Types::CaveVerticalCross, AutomapTile::Types::CaveVerticalWoodCross) && !(IsAnyOf(seTile.type, AutomapTile::Types::Horizontal, AutomapTile::Types::CaveVerticalCross, AutomapTile::Types::CaveVerticalWoodCross, AutomapTile::Types::Corner))) { l = AmLineLength::FullTile; } // Draw the wall line if the wall is solid if (!tile.hasFlag(AutomapTile::Flags::HorizontalPassage)) { DrawMapLineSE(out, center + AmOffset(w, h), AmLine(l), colorDim); return; } // Draw door or grate if (tile.hasFlag(AutomapTile::Flags::HorizontalDoor)) { DrawMapHorizontalDoor(out, center, nwTile, colorBright, colorDim); } else if (tile.hasFlag(AutomapTile::Flags::HorizontalGrate)) { DrawMapHorizontalGrate(out, center, colorDim); } } /** * Right-facing obstacle */ void DrawVertical(const Surface &out, Point center, AutomapTile tile, AutomapTile nwTile, AutomapTile neTile, AutomapTile swTile, uint8_t colorBright, uint8_t colorDim) { AmWidthOffset w = AmWidthOffset::ThreeQuartersTileLeft; AmHeightOffset h = AmHeightOffset::QuarterTileDown; AmLineLength l = AmLineLength::FullAndHalfTile; // Draw a diamond in the top tile if (neTile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::VerticalGrate) // NE tile has an arch, so add a diamond for visual consistency || nwTile.hasAnyFlag(AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::HorizontalGrate) // NW tile has an arch, so add a diamond for visual consistency || tile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::VerticalGrate, AutomapTile::Flags::HorizontalGrate) // Current tile has an arch, add a diamond || tile.type == AutomapTile::Types::VerticalDiamond) { // wall ending in hell that should end with a diamond l = AmLineLength::FullTile; // shorten line to avoid overdraw DrawDiamond(out, center, colorDim); FixVerticalDoor(out, center, nwTile, colorBright); FixVerticalDoor(out, center, neTile, colorBright); } // Shorten line to avoid overdraw and adjust offset to match if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST) && IsAnyOf(tile.type, AutomapTile::Types::CaveHorizontalCross, AutomapTile::Types::CaveHorizontalWoodCross) && !(IsAnyOf(swTile.type, AutomapTile::Types::Vertical, AutomapTile::Types::CaveHorizontalCross, AutomapTile::Types::CaveHorizontalWoodCross, AutomapTile::Types::Corner))) { w = AmWidthOffset::HalfTileLeft; h = AmHeightOffset::None; l = AmLineLength::FullTile; } // Draw the wall line if the wall is solid if (!tile.hasFlag(AutomapTile::Flags::VerticalPassage)) { DrawMapLineNE(out, center + AmOffset(w, h), AmLine(l), colorDim); return; } // Draw door or grate if (tile.hasFlag(AutomapTile::Flags::VerticalDoor)) { DrawMapVerticalDoor(out, center, neTile, colorBright, colorDim); } else if (tile.hasFlag(AutomapTile::Flags::VerticalGrate)) { DrawMapVerticalGrate(out, center, colorDim); } } /** * @brief Draw half-tile length lines to connect walls to any walls to the south-west and/or south-east * (For caves the horizontal/vertical flags are swapped) */ void DrawCaveWallConnections(const Surface &out, Point center, AutomapTile sTile, AutomapTile swTile, AutomapTile seTile, uint8_t colorDim) { if (IsAnyOf(swTile.type, AutomapTile::Types::CaveVerticalWallLava, AutomapTile::Types::CaveVertical, AutomapTile::Types::CaveVerticalWood, AutomapTile::Types::CaveCross, AutomapTile::Types::CaveWoodCross, AutomapTile::Types::CaveRightWoodCross, AutomapTile::Types::CaveLeftWoodCross, AutomapTile::Types::CaveRightCorner)) { DrawMapLineNE(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), AmLine(AmLineLength::HalfTile), colorDim); } if (IsAnyOf(seTile.type, AutomapTile::Types::CaveHorizontalWallLava, AutomapTile::Types::CaveHorizontal, AutomapTile::Types::CaveHorizontalWood, AutomapTile::Types::CaveCross, AutomapTile::Types::CaveWoodCross, AutomapTile::Types::CaveRightWoodCross, AutomapTile::Types::CaveLeftWoodCross, AutomapTile::Types::CaveLeftCorner)) { DrawMapLineSE(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown), AmLine(AmLineLength::HalfTile), colorDim); } } void DrawCaveHorizontalDirt(const Surface &out, Point center, AutomapTile tile, AutomapTile swTile, uint8_t colorDim) { if (swTile.hasFlag(AutomapTile::Flags::Dirt) || (leveltype != DTYPE_TOWN && IsNoneOf(tile.type, AutomapTile::Types::CaveHorizontalWood, AutomapTile::Types::CaveHorizontalWoodCross, AutomapTile::Types::CaveWoodCross, AutomapTile::Types::CaveLeftWoodCross))) { SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::ThreeQuartersTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), colorDim); } } /** * For caves the horizontal/vertical flags are swapped */ void DrawCaveHorizontal(const Surface &out, Point center, AutomapTile tile, AutomapTile nwTile, AutomapTile swTile, uint8_t colorBright, uint8_t colorDim) { if (tile.hasFlag(AutomapTile::Flags::VerticalDoor)) { DrawMapHorizontalDoor(out, center, nwTile, colorBright, colorDim); } else { AmWidthOffset w; AmHeightOffset h; AmLineLength l; if (IsAnyOf(tile.type, AutomapTile::Types::CaveHorizontalCross, AutomapTile::Types::CaveHorizontalWoodCross)) { w = AmWidthOffset::HalfTileLeft; h = AmHeightOffset::None; l = AmLineLength::FullTile; } else { w = AmWidthOffset::ThreeQuartersTileLeft; h = AmHeightOffset::QuarterTileUp; l = AmLineLength::FullAndHalfTile; } DrawCaveHorizontalDirt(out, center, tile, swTile, colorDim); DrawMapLineSE(out, center + AmOffset(w, h), AmLine(l), colorDim); } } void DrawCaveVerticalDirt(const Surface &out, Point center, AutomapTile tile, AutomapTile seTile, uint8_t colorDim) { if (seTile.hasFlag(AutomapTile::Flags::Dirt) || (leveltype != DTYPE_TOWN && IsNoneOf(tile.type, AutomapTile::Types::CaveVerticalWood, AutomapTile::Types::CaveVerticalWoodCross, AutomapTile::Types::CaveWoodCross, AutomapTile::Types::CaveRightWoodCross))) { SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::QuarterTileRight, AmHeightOffset::ThreeQuartersTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), colorDim); SetMapPixel(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileRight, AmHeightOffset::QuarterTileDown), colorDim); } } /** * For caves the horizontal/vertical flags are swapped */ void DrawCaveVertical(const Surface &out, Point center, AutomapTile tile, AutomapTile neTile, AutomapTile seTile, uint8_t colorBright, uint8_t colorDim) { if (tile.hasFlag(AutomapTile::Flags::HorizontalDoor)) { DrawMapVerticalDoor(out, center, neTile, colorBright, colorDim); } else { AmLineLength l; if (IsAnyOf(tile.type, AutomapTile::Types::CaveVerticalCross, AutomapTile::Types::CaveVerticalWoodCross)) { l = AmLineLength::FullTile; } else { l = AmLineLength::FullAndHalfTile; } DrawCaveVerticalDirt(out, center, tile, seTile, colorDim); DrawMapLineNE(out, { center + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown) }, AmLine(l), colorDim); } } void DrawCaveLeftCorner(const Surface &out, Point center, uint8_t colorDim) { DrawMapLineSE(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileUp), AmLine(AmLineLength::HalfTile), colorDim); DrawMapLineNE(out, center + AmOffset(AmWidthOffset::ThreeQuartersTileLeft, AmHeightOffset::QuarterTileDown), AmLine(AmLineLength::HalfTile), colorDim); } void DrawCaveRightCorner(const Surface &out, Point center, uint8_t colorDim) { DrawMapLineSE(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), AmLine(AmLineLength::HalfTile), colorDim); DrawMapLineNE(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None), AmLine(AmLineLength::HalfTile), colorDim); } void DrawMapEllipse(const Surface &out, Point from, int radius, uint8_t colorIndex) { const int a = radius; const int b = radius / 2; int x = 0; int y = b; // Offset ellipse so the center of the ellipse is the center of our megatile on the x plane from.x -= radius; // Initial point SetMapPixel(out, { from.x, from.y + b }, colorIndex); SetMapPixel(out, { from.x, from.y - b }, colorIndex); // Initialize the parameters int p1 = (b * b) - (a * a * b) + (a * a) / 4; // Region 1 while ((b * b * x) < (a * a * y)) { x++; if (p1 < 0) { p1 += (2 * b * b * x) + (b * b); } else { y--; p1 += (2 * b * b * x) - (2 * a * a * y) + (b * b); } SetMapPixel(out, { from.x + x, from.y + y }, colorIndex); SetMapPixel(out, { from.x - x, from.y + y }, colorIndex); SetMapPixel(out, { from.x + x, from.y - y }, colorIndex); SetMapPixel(out, { from.x - x, from.y - y }, colorIndex); } // Initialize the second parameter for Region 2 int p2 = (b * b * ((x + 1) * (x + 1))) + (a * a * ((y - 1) * (y - 1))) - (a * a * b * b); // Region 2 while (y > 0) { y--; if (p2 > 0) { p2 += (-2 * a * a * y) + (a * a); } else { x++; p2 += (2 * b * b * x) - (2 * a * a * y) + (a * a); } SetMapPixel(out, { from.x + x, from.y + y }, colorIndex); SetMapPixel(out, { from.x - x, from.y + y }, colorIndex); SetMapPixel(out, { from.x + x, from.y - y }, colorIndex); SetMapPixel(out, { from.x - x, from.y - y }, colorIndex); } } void DrawMapStar(const Surface &out, Point from, int radius, uint8_t color) { const int scaleFactor = 128; Point anchors[5]; // Offset star so the center of the star is the center of our megatile on the x plane from.x -= radius; anchors[0] = { from.x - (121 * radius / scaleFactor), from.y + (19 * radius / scaleFactor) }; // Left Point anchors[1] = { from.x + (121 * radius / scaleFactor), from.y + (19 * radius / scaleFactor) }; // Right Point anchors[2] = { from.x, from.y + (64 * radius / scaleFactor) }; // Bottom Point anchors[3] = { from.x - (75 * radius / scaleFactor), from.y - (51 * radius / scaleFactor) }; // Top Left Point anchors[4] = { from.x + (75 * radius / scaleFactor), from.y - (51 * radius / scaleFactor) }; // Top Right Point // Draw lines between the anchors to form a star DrawMapFreeLine(out, anchors[3], anchors[1], color); // Connect Top Left -> Right DrawMapFreeLine(out, anchors[1], anchors[0], color); // Connect Right -> Left DrawMapFreeLine(out, anchors[0], anchors[4], color); // Connect Left -> Top Right DrawMapFreeLine(out, anchors[4], anchors[2], color); // Connect Top Right -> Bottom DrawMapFreeLine(out, anchors[2], anchors[3], color); // Connect Bottom -> Top Left } /** * @brief Check if a given tile has the provided AutomapTile flag */ bool HasAutomapFlag(Point position, AutomapTile::Flags type) { if (position.x < 0 || position.x >= DMAXX || position.y < 0 || position.y >= DMAXX) { return false; } return AutomapTypeTiles[dungeon[position.x][position.y]].hasFlag(type); } /** * @brief Returns the automap shape at the given coordinate. */ AutomapTile GetAutomapTileType(Point position) { if (position.x < 0 || position.x >= DMAXX || position.y < 0 || position.y >= DMAXX) { return {}; } AutomapTile tile = AutomapTypeTiles[dungeon[position.x][position.y]]; if (tile.type == AutomapTile::Types::Corner) { if (HasAutomapFlag({ position.x - 1, position.y }, AutomapTile::Flags::HorizontalArch)) { if (HasAutomapFlag({ position.x, position.y - 1 }, AutomapTile::Flags::VerticalArch)) { tile.type = AutomapTile::Types::Diamond; } } } return tile; } /** * @brief Returns the automap shape at the given coordinate. */ AutomapTile GetAutomapTypeView(Point map) { if (map.x == -1 && map.y >= 0 && map.y < DMAXY && AutomapView[0][map.y] != MAP_EXP_NONE) { if (HasAutomapFlag({ 0, map.y + 1 }, AutomapTile::Flags::Dirt) && HasAutomapFlag({ 0, map.y }, AutomapTile::Flags::Dirt) && HasAutomapFlag({ 0, map.y - 1 }, AutomapTile::Flags::Dirt)) { return {}; } return { AutomapTile::Types::None, AutomapTile::Flags::Dirt }; } if (map.y == -1 && map.x >= 0 && map.x < DMAXY && AutomapView[map.x][0] != MAP_EXP_NONE) { if (HasAutomapFlag({ map.x + 1, 0 }, AutomapTile::Flags::Dirt) && HasAutomapFlag({ map.x, 0 }, AutomapTile::Flags::Dirt) && HasAutomapFlag({ map.x - 1, 0 }, AutomapTile::Flags::Dirt)) { return {}; } return { AutomapTile::Types::None, AutomapTile::Flags::Dirt }; } if (map.x < 0 || map.x >= DMAXX) { return {}; } if (map.y < 0 || map.y >= DMAXX) { return {}; } if (AutomapView[map.x][map.y] == MAP_EXP_NONE) { return {}; } return GetAutomapTileType(map); } /** * @brief Renders the given automap shape at the specified screen coordinates. */ void DrawAutomapTile(const Surface &out, Point center, Point map) { uint8_t colorBright = MapColorsBright; uint8_t colorDim = MapColorsDim; const MapExplorationType explorationType = static_cast(AutomapView[std::clamp(map.x, 0, DMAXX - 1)][std::clamp(map.y, 0, DMAXY - 1)]); switch (explorationType) { case MAP_EXP_SHRINE: colorDim = PAL16_GRAY + 11; colorBright = PAL16_GRAY + 3; break; case MAP_EXP_OTHERS: colorDim = PAL16_BEIGE + 10; colorBright = PAL16_BEIGE + 2; break; case MAP_EXP_SELF: case MAP_EXP_NONE: case MAP_EXP_OLD: break; } bool noConnect = false; AutomapTile tile = GetAutomapTypeView(map + Direction::NoDirection); AutomapTile nwTile = GetAutomapTypeView(map + Direction::NorthWest); AutomapTile neTile = GetAutomapTypeView(map + Direction::NorthEast); #ifdef _DEBUG if (DebugVision) { if (IsTileLit(map.megaToWorld())) DrawDiamond(out, center, PAL8_ORANGE + 1); if (IsTileLit(map.megaToWorld() + Direction::South)) DrawDiamond(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), PAL8_ORANGE + 1); if (IsTileLit(map.megaToWorld() + Direction::SouthWest)) DrawDiamond(out, center + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), PAL8_ORANGE + 1); if (IsTileLit(map.megaToWorld() + Direction::SouthEast)) DrawDiamond(out, center + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), PAL8_ORANGE + 1); } #endif // If the tile is an arch, grate, or diamond, we draw a diamond and therefore don't want connection lines if (tile.hasAnyFlag(AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::VerticalArch, AutomapTile::Flags::HorizontalGrate, AutomapTile::Flags::VerticalGrate) || nwTile.hasAnyFlag(AutomapTile::Flags::HorizontalArch, AutomapTile::Flags::HorizontalGrate) || neTile.hasAnyFlag(AutomapTile::Flags::VerticalArch, AutomapTile::Flags::VerticalGrate) || tile.type == AutomapTile::Types::Diamond) { noConnect = true; } // These tilesets have doors where the connection lines would be drawn if (IsAnyOf(leveltype, DTYPE_CATACOMBS, DTYPE_CAVES) && (tile.hasFlag(AutomapTile::Flags::HorizontalDoor) || tile.hasFlag(AutomapTile::Flags::VerticalDoor))) noConnect = true; const AutomapTile swTile = GetAutomapTypeView(map + Direction::SouthWest); const AutomapTile sTile = GetAutomapTypeView(map + Direction::South); const AutomapTile seTile = GetAutomapTypeView(map + Direction::SouthEast); const AutomapTile nTile = GetAutomapTypeView(map + Direction::North); const AutomapTile wTile = GetAutomapTypeView(map + Direction::West); const AutomapTile eTile = GetAutomapTypeView(map + Direction::East); if ((leveltype == DTYPE_TOWN && tile.hasFlag(AutomapTile::Flags::Dirt)) || (tile.hasFlag(AutomapTile::Flags::Dirt) && (tile.type != AutomapTile::Types::None || swTile.type != AutomapTile::Types::None || sTile.type != AutomapTile::Types::None || seTile.type != AutomapTile::Types::None || IsAnyOf(nwTile.type, AutomapTile::Types::CaveCross, AutomapTile::Types::CaveVertical, AutomapTile::Types::CaveVerticalCross, AutomapTile::Types::CaveVerticalWallLava, AutomapTile::Types::CaveLeftWoodCross) || IsAnyOf(nTile.type, AutomapTile::Types::CaveCross) || IsAnyOf(neTile.type, AutomapTile::Types::CaveCross, AutomapTile::Types::CaveHorizontal, AutomapTile::Types::CaveHorizontalCross, AutomapTile::Types::CaveHorizontalWallLava, AutomapTile::Types::CaveRightWoodCross) || IsAnyOf(wTile.type, AutomapTile::Types::CaveVerticalCross) || IsAnyOf(eTile.type, AutomapTile::Types::CaveHorizontalCross)))) { DrawDirt(out, center, nwTile, neTile, colorDim); } if (tile.hasFlag(AutomapTile::Flags::Stairs)) { DrawStairs(out, center, colorBright); } if (!noConnect) { if (IsAnyOf(leveltype, DTYPE_TOWN, DTYPE_CAVES, DTYPE_NEST)) { DrawCaveWallConnections(out, center, sTile, swTile, seTile, colorDim); } DrawWallConnections(out, center, tile, nwTile, neTile, colorBright, colorDim); } uint8_t lavaColor = MapColorsLava; if (leveltype == DTYPE_NEST) { lavaColor = MapColorsAcid; } else if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { if (Quests[Q_PWATER]._qactive != QUEST_DONE) { lavaColor = MapColorsAcid; } else { lavaColor = MapColorsWater; } } switch (tile.type) { case AutomapTile::Types::Diamond: // stand-alone column or other unpassable object DrawDiamond(out, center, colorDim); break; case AutomapTile::Types::Vertical: case AutomapTile::Types::FenceVertical: case AutomapTile::Types::VerticalDiamond: DrawVertical(out, center, tile, nwTile, neTile, swTile, colorBright, colorDim); break; case AutomapTile::Types::Horizontal: case AutomapTile::Types::FenceHorizontal: case AutomapTile::Types::HorizontalDiamond: DrawHorizontal(out, center, tile, nwTile, neTile, seTile, colorBright, colorDim); break; case AutomapTile::Types::Cross: DrawVertical(out, center, tile, nwTile, neTile, swTile, colorBright, colorDim); DrawHorizontal(out, center, tile, nwTile, neTile, seTile, colorBright, colorDim); break; case AutomapTile::Types::CaveHorizontalCross: case AutomapTile::Types::CaveHorizontalWoodCross: DrawVertical(out, center, tile, nwTile, neTile, swTile, colorBright, colorDim); DrawCaveHorizontal(out, center, tile, nwTile, swTile, colorBright, colorDim); break; case AutomapTile::Types::CaveVerticalCross: case AutomapTile::Types::CaveVerticalWoodCross: DrawHorizontal(out, center, tile, nwTile, neTile, seTile, colorBright, colorDim); DrawCaveVertical(out, center, tile, neTile, seTile, colorBright, colorDim); break; case AutomapTile::Types::CaveHorizontal: case AutomapTile::Types::CaveHorizontalWood: DrawCaveHorizontal(out, center, tile, nwTile, swTile, colorBright, colorDim); break; case AutomapTile::Types::CaveVertical: case AutomapTile::Types::CaveVerticalWood: DrawCaveVertical(out, center, tile, neTile, seTile, colorBright, colorDim); break; case AutomapTile::Types::CaveCross: // Add the missing dirt pixel SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), colorDim); [[fallthrough]]; case AutomapTile::Types::CaveWoodCross: case AutomapTile::Types::CaveRightWoodCross: case AutomapTile::Types::CaveLeftWoodCross: DrawCaveHorizontal(out, center, tile, nwTile, swTile, colorBright, colorDim); DrawCaveVertical(out, center, tile, neTile, seTile, colorBright, colorDim); break; case AutomapTile::Types::CaveLeftCorner: DrawCaveLeftCorner(out, center, colorDim); break; case AutomapTile::Types::CaveRightCorner: DrawCaveRightCorner(out, center, colorDim); break; case AutomapTile::Types::Corner: break; case AutomapTile::Types::CaveBottomCorner: // Add the missing dirt pixel // BUGFIX: A tile in poisoned water supply isn't drawing this pixel SetMapPixel(out, center + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown), colorDim); break; case AutomapTile::Types::None: break; case AutomapTile::Types::Bridge: DrawBridge(out, center, MapColorsItem); break; case AutomapTile::Types::River: DrawRiver(out, center, MapColorsItem); break; case AutomapTile::Types::RiverCornerEast: DrawRiverCornerEast(out, center, MapColorsItem); break; case AutomapTile::Types::RiverCornerNorth: DrawRiverCornerNorth(out, center, MapColorsItem); break; case AutomapTile::Types::RiverCornerSouth: DrawRiverCornerSouth(out, center, MapColorsItem); break; case AutomapTile::Types::RiverCornerWest: DrawRiverCornerWest(out, center, MapColorsItem); break; case AutomapTile::Types::RiverForkIn: DrawRiverForkIn(out, center, MapColorsItem); break; case AutomapTile::Types::RiverForkOut: DrawRiverForkOut(out, center, MapColorsItem); break; case AutomapTile::Types::RiverLeftIn: DrawRiverLeftIn(out, center, MapColorsItem); break; case AutomapTile::Types::RiverLeftOut: DrawRiverLeftOut(out, center, MapColorsItem); break; case AutomapTile::Types::RiverRightIn: DrawRiverRightIn(out, center, MapColorsItem); break; case AutomapTile::Types::RiverRightOut: DrawRiverRightOut(out, center, MapColorsItem); break; case AutomapTile::Types::HorizontalLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::VerticalLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::BendSouthLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::BendWestLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::BendEastLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::BendNorthLavaThin: DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::VerticalWallLava: DrawVertical(out, center, tile, nwTile, neTile, swTile, colorBright, colorDim); DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::HorizontalWallLava: DrawHorizontal(out, center, tile, nwTile, neTile, swTile, colorBright, colorDim); DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::SELava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::SWLava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::NELava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::NWLava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::SLava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::WLava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::ELava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::NLava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::Lava: DrawLava(out, center, lavaColor); break; case AutomapTile::Types::CaveHorizontalWallLava: DrawCaveHorizontal(out, center, tile, nwTile, swTile, colorBright, colorDim); DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::CaveVerticalWallLava: DrawCaveVertical(out, center, tile, neTile, seTile, colorBright, colorDim); DrawLavaRiver(out, center, lavaColor, false); break; case AutomapTile::Types::HorizontalBridgeLava: DrawLavaRiver(out, center, lavaColor, true); break; case AutomapTile::Types::VerticalBridgeLava: DrawLavaRiver(out, center, lavaColor, true); break; case AutomapTile::Types::PentagramClosed: // Functions are called twice to integrate shadow. Shadows are not drawn inside these functions to avoid shadows being drawn on top of normal pixels. DrawMapEllipse(out, center + Displacement { 0, 1 }, AmLine(AmLineLength::OctupleTile), 0); // shadow DrawMapStar(out, center + Displacement { 0, 1 }, AmLine(AmLineLength::OctupleTile), 0); // shadow DrawMapEllipse(out, center, AmLine(AmLineLength::OctupleTile), colorDim); DrawMapStar(out, center, AmLine(AmLineLength::OctupleTile), colorDim); break; case AutomapTile::Types::PentagramOpen: // Functions are called twice to integrate shadow. Shadows are not drawn inside these functions to avoid shadows being drawn on top of normal pixels. DrawMapEllipse(out, center + Displacement { 0, 1 }, AmLine(AmLineLength::OctupleTile), 0); // shadow DrawMapStar(out, center + Displacement { 0, 1 }, AmLine(AmLineLength::OctupleTile), 0); // shadow DrawMapEllipse(out, center, AmLine(AmLineLength::OctupleTile), MapColorsPentagramOpen); DrawMapStar(out, center, AmLine(AmLineLength::OctupleTile), MapColorsPentagramOpen); break; } } Displacement GetAutomapScreen() { Displacement screen = {}; if (GetAutomapType() == AutomapType::Minimap) { screen = { MinimapRect.position.x + MinimapRect.size.width / 2, MinimapRect.position.y + MinimapRect.size.height / 2 }; } else { screen = { gnScreenWidth / 2, (gnScreenHeight - GetMainPanel().size.height) / 2 }; } screen += AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown); return screen; } void SearchAutomapItem(const Surface &out, const Displacement &myPlayerOffset, int searchRadius, tl::function_ref highlightTile) { const Player &player = *MyPlayer; Point tile = player.position.tile; if (player._pmode == PM_WALK_SIDEWAYS) { tile = player.position.future; if (player._pdir == Direction::West) tile.x++; else tile.y++; } const int startX = std::clamp(tile.x - searchRadius, 0, MAXDUNX); const int startY = std::clamp(tile.y - searchRadius, 0, MAXDUNY); const int endX = std::clamp(tile.x + searchRadius, 0, MAXDUNX); const int endY = std::clamp(tile.y + searchRadius, 0, MAXDUNY); const AutomapType mapType = GetAutomapType(); const int scale = (mapType == AutomapType::Minimap) ? MinimapScale : AutoMapScale; for (int i = startX; i < endX; i++) { for (int j = startY; j < endY; j++) { if (!highlightTile({ i, j })) continue; const int px = i - 2 * AutomapOffset.deltaX - ViewPosition.x; const int py = j - 2 * AutomapOffset.deltaY - ViewPosition.y; Point screen = { (myPlayerOffset.deltaX * scale / 100 / 2) + (px - py) * AmLine(AmLineLength::DoubleTile), (myPlayerOffset.deltaY * scale / 100 / 2) + (px + py) * AmLine(AmLineLength::FullTile), }; screen += GetAutomapScreen(); if (mapType != AutomapType::Minimap && CanPanelsCoverView()) { if (IsRightPanelOpen()) screen.x -= gnScreenWidth / 4; if (IsLeftPanelOpen()) screen.x += gnScreenWidth / 4; } screen.y -= AmLine(AmLineLength::FullTile); DrawDiamond(out, screen, MapColorsItem); } } } uint8_t GetPlayerMapColor(int id) { static constexpr uint8_t PlayerMapColors[] = { MapColorsPlayer1, MapColorsPlayer2, MapColorsPlayer3, MapColorsPlayer4, }; if (id < 0 || id >= static_cast(SDL_arraysize(PlayerMapColors))) return MapColorsPlayer1; return PlayerMapColors[id]; } /** * @brief Renders an arrow on the automap, centered on and facing the direction of the player. */ void DrawAutomapPlr(const Surface &out, const Displacement &myPlayerOffset, const Player &player) { const uint8_t playerColor = GetPlayerMapColor(player.getId()); const Point tile = player.position.tile; const int px = tile.x - 2 * AutomapOffset.deltaX - ViewPosition.x; const int py = tile.y - 2 * AutomapOffset.deltaY - ViewPosition.y; Displacement playerOffset = {}; if (player.isWalking()) playerOffset = GetOffsetForWalking(player.AnimInfo, player._pdir); const int scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; Point base = { ((playerOffset.deltaX + myPlayerOffset.deltaX) * scale / 100 / 2) + (px - py) * AmLine(AmLineLength::DoubleTile), ((playerOffset.deltaY + myPlayerOffset.deltaY) * scale / 100 / 2) + (px + py) * AmLine(AmLineLength::FullTile) + AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileUp).deltaY }; base += GetAutomapScreen(); if (CanPanelsCoverView()) { if (IsRightPanelOpen()) base.x -= gnScreenWidth / 4; if (IsLeftPanelOpen()) base.x += gnScreenWidth / 4; } switch (player._pdir) { case Direction::North: { const Point point = base + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileUp); DrawMapLineNS(out, point, AmLine(AmLineLength::DoubleTile), playerColor); DrawMapLineSteepNE(out, point + AmOffset(AmWidthOffset::EighthTileLeft, AmHeightOffset::HalfTileDown), AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineSteepNW(out, point + AmOffset(AmWidthOffset::EighthTileRight, AmHeightOffset::HalfTileDown), AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::NorthEast: { const Point point = base + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileUp); DrawMapLineWE(out, point + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::None), AmLine(AmLineLength::FullTile), playerColor); DrawMapLineNE(out, point + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown), AmLine(AmLineLength::FullTile), playerColor); DrawMapLineSteepSW(out, point, AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::East: { const Point point = base + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None); DrawMapLineNW(out, point, AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineWE(out, point + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None), AmLine(AmLineLength::DoubleTile), playerColor); DrawMapLineSW(out, point, AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::SouthEast: { const Point point = base + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown); DrawMapLineSteepNW(out, point, AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineSE(out, point + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::FullTile), playerColor); DrawMapLineWE(out, point + AmOffset(AmWidthOffset::QuarterTileLeft, AmHeightOffset::None) + Displacement { -1, 0 }, AmLine(AmLineLength::FullTile) + 1, playerColor); } break; case Direction::South: { const Point point = base + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown); DrawMapLineNS(out, point + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileUp), AmLine(AmLineLength::DoubleTile), playerColor); DrawMapLineSteepSW(out, point + AmOffset(AmWidthOffset::EighthTileRight, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineSteepSE(out, point + AmOffset(AmWidthOffset::EighthTileLeft, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::SouthWest: { const Point point = base + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileDown); DrawMapLineSteepNE(out, point, AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineSW(out, point + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileUp), AmLine(AmLineLength::FullTile), playerColor); DrawMapLineWE(out, point, AmLine(AmLineLength::FullTile) + 1, playerColor); } break; case Direction::West: { const Point point = base + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::None); DrawMapLineNE(out, point, AmLine(AmLineLength::HalfTile), playerColor); DrawMapLineWE(out, point, AmLine(AmLineLength::DoubleTile) + 1, playerColor); DrawMapLineSE(out, point, AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::NorthWest: { const Point point = base + AmOffset(AmWidthOffset::HalfTileLeft, AmHeightOffset::HalfTileUp); DrawMapLineNW(out, point + AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::HalfTileDown), AmLine(AmLineLength::FullTile), playerColor); DrawMapLineWE(out, point, AmLine(AmLineLength::FullTile) + 1, playerColor); DrawMapLineSteepSE(out, point, AmLine(AmLineLength::HalfTile), playerColor); } break; case Direction::NoDirection: break; } } /** * @brief Renders game info, such as the name of the current level, and in multi player the name of the game and the game password. */ void DrawAutomapText(const Surface &out) { Point linePosition { 8, 8 }; auto advanceLine = [&](int numLines = 1) { linePosition.y += 15 * numLines; }; auto drawStringAndAdvanceLine = [&](std::string_view text, TextRenderOptions opts = {}, int numLines = 1) { DrawString(out, text, linePosition, opts); advanceLine(numLines); }; if (*GetOptions().Graphics.showFPS) { advanceLine(); } if (gbIsMultiplayer) { if (GameName != "0.0.0.0" && !IsLoopback) { std::string description = std::string(_("Game: ")); description.append(GameName); drawStringAndAdvanceLine(description); } std::string description; if (IsLoopback) { description = std::string(_("Offline Game")); } else if (PublicGame) { description = std::string(_("Public Game")); } else { description = std::string(_("Password: ")); description.append(GamePassword); } drawStringAndAdvanceLine(description); } if (setlevel) { drawStringAndAdvanceLine(_(QuestLevelNames[setlvlnum])); } else { std::string description; switch (leveltype) { case DTYPE_NEST: description = fmt::format(fmt::runtime(_("Level: Nest {:d}")), currlevel - 16); break; case DTYPE_CRYPT: description = fmt::format(fmt::runtime(_("Level: Crypt {:d}")), currlevel - 20); break; case DTYPE_TOWN: description = std::string(_("Town")); break; default: description = fmt::format(fmt::runtime(_("Level: {:d}")), currlevel); break; } drawStringAndAdvanceLine(description); } std::string_view difficulty; switch (sgGameInitInfo.nDifficulty) { case DIFF_NORMAL: difficulty = _("Normal"); break; case DIFF_NIGHTMARE: difficulty = _("Nightmare"); break; case DIFF_HELL: difficulty = _("Hell"); break; } const std::string description = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Game Difficulty. */ "Difficulty: {:s}")), difficulty); drawStringAndAdvanceLine(description); #ifdef _DEBUG if (DebugGodMode || DebugInvisible || DisableLighting || DebugVision || DebugPath || DebugGrid || DebugScrollViewEnabled) { const TextRenderOptions disabled { .flags = UiFlags::ColorBlack, }; const TextRenderOptions enabled { .flags = UiFlags::ColorOrange, }; advanceLine(); drawStringAndAdvanceLine("Debug toggles:"); drawStringAndAdvanceLine("Player:"); drawStringAndAdvanceLine("God Mode", DebugGodMode ? enabled : disabled); drawStringAndAdvanceLine("Invisible", DebugInvisible ? enabled : disabled); drawStringAndAdvanceLine("Display:"); drawStringAndAdvanceLine("Fullbright", DisableLighting ? enabled : disabled); drawStringAndAdvanceLine("Draw Vision", DebugVision ? enabled : disabled); drawStringAndAdvanceLine("Draw Path", DebugPath ? enabled : disabled); drawStringAndAdvanceLine("Draw Grid", DebugGrid ? enabled : disabled); drawStringAndAdvanceLine("Scroll View", DebugScrollViewEnabled ? enabled : disabled); } #endif } std::unique_ptr LoadAutomapData(size_t &tileCount) { switch (leveltype) { case DTYPE_TOWN: return LoadFileInMem("levels\\towndata\\automap.amp", &tileCount); case DTYPE_CATHEDRAL: return LoadFileInMem("levels\\l1data\\l1.amp", &tileCount); case DTYPE_CATACOMBS: return LoadFileInMem("levels\\l2data\\l2.amp", &tileCount); case DTYPE_CAVES: return LoadFileInMem("levels\\l3data\\l3.amp", &tileCount); case DTYPE_HELL: return LoadFileInMem("levels\\l4data\\l4.amp", &tileCount); case DTYPE_NEST: return LoadFileInMem("nlevels\\l6data\\l6.amp", &tileCount); case DTYPE_CRYPT: return LoadFileInMem("nlevels\\l5data\\l5.amp", &tileCount); default: return nullptr; } } } // namespace bool AutomapActive; AutomapType CurrentAutomapType = AutomapType::Opaque; uint8_t AutomapView[DMAXX][DMAXY]; int AutoMapScale; int MinimapScale; Displacement AutomapOffset; Rectangle MinimapRect {}; void InitAutomapOnce() { AutomapActive = false; AutoMapScale = 50; // Set the dimensions and screen position of the minimap relative to the screen dimensions const int minimapWidth = gnScreenWidth / 4; const Size minimapSize { minimapWidth, minimapWidth / 2 }; const int minimapPadding = gnScreenWidth / 128; MinimapRect = Rectangle { { gnScreenWidth - minimapPadding - minimapSize.width, minimapPadding }, minimapSize }; // Set minimap scale const int height = 480; const int scale = 25; const int factor = gnScreenHeight / height; if (factor >= 8) { MinimapScale = scale * 8; } else { MinimapScale = scale * factor; } } void InitAutomap() { size_t tileCount = 0; const std::unique_ptr tileTypes = LoadAutomapData(tileCount); switch (leveltype) { case DTYPE_CATACOMBS: tileTypes[41] = { AutomapTile::Types::FenceHorizontal }; break; case DTYPE_TOWN: // Town automap uses a dun file that contains caves tileset case DTYPE_CAVES: case DTYPE_NEST: tileTypes[4] = { AutomapTile::Types::CaveBottomCorner }; tileTypes[12] = { AutomapTile::Types::CaveRightCorner }; tileTypes[13] = { AutomapTile::Types::CaveLeftCorner }; if (IsAnyOf(leveltype, DTYPE_CAVES)) { tileTypes[129] = { AutomapTile::Types::CaveHorizontalWoodCross }; tileTypes[131] = { AutomapTile::Types::CaveHorizontalWoodCross }; tileTypes[133] = { AutomapTile::Types::CaveHorizontalWood }; tileTypes[135] = { AutomapTile::Types::CaveHorizontalWood }; tileTypes[150] = { AutomapTile::Types::CaveHorizontalWood }; tileTypes[145] = { AutomapTile::Types::CaveHorizontalWood, AutomapTile::Flags::VerticalDoor }; tileTypes[147] = { AutomapTile::Types::CaveHorizontalWood, AutomapTile::Flags::VerticalDoor }; tileTypes[130] = { AutomapTile::Types::CaveVerticalWoodCross }; tileTypes[132] = { AutomapTile::Types::CaveVerticalWoodCross }; tileTypes[134] = { AutomapTile::Types::CaveVerticalWood }; tileTypes[136] = { AutomapTile::Types::CaveVerticalWood }; tileTypes[151] = { AutomapTile::Types::CaveVerticalWood }; tileTypes[146] = { AutomapTile::Types::CaveVerticalWood, AutomapTile::Flags::HorizontalDoor }; tileTypes[148] = { AutomapTile::Types::CaveVerticalWood, AutomapTile::Flags::HorizontalDoor }; tileTypes[137] = { AutomapTile::Types::CaveWoodCross }; tileTypes[140] = { AutomapTile::Types::CaveWoodCross }; tileTypes[141] = { AutomapTile::Types::CaveWoodCross }; tileTypes[142] = { AutomapTile::Types::CaveWoodCross }; tileTypes[138] = { AutomapTile::Types::CaveRightWoodCross }; tileTypes[139] = { AutomapTile::Types::CaveLeftWoodCross }; tileTypes[14] = { AutomapTile::Types::HorizontalLavaThin }; tileTypes[15] = { AutomapTile::Types::HorizontalLavaThin }; tileTypes[16] = { AutomapTile::Types::VerticalLavaThin }; tileTypes[17] = { AutomapTile::Types::VerticalLavaThin }; tileTypes[18] = { AutomapTile::Types::BendSouthLavaThin }; tileTypes[19] = { AutomapTile::Types::BendWestLavaThin }; tileTypes[20] = { AutomapTile::Types::BendEastLavaThin }; tileTypes[21] = { AutomapTile::Types::BendNorthLavaThin }; tileTypes[22] = { AutomapTile::Types::VerticalWallLava }; tileTypes[23] = { AutomapTile::Types::HorizontalWallLava }; tileTypes[24] = { AutomapTile::Types::SELava }; tileTypes[25] = { AutomapTile::Types::SWLava }; tileTypes[26] = { AutomapTile::Types::NELava }; tileTypes[27] = { AutomapTile::Types::NWLava }; tileTypes[28] = { AutomapTile::Types::SLava }; tileTypes[29] = { AutomapTile::Types::WLava }; tileTypes[30] = { AutomapTile::Types::ELava }; tileTypes[31] = { AutomapTile::Types::NLava }; tileTypes[32] = { AutomapTile::Types::Lava }; tileTypes[33] = { AutomapTile::Types::Lava }; tileTypes[34] = { AutomapTile::Types::Lava }; tileTypes[35] = { AutomapTile::Types::Lava }; tileTypes[36] = { AutomapTile::Types::Lava }; tileTypes[37] = { AutomapTile::Types::Lava }; tileTypes[38] = { AutomapTile::Types::Lava }; tileTypes[39] = { AutomapTile::Types::Lava }; tileTypes[40] = { AutomapTile::Types::Lava }; tileTypes[41] = { AutomapTile::Types::CaveHorizontalWallLava }; tileTypes[42] = { AutomapTile::Types::CaveVerticalWallLava }; tileTypes[43] = { AutomapTile::Types::HorizontalBridgeLava }; tileTypes[44] = { AutomapTile::Types::VerticalBridgeLava }; } else if (IsAnyOf(leveltype, DTYPE_NEST)) { tileTypes[102] = { AutomapTile::Types::HorizontalLavaThin }; tileTypes[103] = { AutomapTile::Types::HorizontalLavaThin }; tileTypes[108] = { AutomapTile::Types::HorizontalLavaThin }; tileTypes[104] = { AutomapTile::Types::VerticalLavaThin }; tileTypes[105] = { AutomapTile::Types::VerticalLavaThin }; tileTypes[107] = { AutomapTile::Types::VerticalLavaThin }; tileTypes[112] = { AutomapTile::Types::BendSouthLavaThin }; tileTypes[113] = { AutomapTile::Types::BendWestLavaThin }; tileTypes[110] = { AutomapTile::Types::BendEastLavaThin }; tileTypes[111] = { AutomapTile::Types::BendNorthLavaThin }; tileTypes[134] = { AutomapTile::Types::VerticalWallLava }; tileTypes[135] = { AutomapTile::Types::HorizontalWallLava }; tileTypes[118] = { AutomapTile::Types::SELava }; tileTypes[119] = { AutomapTile::Types::SWLava }; tileTypes[120] = { AutomapTile::Types::NELava }; tileTypes[121] = { AutomapTile::Types::NWLava }; tileTypes[106] = { AutomapTile::Types::SLava }; tileTypes[114] = { AutomapTile::Types::WLava }; tileTypes[130] = { AutomapTile::Types::ELava }; tileTypes[122] = { AutomapTile::Types::NLava }; tileTypes[117] = { AutomapTile::Types::Lava }; tileTypes[124] = { AutomapTile::Types::Lava }; tileTypes[126] = { AutomapTile::Types::Lava }; tileTypes[127] = { AutomapTile::Types::Lava }; tileTypes[128] = { AutomapTile::Types::Lava }; tileTypes[129] = { AutomapTile::Types::Lava }; tileTypes[131] = { AutomapTile::Types::Lava }; tileTypes[132] = { AutomapTile::Types::Lava }; tileTypes[133] = { AutomapTile::Types::Lava }; tileTypes[136] = { AutomapTile::Types::CaveHorizontalWallLava }; tileTypes[137] = { AutomapTile::Types::CaveVerticalWallLava }; tileTypes[115] = { AutomapTile::Types::HorizontalBridgeLava }; tileTypes[116] = { AutomapTile::Types::VerticalBridgeLava }; } break; case DTYPE_HELL: tileTypes[51] = { AutomapTile::Types::VerticalDiamond }; tileTypes[55] = { AutomapTile::Types::HorizontalDiamond }; tileTypes[102] = { AutomapTile::Types::PentagramClosed }; tileTypes[111] = { AutomapTile::Types::PentagramOpen }; break; default: break; } for (unsigned i = 0; i < tileCount; i++) { AutomapTypeTiles[i + 1] = tileTypes[i]; } memset(AutomapView, 0, sizeof(AutomapView)); for (auto &column : dFlags) for (auto &dFlag : column) dFlag &= ~DungeonFlag::Explored; } void StartAutomap() { AutomapOffset = { 0, 0 }; AutomapActive = true; } void AutomapUp() { AutomapOffset.deltaX--; AutomapOffset.deltaY--; } void AutomapDown() { AutomapOffset.deltaX++; AutomapOffset.deltaY++; } void AutomapLeft() { AutomapOffset.deltaX--; AutomapOffset.deltaY++; } void AutomapRight() { AutomapOffset.deltaX++; AutomapOffset.deltaY--; } void AutomapZoomIn() { int &scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; if (scale >= 200) return; scale += 25; } void AutomapZoomOut() { int &scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; if (scale <= 25) return; scale -= 25; } void DrawAutomap(const Surface &out) { Automap = { (ViewPosition.x - 8) / 2, (ViewPosition.y - 8) / 2 }; if (leveltype != DTYPE_TOWN) { Automap += { -4, -4 }; } while (Automap.x + AutomapOffset.deltaX < 0) AutomapOffset.deltaX++; while (Automap.x + AutomapOffset.deltaX >= DMAXX) AutomapOffset.deltaX--; while (Automap.y + AutomapOffset.deltaY < 0) AutomapOffset.deltaY++; while (Automap.y + AutomapOffset.deltaY >= DMAXY) AutomapOffset.deltaY--; Automap += AutomapOffset; const Player &myPlayer = *MyPlayer; Displacement myPlayerOffset = {}; if (myPlayer.isWalking()) myPlayerOffset = GetOffsetForWalking(myPlayer.AnimInfo, myPlayer._pdir, true); const int scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; const int d = (scale * 64) / 100; int cells = 2 * (gnScreenWidth / 2 / d) + 1; if (((gnScreenWidth / 2) % d) != 0) cells++; if (((gnScreenWidth / 2) % d) >= (scale * 32) / 100) cells++; if ((myPlayerOffset.deltaX + myPlayerOffset.deltaY) != 0) cells++; if (GetAutomapType() == AutomapType::Minimap) { // Background fill DrawHalfTransparentRectTo(out, MinimapRect.position.x, MinimapRect.position.y, MinimapRect.size.width, MinimapRect.size.height); const uint8_t frameShadowColor = PAL16_YELLOW + 12; // Shadow DrawHorizontalLine(out, MinimapRect.position + Displacement { -1, -1 }, MinimapRect.size.width + 1, frameShadowColor); DrawHorizontalLine(out, MinimapRect.position + Displacement { -2, MinimapRect.size.height + 1 }, MinimapRect.size.width + 4, frameShadowColor); DrawVerticalLine(out, MinimapRect.position + Displacement { -1, 0 }, MinimapRect.size.height, frameShadowColor); DrawVerticalLine(out, MinimapRect.position + Displacement { MinimapRect.size.width + 1, -2 }, MinimapRect.size.height + 3, frameShadowColor); // Frame DrawHorizontalLine(out, MinimapRect.position + Displacement { -2, -2 }, MinimapRect.size.width + 3, MapColorsDim); DrawHorizontalLine(out, MinimapRect.position + Displacement { -2, MinimapRect.size.height }, MinimapRect.size.width + 3, MapColorsDim); DrawVerticalLine(out, MinimapRect.position + Displacement { -2, -1 }, MinimapRect.size.height + 1, MapColorsDim); DrawVerticalLine(out, MinimapRect.position + Displacement { MinimapRect.size.width, -1 }, MinimapRect.size.height + 1, MapColorsDim); if (AutoMapShowItems) SearchAutomapItem(out, myPlayerOffset, 8, [](Point position) { return dItem[position.x][position.y] != 0; }); } Point screen = {}; screen += GetAutomapScreen(); if ((cells & 1) != 0) { screen.x -= AmOffset(AmWidthOffset::DoubleTileRight, AmHeightOffset::None).deltaX * ((cells - 1) / 2); screen.y -= AmOffset(AmWidthOffset::None, AmHeightOffset::DoubleTileDown).deltaY * ((cells + 1) / 2); } else { screen.x -= AmOffset(AmWidthOffset::DoubleTileRight, AmHeightOffset::None).deltaX * (cells / 2) + AmOffset(AmWidthOffset::FullTileLeft, AmHeightOffset::None).deltaX; screen.y -= AmOffset(AmWidthOffset::None, AmHeightOffset::DoubleTileDown).deltaY * (cells / 2) + AmOffset(AmWidthOffset::None, AmHeightOffset::FullTileDown).deltaY; } if ((ViewPosition.x & 1) != 0) { screen.x -= AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None).deltaX; screen.y -= AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown).deltaY; } if ((ViewPosition.y & 1) != 0) { screen.x += AmOffset(AmWidthOffset::HalfTileRight, AmHeightOffset::None).deltaX; screen.y -= AmOffset(AmWidthOffset::None, AmHeightOffset::HalfTileDown).deltaY; } screen.x += scale * myPlayerOffset.deltaX / 100 / 2; screen.y += scale * myPlayerOffset.deltaY / 100 / 2; if (CanPanelsCoverView()) { if (IsRightPanelOpen()) { screen.x -= gnScreenWidth / 4; } if (IsLeftPanelOpen()) { screen.x += gnScreenWidth / 4; } } Point map = { Automap.x - cells, Automap.y - 1 }; for (int i = 0; i <= cells + 1; i++) { Point tile1 = screen; for (int j = 0; j < cells; j++) { DrawAutomapTile(out, tile1, { map.x + j, map.y - j }); tile1.x += AmOffset(AmWidthOffset::DoubleTileRight, AmHeightOffset::None).deltaX; } map.y++; Point tile2 = screen + AmOffset(AmWidthOffset::FullTileLeft, AmHeightOffset::FullTileDown); for (int j = 0; j <= cells; j++) { DrawAutomapTile(out, tile2, { map.x + j, map.y - j }); tile2.x += AmOffset(AmWidthOffset::DoubleTileRight, AmHeightOffset::None).deltaX; } map.x++; screen.y += AmOffset(AmWidthOffset::None, AmHeightOffset::DoubleTileDown).deltaY; } for (const Player &player : Players) { if (player.isOnActiveLevel() && player.plractive && !player._pLvlChanging && (&player == MyPlayer || player.friendlyMode)) { DrawAutomapPlr(out, myPlayerOffset, player); } } if (AutoMapShowItems) SearchAutomapItem(out, myPlayerOffset, 8, [](Point position) { return dItem[position.x][position.y] != 0; }); #ifdef _DEBUG if (IsDebugAutomapHighlightNeeded()) SearchAutomapItem(out, myPlayerOffset, std::max(MAXDUNX, MAXDUNY), ShouldHighlightDebugAutomapTile); #endif DrawAutomapText(out); } void UpdateAutomapExplorer(Point map, MapExplorationType explorer) { if (AutomapView[map.x][map.y] < explorer) AutomapView[map.x][map.y] = explorer; } void SetAutomapView(Point position, MapExplorationType explorer) { const Point map { (position.x - 16) / 2, (position.y - 16) / 2 }; if (map.x < 0 || map.x >= DMAXX || map.y < 0 || map.y >= DMAXY) { return; } UpdateAutomapExplorer(map, explorer); const AutomapTile tile = GetAutomapTileType(map); const bool solid = tile.hasFlag(AutomapTile::Flags::Dirt); switch (tile.type) { case AutomapTile::Types::Vertical: if (solid) { auto tileSW = GetAutomapTileType({ map.x, map.y + 1 }); if (tileSW.type == AutomapTile::Types::Corner && tileSW.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x, map.y + 1 }, explorer); } else if (HasAutomapFlag({ map.x - 1, map.y }, AutomapTile::Flags::Dirt)) { UpdateAutomapExplorer({ map.x - 1, map.y }, explorer); } break; case AutomapTile::Types::Horizontal: if (solid) { auto tileSE = GetAutomapTileType({ map.x + 1, map.y }); if (tileSE.type == AutomapTile::Types::Corner && tileSE.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x + 1, map.y }, explorer); } else if (HasAutomapFlag({ map.x, map.y - 1 }, AutomapTile::Flags::Dirt)) { UpdateAutomapExplorer({ map.x, map.y - 1 }, explorer); } break; case AutomapTile::Types::Cross: if (solid) { auto tileSW = GetAutomapTileType({ map.x, map.y + 1 }); if (tileSW.type == AutomapTile::Types::Corner && tileSW.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x, map.y + 1 }, explorer); auto tileSE = GetAutomapTileType({ map.x + 1, map.y }); if (tileSE.type == AutomapTile::Types::Corner && tileSE.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x + 1, map.y }, explorer); } else { if (HasAutomapFlag({ map.x - 1, map.y }, AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x - 1, map.y }, explorer); if (HasAutomapFlag({ map.x, map.y - 1 }, AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x, map.y - 1 }, explorer); if (HasAutomapFlag({ map.x - 1, map.y - 1 }, AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x - 1, map.y - 1 }, explorer); } break; case AutomapTile::Types::FenceVertical: if (solid) { if (HasAutomapFlag({ map.x, map.y - 1 }, AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x, map.y - 1 }, explorer); auto tileSW = GetAutomapTileType({ map.x, map.y + 1 }); if (tileSW.type == AutomapTile::Types::Corner && tileSW.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x, map.y + 1 }, explorer); } else if (HasAutomapFlag({ map.x - 1, map.y }, AutomapTile::Flags::Dirt)) { UpdateAutomapExplorer({ map.x - 1, map.y }, explorer); } break; case AutomapTile::Types::FenceHorizontal: if (solid) { if (HasAutomapFlag({ map.x - 1, map.y }, AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x - 1, map.y }, explorer); auto tileSE = GetAutomapTileType({ map.x + 1, map.y }); if (tileSE.type == AutomapTile::Types::Corner && tileSE.hasFlag(AutomapTile::Flags::Dirt)) UpdateAutomapExplorer({ map.x + 1, map.y }, explorer); } else if (HasAutomapFlag({ map.x, map.y - 1 }, AutomapTile::Flags::Dirt)) { UpdateAutomapExplorer({ map.x, map.y - 1 }, explorer); } break; default: break; } } void AutomapZoomReset() { AutomapOffset = { 0, 0 }; } } // namespace devilution ================================================ FILE: Source/automap.h ================================================ /** * @file automap.h * * Interface of the in-game map overlay. */ #pragma once #include #include "engine/displacement.hpp" #include "engine/point.hpp" #include "engine/surface.hpp" #include "levels/gendung.h" #include "utils/attributes.h" namespace devilution { enum MapExplorationType : uint8_t { /** unexplored map tile */ MAP_EXP_NONE, /** map tile explored in vanilla - compatibility reasons */ MAP_EXP_OLD, /** map explored by a shrine */ MAP_EXP_SHRINE, /** map tile explored by someone else in multiplayer */ MAP_EXP_OTHERS, /** map tile explored by current player */ MAP_EXP_SELF, }; /** Specifies whether the automap is enabled. */ extern DVL_API_FOR_TEST bool AutomapActive; /** Tracks the explored areas of the map. */ extern uint8_t AutomapView[DMAXX][DMAXY]; /** Specifies the scale of the automap. */ extern DVL_API_FOR_TEST int AutoMapScale; extern DVL_API_FOR_TEST int MinimapScale; extern DVL_API_FOR_TEST Displacement AutomapOffset; extern Rectangle MinimapRect; /** Defines the offsets used for Automap lines */ enum class AmWidthOffset : int8_t { None, EighthTileRight = TILE_WIDTH >> 4, QuarterTileRight = TILE_WIDTH >> 3, HalfTileRight = TILE_WIDTH >> 2, ThreeQuartersTileRight = (TILE_WIDTH >> 1) - (TILE_WIDTH >> 3), FullTileRight = TILE_WIDTH >> 1, DoubleTileRight = TILE_WIDTH, EighthTileLeft = -EighthTileRight, QuarterTileLeft = -QuarterTileRight, HalfTileLeft = -HalfTileRight, ThreeQuartersTileLeft = -ThreeQuartersTileRight, FullTileLeft = -FullTileRight, DoubleTileLeft = -DoubleTileRight, }; enum class AmHeightOffset : int8_t { None, EighthTileDown = TILE_HEIGHT >> 4, QuarterTileDown = TILE_HEIGHT >> 3, HalfTileDown = TILE_HEIGHT >> 2, ThreeQuartersTileDown = (TILE_HEIGHT >> 1) - (TILE_HEIGHT >> 3), FullTileDown = TILE_HEIGHT >> 1, DoubleTileDown = TILE_HEIGHT, EighthTileUp = -EighthTileDown, QuarterTileUp = -QuarterTileDown, HalfTileUp = -HalfTileDown, ThreeQuartersTileUp = -ThreeQuartersTileDown, FullTileUp = -FullTileDown, DoubleTileUp = -DoubleTileDown, }; enum class AmLineLength : uint8_t { QuarterTile = 2, HalfTile = 4, FullTile = 8, FullAndHalfTile = 12, DoubleTile = 16, OctupleTile = 64, }; enum class AutomapType : uint8_t { Opaque, FIRST = Opaque, Transparent, Minimap, LAST = Minimap }; extern DVL_API_FOR_TEST AutomapType CurrentAutomapType; /** * @brief Sets the map type. Does not change `AutomapActive`. */ inline void SetAutomapType(AutomapType type) { CurrentAutomapType = type; } /** * @brief Sets the map type. Does not change `AutomapActive`. */ inline AutomapType GetAutomapType() { return CurrentAutomapType; } inline Displacement AmOffset(AmWidthOffset x, AmHeightOffset y) { int scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; return { scale * static_cast(x) / 100, scale * static_cast(y) / 100 }; } inline int AmLine(AmLineLength l) { int scale = (GetAutomapType() == AutomapType::Minimap) ? MinimapScale : AutoMapScale; return scale * static_cast(l) / 100; } /** * @brief Sets the map type. Does not change `AutomapActive`. */ void SetAutomapType(AutomapType type); AutomapType GetAutomapType(); /** * @brief Initializes the automap. */ void InitAutomapOnce(); /** * @brief Loads the mapping between tile IDs and automap shapes. */ void InitAutomap(); /** * @brief Displays the automap. */ void StartAutomap(); /** * @brief Displays the minimap. */ void StartMinimap(); /** * @brief Scrolls the automap upwards. */ void AutomapUp(); /** * @brief Scrolls the automap downwards. */ void AutomapDown(); /** * @brief Scrolls the automap leftwards. */ void AutomapLeft(); /** * @brief Scrolls the automap rightwards. */ void AutomapRight(); /** * @brief Increases the zoom level of the automap. */ void AutomapZoomIn(); /** * @brief Decreases the zoom level of the automap. */ void AutomapZoomOut(); /** * @brief Renders the automap to the given buffer. */ void DrawAutomap(const Surface &out); /** * @brief Updates automap explorer at point if value is higher than existing. */ void UpdateAutomapExplorer(Point map, MapExplorationType explorer); /** * @brief Marks the given coordinate as within view on the automap. */ void SetAutomapView(Point tile, MapExplorationType explorer); /** * @brief Resets the zoom level of the automap. */ void AutomapZoomReset(); } // namespace devilution ================================================ FILE: Source/capture.cpp ================================================ /** * @file capture.cpp * * Implementation of the screenshot function. */ #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #include "utils/sdl_compat.h" #endif #include #define DEVILUTIONX_SCREENSHOT_FORMAT_PCX 0 #define DEVILUTIONX_SCREENSHOT_FORMAT_PNG 1 #if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX #include "utils/surface_to_pcx.hpp" #endif #if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG #include "utils/surface_to_png.hpp" #endif #include "engine/backbuffer_state.hpp" #include "engine/dx.h" #include "engine/palette.h" #include "engine/render/scrollrt.h" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/str_cat.hpp" namespace devilution { namespace { SDL_IOStream *CaptureFile(std::string *dstPath) { const char *ext = #if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX ".pcx"; #elif DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG ".png"; #endif const std::time_t tt = std::time(nullptr); const std::tm *tm = std::localtime(&tt); const std::string filename = tm != nullptr ? StrCat("Screenshot from ", LeftPad(tm->tm_year + 1900, 4, '0'), "-", LeftPad(tm->tm_mon + 1, 2, '0'), "-", LeftPad(tm->tm_mday, 2, '0'), "-", LeftPad(tm->tm_hour, 2, '0'), "-", LeftPad(tm->tm_min, 2, '0'), "-", LeftPad(tm->tm_sec, 2, '0')) : "Screenshot"; *dstPath = StrCat(paths::PrefPath(), filename, ext); int i = 0; while (FileExists(dstPath->c_str())) { i++; *dstPath = StrCat(paths::PrefPath(), filename, "-", i, ext); } return SDL_IOFromFile(dstPath->c_str(), "wb"); } /** * @brief Make a red version of the given palette and apply it to the screen. */ void RedPalette() { for (int i = 0; i < 256; i++) { system_palette[i].g = 0; system_palette[i].b = 0; } SystemPaletteUpdated(); BltFast(nullptr, nullptr); RenderPresent(); } } // namespace void CaptureScreen() { std::string fileName; const uint32_t startTime = SDL_GetTicks(); auto *outStream = CaptureFile(&fileName); if (outStream == nullptr) { LogError("Failed to open {} for writing: {}", fileName, SDL_GetError()); SDL_ClearError(); return; } DrawAndBlit(); const std::array origSystemPalette = system_palette; RedPalette(); system_palette = origSystemPalette; SystemPaletteUpdated(); const tl::expected result = #if DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PCX WriteSurfaceToFilePcx(GlobalBackBuffer(), outStream); #elif DEVILUTIONX_SCREENSHOT_FORMAT == DEVILUTIONX_SCREENSHOT_FORMAT_PNG WriteSurfaceToFilePng(GlobalBackBuffer(), outStream); #endif if (!result.has_value()) { LogError("Failed to save screenshot at {}: ", fileName, result.error()); RemoveFile(fileName.c_str()); } else { Log("Screenshot saved at {}", fileName); } const uint32_t timePassed = SDL_GetTicks() - startTime; if (timePassed < 300) { SDL_Delay(300 - timePassed); } RedrawEverything(); } } // namespace devilution ================================================ FILE: Source/capture.h ================================================ /** * @file capture.h * * Interface of the screenshot function. */ #pragma once namespace devilution { /** * @brief Save the current screen to a screen??.pcx (00-99) in file if available, then make the screen red for 200ms. */ void CaptureScreen(); } // namespace devilution ================================================ FILE: Source/codec.cpp ================================================ #include #include #include #include #include "appfat.h" #include "sha.h" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/log.hpp" namespace devilution { namespace { struct CodecSignature { uint32_t checksum; uint8_t error; uint8_t lastChunkSize; }; constexpr size_t BlockSizeBytes = BlockSize * sizeof(uint32_t); constexpr size_t SignatureSize = 8; SHA1Context CodecInitKey(const char *pszPassword) { uint32_t pw[BlockSize]; // Repeat password until 64 char long std::size_t j = 0; for (uint32_t &value : pw) { if (pszPassword[j] == '\0') j = 0; value = LoadLE32(&pszPassword[j]); j += sizeof(uint32_t); } uint32_t digest[SHA1HashSize]; { SHA1Context context; SHA1Calculate(context, pw); SHA1Result(context, digest); } uint32_t key[BlockSize] { 2908958655, 4146550480, 658981742, 1113311088, 3927878744, 679301322, 1760465731, 3305370375, 2269115995, 3928541685, 580724401, 2607446661, 2233092279, 2416822349, 4106933702, 3046442503 }; for (unsigned i = 0; i < BlockSize; ++i) { key[i] ^= digest[(i + 3) % SHA1HashSize]; } SHA1Context context; SHA1Calculate(context, key); return context; } CodecSignature GetCodecSignature(std::byte *src) { CodecSignature result; result.checksum = LoadLE32(src); src += 4; result.error = static_cast(*src++); result.lastChunkSize = static_cast(*src); return result; } void SetCodecSignature(std::byte *dst, CodecSignature sig) { *dst++ = static_cast(sig.checksum); *dst++ = static_cast(sig.checksum >> 8); *dst++ = static_cast(sig.checksum >> 16); *dst++ = static_cast(sig.checksum >> 24); *dst++ = static_cast(sig.error); *dst++ = static_cast(sig.lastChunkSize); *dst++ = static_cast(0); *dst++ = static_cast(0); } void ByteSwapBlock(uint32_t *data) { for (size_t i = 0; i < BlockSize; ++i) data[i] = Swap32LE(data[i]); } void XorBlock(const uint32_t *shaResult, uint32_t *out) { for (unsigned i = 0; i < BlockSize; ++i) out[i] ^= shaResult[i % SHA1HashSize]; } } // namespace std::size_t codec_decode(std::byte *pbSrcDst, std::size_t size, const char *pszPassword) { uint32_t buf[BlockSize]; uint32_t dst[SHA1HashSize]; SHA1Context context = CodecInitKey(pszPassword); if (size <= SignatureSize) return 0; size -= SignatureSize; if (size % BlockSize != 0) return 0; for (size_t i = 0; i < size; pbSrcDst += BlockSizeBytes, i += BlockSizeBytes) { memcpy(buf, pbSrcDst, BlockSizeBytes); ByteSwapBlock(buf); SHA1Result(context, dst); XorBlock(dst, buf); SHA1Calculate(context, buf); ByteSwapBlock(buf); memcpy(pbSrcDst, buf, BlockSizeBytes); } memset(buf, 0, sizeof(buf)); const CodecSignature sig = GetCodecSignature(pbSrcDst); if (sig.error > 0) { return 0; } SHA1Result(context, dst); if (sig.checksum != dst[0]) { LogError("Checksum mismatch signature={} vs calculated={}", sig.checksum, dst[0]); memset(dst, 0, sizeof(dst)); return 0; } size += sig.lastChunkSize - BlockSizeBytes; return size; } std::size_t codec_get_encoded_len(std::size_t dwSrcBytes) { if (dwSrcBytes % BlockSizeBytes != 0) dwSrcBytes += BlockSizeBytes - (dwSrcBytes % BlockSizeBytes); return dwSrcBytes + SignatureSize; } void codec_encode(std::byte *pbSrcDst, std::size_t size, std::size_t size64, const char *pszPassword) { uint32_t buf[BlockSize]; uint32_t tmp[SHA1HashSize]; uint32_t dst[SHA1HashSize]; if (size64 != codec_get_encoded_len(size)) app_fatal("Invalid encode parameters"); SHA1Context context = CodecInitKey(pszPassword); size_t lastChunk = 0; while (size != 0) { const size_t chunk = std::min(size, BlockSizeBytes); memset(buf, 0, sizeof(buf)); memcpy(buf, pbSrcDst, chunk); ByteSwapBlock(buf); SHA1Result(context, dst); SHA1Calculate(context, buf); XorBlock(dst, buf); ByteSwapBlock(buf); memcpy(pbSrcDst, buf, BlockSizeBytes); pbSrcDst += BlockSizeBytes; lastChunk = chunk; size -= chunk; } memset(buf, 0, sizeof(buf)); SHA1Result(context, tmp); SetCodecSignature(pbSrcDst, CodecSignature { /*.checksum=*/*reinterpret_cast(tmp), /*.error=*/0, // lastChunk is at most 64 so will always fit in an 8 bit var /*.lastChunkSize=*/static_cast(lastChunk) }); } } // namespace devilution ================================================ FILE: Source/codec.h ================================================ /** * @file codec.h * * Interface of save game encryption algorithm. */ #pragma once #include namespace devilution { std::size_t codec_decode(std::byte *pbSrcDst, std::size_t size, const char *pszPassword); std::size_t codec_get_encoded_len(std::size_t dwSrcBytes); void codec_encode(std::byte *pbSrcDst, std::size_t size, std::size_t size_64, const char *pszPassword); } // namespace devilution ================================================ FILE: Source/control/control.hpp ================================================ #pragma once #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include #include "DiabloUI/text_input.hpp" #include "DiabloUI/ui_flags.hpp" #include "engine/displacement.hpp" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/render/text_render.hpp" #include "engine/size.hpp" #include "panels/ui_panels.hpp" #include "spells.h" #include "tables/spelldat.h" #include "utils/attributes.h" #include "utils/string_or_view.hpp" #include "utils/ui_fwd.h" namespace devilution { constexpr Size SidePanelSize { 320, 352 }; constexpr Rectangle InfoBoxRect = { { 177, 46 }, { 288, 64 } }; extern bool CharPanelButton[4]; extern bool CharPanelButtonActive; extern int SpellbookTab; extern UiFlags InfoColor; extern StringOrView InfoString; extern StringOrView FloatingInfoString; extern Rectangle MainPanelButtonRect[8]; extern Rectangle CharPanelButtonRect[4]; extern bool MainPanelButtonDown; extern bool LevelButtonDown; extern std::optional BottomBuffer; extern OptionalOwnedClxSpriteList GoldBoxBuffer; extern bool MainPanelFlag; extern bool ChatFlag; extern bool SpellbookFlag; extern bool CharFlag; extern bool SpellSelectFlag; [[nodiscard]] const Rectangle &GetMainPanel(); [[nodiscard]] const Rectangle &GetLeftPanel(); [[nodiscard]] const Rectangle &GetRightPanel(); bool IsLeftPanelOpen(); bool IsRightPanelOpen(); void CalculatePanelAreas(); /** * @brief Moves the mouse to the first attribute "+" button. */ void FocusOnCharInfo(); void OpenCharPanel(); void CloseCharPanel(); void ToggleCharPanel(); /** * @brief Check if the UI can cover the game area entirely */ [[nodiscard]] inline bool CanPanelsCoverView() { const Rectangle &mainPanel = GetMainPanel(); return GetScreenWidth() <= mainPanel.size.width && GetScreenHeight() <= SidePanelSize.height + mainPanel.size.height; } void AddInfoBoxString(std::string_view str, bool floatingBox = false); void AddInfoBoxString(std::string &&str, bool floatingBox = false); void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition); Point GetPanelPosition(UiPanels panel, Point offset = { 0, 0 }); tl::expected InitMainPanel(); void DrawMainPanel(const Surface &out); /** * Draws the control panel buttons in their current state. If the button is in the default * state draw it from the panel cel(extract its sub-rect). Else draw it from the buttons cel. */ void DrawMainPanelButtons(const Surface &out); /** * Clears panel button flags. */ void ResetMainPanelButtons(); /** * Checks if the mouse cursor is within any of the panel buttons and flag it if so. */ void CheckMainPanelButton(); void CheckMainPanelButtonDead(); void DoAutoMap(); void CycleAutomapType(); /** * Checks the mouse cursor position within the control panel and sets information * strings if needed. */ void CheckPanelInfo(); /** * Check if the mouse is within a control panel button that's flagged. * Takes appropriate action if so. */ void CheckMainPanelButtonUp(); void FreeControlPan(); /** * Sets a string to be drawn in the info box and then draws it. */ void DrawInfoBox(const Surface &out); void DrawFloatingInfoBox(const Surface &out); void CheckLevelButton(); void CheckLevelButtonUp(); void DrawLevelButton(const Surface &out); void CheckChrBtns(); void ReleaseChrBtns(bool addAllStatPoints); void DrawDurIcon(const Surface &out); void RedBack(const Surface &out); void DrawDeathText(const Surface &out); void DrawSpellBook(const Surface &out); extern Rectangle CharPanelButtonRect[4]; bool CheckKeypress(SDL_Keycode vkey); void DiabloHotkeyMsg(uint32_t dwMsg); void DrawChatBox(const Surface &out); bool CheckMuteButton(); void CheckMuteButtonUp(); void TypeChatMessage(); void ResetChat(); bool IsChatActive(); bool IsChatAvailable(); bool HandleTalkTextInputEvent(const SDL_Event &event); /** * Draws the top dome of the life flask (that part that protrudes out of the control panel). * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and * the filled flask cel is drawn from that level to the top of the control panel if required. */ void DrawLifeFlaskUpper(const Surface &out); /** * Controls the drawing of the area of the life flask within the control panel. * First sets the fill amount then draws the empty flask cel portion then the filled * flask portion. */ void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion); /** * Draws the top dome of the mana flask (that part that protrudes out of the control panel). * The empty flask cel is drawn from the top of the flask to the fill level (there is always a 2 pixel "air gap") and * the filled flask cel is drawn from that level to the top of the control panel if required. */ void DrawManaFlaskUpper(const Surface &out); /** * Controls the drawing of the area of the mana flask within the control panel. */ void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion); /** * Controls drawing of current / max values (health, mana) within the control panel. */ void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue); /** * @brief calls on the active player object to update HP/Mana percentage variables * * This is used to ensure that DrawFlaskAbovePanel routines display an accurate representation of the players health/mana * * @see Player::UpdateHitPointPercentage() and Player::UpdateManaPercentage() */ void UpdateLifeManaPercent(); extern bool DropGoldFlag; void DrawGoldSplit(const Surface &out); void control_drop_gold(SDL_Keycode vkey); void OpenGoldDrop(int8_t invIndex, int max); void CloseGoldDrop(); bool HandleGoldDropTextInputEvent(const SDL_Event &event); } // namespace devilution ================================================ FILE: Source/control/control_chat.cpp ================================================ #include "control_chat.hpp" #include "control.hpp" #include "control_panel.hpp" #include "control/control_chat_commands.hpp" #include "engine/backbuffer_state.hpp" #include "engine/render/clx_render.hpp" #include "options.h" #include "panels/console.hpp" #include "panels/mainpanel.hpp" #include "quick_messages.hpp" #include "utils/display.h" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" namespace devilution { std::optional ChatInputState; char TalkMessage[MAX_SEND_STR_LEN]; bool TalkButtonsDown[3]; int sgbPlrTalkTbl; bool WhisperList[MAX_PLRS]; OptionalOwnedClxSpriteList talkButtons; namespace { char TalkSave[8][MAX_SEND_STR_LEN]; uint8_t TalkSaveIndex; uint8_t NextTalkSave; TextInputCursorState ChatCursor; int MuteButtons = 3; int MuteButtonPadding = 2; Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } }; void ResetChatMessage() { if (CheckChatCommand(TalkMessage)) return; uint32_t pmask = 0; for (size_t i = 0; i < Players.size(); i++) { if (WhisperList[i]) pmask |= 1 << i; } NetSendCmdString(pmask, TalkMessage); } void ControlPressEnter() { if (TalkMessage[0] != 0) { ResetChatMessage(); uint8_t i = 0; for (; i < 8; i++) { if (strcmp(TalkSave[i], TalkMessage) == 0) break; } if (i >= 8) { strcpy(TalkSave[NextTalkSave], TalkMessage); NextTalkSave++; NextTalkSave &= 7; } else { uint8_t talkSave = NextTalkSave - 1; talkSave &= 7; if (i != talkSave) { strcpy(TalkSave[i], TalkSave[talkSave]); *BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0'; } } TalkMessage[0] = '\0'; TalkSaveIndex = NextTalkSave; } ResetChat(); } void ControlUpDown(int v) { for (int i = 0; i < 8; i++) { TalkSaveIndex = (v + TalkSaveIndex) & 7; if (TalkSave[TalkSaveIndex][0] != 0) { ChatInputState->assign(TalkSave[TalkSaveIndex]); return; } } } } // namespace void DrawChatBox(const Surface &out) { if (!ChatFlag) return; const Point mainPanelPosition = GetMainPanel().position; DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 }); int off = 0; for (int i = 293; i > 283; off++, i--) { DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 }); } DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 }); DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 }); for (int i = 0; i < 10; i++) { DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 }); } DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 }); int x = mainPanelPosition.x + 200; const int y = mainPanelPosition.y + 10; const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } }, { .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, .lineHeight = 13, .cursorPosition = static_cast(ChatCursor.position), .highlightRange = { static_cast(ChatCursor.selection.begin), static_cast(ChatCursor.selection.end) }, }); ChatInputState->truncate(len); x += 46; int talkBtn = 0; for (size_t i = 0; i < Players.size(); i++) { Player &player = Players[i]; if (&player == MyPlayer) continue; const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed; const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn }; if (WhisperList[i]) { // the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held if (TalkButtonsDown[talkBtn]) { const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest. ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting // the first button be treated the same as the other two further down the panel. RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 }); } } else { unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest. if (TalkButtonsDown[talkBtn]) spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively) ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]); // Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting // the first button be treated the same as the other two further down the panel. RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 }); } if (player.plractive) { DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, { .flags = color }); } talkBtn++; } } bool CheckMuteButton() { if (!ChatFlag) return false; Rectangle buttons = MuteButtonRect; SetPanelObjectPosition(UiPanels::Main, buttons); buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); if (!buttons.contains(MousePosition)) return false; for (bool &talkButtonDown : TalkButtonsDown) { talkButtonDown = false; } const Point mainPanelPosition = GetMainPanel().position; TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true; return true; } void CheckMuteButtonUp() { if (!ChatFlag) return; for (bool &talkButtonDown : TalkButtonsDown) talkButtonDown = false; Rectangle buttons = MuteButtonRect; SetPanelObjectPosition(UiPanels::Main, buttons); buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding); if (!buttons.contains(MousePosition)) return; int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding); size_t playerId = 0; for (; playerId < Players.size() && off != -1; ++playerId) { if (playerId != MyPlayerId) off--; } if (playerId > 0 && playerId <= Players.size()) WhisperList[playerId - 1] = !WhisperList[playerId - 1]; } void TypeChatMessage() { if (!IsChatAvailable()) return; ChatFlag = true; TalkMessage[0] = '\0'; ChatInputState.emplace(TextInputState::Options { .value = TalkMessage, .cursor = &ChatCursor, .maxLength = sizeof(TalkMessage) - 1 }); for (bool &talkButtonDown : TalkButtonsDown) { talkButtonDown = false; } sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight; RedrawEverything(); TalkSaveIndex = NextTalkSave; SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27); SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); SDLC_StartTextInput(ghMainWnd); } void ResetChat() { ChatFlag = false; SDLC_StopTextInput(ghMainWnd); ChatCursor = {}; ChatInputState = std::nullopt; sgbPlrTalkTbl = 0; RedrawEverything(); } bool IsChatActive() { if (!IsChatAvailable()) return false; if (!ChatFlag) return false; return true; } bool CheckKeypress(SDL_Keycode vkey) { if (!IsChatAvailable()) return false; if (!ChatFlag) return false; switch (vkey) { case SDLK_ESCAPE: ResetChat(); return true; case SDLK_RETURN: case SDLK_KP_ENTER: ControlPressEnter(); return true; case SDLK_DOWN: ControlUpDown(1); return true; case SDLK_UP: ControlUpDown(-1); return true; default: return vkey >= SDLK_SPACE && vkey <= SDLK_Z; } } void DiabloHotkeyMsg(uint32_t dwMsg) { assert(dwMsg < QuickMessages.size()); #ifdef _DEBUG constexpr std::string_view LuaPrefix = "/lua "; for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { if (!msg.starts_with(LuaPrefix)) continue; InitConsole(); RunInConsole(std::string_view(msg).substr(LuaPrefix.size())); } #endif if (!IsChatAvailable()) { return; } for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) { #ifdef _DEBUG if (msg.starts_with(LuaPrefix)) continue; #endif char charMsg[MAX_SEND_STR_LEN]; CopyUtf8(charMsg, msg, sizeof(charMsg)); NetSendCmdString(0xFFFFFF, charMsg); } } bool IsChatAvailable() { return gbIsMultiplayer; } bool HandleTalkTextInputEvent(const SDL_Event &event) { return HandleInputEvent(event, ChatInputState); } } // namespace devilution ================================================ FILE: Source/control/control_chat.hpp ================================================ #pragma once #include #include #include "DiabloUI/text_input.hpp" #include "engine/clx_sprite.hpp" #include "msg.h" #include "multi.h" #ifdef USE_SDL3 #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif namespace devilution { extern OptionalOwnedClxSpriteList talkButtons; extern std::optional ChatInputState; extern char TalkMessage[MAX_SEND_STR_LEN]; extern bool TalkButtonsDown[3]; extern int sgbPlrTalkTbl; extern bool WhisperList[MAX_PLRS]; bool CheckChatCommand(std::string_view text); template bool HandleInputEvent(const SDL_Event &event, std::optional &inputState) { if (!inputState) { return false; // No input state to handle } if constexpr (std::is_same_v) { return HandleTextInputEvent(event, *inputState); } else if constexpr (std::is_same_v) { return HandleNumberInputEvent(event, *inputState); } return false; // Unknown input state type } } // namespace devilution ================================================ FILE: Source/control/control_chat_commands.cpp ================================================ #include "control_chat_commands.hpp" #include "control.hpp" #include "diablo_msg.hpp" #include "engine/backbuffer_state.hpp" #include "inv.h" #include "levels/setmaps.h" #include "storm/storm_net.hpp" #include "utils/algorithm/container.hpp" #include "utils/log.hpp" #include "utils/parse_int.hpp" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" #ifdef _DEBUG #include "debug.h" #endif namespace devilution { namespace { struct TextCmdItem { const std::string text; const std::string description; const std::string requiredParameter; std::string (*actionProc)(const std::string_view); }; extern std::vector TextCmdList; std::string TextCmdHelp(const std::string_view parameter) { if (parameter.empty()) { std::string ret; StrAppend(ret, _("Available Commands:")); for (const TextCmdItem &textCmd : TextCmdList) { StrAppend(ret, " ", _(textCmd.text)); } return ret; } auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return elem.text == parameter; }); if (textCmdIterator == TextCmdList.end()) return StrCat(_("Command "), parameter, _(" is unknown.")); auto &textCmdItem = *textCmdIterator; if (textCmdItem.requiredParameter.empty()) return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: No additional parameter needed.")); return StrCat(_("Description: "), _(textCmdItem.description), _("\nParameters: "), _(textCmdItem.requiredParameter)); } void AppendArenaOverview(std::string &ret) { for (int arena = SL_FIRST_ARENA; arena <= SL_LAST; arena++) { StrAppend(ret, "\n", arena - SL_FIRST_ARENA + 1, " (", QuestLevelNames[arena], ")"); } } std::string TextCmdArena(const std::string_view parameter) { std::string ret; if (!gbIsMultiplayer) { StrAppend(ret, _("Arenas are only supported in multiplayer.")); return ret; } if (parameter.empty()) { StrAppend(ret, _("What arena do you want to visit?")); AppendArenaOverview(ret); return ret; } const ParseIntResult parsedParam = ParseInt(parameter, /*min=*/0); const _setlevels arenaLevel = parsedParam.has_value() ? static_cast<_setlevels>(parsedParam.value() - 1 + SL_FIRST_ARENA) : _setlevels::SL_NONE; if (!IsArenaLevel(arenaLevel)) { StrAppend(ret, _("Invalid arena-number. Valid numbers are:")); AppendArenaOverview(ret); return ret; } if (!MyPlayer->isOnLevel(0) && !MyPlayer->isOnArenaLevel()) { StrAppend(ret, _("To enter a arena, you need to be in town or another arena.")); return ret; } setlvltype = GetArenaLevelType(arenaLevel); StartNewLvl(*MyPlayer, WM_DIABSETLVL, arenaLevel); return ret; } std::string TextCmdArenaPot(const std::string_view parameter) { std::string ret; if (!gbIsMultiplayer) { StrAppend(ret, _("Arenas are only supported in multiplayer.")); return ret; } const int numPots = ParseInt(parameter, /*min=*/1).value_or(1); Player &myPlayer = *MyPlayer; for (int potNumber = numPots; potNumber > 0; potNumber--) { Item item {}; InitializeItem(item, IDI_ARENAPOT); GenerateNewSeed(item); item.updateRequiredStatsCacheForPlayer(myPlayer); if (!AutoPlaceItemInBelt(myPlayer, item, true, true) && !AutoPlaceItemInInventory(myPlayer, item, true)) { break; // inventory is full } } return ret; } std::string TextCmdInspect(const std::string_view parameter) { std::string ret; if (!gbIsMultiplayer) { StrAppend(ret, _("Inspecting only supported in multiplayer.")); return ret; } if (parameter.empty()) { StrAppend(ret, _("Stopped inspecting players.")); InspectPlayer = MyPlayer; return ret; } const std::string param = AsciiStrToLower(parameter); auto it = c_find_if(Players, [¶m](const Player &player) { return AsciiStrToLower(player._pName) == param; }); if (it == Players.end()) { it = c_find_if(Players, [¶m](const Player &player) { return AsciiStrToLower(player._pName).find(param) != std::string::npos; }); } if (it == Players.end()) { StrAppend(ret, _("No players found with such a name")); return ret; } Player &player = *it; InspectPlayer = &player; StrAppend(ret, _("Inspecting player: ")); StrAppend(ret, player._pName); OpenCharPanel(); if (!SpellbookFlag) invflag = true; RedrawEverything(); return ret; } bool IsQuestEnabled(const Quest &quest) { switch (quest._qidx) { case Q_FARMER: return gbIsHellfire && !sgGameInitInfo.bCowQuest; case Q_JERSEY: return gbIsHellfire && sgGameInitInfo.bCowQuest; case Q_GIRL: return gbIsHellfire && sgGameInitInfo.bTheoQuest; case Q_CORNSTN: return gbIsHellfire && !gbIsMultiplayer; case Q_GRAVE: case Q_DEFILER: case Q_NAKRUL: return gbIsHellfire; case Q_TRADER: return false; default: return quest._qactive != QUEST_NOTAVAIL; } } std::string TextCmdLevelSeed(const std::string_view parameter) { const std::string_view levelType = setlevel ? "set level" : "dungeon level"; char gameId[] = { static_cast((sgGameInitInfo.programid >> 24) & 0xFF), static_cast((sgGameInitInfo.programid >> 16) & 0xFF), static_cast((sgGameInitInfo.programid >> 8) & 0xFF), static_cast(sgGameInitInfo.programid & 0xFF), '\0' }; const std::string_view mode = gbIsMultiplayer ? "MP" : "SP"; const std::string_view questPool = UseMultiplayerQuests() ? "MP" : "Full"; uint32_t questFlags = 0; for (const Quest &quest : Quests) { questFlags <<= 1; if (IsQuestEnabled(quest)) questFlags |= 1; } return StrCat( "Seedinfo for ", levelType, " ", currlevel, "\n", "seed: ", DungeonSeeds[currlevel], "\n", #ifdef _DEBUG "Mid1: ", glMid1Seed[currlevel], "\n", "Mid2: ", glMid2Seed[currlevel], "\n", "Mid3: ", glMid3Seed[currlevel], "\n", "End: ", glEndSeed[currlevel], "\n", #endif "\n", gameId, " ", mode, "\n", questPool, " quests: ", questFlags, "\n", "Storybook: ", DungeonSeeds[16]); } std::string TextCmdPing(const std::string_view parameter) { std::string ret; const std::string param = AsciiStrToLower(parameter); auto it = c_find_if(Players, [¶m](const Player &player) { return AsciiStrToLower(player._pName) == param; }); if (it == Players.end()) { it = c_find_if(Players, [¶m](const Player &player) { return AsciiStrToLower(player._pName).find(param) != std::string::npos; }); } if (it == Players.end()) { StrAppend(ret, _("No players found with such a name")); return ret; } Player &player = *it; DvlNetLatencies latencies = DvlNet_GetLatencies(player.getId()); StrAppend(ret, fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Latency statistics for {:s}:")), player.name())); StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); if (latencies.providerLatency) { if (latencies.isRelayed && *latencies.isRelayed) { StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); } else { StrAppend(ret, "\n", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); } } return ret; } std::vector TextCmdList = { { "/help", N_("Prints help overview or help for a specific command."), N_("[command]"), &TextCmdHelp }, { "/arena", N_("Enter a PvP Arena."), N_(""), &TextCmdArena }, { "/arenapot", N_("Gives Arena Potions."), N_(""), &TextCmdArenaPot }, { "/inspect", N_("Inspects stats and equipment of another player."), N_(""), &TextCmdInspect }, { "/seedinfo", N_("Show seed infos for current level."), "", &TextCmdLevelSeed }, { "/ping", N_("Show latency statistics for another player."), N_(""), &TextCmdPing }, }; } // namespace bool CheckChatCommand(const std::string_view text) { if (text.size() < 1 || text[0] != '/') return false; auto textCmdIterator = c_find_if(TextCmdList, [&](const TextCmdItem &elem) { return text.find(elem.text) == 0 && (text.length() == elem.text.length() || text[elem.text.length()] == ' '); }); if (textCmdIterator == TextCmdList.end()) { InitDiabloMsg(StrCat(_("Command "), "\"", text, "\"", _(" is unknown."))); return true; } const TextCmdItem &textCmd = *textCmdIterator; std::string_view parameter = ""; if (text.length() > (textCmd.text.length() + 1)) parameter = text.substr(textCmd.text.length() + 1); const std::string result = textCmd.actionProc(parameter); if (result != "") InitDiabloMsg(result); return true; } } // namespace devilution ================================================ FILE: Source/control/control_chat_commands.hpp ================================================ #pragma once #include namespace devilution { bool CheckChatCommand(std::string_view text); } // namespace devilution ================================================ FILE: Source/control/control_flasks.cpp ================================================ #include "control_flasks.hpp" #include "control.hpp" #include "engine/surface.hpp" #include "utils/str_cat.hpp" namespace devilution { std::optional pLifeBuff; std::optional pManaBuff; namespace { Rectangle FlaskTopRect { { 11, 3 }, { 62, 13 } }; Rectangle FlaskBottomRect { { 0, 16 }, { 88, 69 } }; /** * Draws the dome of the flask that protrudes above the panel top line. * It draws a rectangle of fixed width 59 and height 'h' from the source buffer * into the target buffer. * @param out The target buffer. * @param celBuf Buffer of the flask cel. * @param targetPosition Target buffer coordinate. */ void DrawFlaskAbovePanel(const Surface &out, const Surface &celBuf, Point targetPosition) { out.BlitFromSkipColorIndexZero(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); } /** * @brief Draws the part of the life/mana flasks protruding above the bottom panel * @see DrawFlaskLower() * @param out The display region to draw to * @param sourceBuffer A sprite representing the appropriate background/empty flask style * @param offset X coordinate offset for where the flask should be drawn * @param fillPer How full the flask is (a value from 0 to 81) */ void DrawFlaskUpper(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer) { const Rectangle &rect = FlaskTopRect; const int emptyRows = std::clamp(81 - fillPer, 0, rect.size.height); const int filledRows = rect.size.height - emptyRows; // Draw the empty part of the flask DrawFlaskAbovePanel(out, sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height), GetMainPanel().position + Displacement { offset, -rect.size.height }); // Draw the filled part of the flask over the empty part if (filledRows > 0) { DrawFlaskAbovePanel(out, BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), GetMainPanel().position + Displacement { offset, -rect.size.height + emptyRows }); } } /** * Draws a section of the empty flask cel on top of the panel to create the illusion * of the flask getting empty. This function takes a cel and draws a * horizontal stripe of height (max-min) onto the given buffer. * @param out Target buffer. * @param celBuf Buffer of the flask cel. * @param targetPosition Target buffer coordinate. */ void DrawFlaskOnPanel(const Surface &out, const Surface &celBuf, Point targetPosition) { out.BlitFrom(celBuf, MakeSdlRect(0, 0, celBuf.w(), celBuf.h()), targetPosition); } /** * @brief Draws the part of the life/mana flasks inside the bottom panel * @see DrawFlaskUpper() * @param out The display region to draw to * @param sourceBuffer A sprite representing the appropriate background/empty flask style * @param offset X coordinate offset for where the flask should be drawn * @param fillPer How full the flask is (a value from 0 to 80) * @param drawFilledPortion Indicates whether to draw the filled portion of the flask */ void DrawFlaskLower(const Surface &out, const Surface &sourceBuffer, int offset, int fillPer, bool drawFilledPortion) { const Rectangle &rect = FlaskBottomRect; const int filledRows = std::clamp(fillPer, 0, rect.size.height); const int emptyRows = rect.size.height - filledRows; // Draw the empty part of the flask if (emptyRows > 0) { DrawFlaskOnPanel(out, sourceBuffer.subregion(rect.position.x, rect.position.y, rect.size.width, emptyRows), GetMainPanel().position + Displacement { offset, 0 }); } // Draw the filled part of the flask if (drawFilledPortion && filledRows > 0) { DrawFlaskOnPanel(out, BottomBuffer->subregion(offset, rect.position.y + emptyRows, rect.size.width, filledRows), GetMainPanel().position + Displacement { offset, emptyRows }); } } } // namespace void DrawLifeFlaskUpper(const Surface &out) { constexpr int LifeFlaskUpperOffset = 107; DrawFlaskUpper(out, *pLifeBuff, LifeFlaskUpperOffset, MyPlayer->_pHPPer); } void DrawManaFlaskUpper(const Surface &out) { constexpr int ManaFlaskUpperOffset = 475; DrawFlaskUpper(out, *pManaBuff, ManaFlaskUpperOffset, MyPlayer->_pManaPer); } void DrawLifeFlaskLower(const Surface &out, bool drawFilledPortion) { constexpr int LifeFlaskLowerOffset = 96; DrawFlaskLower(out, *pLifeBuff, LifeFlaskLowerOffset, MyPlayer->_pHPPer, drawFilledPortion); } void DrawManaFlaskLower(const Surface &out, bool drawFilledPortion) { constexpr int ManaFlaskLowerOffset = 464; DrawFlaskLower(out, *pManaBuff, ManaFlaskLowerOffset, MyPlayer->_pManaPer, drawFilledPortion); } void DrawFlaskValues(const Surface &out, Point pos, int currValue, int maxValue) { const UiFlags color = (currValue > 0 ? (currValue == maxValue ? UiFlags::ColorGold : UiFlags::ColorWhite) : UiFlags::ColorRed); auto drawStringWithShadow = [out, color](std::string_view text, Point pos) { DrawString(out, text, pos + Displacement { -1, -1 }, { .flags = UiFlags::ColorBlack | UiFlags::KerningFitSpacing, .spacing = 0 }); DrawString(out, text, pos, { .flags = color | UiFlags::KerningFitSpacing, .spacing = 0 }); }; const std::string currText = StrCat(currValue); drawStringWithShadow(currText, pos - Displacement { GetLineWidth(currText, GameFont12) + 1, 0 }); drawStringWithShadow("/", pos); drawStringWithShadow(StrCat(maxValue), pos + Displacement { GetLineWidth("/", GameFont12) + 1, 0 }); } void UpdateLifeManaPercent() { MyPlayer->UpdateManaPercentage(); MyPlayer->UpdateHitPointPercentage(); } } // namespace devilution ================================================ FILE: Source/control/control_flasks.hpp ================================================ #pragma once #include #include "engine/surface.hpp" namespace devilution { extern std::optional pLifeBuff; extern std::optional pManaBuff; } // namespace devilution ================================================ FILE: Source/control/control_gold.cpp ================================================ #include "control.hpp" #include "control_chat.hpp" #include "DiabloUI/text_input.hpp" #include "engine/render/clx_render.hpp" #include "inv.h" #include "utils/display.h" #include "utils/format_int.hpp" #include "utils/log.hpp" #include "utils/sdl_compat.h" namespace devilution { bool DropGoldFlag; TextInputCursorState GoldDropCursor; char GoldDropText[21]; namespace { int8_t GoldDropInvIndex; std::optional GoldDropInputState; void RemoveGold(Player &player, int goldIndex, int amount) { const int gi = goldIndex - INVITEM_INV_FIRST; player.InvList[gi]._ivalue -= amount; if (player.InvList[gi]._ivalue > 0) { SetPlrHandGoldCurs(player.InvList[gi]); NetSyncInvItem(player, gi); } else { player.RemoveInvItem(gi); } MakeGoldStack(player.HoldItem, amount); NewCursor(player.HoldItem); player._pGold = CalculateGold(player); } int GetGoldDropMax() { return GoldDropInputState->max(); } } // namespace void DrawGoldSplit(const Surface &out) { const int dialogX = 30; ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { dialogX, 178 }), (*GoldBoxBuffer)[0]); const std::string_view amountText = GoldDropText; const TextInputCursorState &cursor = GoldDropCursor; const int max = GetGoldDropMax(); const std::string description = fmt::format( fmt::runtime(ngettext( /* TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold.*/ "You have {:s} gold piece. How many do you want to remove?", "You have {:s} gold pieces. How many do you want to remove?", max)), FormatInteger(max)); // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words const std::string wrapped = WordWrapString(description, 200); // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left // for the text entered by the player. DrawString(out, wrapped, { GetPanelPosition(UiPanels::Inventory, { dialogX + 31, 75 }), { 200, 50 } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 }); // Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we // use the Point form of DrawString. DrawString(out, amountText, GetPanelPosition(UiPanels::Inventory, { dialogX + 37, 128 }), { .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, .cursorPosition = static_cast(cursor.position), .highlightRange = { static_cast(cursor.selection.begin), static_cast(cursor.selection.end) }, }); } void control_drop_gold(SDL_Keycode vkey) { Player &myPlayer = *MyPlayer; if (myPlayer.hasNoLife()) { CloseGoldDrop(); return; } switch (vkey) { case SDLK_RETURN: case SDLK_KP_ENTER: if (const int value = GoldDropInputState->value(); value != 0) { RemoveGold(myPlayer, GoldDropInvIndex, value); } CloseGoldDrop(); break; case SDLK_ESCAPE: CloseGoldDrop(); break; default: break; } } void OpenGoldDrop(int8_t invIndex, int max) { DropGoldFlag = true; GoldDropInvIndex = invIndex; GoldDropText[0] = '\0'; GoldDropInputState.emplace(NumberInputState::Options { .textOptions { .value = GoldDropText, .cursor = &GoldDropCursor, .maxLength = sizeof(GoldDropText) - 1, }, .min = 0, .max = max, }); SDLC_StartTextInput(ghMainWnd); } void CloseGoldDrop() { if (!DropGoldFlag) return; SDLC_StopTextInput(ghMainWnd); DropGoldFlag = false; GoldDropInputState = std::nullopt; GoldDropInvIndex = 0; } bool HandleGoldDropTextInputEvent(const SDL_Event &event) { return HandleInputEvent(event, GoldDropInputState); } } // namespace devilution ================================================ FILE: Source/control/control_infobox.cpp ================================================ #include "control.hpp" #include "control_panel.hpp" #include "engine/render/primitive_render.hpp" #include "inv.h" #include "levels/trigs.h" #include "panels/partypanel.hpp" #include "qol/stash.h" #include "qol/xpbar.h" #include "towners.h" #include "utils/algorithm/container.hpp" #include "utils/format_int.hpp" #include "utils/log.hpp" #include "utils/screen_reader.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution { StringOrView InfoString; StringOrView FloatingInfoString; namespace { void PrintInfo(const Surface &out) { if (ChatFlag) return; const int space[] = { 18, 12, 6, 3, 0 }; Rectangle infoBox = InfoBoxRect; SetPanelObjectPosition(UiPanels::Main, infoBox); const auto newLineCount = static_cast(c_count(InfoString.str(), '\n')); const int spaceIndex = std::min(4, newLineCount); const int spacing = space[spaceIndex]; const int lineHeight = 12 + spacing; // Adjusting the line height to add spacing between lines // will also add additional space beneath the last line // which throws off the vertical centering infoBox.position.y += spacing / 2; SpeakText(InfoString); DrawString(out, InfoString, infoBox, { .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter | UiFlags::KerningFitSpacing, .spacing = 2, .lineHeight = lineHeight, }); } Rectangle GetFloatingInfoRect(const int lineHeight, const int textSpacing) { // Calculate the width and height of the floating info box const std::string txt = std::string(FloatingInfoString); auto lines = SplitByChar(txt, '\n'); const GameFontTables font = GameFont12; int maxW = 0; for (const auto &line : lines) { const int w = GetLineWidth(line, font, textSpacing, nullptr); maxW = std::max(maxW, w); } const auto lineCount = 1 + static_cast(c_count(FloatingInfoString.str(), '\n')); const int totalH = lineCount * lineHeight; const Player &player = *InspectPlayer; // 1) Equipment (Rect position) if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { const int slot = pcursinvitem - INVITEM_HEAD; static constexpr Point equipLocal[] = { { 133, 59 }, { 48, 205 }, { 249, 205 }, { 205, 60 }, { 17, 160 }, { 248, 160 }, { 133, 160 }, }; Point itemPosition = equipLocal[slot]; auto &item = player.InvBody[slot]; const Size frame = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); if (slot == INVLOC_HAND_LEFT) { itemPosition.x += frame.width == InventorySlotSizeInPixels.width ? InventorySlotSizeInPixels.width : 0; itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height ? 0 : -InventorySlotSizeInPixels.height; } else if (slot == INVLOC_HAND_RIGHT) { itemPosition.x += frame.width == InventorySlotSizeInPixels.width ? (InventorySlotSizeInPixels.width - 1) : 1; itemPosition.y += frame.height == 3 * InventorySlotSizeInPixels.height ? 0 : -InventorySlotSizeInPixels.height; } itemPosition.y++; // Align position to bottom left of the item graphic itemPosition.x += frame.width / 2; // Align position to center of the item graphic itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); return { { screen.x, screen.y }, { maxW, totalH } }; } // 2) Inventory grid (Rect position) if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { const int itemIdx = pcursinvitem - INVITEM_INV_FIRST; for (int j = 0; j < InventoryGridCells; ++j) { if (player.InvGrid[j] > 0 && player.InvGrid[j] - 1 == itemIdx) { const Item &it = player.InvList[itemIdx]; Point itemPosition = InvRect[j + SLOTXY_INV_FIRST].position; itemPosition.x += GetInventorySize(it).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box const Point screen = GetPanelPosition(UiPanels::Inventory, itemPosition); return { { screen.x, screen.y }, { maxW, totalH } }; } } } // 3) Belt (Rect position) if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { const int itemIdx = pcursinvitem - INVITEM_BELT_FIRST; for (int i = 0; i < MaxBeltItems; ++i) { if (player.SpdList[i].isEmpty()) continue; if (i != itemIdx) continue; const Item &item = player.SpdList[i]; Point itemPosition = InvRect[i + SLOTXY_BELT_FIRST].position; itemPosition.x += GetInventorySize(item).width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box const Point screen = GetMainPanel().position + Displacement { itemPosition.x, itemPosition.y }; return { { screen.x, screen.y }, { maxW, totalH } }; } } // 4) Stash (Rect position) if (pcursstashitem != StashStruct::EmptyCell) { for (auto slot : StashGridRange) { auto itemId = Stash.GetItemIdAtPosition(slot); if (itemId == StashStruct::EmptyCell) continue; if (itemId != pcursstashitem) continue; const Item &item = Stash.stashList[itemId]; Point itemPosition = GetStashSlotCoord(slot); const Size itemGridSize = GetInventorySize(item); itemPosition.y += itemGridSize.height * (InventorySlotSizeInPixels.height + 1) - 1; // Align position to bottom left of the item graphic itemPosition.x += itemGridSize.width * InventorySlotSizeInPixels.width / 2; // Align position to center of the item graphic itemPosition.x -= maxW / 2; // Align position to the center of the floating item info box return { { itemPosition.x, itemPosition.y }, { maxW, totalH } }; } } return { { 0, 0 }, { 0, 0 } }; } int GetHoverSpriteHeight() { if (pcursinvitem >= INVITEM_HEAD && pcursinvitem < INVITEM_INV_FIRST) { auto &it = (*InspectPlayer).InvBody[pcursinvitem - INVITEM_HEAD]; return GetInvItemSize(it._iCurs + CURSOR_FIRSTITEM).height + 1; } if (pcursinvitem >= INVITEM_INV_FIRST && pcursinvitem < INVITEM_INV_FIRST + InventoryGridCells) { const int idx = pcursinvitem - INVITEM_INV_FIRST; auto &it = (*InspectPlayer).InvList[idx]; return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) - InventorySlotSizeInPixels.height; } if (pcursinvitem >= INVITEM_BELT_FIRST && pcursinvitem < INVITEM_BELT_FIRST + MaxBeltItems) { const int idx = pcursinvitem - INVITEM_BELT_FIRST; auto &it = (*InspectPlayer).SpdList[idx]; return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1) - InventorySlotSizeInPixels.height - 1; } if (pcursstashitem != StashStruct::EmptyCell) { auto &it = Stash.stashList[pcursstashitem]; return GetInventorySize(it).height * (InventorySlotSizeInPixels.height + 1); } return InventorySlotSizeInPixels.height; } int ClampAboveOrBelow(int anchorY, int spriteH, int boxH, int pad, int linePad) { const int yAbove = anchorY - spriteH - boxH - pad; const int yBelow = anchorY + linePad / 2 + pad; return (yAbove >= 0) ? yAbove : yBelow; } void PrintFloatingInfo(const Surface &out) { if (ChatFlag) return; if (FloatingInfoString.empty()) return; const int verticalSpacing = 3; const int lineHeight = 12 + verticalSpacing; const int textSpacing = 2; const int hPadding = 5; const int vPadding = 4; Rectangle floatingInfoBox = GetFloatingInfoRect(lineHeight, textSpacing); // Prevent the floating info box from going off-screen horizontally floatingInfoBox.position.x = std::clamp(floatingInfoBox.position.x, hPadding, GetScreenWidth() - (floatingInfoBox.size.width + hPadding)); const int spriteH = GetHoverSpriteHeight(); const int anchorY = floatingInfoBox.position.y; // Prevent the floating info box from going off-screen vertically floatingInfoBox.position.y = ClampAboveOrBelow(anchorY, spriteH, floatingInfoBox.size.height, vPadding, verticalSpacing); SpeakText(FloatingInfoString); for (int i = 0; i < 3; i++) DrawHalfTransparentRectTo(out, floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding, floatingInfoBox.size.width + hPadding * 2, floatingInfoBox.size.height + vPadding * 2); DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x - hPadding - 1, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); DrawHalfTransparentVerticalLine(out, { floatingInfoBox.position.x + hPadding + floatingInfoBox.size.width, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.height + (vPadding * 2) + 2, PAL16_GRAY + 10); DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y - vPadding - 1 }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); DrawHalfTransparentHorizontalLine(out, { floatingInfoBox.position.x - hPadding, floatingInfoBox.position.y + vPadding + floatingInfoBox.size.height }, floatingInfoBox.size.width + (hPadding * 2), PAL16_GRAY + 10); DrawString(out, FloatingInfoString, floatingInfoBox, { .flags = InfoColor | UiFlags::AlignCenter | UiFlags::VerticalCenter, .spacing = textSpacing, .lineHeight = lineHeight, }); } } // namespace void AddInfoBoxString(std::string_view str, bool floatingBox /*= false*/) { StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; if (infoString.empty()) infoString = str; else infoString = StrCat(infoString, "\n", str); } void AddInfoBoxString(std::string &&str, bool floatingBox /*= false*/) { StringOrView &infoString = floatingBox ? FloatingInfoString : InfoString; if (infoString.empty()) infoString = std::move(str); else infoString = StrCat(infoString, "\n", str); } void CheckPanelInfo() { MainPanelFlag = false; InfoString = StringOrView {}; FloatingInfoString = StringOrView {}; const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; for (int i = 0; i < totalButtons; i++) { Rectangle button = MainPanelButtonRect[i]; SetPanelObjectPosition(UiPanels::Main, button); if (button.contains(MousePosition)) { if (i != 7) { InfoString = _(PanBtnStr[i]); } else { if (MyPlayer->friendlyMode) InfoString = _("Player friendly"); else InfoString = _("Player attack"); } if (PanBtnHotKey[i] != nullptr) { AddInfoBoxString(fmt::format(fmt::runtime(_("Hotkey: {:s}")), _(PanBtnHotKey[i]))); } InfoColor = UiFlags::ColorWhite; MainPanelFlag = true; } } Rectangle spellSelectButton = SpellButtonRect; SetPanelObjectPosition(UiPanels::Main, spellSelectButton); if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { InfoString = _("Select current spell button"); InfoColor = UiFlags::ColorWhite; MainPanelFlag = true; AddInfoBoxString(_("Hotkey: 's'")); const Player &myPlayer = *MyPlayer; const SpellID spellId = myPlayer._pRSpell; if (IsValidSpell(spellId)) { switch (myPlayer._pRSplType) { case SpellType::Skill: AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", GetSpellData(spellId).sNameText))); break; case SpellType::Spell: { AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", GetSpellData(spellId).sNameText))); const int spellLevel = myPlayer.GetSpellLevel(spellId); AddInfoBoxString(spellLevel == 0 ? _("Spell Level 0 - Unusable") : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); } break; case SpellType::Scroll: { AddInfoBoxString(fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) { return item.isScrollOf(spellId); }); AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); } break; case SpellType::Charges: AddInfoBoxString(fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", GetSpellData(spellId).sNameText))); AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)), myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges)); break; case SpellType::Invalid: break; } } } Rectangle belt = BeltRect; SetPanelObjectPosition(UiPanels::Main, belt); if (belt.contains(MousePosition)) pcursinvitem = CheckInvHLight(); if (CheckXPBarInfo()) MainPanelFlag = true; } void DrawInfoBox(const Surface &out) { DrawPanelBox(out, MakeSdlRect(InfoBoxRect.position.x, InfoBoxRect.position.y + PanelPaddingHeight, InfoBoxRect.size.width, InfoBoxRect.size.height), GetMainPanel().position + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y }); if (!MainPanelFlag && !trigflag && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && !SpellSelectFlag && pcurs != CURSOR_HOURGLASS) { InfoString = StringOrView {}; InfoColor = UiFlags::ColorWhite; } const Player &myPlayer = *MyPlayer; if (SpellSelectFlag || trigflag || pcurs == CURSOR_HOURGLASS) { InfoColor = UiFlags::ColorWhite; } else if (!myPlayer.HoldItem.isEmpty()) { if (myPlayer.HoldItem._itype == ItemType::Gold) { const int nGold = myPlayer.HoldItem._ivalue; InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); } else if (!myPlayer.CanUseItem(myPlayer.HoldItem)) { InfoString = _("Requirements not met"); } else { InfoString = myPlayer.HoldItem.getName(); InfoColor = myPlayer.HoldItem.getTextColor(); } } else { if (pcursitem != -1) GetItemStr(Items[pcursitem]); else if (ObjectUnderCursor != nullptr) GetObjectStr(*ObjectUnderCursor); if (pcursmonst != -1) { if (leveltype != DTYPE_TOWN) { const Monster &monster = Monsters[pcursmonst]; InfoColor = UiFlags::ColorWhite; InfoString = monster.name(); if (monster.isUnique()) { InfoColor = UiFlags::ColorWhitegold; PrintUniqueHistory(); } else { PrintMonstHistory(monster.type().type); } } else if (pcursitem == -1) { InfoString = std::string_view(Towners[pcursmonst].name); } } if (PlayerUnderCursor != nullptr) { InfoColor = UiFlags::ColorWhitegold; const auto &target = *PlayerUnderCursor; InfoString = std::string_view(target._pName); AddInfoBoxString(fmt::format(fmt::runtime(_("{:s}, Level: {:d}")), target.getClassName(), target.getCharacterLevel())); AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points {:d} of {:d}")), target._pHitPoints >> 6, target._pMaxHP >> 6)); } if (PortraitIdUnderCursor != -1) { InfoColor = UiFlags::ColorWhitegold; auto &target = Players[PortraitIdUnderCursor]; InfoString = std::string_view(target._pName); AddInfoBoxString(_("Right click to inspect")); } } if (!InfoString.empty()) PrintInfo(out); } void DrawFloatingInfoBox(const Surface &out) { if (pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell) { FloatingInfoString = StringOrView {}; InfoColor = UiFlags::ColorWhite; } if (!FloatingInfoString.empty()) PrintFloatingInfo(out); } } // namespace devilution ================================================ FILE: Source/control/control_panel.cpp ================================================ #include "control_panel.hpp" #include "control.hpp" #include "control_chat.hpp" #include "control_flasks.hpp" #include "automap.h" #include "controls/control_mode.hpp" #include "controls/modifier_hints.h" #include "diablo_msg.hpp" #include "engine/backbuffer_state.hpp" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" #include "engine/trn.hpp" #include "gamemenu.h" #include "headless_mode.hpp" #include "minitext.h" #include "options.h" #include "panels/charpanel.hpp" #include "panels/mainpanel.hpp" #include "panels/partypanel.hpp" #include "panels/spell_book.hpp" #include "panels/spell_icons.hpp" #include "panels/spell_list.hpp" #include "pfile.h" #include "qol/stash.h" #include "stores.h" #include "utils/sdl_compat.h" namespace devilution { bool CharPanelButton[4]; bool LevelButtonDown; bool CharPanelButtonActive; UiFlags InfoColor; int SpellbookTab; bool ChatFlag; bool SpellbookFlag; bool CharFlag; bool MainPanelFlag; bool MainPanelButtonDown; bool SpellSelectFlag; Rectangle MainPanel; Rectangle LeftPanel; Rectangle RightPanel; std::optional BottomBuffer; OptionalOwnedClxSpriteList GoldBoxBuffer; const Rectangle &GetMainPanel() { return MainPanel; } const Rectangle &GetLeftPanel() { return LeftPanel; } const Rectangle &GetRightPanel() { return RightPanel; } bool IsLeftPanelOpen() { return CharFlag || QuestLogIsOpen || IsStashOpen; } bool IsRightPanelOpen() { return invflag || SpellbookFlag; } constexpr Size IncrementAttributeButtonSize { 41, 22 }; /** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ Rectangle CharPanelButtonRect[4] = { { { 137, 138 }, IncrementAttributeButtonSize }, { { 137, 166 }, IncrementAttributeButtonSize }, { { 137, 195 }, IncrementAttributeButtonSize }, { { 137, 223 }, IncrementAttributeButtonSize } }; constexpr Size WidePanelButtonSize { 71, 20 }; constexpr Size PanelButtonSize { 33, 32 }; /** Positions of panel buttons. */ Rectangle MainPanelButtonRect[8] = { // clang-format off { { 9, 9 }, WidePanelButtonSize }, // char button { { 9, 35 }, WidePanelButtonSize }, // quests button { { 9, 75 }, WidePanelButtonSize }, // map button { { 9, 101 }, WidePanelButtonSize }, // menu button { { 560, 9 }, WidePanelButtonSize }, // inv button { { 560, 35 }, WidePanelButtonSize }, // spells button { { 87, 91 }, PanelButtonSize }, // chat button { { 527, 91 }, PanelButtonSize }, // friendly fire button // clang-format on }; Rectangle LevelButtonRect = { { 40, -39 }, { 41, 22 } }; constexpr int BeltItems = 8; constexpr Size BeltSize { (INV_SLOT_SIZE_PX + 1) * BeltItems, INV_SLOT_SIZE_PX }; Rectangle BeltRect { { 205, 5 }, BeltSize }; Rectangle SpellButtonRect { { 565, 64 }, { 56, 56 } }; int PanelPaddingHeight = 16; /** Maps from panel_button_id to panel button description. */ const char *const PanBtnStr[8] = { N_("Character Information"), N_("Quests log"), N_("Automap"), N_("Main Menu"), N_("Inventory"), N_("Spell book"), N_("Send Message"), "" // Player attack }; /** Maps from panel_button_id to hotkey name. */ const char *const PanBtnHotKey[8] = { "'c'", "'q'", N_("Tab"), N_("Esc"), "'i'", "'b'", N_("Enter"), nullptr }; int TotalSpMainPanelButtons = 6; int TotalMpMainPanelButtons = 8; namespace { OptionalOwnedClxSpriteList pDurIcons; OptionalOwnedClxSpriteList multiButtons; OptionalOwnedClxSpriteList pMainPanelButtons; enum panel_button_id : uint8_t { PanelButtonCharinfo, PanelButtonFirst = PanelButtonCharinfo, PanelButtonQlog, PanelButtonAutomap, PanelButtonMainmenu, PanelButtonInventory, PanelButtonSpellbook, PanelButtonSendmsg, PanelButtonFriendly, PanelButtonLast = PanelButtonFriendly, }; bool MainPanelButtons[PanelButtonLast + 1]; void SetMainPanelButtonDown(int btnId) { MainPanelButtons[btnId] = true; RedrawComponent(PanelDrawComponent::ControlButtons); MainPanelButtonDown = true; } void SetMainPanelButtonUp() { RedrawComponent(PanelDrawComponent::ControlButtons); MainPanelButtonDown = false; } int CapStatPointsToAdd(int remainingStatPoints, const Player &player, CharacterAttribute attribute) { const int pointsToReachCap = player.GetMaximumAttributeValue(attribute) - player.GetBaseAttributeValue(attribute); return std::min(remainingStatPoints, pointsToReachCap); } int DrawDurIcon4Item(const Surface &out, Item &pItem, int x, int c) { const int durabilityThresholdGold = 5; const int durabilityThresholdRed = 2; if (pItem.isEmpty()) return x; if (pItem._iDurability > durabilityThresholdGold) return x; if (c == 0) { switch (pItem._itype) { case ItemType::Sword: c = 1; break; case ItemType::Axe: c = 5; break; case ItemType::Bow: c = 6; break; case ItemType::Mace: c = 4; break; case ItemType::Staff: c = 7; break; case ItemType::Shield: default: c = 0; break; } } // Calculate how much of the icon should be gold and red const int height = (*pDurIcons)[c].height(); // Height of durability icon CEL int partition = 0; if (pItem._iDurability > durabilityThresholdRed) { const int current = pItem._iDurability - durabilityThresholdRed; partition = (height * current) / (durabilityThresholdGold - durabilityThresholdRed); } // Draw icon const int y = -17 + GetMainPanel().position.y; if (partition > 0) { const Surface stenciledBuffer = out.subregionY(y - partition, partition); ClxDraw(stenciledBuffer, { x, partition }, (*pDurIcons)[c + 8]); // Gold icon } if (partition != height) { const Surface stenciledBuffer = out.subregionY(y - height, height - partition); ClxDraw(stenciledBuffer, { x, height }, (*pDurIcons)[c]); // Red icon } return x - (*pDurIcons)[c].height() - 8; // Add in spacing for the next durability icon } bool IsLevelUpButtonVisible() { if (SpellSelectFlag || CharFlag || MyPlayer->_pStatPts == 0) { return false; } if (ControlMode == ControlTypes::VirtualGamepad) { return false; } if (IsPlayerInStore() || IsStashOpen) { return false; } if (QuestLogIsOpen && GetLeftPanel().contains(GetMainPanel().position + Displacement { 0, -74 })) { return false; } return true; } } // namespace void CalculatePanelAreas() { constexpr Size MainPanelSize { 640, 128 }; MainPanel = { { (gnScreenWidth - MainPanelSize.width) / 2, gnScreenHeight - MainPanelSize.height }, MainPanelSize }; LeftPanel = { { 0, 0 }, SidePanelSize }; RightPanel = { { 0, 0 }, SidePanelSize }; if (ControlMode == ControlTypes::VirtualGamepad) { LeftPanel.position.x = gnScreenWidth / 2 - LeftPanel.size.width; } else { if (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width > MainPanel.size.width) { LeftPanel.position.x = (gnScreenWidth - LeftPanel.size.width - RightPanel.size.width - MainPanel.size.width) / 2; } } LeftPanel.position.y = (gnScreenHeight - LeftPanel.size.height - MainPanel.size.height) / 2; if (ControlMode == ControlTypes::VirtualGamepad) { RightPanel.position.x = gnScreenWidth / 2; } else { RightPanel.position.x = gnScreenWidth - RightPanel.size.width - LeftPanel.position.x; } RightPanel.position.y = LeftPanel.position.y; gnViewportHeight = gnScreenHeight; if (gnScreenWidth <= MainPanel.size.width) { // Part of the screen is fully obscured by the UI gnViewportHeight -= MainPanel.size.height; } } void FocusOnCharInfo() { const Player &myPlayer = *MyPlayer; if (invflag || myPlayer._pStatPts <= 0) return; // Find the first incrementable stat. int stat = -1; for (auto attribute : enum_values()) { if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) continue; stat = static_cast(attribute); } if (stat == -1) return; SetCursorPos(CharPanelButtonRect[stat].Center()); } void OpenCharPanel() { QuestLogIsOpen = false; CloseGoldWithdraw(); CloseStash(); CharFlag = true; } void CloseCharPanel() { CharFlag = false; if (IsInspectingPlayer()) { InspectPlayer = MyPlayer; RedrawEverything(); if (InspectingFromPartyPanel) InspectingFromPartyPanel = false; else InitDiabloMsg(_("Stopped inspecting players.")); } } void ToggleCharPanel() { if (CharFlag) CloseCharPanel(); else OpenCharPanel(); } Point GetPanelPosition(UiPanels panel, Point offset) { const Displacement displacement { offset.x, offset.y }; switch (panel) { case UiPanels::Main: return GetMainPanel().position + displacement; case UiPanels::Quest: case UiPanels::Character: case UiPanels::Stash: return GetLeftPanel().position + displacement; case UiPanels::Spell: case UiPanels::Inventory: return GetRightPanel().position + displacement; default: return GetMainPanel().position + displacement; } } void DrawPanelBox(const Surface &out, SDL_Rect srcRect, Point targetPosition) { out.BlitFrom(*BottomBuffer, srcRect, targetPosition); } tl::expected InitMainPanel() { if (!HeadlessMode) { BottomBuffer.emplace(GetMainPanel().size.width, (GetMainPanel().size.height + PanelPaddingHeight) * (IsChatAvailable() ? 2 : 1)); pManaBuff.emplace(88, 88); pLifeBuff.emplace(88, 88); RETURN_IF_ERROR(LoadPartyPanel()); RETURN_IF_ERROR(LoadCharPanel()); RETURN_IF_ERROR(LoadLargeSpellIcons()); { ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\panel8", GetMainPanel().size.width)); ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) - 1 }, sprite[0]); } { const Point bulbsPosition { 0, 87 }; ASSIGN_OR_RETURN(const OwnedClxSpriteList statusPanel, LoadCelWithStatus("ctrlpan\\p8bulbs", 88)); ClxDraw(*pLifeBuff, bulbsPosition, statusPanel[0]); ClxDraw(*pManaBuff, bulbsPosition, statusPanel[1]); } } ChatFlag = false; ChatInputState = std::nullopt; if (IsChatAvailable()) { if (!HeadlessMode) { { ASSIGN_OR_RETURN(const OwnedClxSpriteList sprite, LoadCelWithStatus("ctrlpan\\talkpanl", GetMainPanel().size.width)); ClxDraw(*BottomBuffer, { 0, (GetMainPanel().size.height + PanelPaddingHeight) * 2 - 1 }, sprite[0]); } multiButtons = LoadCel("ctrlpan\\p8but2", 33); talkButtons = LoadCel("ctrlpan\\talkbutt", 61); } sgbPlrTalkTbl = 0; TalkMessage[0] = '\0'; for (bool &whisper : WhisperList) whisper = true; for (bool &talkButtonDown : TalkButtonsDown) talkButtonDown = false; } MainPanelFlag = false; LevelButtonDown = false; if (!HeadlessMode) { RETURN_IF_ERROR(LoadMainPanel()); ASSIGN_OR_RETURN(pMainPanelButtons, LoadCelWithStatus("ctrlpan\\panel8bu", 71)); static const uint16_t CharButtonsFrameWidths[9] { 95, 41, 41, 41, 41, 41, 41, 41, 41 }; ASSIGN_OR_RETURN(pChrButtons, LoadCelWithStatus("data\\charbut", CharButtonsFrameWidths)); } ResetMainPanelButtons(); if (!HeadlessMode) pDurIcons = LoadCel("items\\duricons", 32); for (bool &buttonEnabled : CharPanelButton) buttonEnabled = false; CharPanelButtonActive = false; InfoString = StringOrView {}; FloatingInfoString = StringOrView {}; RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); CloseCharPanel(); SpellSelectFlag = false; SpellbookTab = 0; SpellbookFlag = false; if (!HeadlessMode) { InitSpellBook(); ASSIGN_OR_RETURN(pQLogCel, LoadCelWithStatus("data\\quest", static_cast(SidePanelSize.width))); ASSIGN_OR_RETURN(GoldBoxBuffer, LoadCelWithStatus("ctrlpan\\golddrop", 261)); } CloseGoldDrop(); CalculatePanelAreas(); if (!HeadlessMode) InitModifierHints(); return {}; } void DrawMainPanel(const Surface &out) { DrawPanelBox(out, MakeSdlRect(0, sgbPlrTalkTbl + PanelPaddingHeight, GetMainPanel().size.width, GetMainPanel().size.height), GetMainPanel().position); DrawInfoBox(out); } void DrawMainPanelButtons(const Surface &out) { const Point mainPanelPosition = GetMainPanel().position; for (int i = 0; i < TotalSpMainPanelButtons; i++) { if (!MainPanelButtons[i]) { DrawPanelBox(out, MakeSdlRect(MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y + PanelPaddingHeight, MainPanelButtonRect[i].size.width, MainPanelButtonRect[i].size.height + 1), mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }); } else { const Point position = mainPanelPosition + Displacement { MainPanelButtonRect[i].position.x, MainPanelButtonRect[i].position.y }; RenderClxSprite(out, (*pMainPanelButtons)[i], position); RenderClxSprite(out, (*PanelButtonDown)[i], position + Displacement { 4, 0 }); } } if (IsChatAvailable()) { RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonSendmsg] ? 1 : 0], mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonSendmsg].position.x, MainPanelButtonRect[PanelButtonSendmsg].position.y }); const Point friendlyButtonPosition = mainPanelPosition + Displacement { MainPanelButtonRect[PanelButtonFriendly].position.x, MainPanelButtonRect[PanelButtonFriendly].position.y }; if (MyPlayer->friendlyMode) RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 3 : 2], friendlyButtonPosition); else RenderClxSprite(out, (*multiButtons)[MainPanelButtons[PanelButtonFriendly] ? 5 : 4], friendlyButtonPosition); } } void ResetMainPanelButtons() { for (bool &panelButton : MainPanelButtons) panelButton = false; SetMainPanelButtonUp(); } void CheckMainPanelButton() { const int totalButtons = IsChatAvailable() ? TotalMpMainPanelButtons : TotalSpMainPanelButtons; for (int i = 0; i < totalButtons; i++) { Rectangle button = MainPanelButtonRect[i]; SetPanelObjectPosition(UiPanels::Main, button); if (button.contains(MousePosition)) { SetMainPanelButtonDown(i); } } Rectangle spellSelectButton = SpellButtonRect; SetPanelObjectPosition(UiPanels::Main, spellSelectButton); if (!SpellSelectFlag && spellSelectButton.contains(MousePosition)) { if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { Player &myPlayer = *MyPlayer; myPlayer._pRSpell = SpellID::Invalid; myPlayer._pRSplType = SpellType::Invalid; RedrawEverything(); return; } DoSpeedBook(); gamemenu_off(); } } void CheckMainPanelButtonDead() { Rectangle menuButton = MainPanelButtonRect[PanelButtonMainmenu]; SetPanelObjectPosition(UiPanels::Main, menuButton); if (menuButton.contains(MousePosition)) { SetMainPanelButtonDown(PanelButtonMainmenu); return; } Rectangle chatButton = MainPanelButtonRect[PanelButtonSendmsg]; SetPanelObjectPosition(UiPanels::Main, chatButton); if (chatButton.contains(MousePosition)) { SetMainPanelButtonDown(PanelButtonSendmsg); } } void DoAutoMap() { if (!AutomapActive) StartAutomap(); else AutomapActive = false; } void CycleAutomapType() { if (!AutomapActive) { StartAutomap(); return; } const AutomapType newType { static_cast>( (static_cast(GetAutomapType()) + 1) % enum_size::value) }; SetAutomapType(newType); if (newType == AutomapType::FIRST) { AutomapActive = false; } } void CheckMainPanelButtonUp() { bool gamemenuOff = true; SetMainPanelButtonUp(); for (int i = PanelButtonFirst; i <= PanelButtonLast; i++) { if (!MainPanelButtons[i]) continue; MainPanelButtons[i] = false; Rectangle button = MainPanelButtonRect[i]; SetPanelObjectPosition(UiPanels::Main, button); if (!button.contains(MousePosition)) continue; switch (i) { case PanelButtonCharinfo: ToggleCharPanel(); break; case PanelButtonQlog: CloseCharPanel(); CloseGoldWithdraw(); CloseStash(); if (!QuestLogIsOpen) StartQuestlog(); else QuestLogIsOpen = false; break; case PanelButtonAutomap: DoAutoMap(); break; case PanelButtonMainmenu: if (MyPlayerIsDead) { if (!gbIsMultiplayer) { if (gbValidSaveFile) gamemenu_load_game(false); else gamemenu_exit_game(false); } else { NetSendCmd(true, CMD_RETOWN); } break; } else if (MyPlayer->hasNoLife()) { break; } qtextflag = false; gamemenu_handle_previous(); gamemenuOff = false; break; case PanelButtonInventory: SpellbookFlag = false; CloseGoldWithdraw(); CloseStash(); invflag = !invflag; CloseGoldDrop(); break; case PanelButtonSpellbook: CloseInventory(); CloseGoldDrop(); SpellbookFlag = !SpellbookFlag; break; case PanelButtonSendmsg: if (ChatFlag) ResetChat(); else TypeChatMessage(); break; case PanelButtonFriendly: // Toggle friendly Mode NetSendCmd(true, CMD_FRIENDLYMODE); break; } } if (gamemenuOff) gamemenu_off(); } void FreeControlPan() { BottomBuffer = std::nullopt; pManaBuff = std::nullopt; pLifeBuff = std::nullopt; FreeLargeSpellIcons(); FreeSpellBook(); pMainPanelButtons = std::nullopt; multiButtons = std::nullopt; talkButtons = std::nullopt; pChrButtons = std::nullopt; pDurIcons = std::nullopt; pQLogCel = std::nullopt; GoldBoxBuffer = std::nullopt; FreeMainPanel(); FreePartyPanel(); FreeCharPanel(); FreeModifierHints(); } void CheckLevelButton() { if (!IsLevelUpButtonVisible()) { return; } Rectangle button = LevelButtonRect; SetPanelObjectPosition(UiPanels::Main, button); if (!LevelButtonDown && button.contains(MousePosition)) LevelButtonDown = true; } void CheckLevelButtonUp() { Rectangle button = LevelButtonRect; SetPanelObjectPosition(UiPanels::Main, button); if (button.contains(MousePosition)) { OpenCharPanel(); } LevelButtonDown = false; } void DrawLevelButton(const Surface &out) { if (IsLevelUpButtonVisible()) { const int nCel = LevelButtonDown ? 2 : 1; DrawString(out, _("Level Up"), { GetMainPanel().position + Displacement { 0, LevelButtonRect.position.y - 23 }, { 120, 0 } }, { .flags = UiFlags::ColorWhite | UiFlags::AlignCenter | UiFlags::KerningFitSpacing }); RenderClxSprite(out, (*pChrButtons)[nCel], GetMainPanel().position + Displacement { LevelButtonRect.position.x, LevelButtonRect.position.y }); } } void CheckChrBtns() { const Player &myPlayer = *MyPlayer; if (myPlayer._pmode == PM_DEATH) return; if (CharPanelButtonActive || myPlayer._pStatPts == 0) return; for (auto attribute : enum_values()) { if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) continue; auto buttonId = static_cast(attribute); Rectangle button = CharPanelButtonRect[buttonId]; SetPanelObjectPosition(UiPanels::Character, button); if (button.contains(MousePosition)) { CharPanelButton[buttonId] = true; CharPanelButtonActive = true; } } } void ReleaseChrBtns(bool addAllStatPoints) { const Player &myPlayer = *MyPlayer; if (myPlayer._pmode == PM_DEATH) return; CharPanelButtonActive = false; for (auto attribute : enum_values()) { auto buttonId = static_cast(attribute); if (!CharPanelButton[buttonId]) continue; CharPanelButton[buttonId] = false; Rectangle button = CharPanelButtonRect[buttonId]; SetPanelObjectPosition(UiPanels::Character, button); if (button.contains(MousePosition)) { Player &myPlayer = *MyPlayer; int statPointsToAdd = 1; if (addAllStatPoints) statPointsToAdd = CapStatPointsToAdd(myPlayer._pStatPts, myPlayer, attribute); switch (attribute) { case CharacterAttribute::Strength: NetSendCmdParam1(true, CMD_ADDSTR, statPointsToAdd); myPlayer._pStatPts -= statPointsToAdd; break; case CharacterAttribute::Magic: NetSendCmdParam1(true, CMD_ADDMAG, statPointsToAdd); myPlayer._pStatPts -= statPointsToAdd; break; case CharacterAttribute::Dexterity: NetSendCmdParam1(true, CMD_ADDDEX, statPointsToAdd); myPlayer._pStatPts -= statPointsToAdd; break; case CharacterAttribute::Vitality: NetSendCmdParam1(true, CMD_ADDVIT, statPointsToAdd); myPlayer._pStatPts -= statPointsToAdd; break; } } } } void DrawDurIcon(const Surface &out) { const bool hasRoomBetweenPanels = RightPanel.position.x - (LeftPanel.position.x + LeftPanel.size.width) >= 16 + (32 + 8 + 32 + 8 + 32 + 8 + 32) + 16; const bool hasRoomUnderPanels = MainPanel.position.y - (RightPanel.position.y + RightPanel.size.height) >= 16 + 32 + 16; if (!hasRoomBetweenPanels && !hasRoomUnderPanels) { if (IsLeftPanelOpen() && IsRightPanelOpen()) return; } int x = MainPanel.position.x + MainPanel.size.width - 32 - 16; if (!hasRoomUnderPanels) { if (IsRightPanelOpen() && MainPanel.position.x + MainPanel.size.width > RightPanel.position.x) x -= MainPanel.position.x + MainPanel.size.width - RightPanel.position.x; } Player &myPlayer = *MyPlayer; x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HEAD], x, 3); x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_CHEST], x, 2); x = DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_LEFT], x, 0); DrawDurIcon4Item(out, myPlayer.InvBody[INVLOC_HAND_RIGHT], x, 0); } void RedBack(const Surface &out) { uint8_t *dst = out.begin(); uint8_t *tbl = GetPauseTRN(); for (int h = gnViewportHeight; h != 0; h--, dst += out.pitch() - gnScreenWidth) { for (int w = gnScreenWidth; w != 0; w--) { if (leveltype != DTYPE_HELL || *dst >= 32) *dst = tbl[*dst]; dst++; } } } void DrawDeathText(const Surface &out) { const TextRenderOptions largeTextOptions { .flags = UiFlags::FontSize42 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, .spacing = 2 }; const TextRenderOptions smallTextOptions { .flags = UiFlags::FontSize30 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, .spacing = 2 }; std::string text; const int verticalPadding = 42; Point linePosition { 0, gnScreenHeight / 2 - (verticalPadding * 2) }; text = _("You have died"); DrawString(out, text, linePosition, largeTextOptions); linePosition.y += verticalPadding; std::string buttonText; switch (ControlMode) { case ControlTypes::KeyboardAndMouse: buttonText = _("ESC"); break; case ControlTypes::Gamepad: buttonText = ToString(GamepadType, ControllerButton_BUTTON_START); break; case ControlTypes::VirtualGamepad: buttonText = _("Menu Button"); break; default: break; } if (!gbIsMultiplayer) { if (gbValidSaveFile) text = fmt::format(fmt::runtime(_("Press {} to load last save.")), buttonText); else text = fmt::format(fmt::runtime(_("Press {} to return to Main Menu.")), buttonText); } else { text = fmt::format(fmt::runtime(_("Press {} to restart in town.")), buttonText); } DrawString(out, text, linePosition, smallTextOptions); } void SetPanelObjectPosition(UiPanels panel, Rectangle &button) { button.position = GetPanelPosition(panel, button.position); } } // namespace devilution ================================================ FILE: Source/control/control_panel.hpp ================================================ #pragma once #include "engine/rectangle.hpp" #include "panels/ui_panels.hpp" namespace devilution { extern int TotalSpMainPanelButtons; extern int TotalMpMainPanelButtons; extern int PanelPaddingHeight; extern const char *const PanBtnStr[8]; extern const char *const PanBtnHotKey[8]; extern Rectangle SpellButtonRect; extern Rectangle BeltRect; void SetPanelObjectPosition(UiPanels panel, Rectangle &button); } // namespace devilution ================================================ FILE: Source/controls/README.md ================================================ # Controls handling DevilutionX supports mouse & keyboard and gamepad input. This directory currently mostly handles gamepad input. Low-level gamepad handling is abstracted and 3 implementations are provided: 1. SDL2 controller API. 2. SDL 1&2 joystick API. This can be used in SDL1 joystick platforms and for mapping additional buttons not defined by SDL2 controller mappings (e.g. additional Nintendo Switch arrows). 3. Keyboard keys acting as controller buttons. This can be used for testing, or on devices where this is the only or the easiest API to use (e.g. RetroFW). Example keyboard-as-controller build flags: ```bash cmake .. -DUSE_SDL1=ON -DHAS_KBCTRL=1 -DPREFILL_PLAYER_NAME=ON \ -DKBCTRL_BUTTON_DPAD_LEFT=SDLK_LEFT \ -DKBCTRL_BUTTON_DPAD_RIGHT=SDLK_RIGHT \ -DKBCTRL_BUTTON_DPAD_UP=SDLK_UP \ -DKBCTRL_BUTTON_DPAD_DOWN=SDLK_DOWN \ -DKBCTRL_BUTTON_X=SDLK_y \ -DKBCTRL_BUTTON_Y=SDLK_x \ -DKBCTRL_BUTTON_B=SDLK_a \ -DKBCTRL_BUTTON_A=SDLK_b \ -DKBCTRL_BUTTON_RIGHTSHOULDER=SDLK_RIGHTBRACKET \ -DKBCTRL_BUTTON_LEFTSHOULDER=SDLK_LEFTBRACKET \ -DKBCTRL_BUTTON_LEFTSTICK=SDLK_TAB \ -DKBCTRL_BUTTON_START=SDLK_RETURN \ -DKBCTRL_BUTTON_BACK=SDLK_LSHIFT ``` ================================================ FILE: Source/controls/axis_direction.cpp ================================================ #include "axis_direction.h" #ifdef USE_SDL3 #include #else #include #endif namespace devilution { AxisDirection AxisDirectionRepeater::Get(AxisDirection axisDirection) { const int now = SDL_GetTicks(); switch (axisDirection.x) { case AxisDirectionX_LEFT: last_right_ = 0; if (now - last_left_ < min_interval_ms_) { axisDirection.x = AxisDirectionX_NONE; } else { last_left_ = now; } break; case AxisDirectionX_RIGHT: last_left_ = 0; if (now - last_right_ < min_interval_ms_) { axisDirection.x = AxisDirectionX_NONE; } else { last_right_ = now; } break; case AxisDirectionX_NONE: last_left_ = last_right_ = 0; break; } switch (axisDirection.y) { case AxisDirectionY_UP: last_down_ = 0; if (now - last_up_ < min_interval_ms_) { axisDirection.y = AxisDirectionY_NONE; } else { last_up_ = now; } break; case AxisDirectionY_DOWN: last_up_ = 0; if (now - last_down_ < min_interval_ms_) { axisDirection.y = AxisDirectionY_NONE; } else { last_down_ = now; } break; case AxisDirectionY_NONE: last_up_ = last_down_ = 0; break; } return axisDirection; } } // namespace devilution ================================================ FILE: Source/controls/axis_direction.h ================================================ #pragma once #include namespace devilution { enum AxisDirectionX : uint8_t { AxisDirectionX_NONE, AxisDirectionX_LEFT, AxisDirectionX_RIGHT }; enum AxisDirectionY : uint8_t { AxisDirectionY_NONE, AxisDirectionY_UP, AxisDirectionY_DOWN }; /** * @brief 8-way direction of a D-Pad or a thumb stick. */ struct AxisDirection { AxisDirectionX x; AxisDirectionY y; }; /** * @brief Returns a non-empty AxisDirection at most once per the given time interval. */ class AxisDirectionRepeater { public: AxisDirectionRepeater(int min_interval_ms = 200) : last_left_(0) , last_right_(0) , last_up_(0) , last_down_(0) , min_interval_ms_(min_interval_ms) { } AxisDirection Get(AxisDirection axisDirection); private: int last_left_; int last_right_; int last_up_; int last_down_; int min_interval_ms_; }; } // namespace devilution ================================================ FILE: Source/controls/control_mode.cpp ================================================ #include "controls/control_mode.hpp" namespace devilution { ControlTypes ControlMode = ControlTypes::None; ControlTypes ControlDevice = ControlTypes::None; GamepadLayout GamepadType = #if defined(DEVILUTIONX_GAMEPAD_TYPE) GamepadLayout:: DEVILUTIONX_GAMEPAD_TYPE; #else GamepadLayout::Generic; #endif } // namespace devilution ================================================ FILE: Source/controls/control_mode.hpp ================================================ #pragma once #include #include "controls/controller_buttons.h" namespace devilution { enum class ControlTypes : uint8_t { None, KeyboardAndMouse, Gamepad, VirtualGamepad, }; extern ControlTypes ControlMode; /** * @brief Controlling device type. * * While simulating a mouse, `ControlMode` is set to `KeyboardAndMouse`, * even though a gamepad is used to control it. * * This value is always set to the actual active device type. */ extern ControlTypes ControlDevice; extern GamepadLayout GamepadType; } // namespace devilution ================================================ FILE: Source/controls/controller.cpp ================================================ #include "controls/controller.h" #include #ifdef USE_SDL3 #include #else #include #endif #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" #include "controls/devices/kbcontroller.h" #include "engine/demomode.h" #include "utils/sdl_compat.h" namespace devilution { void UnlockControllerState(const SDL_Event &event) { #ifndef USE_SDL1 GameController *const controller = GameController::Get(event); if (controller != nullptr) { controller->UnlockTriggerState(); } #endif Joystick *const joystick = Joystick::Get(event); if (joystick != nullptr) { joystick->UnlockHatState(); } } StaticVector ToControllerButtonEvents(const SDL_Event &event) { ControllerButtonEvent result { ControllerButton_NONE, false }; switch (event.type) { case SDL_EVENT_JOYSTICK_BUTTON_UP: case SDL_EVENT_KEY_UP: #ifndef USE_SDL1 case SDL_EVENT_GAMEPAD_BUTTON_UP: #endif result.up = true; break; default: break; } #if HAS_KBCTRL == 1 if (!demo::IsRunning()) { result.button = KbCtrlToControllerButton(event); if (result.button != ControllerButton_NONE) return { result }; } #endif #ifndef USE_SDL1 GameController *const controller = GameController::Get(event); if (controller != nullptr) { result.button = controller->ToControllerButton(event); if (result.button != ControllerButton_NONE) { if (result.button == ControllerButton_AXIS_TRIGGERLEFT || result.button == ControllerButton_AXIS_TRIGGERRIGHT) { result.up = !controller->IsPressed(result.button); } return { result }; } } #endif const Joystick *joystick = Joystick::Get(event); if (joystick != nullptr) { return devilution::Joystick::ToControllerButtonEvents(event); } return { result }; } bool IsControllerButtonPressed(ControllerButton button) { #ifndef USE_SDL1 if (GameController::IsPressedOnAnyController(button)) return true; #endif #if HAS_KBCTRL == 1 if (!demo::IsRunning() && IsKbCtrlButtonPressed(button)) return true; #endif return Joystick::IsPressedOnAnyJoystick(button); } bool IsControllerButtonComboPressed(ControllerButtonCombo combo) { return IsControllerButtonPressed(combo.button) && (combo.modifier == ControllerButton_NONE || IsControllerButtonPressed(combo.modifier)); } bool HandleControllerAddedOrRemovedEvent(const SDL_Event &event) { #ifndef USE_SDL1 switch (event.type) { case SDL_EVENT_GAMEPAD_ADDED: GameController::Add(SDLC_EventGamepadDevice(event).which); break; case SDL_EVENT_GAMEPAD_REMOVED: GameController::Remove(SDLC_EventGamepadDevice(event).which); break; case SDL_EVENT_JOYSTICK_ADDED: Joystick::Add(event.jdevice.which); break; case SDL_EVENT_JOYSTICK_REMOVED: Joystick::Remove(event.jdevice.which); break; default: return false; } return true; #else return false; #endif } } // namespace devilution ================================================ FILE: Source/controls/controller.h ================================================ #pragma once #ifdef USE_SDL3 #include #else #include #endif #include "controller_buttons.h" #include "utils/static_vector.hpp" namespace devilution { // Must be called exactly once at the start of each SDL input event. void UnlockControllerState(const SDL_Event &event); StaticVector ToControllerButtonEvents(const SDL_Event &event); bool IsControllerButtonPressed(ControllerButton button); bool IsControllerButtonComboPressed(ControllerButtonCombo combo); bool HandleControllerAddedOrRemovedEvent(const SDL_Event &event); } // namespace devilution ================================================ FILE: Source/controls/controller_buttons.cpp ================================================ #include "controller_buttons.h" namespace devilution { namespace { namespace controller_button_icon { [[maybe_unused]] const std::string_view Playstation_Triangle = "\uE000"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_Square = "\uE001"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_X = "\uE002"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_Circle = "\uE003"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_Options = "\uE004"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_Share = "\uE005"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_L2 = "\uE006"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_R2 = "\uE007"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_L1 = "\uE008"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_R1 = "\uE009"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_DPad_Up = "\uE00A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_DPad_Right = "\uE00B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_DPad_Down = "\uE00C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_DPad_Left = "\uE00D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_NW = "\uE00E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_W = "\uE00F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_SW = "\uE010"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_N = "\uE011"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick = "\uE012"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_S = "\uE013"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_NE = "\uE014"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_E = "\uE015"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_LStick_SE = "\uE016"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_L3 = "\uE017"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_NW = "\uE018"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_W = "\uE019"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_SW = "\uE01A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_N = "\uE01B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick = "\uE01C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_S = "\uE01D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_NE = "\uE01E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_E = "\uE01F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_RStick_SE = "\uE020"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_R3 = "\uE021"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Playstation_Touchpad = "\uE022"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_X = "\uE023"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_Y = "\uE024"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_B = "\uE025"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_A = "\uE026"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_Plus = "\uE027"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_Minus = "\uE028"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_ZL = "\uE029"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_ZR = "\uE02A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_L = "\uE02B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_R = "\uE02C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_DPad_Up = "\uE02D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_DPad_Right = "\uE02E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_DPad_Down = "\uE02F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_DPad_Left = "\uE030"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_NW = "\uE031"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_W = "\uE032"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_SW = "\uE033"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_N = "\uE034"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick = "\uE035"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_S = "\uE036"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_NE = "\uE037"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_E = "\uE038"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_SE = "\uE039"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_LStick_Click = "\uE03A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_NW = "\uE03B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_W = "\uE03C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_SW = "\uE03D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_N = "\uE03E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick = "\uE03F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_S = "\uE040"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_NE = "\uE041"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_E = "\uE042"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_SE = "\uE043"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_RStick_Click = "\uE044"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_Home = "\uE045"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_Screenshot = "\uE046"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_SL = "\uE047"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Nintendo_SR = "\uE048"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_Y = "\uE049"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_X = "\uE04A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_A = "\uE04B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_B = "\uE04C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_Menu = "\uE04D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_View = "\uE04E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LT = "\uE04F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RT = "\uE050"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LB = "\uE051"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RB = "\uE052"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_DPad_Up = "\uE053"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_DPad_Right = "\uE054"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_DPad_Down = "\uE055"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_DPad_Left = "\uE056"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_NW = "\uE057"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_W = "\uE058"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_SW = "\uE059"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_N = "\uE05A"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick = "\uE05B"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_NE = "\uE05C"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_E = "\uE05D"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_SE = "\uE05E"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_LStick_Click = "\uE05F"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_NW = "\uE060"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_W = "\uE061"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_SW = "\uE062"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_N = "\uE063"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick = "\uE064"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_S = "\uE065"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_NE = "\uE066"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_E = "\uE067"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_SE = "\uE068"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_RStick_Click = "\uE069"; // NOLINT(readability-identifier-naming) [[maybe_unused]] const std::string_view Xbox_Xbox = "\uE06A"; // NOLINT(readability-identifier-naming) } // namespace controller_button_icon std::string_view ToGenericButtonText(ControllerButton button) { switch (button) { case devilution::ControllerButton_BUTTON_A: return "A"; case devilution::ControllerButton_BUTTON_B: return "B"; case devilution::ControllerButton_BUTTON_X: return "X"; case devilution::ControllerButton_BUTTON_Y: return "Y"; case devilution::ControllerButton_BUTTON_START: return "Start"; case devilution::ControllerButton_BUTTON_BACK: return "Select"; case devilution::ControllerButton_AXIS_TRIGGERLEFT: return "LT"; case devilution::ControllerButton_AXIS_TRIGGERRIGHT: return "RT"; case devilution::ControllerButton_BUTTON_LEFTSHOULDER: return "LB"; case devilution::ControllerButton_BUTTON_RIGHTSHOULDER: return "RB"; case devilution::ControllerButton_BUTTON_LEFTSTICK: return "LS"; case devilution::ControllerButton_BUTTON_RIGHTSTICK: return "RS"; case devilution::ControllerButton_BUTTON_DPAD_UP: return "Up"; case devilution::ControllerButton_BUTTON_DPAD_DOWN: return "Down"; case devilution::ControllerButton_BUTTON_DPAD_LEFT: return "Left"; case devilution::ControllerButton_BUTTON_DPAD_RIGHT: return "Right"; case devilution::ControllerButton_NONE: return "None"; case devilution::ControllerButton_IGNORE: return "Ignored"; default: return "Unknown"; } } std::string_view ToPlayStationIcon(ControllerButton button) { switch (button) { case devilution::ControllerButton_BUTTON_A: return controller_button_icon::Playstation_X; case devilution::ControllerButton_BUTTON_B: return controller_button_icon::Playstation_Circle; case devilution::ControllerButton_BUTTON_X: return controller_button_icon::Playstation_Square; case devilution::ControllerButton_BUTTON_Y: return controller_button_icon::Playstation_Triangle; case devilution::ControllerButton_BUTTON_START: return controller_button_icon::Playstation_Options; case devilution::ControllerButton_BUTTON_BACK: return controller_button_icon::Playstation_Share; case devilution::ControllerButton_AXIS_TRIGGERLEFT: return controller_button_icon::Playstation_L2; case devilution::ControllerButton_AXIS_TRIGGERRIGHT: return controller_button_icon::Playstation_R2; case devilution::ControllerButton_BUTTON_LEFTSHOULDER: return controller_button_icon::Playstation_L1; case devilution::ControllerButton_BUTTON_RIGHTSHOULDER: return controller_button_icon::Playstation_R1; case devilution::ControllerButton_BUTTON_LEFTSTICK: return controller_button_icon::Playstation_L3; case devilution::ControllerButton_BUTTON_RIGHTSTICK: return controller_button_icon::Playstation_R3; case devilution::ControllerButton_BUTTON_DPAD_UP: return controller_button_icon::Playstation_DPad_Up; case devilution::ControllerButton_BUTTON_DPAD_DOWN: return controller_button_icon::Playstation_DPad_Down; case devilution::ControllerButton_BUTTON_DPAD_LEFT: return controller_button_icon::Playstation_DPad_Left; case devilution::ControllerButton_BUTTON_DPAD_RIGHT: return controller_button_icon::Playstation_DPad_Right; default: return ToGenericButtonText(button); } } std::string_view ToNintendoIcon(ControllerButton button) { switch (button) { case devilution::ControllerButton_BUTTON_A: return controller_button_icon::Nintendo_B; case devilution::ControllerButton_BUTTON_B: return controller_button_icon::Nintendo_A; case devilution::ControllerButton_BUTTON_X: return controller_button_icon::Nintendo_Y; case devilution::ControllerButton_BUTTON_Y: return controller_button_icon::Nintendo_X; case devilution::ControllerButton_BUTTON_START: return controller_button_icon::Nintendo_Plus; case devilution::ControllerButton_BUTTON_BACK: return controller_button_icon::Nintendo_Minus; case devilution::ControllerButton_AXIS_TRIGGERLEFT: return controller_button_icon::Nintendo_ZL; case devilution::ControllerButton_AXIS_TRIGGERRIGHT: return controller_button_icon::Nintendo_ZR; case devilution::ControllerButton_BUTTON_LEFTSHOULDER: return controller_button_icon::Nintendo_L; case devilution::ControllerButton_BUTTON_RIGHTSHOULDER: return controller_button_icon::Nintendo_R; case devilution::ControllerButton_BUTTON_LEFTSTICK: return controller_button_icon::Nintendo_LStick_Click; case devilution::ControllerButton_BUTTON_RIGHTSTICK: return controller_button_icon::Nintendo_RStick_Click; case devilution::ControllerButton_BUTTON_DPAD_UP: return controller_button_icon::Nintendo_DPad_Up; case devilution::ControllerButton_BUTTON_DPAD_DOWN: return controller_button_icon::Nintendo_DPad_Down; case devilution::ControllerButton_BUTTON_DPAD_LEFT: return controller_button_icon::Nintendo_DPad_Left; case devilution::ControllerButton_BUTTON_DPAD_RIGHT: return controller_button_icon::Nintendo_DPad_Right; default: return ToGenericButtonText(button); } } std::string_view ToXboxIcon(ControllerButton button) { switch (button) { case devilution::ControllerButton_BUTTON_A: return controller_button_icon::Xbox_A; case devilution::ControllerButton_BUTTON_B: return controller_button_icon::Xbox_B; case devilution::ControllerButton_BUTTON_X: return controller_button_icon::Xbox_X; case devilution::ControllerButton_BUTTON_Y: return controller_button_icon::Xbox_Y; case devilution::ControllerButton_BUTTON_START: return controller_button_icon::Xbox_Menu; case devilution::ControllerButton_BUTTON_BACK: return controller_button_icon::Xbox_View; case devilution::ControllerButton_AXIS_TRIGGERLEFT: return controller_button_icon::Xbox_LT; case devilution::ControllerButton_AXIS_TRIGGERRIGHT: return controller_button_icon::Xbox_RT; case devilution::ControllerButton_BUTTON_LEFTSHOULDER: return controller_button_icon::Xbox_LB; case devilution::ControllerButton_BUTTON_RIGHTSHOULDER: return controller_button_icon::Xbox_RB; case devilution::ControllerButton_BUTTON_LEFTSTICK: return controller_button_icon::Xbox_LStick_Click; case devilution::ControllerButton_BUTTON_RIGHTSTICK: return controller_button_icon::Xbox_RStick_Click; case devilution::ControllerButton_BUTTON_DPAD_UP: return controller_button_icon::Xbox_DPad_Up; case devilution::ControllerButton_BUTTON_DPAD_DOWN: return controller_button_icon::Xbox_DPad_Down; case devilution::ControllerButton_BUTTON_DPAD_LEFT: return controller_button_icon::Xbox_DPad_Left; case devilution::ControllerButton_BUTTON_DPAD_RIGHT: return controller_button_icon::Xbox_DPad_Right; default: return ToGenericButtonText(button); } } } // namespace std::string_view ToString(GamepadLayout gamepadType, ControllerButton button) { switch (gamepadType) { case GamepadLayout::PlayStation: return ToPlayStationIcon(button); case GamepadLayout::Nintendo: return ToNintendoIcon(button); case GamepadLayout::Xbox: return ToXboxIcon(button); default: case GamepadLayout::Generic: return ToGenericButtonText(button); } } } // namespace devilution ================================================ FILE: Source/controls/controller_buttons.h ================================================ #pragma once // Unifies joystick, gamepad, and keyboard controller APIs. #include #include #include namespace devilution { // NOTE: A, B, X, Y refer to physical positions on an XBox 360 controller. // A<->B and X<->Y are reversed on a Nintendo controller. enum ControllerButton : uint8_t { ControllerButton_NONE, ControllerButton_IGNORE, ControllerButton_AXIS_TRIGGERLEFT, // ZL (aka L2) ControllerButton_AXIS_TRIGGERRIGHT, // ZR (aka R2) ControllerButton_BUTTON_A, // Bottom button ControllerButton_BUTTON_B, // Right button ControllerButton_BUTTON_X, // Left button ControllerButton_BUTTON_Y, // TOP button ControllerButton_BUTTON_LEFTSTICK, ControllerButton_BUTTON_RIGHTSTICK, ControllerButton_BUTTON_LEFTSHOULDER, ControllerButton_BUTTON_RIGHTSHOULDER, ControllerButton_BUTTON_START, ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP, ControllerButton_BUTTON_DPAD_DOWN, ControllerButton_BUTTON_DPAD_LEFT, ControllerButton_BUTTON_DPAD_RIGHT, FIRST = ControllerButton_NONE, LAST = ControllerButton_BUTTON_DPAD_RIGHT }; struct ControllerButtonCombo { constexpr ControllerButtonCombo() : modifier(ControllerButton_NONE) , button(ControllerButton_NONE) { } constexpr ControllerButtonCombo(ControllerButton button) : modifier(ControllerButton_NONE) , button(button) { } constexpr ControllerButtonCombo(ControllerButton modifier, ControllerButton button) : modifier(modifier) , button(button) { } ControllerButton modifier; ControllerButton button; }; struct ControllerButtonEvent { ControllerButtonEvent(ControllerButton button, bool up) : button(button) , up(up) { } ControllerButton button; bool up; }; inline bool IsDPadButton(ControllerButton button) { return button == ControllerButton_BUTTON_DPAD_UP || button == ControllerButton_BUTTON_DPAD_DOWN || button == ControllerButton_BUTTON_DPAD_LEFT || button == ControllerButton_BUTTON_DPAD_RIGHT; } enum class GamepadLayout : uint8_t { Generic, Nintendo, PlayStation, Xbox, }; [[nodiscard]] std::string_view ToString(GamepadLayout gamepadType, ControllerButton button); } // namespace devilution ================================================ FILE: Source/controls/controller_motion.cpp ================================================ #include "controls/controller_motion.h" #include #ifdef USE_SDL3 #include #include #else #include #endif #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/controller.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" #include "controls/game_controls.h" #include "controls/padmapper.hpp" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "engine/demomode.h" #include "options.h" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/sdl_compat.h" namespace devilution { bool SimulatingMouseWithPadmapper; namespace { void ScaleJoystickAxes(float *x, float *y, float deadzone) { // radial and scaled dead_zone // https://web.archive.org/web/20200130014626/www.third-helix.com:80/2013/04/12/doing-thumbstick-dead-zones-right.html // input values go from -32767.0...+32767.0, output values are from -1.0 to 1.0; if (deadzone == 0) { return; } if (deadzone >= 1.0) { *x = 0; *y = 0; return; } const float maximum = 32767.0; float analogX = *x; float analogY = *y; const float deadZone = deadzone * maximum; const float magnitude = std::sqrt(analogX * analogX + analogY * analogY); if (magnitude >= deadZone) { // find scaled axis values with magnitudes between zero and maximum const float scalingFactor = 1.F / magnitude * (magnitude - deadZone) / (maximum - deadZone); analogX = (analogX * scalingFactor); analogY = (analogY * scalingFactor); // std::clamp to ensure results will never exceed the max_axis value float clampingFactor = 1.F; const float absAnalogX = std::fabs(analogX); const float absAnalogY = std::fabs(analogY); if (absAnalogX > 1.0 || absAnalogY > 1.0) { if (absAnalogX > absAnalogY) { clampingFactor = 1.F / absAnalogX; } else { clampingFactor = 1.F / absAnalogY; } } *x = (clampingFactor * analogX); *y = (clampingFactor * analogY); } else { *x = 0; *y = 0; } } bool IsMovementOverriddenByPadmapper(ControllerButton button) { const ControllerButtonEvent releaseEvent { button, true }; const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent); const ControllerButtonCombo buttonCombo = GetOptions().Padmapper.ButtonComboForAction(actionName); return buttonCombo.modifier != ControllerButton_NONE; } bool TriggersQuickSpellAction(ControllerButton button) { const ControllerButtonEvent releaseEvent { button, true }; const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(releaseEvent); const std::string_view prefix { "QuickSpell" }; if (actionName.size() < prefix.size()) return false; const std::string_view truncatedActionName { actionName.data(), prefix.size() }; return truncatedActionName == prefix; } bool IsPressedForMovement(ControllerButton button) { return !PadMenuNavigatorActive && IsControllerButtonPressed(button) && !IsMovementOverriddenByPadmapper(button) && !(SpellSelectFlag && TriggersQuickSpellAction(button)); } void SetSimulatingMouseWithPadmapper(bool value) { if (SimulatingMouseWithPadmapper == value) return; SimulatingMouseWithPadmapper = value; if (value) { LogVerbose("Control: begin simulating mouse with D-Pad"); } else { LogVerbose("Control: end simulating mouse with D-Pad"); } } } // namespace float leftStickX, leftStickY, rightStickX, rightStickY; float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; bool leftStickNeedsScaling, rightStickNeedsScaling; namespace { void ScaleJoysticks() { const Options &options = GetOptions(); const float rightDeadzone = options.Controller.fDeadzone; const float leftDeadzone = options.Controller.fDeadzone; if (leftStickNeedsScaling) { leftStickX = leftStickXUnscaled; leftStickY = leftStickYUnscaled; ScaleJoystickAxes(&leftStickX, &leftStickY, leftDeadzone); leftStickNeedsScaling = false; } if (rightStickNeedsScaling) { rightStickX = rightStickXUnscaled; rightStickY = rightStickYUnscaled; ScaleJoystickAxes(&rightStickX, &rightStickY, rightDeadzone); rightStickNeedsScaling = false; } } } // namespace bool IsControllerMotion(const SDL_Event &event) { #ifndef USE_SDL1 if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { return IsAnyOf(SDLC_EventGamepadAxis(event).axis, SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY); } #endif #if defined(JOY_AXIS_LEFTX) || defined(JOY_AXIS_LEFTY) || defined(JOY_AXIS_RIGHTX) || defined(JOY_AXIS_RIGHTY) if (event.type == SDL_EVENT_JOYSTICK_AXIS_MOTION) { switch (event.jaxis.axis) { #ifdef JOY_AXIS_LEFTX case JOY_AXIS_LEFTX: return true; #endif #ifdef JOY_AXIS_LEFTY case JOY_AXIS_LEFTY: return true; #endif #ifdef JOY_AXIS_RIGHTX case JOY_AXIS_RIGHTX: return true; #endif #ifdef JOY_AXIS_RIGHTY case JOY_AXIS_RIGHTY: return true; #endif default: return false; } } #endif return false; } // Updates motion state for mouse and joystick sticks. void ProcessControllerMotion(const SDL_Event &event) { #ifndef USE_SDL1 GameController *const controller = GameController::Get(event); if (controller != nullptr && devilution::GameController::ProcessAxisMotion(event)) { ScaleJoysticks(); SetSimulatingMouseWithPadmapper(false); return; } #endif Joystick *const joystick = Joystick::Get(event); if (joystick != nullptr && devilution::Joystick::ProcessAxisMotion(event)) { ScaleJoysticks(); SetSimulatingMouseWithPadmapper(false); } } AxisDirection GetAnalogStickDirection(float stickX, float stickY) { // avoid sqrt() by comparing squared magnitudes const float magnitudeSquared = stickX * stickX + stickY * stickY; const float thresholdSquared = StickDirectionThreshold * StickDirectionThreshold; if (magnitudeSquared < thresholdSquared) return { AxisDirectionX_NONE, AxisDirectionY_NONE }; const float absX = std::fabs(stickX); const float absY = std::fabs(stickY); AxisDirection result { AxisDirectionX_NONE, AxisDirectionY_NONE }; // 8-way sectoring with 22.5° cutoffs constexpr float DiagonalCutoff = 0.41421356F; // tan(22.5°) if (absX == 0.0F) { result.y = stickY > 0 ? AxisDirectionY_UP : AxisDirectionY_DOWN; return result; } const float ratio = absY / absX; if (ratio <= DiagonalCutoff) { result.x = stickX > 0 ? AxisDirectionX_RIGHT : AxisDirectionX_LEFT; return result; } if (ratio >= 1.0F / DiagonalCutoff) { result.y = stickY > 0 ? AxisDirectionY_UP : AxisDirectionY_DOWN; return result; } result.x = stickX > 0 ? AxisDirectionX_RIGHT : AxisDirectionX_LEFT; result.y = stickY > 0 ? AxisDirectionY_UP : AxisDirectionY_DOWN; return result; } AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper) { AxisDirection result = GetAnalogStickDirection(leftStickX, leftStickY); bool isUpPressed = false; bool isDownPressed = false; bool isLeftPressed = false; bool isRightPressed = false; if (usePadmapper) { isUpPressed |= PadmapperIsActionActive("MoveUp"); isDownPressed |= PadmapperIsActionActive("MoveDown"); isLeftPressed |= PadmapperIsActionActive("MoveLeft"); isRightPressed |= PadmapperIsActionActive("MoveRight"); } else if (!SimulatingMouseWithPadmapper) { isUpPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_UP); isDownPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_DOWN); isLeftPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_LEFT); isRightPressed |= IsPressedForMovement(ControllerButton_BUTTON_DPAD_RIGHT); } #ifndef USE_SDL1 if (ControlMode == ControlTypes::VirtualGamepad) { isUpPressed |= VirtualGamepadState.isActive && VirtualGamepadState.directionPad.isUpPressed; isDownPressed |= VirtualGamepadState.isActive && VirtualGamepadState.directionPad.isDownPressed; isLeftPressed |= VirtualGamepadState.isActive && VirtualGamepadState.directionPad.isLeftPressed; isRightPressed |= VirtualGamepadState.isActive && VirtualGamepadState.directionPad.isRightPressed; } #endif if (isUpPressed) { result.y = AxisDirectionY_UP; } else if (isDownPressed) { result.y = AxisDirectionY_DOWN; } if (isLeftPressed) { result.x = AxisDirectionX_LEFT; } else if (isRightPressed) { result.x = AxisDirectionX_RIGHT; } return result; } void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent) { if (ctrlEvent.button == ControllerButton_NONE) return; if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) return; const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent); const bool upTriggered = actionName == "MouseUp"; const bool downTriggered = actionName == "MouseDown"; const bool leftTriggered = actionName == "MouseLeft"; const bool rightTriggered = actionName == "MouseRight"; if (!upTriggered && !downTriggered && !leftTriggered && !rightTriggered) { if (rightStickX == 0 && rightStickY == 0) SetSimulatingMouseWithPadmapper(false); return; } const bool upActive = (upTriggered && !ctrlEvent.up) || (!upTriggered && PadmapperIsActionActive("MouseUp")); const bool downActive = (downTriggered && !ctrlEvent.up) || (!downTriggered && PadmapperIsActionActive("MouseDown")); const bool leftActive = (leftTriggered && !ctrlEvent.up) || (!leftTriggered && PadmapperIsActionActive("MouseLeft")); const bool rightActive = (rightTriggered && !ctrlEvent.up) || (!rightTriggered && PadmapperIsActionActive("MouseRight")); rightStickX = 0; rightStickY = 0; if (upActive) rightStickY += 1.F; if (downActive) rightStickY -= 1.F; if (leftActive) rightStickX -= 1.F; if (rightActive) rightStickX += 1.F; SetSimulatingMouseWithPadmapper(true); } } // namespace devilution ================================================ FILE: Source/controls/controller_motion.h ================================================ #pragma once // Processes and stores mouse and joystick motion. #ifdef USE_SDL3 #include #else #include #endif #include "controls/axis_direction.h" #include "controls/controller.h" namespace devilution { // Whether we're currently simulating the mouse with SELECT + D-Pad. extern bool SimulatingMouseWithPadmapper; // Raw axis values. extern float leftStickXUnscaled, leftStickYUnscaled, rightStickXUnscaled, rightStickYUnscaled; // Axis values scaled to [-1, 1] range and clamped to a deadzone. extern float leftStickX, leftStickY, rightStickX, rightStickY; // Whether stick positions have been updated and need rescaling. extern bool leftStickNeedsScaling, rightStickNeedsScaling; // Minimum scaled stick magnitude to register a direction. constexpr float StickDirectionThreshold = 0.4F; // Updates motion state for mouse and joystick sticks. void ProcessControllerMotion(const SDL_Event &event); // Indicates whether the event represents movement of an analog thumbstick. bool IsControllerMotion(const SDL_Event &event); // Returns direction of the left thumb stick or DPad (if allow_dpad = true). AxisDirection GetLeftStickOrDpadDirection(bool usePadmapper); // Simulates right-stick movement based on input from padmapper mouse movement actions. void SimulateRightStickWithPadmapper(ControllerButtonEvent ctrlEvent); } // namespace devilution ================================================ FILE: Source/controls/devices/game_controller.cpp ================================================ #include "controls/devices/game_controller.h" #include #include #ifdef USE_SDL3 #include #include #include #else #include #include "utils/sdl2_backports.h" #endif #include "controls/controller_motion.h" #include "controls/devices/joystick.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_ptrs.h" #include "utils/stubs.h" namespace devilution { std::vector GameController::controllers_; void GameController::UnlockTriggerState() { trigger_left_state_ = ControllerButton_NONE; trigger_right_state_ = ControllerButton_NONE; } ControllerButton GameController::ToControllerButton(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_GAMEPAD_AXIS_MOTION: { const SDL_GamepadAxisEvent &axis = SDLC_EventGamepadAxis(event); switch (axis.axis) { case SDL_GAMEPAD_AXIS_LEFT_TRIGGER: if (axis.value < 8192 && trigger_left_is_down_) { // 25% pressed trigger_left_is_down_ = false; trigger_left_state_ = ControllerButton_AXIS_TRIGGERLEFT; } if (axis.value > 16384 && !trigger_left_is_down_) { // 50% pressed trigger_left_is_down_ = true; trigger_left_state_ = ControllerButton_AXIS_TRIGGERLEFT; } return trigger_left_state_; case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER: if (axis.value < 8192 && trigger_right_is_down_) { // 25% pressed trigger_right_is_down_ = false; trigger_right_state_ = ControllerButton_AXIS_TRIGGERRIGHT; } if (axis.value > 16384 && !trigger_right_is_down_) { // 50% pressed trigger_right_is_down_ = true; trigger_right_state_ = ControllerButton_AXIS_TRIGGERRIGHT; } return trigger_right_state_; } } break; case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: switch (SDLC_EventGamepadButton(event).button) { case SDL_GAMEPAD_BUTTON_SOUTH: return ControllerButton_BUTTON_A; case SDL_GAMEPAD_BUTTON_EAST: return ControllerButton_BUTTON_B; case SDL_GAMEPAD_BUTTON_WEST: return ControllerButton_BUTTON_X; case SDL_GAMEPAD_BUTTON_NORTH: return ControllerButton_BUTTON_Y; case SDL_GAMEPAD_BUTTON_LEFT_STICK: return ControllerButton_BUTTON_LEFTSTICK; case SDL_GAMEPAD_BUTTON_RIGHT_STICK: return ControllerButton_BUTTON_RIGHTSTICK; case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER: return ControllerButton_BUTTON_LEFTSHOULDER; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return ControllerButton_BUTTON_RIGHTSHOULDER; case SDL_GAMEPAD_BUTTON_START: return ControllerButton_BUTTON_START; case SDL_GAMEPAD_BUTTON_BACK: return ControllerButton_BUTTON_BACK; case SDL_GAMEPAD_BUTTON_DPAD_UP: return ControllerButton_BUTTON_DPAD_UP; case SDL_GAMEPAD_BUTTON_DPAD_DOWN: return ControllerButton_BUTTON_DPAD_DOWN; case SDL_GAMEPAD_BUTTON_DPAD_LEFT: return ControllerButton_BUTTON_DPAD_LEFT; case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: return ControllerButton_BUTTON_DPAD_RIGHT; default: break; } default: break; } return ControllerButton_NONE; } SDL_GamepadButton GameController::ToSdlGameControllerButton(ControllerButton button) { if (button == ControllerButton_AXIS_TRIGGERLEFT || button == ControllerButton_AXIS_TRIGGERRIGHT) UNIMPLEMENTED(); switch (button) { case ControllerButton_BUTTON_A: return SDL_GAMEPAD_BUTTON_SOUTH; case ControllerButton_BUTTON_B: return SDL_GAMEPAD_BUTTON_EAST; case ControllerButton_BUTTON_X: return SDL_GAMEPAD_BUTTON_WEST; case ControllerButton_BUTTON_Y: return SDL_GAMEPAD_BUTTON_NORTH; case ControllerButton_BUTTON_BACK: return SDL_GAMEPAD_BUTTON_BACK; case ControllerButton_BUTTON_START: return SDL_GAMEPAD_BUTTON_START; case ControllerButton_BUTTON_LEFTSTICK: return SDL_GAMEPAD_BUTTON_LEFT_STICK; case ControllerButton_BUTTON_RIGHTSTICK: return SDL_GAMEPAD_BUTTON_RIGHT_STICK; case ControllerButton_BUTTON_LEFTSHOULDER: return SDL_GAMEPAD_BUTTON_LEFT_SHOULDER; case ControllerButton_BUTTON_RIGHTSHOULDER: return SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER; case ControllerButton_BUTTON_DPAD_UP: return SDL_GAMEPAD_BUTTON_DPAD_UP; case ControllerButton_BUTTON_DPAD_DOWN: return SDL_GAMEPAD_BUTTON_DPAD_DOWN; case ControllerButton_BUTTON_DPAD_LEFT: return SDL_GAMEPAD_BUTTON_DPAD_LEFT; case ControllerButton_BUTTON_DPAD_RIGHT: return SDL_GAMEPAD_BUTTON_DPAD_RIGHT; default: return SDL_GAMEPAD_BUTTON_INVALID; } } bool GameController::IsPressed(ControllerButton button) const { if (button == ControllerButton_AXIS_TRIGGERLEFT) return trigger_left_is_down_; if (button == ControllerButton_AXIS_TRIGGERRIGHT) return trigger_right_is_down_; const SDL_GamepadButton gcButton = ToSdlGameControllerButton(button); return SDL_GamepadHasButton(sdl_game_controller_, gcButton) && SDL_GetGamepadButton(sdl_game_controller_, gcButton); } bool GameController::ProcessAxisMotion(const SDL_Event &event) { if (event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION) return false; const SDL_GamepadAxisEvent &axis = SDLC_EventGamepadAxis(event); switch (axis.axis) { case SDL_GAMEPAD_AXIS_LEFTX: leftStickXUnscaled = static_cast(axis.value); leftStickNeedsScaling = true; break; case SDL_GAMEPAD_AXIS_LEFTY: leftStickYUnscaled = static_cast(-axis.value); leftStickNeedsScaling = true; break; case SDL_GAMEPAD_AXIS_RIGHTX: rightStickXUnscaled = static_cast(axis.value); rightStickNeedsScaling = true; break; case SDL_GAMEPAD_AXIS_RIGHTY: rightStickYUnscaled = static_cast(-axis.value); rightStickNeedsScaling = true; break; default: return false; } return true; } #ifdef USE_SDL3 void GameController::Add(SDL_JoystickID joystickId) #else void GameController::Add(int joystickIndex) #endif { GameController result; #ifdef USE_SDL3 Log("Opening game controller for joystick with ID {}", joystickId); result.sdl_game_controller_ = SDL_OpenGamepad(joystickId); #else Log("Opening game controller for joystick at index {}", joystickIndex); result.sdl_game_controller_ = SDL_GameControllerOpen(joystickIndex); #endif if (result.sdl_game_controller_ == nullptr) { Log("{}", SDL_GetError()); SDL_ClearError(); return; } #ifdef USE_SDL3 result.instance_id_ = joystickId; const SDLUniquePtr mapping { SDL_GetGamepadMappingForID(joystickId) }; #else SDL_Joystick *const sdlJoystick = SDL_GameControllerGetJoystick(result.sdl_game_controller_); result.instance_id_ = SDL_JoystickInstanceID(sdlJoystick); const SDL_JoystickGUID guid = SDL_JoystickGetGUID(sdlJoystick); const SDLUniquePtr mapping { SDL_GameControllerMappingForGUID(guid) }; #endif controllers_.push_back(result); if (mapping) { Log("Opened game controller with mapping:\n{}", mapping.get()); } } void GameController::Remove(SDL_JoystickID instanceId) { Log("Removing game controller with instance id {}", instanceId); for (std::size_t i = 0; i < controllers_.size(); ++i) { const GameController &controller = controllers_[i]; if (controller.instance_id_ != instanceId) continue; controllers_.erase(controllers_.begin() + i); return; } Log("Game controller not found with instance id: {}", instanceId); } GameController *GameController::Get(SDL_JoystickID instanceId) { for (auto &controller : controllers_) { if (controller.instance_id_ == instanceId) return &controller; } return nullptr; } GameController *GameController::Get(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_GAMEPAD_AXIS_MOTION: return Get(SDLC_EventGamepadAxis(event).which); case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: return Get(SDLC_EventGamepadButton(event).which); default: return nullptr; } } const std::vector &GameController::All() { return controllers_; } bool GameController::IsPressedOnAnyController(ControllerButton button, SDL_JoystickID *which) { for (auto &controller : controllers_) if (controller.IsPressed(button)) { if (which != nullptr) *which = controller.instance_id_; return true; } return false; } GamepadLayout GameController::getLayout(const SDL_Event &event) { #if defined(DEVILUTIONX_GAMEPAD_TYPE) return GamepadLayout:: DEVILUTIONX_GAMEPAD_TYPE; #elif USE_SDL3 switch (SDL_GetGamepadTypeForID(event.gdevice.which)) { case SDL_GAMEPAD_TYPE_XBOX360: case SDL_GAMEPAD_TYPE_XBOXONE: return GamepadLayout::Xbox; case SDL_GAMEPAD_TYPE_PS3: case SDL_GAMEPAD_TYPE_PS4: case SDL_GAMEPAD_TYPE_PS5: return GamepadLayout::PlayStation; case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: return GamepadLayout::Nintendo; default: return GamepadLayout::Generic; } #else #if SDL_VERSION_ATLEAST(2, 0, 12) const int index = event.cdevice.which; const SDL_GameControllerType gamepadType = SDL_GameControllerTypeForIndex(index); switch (gamepadType) { case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO: #if SDL_VERSION_ATLEAST(2, 24, 0) case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT: case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT: case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR: #endif return GamepadLayout::Nintendo; case SDL_CONTROLLER_TYPE_PS3: case SDL_CONTROLLER_TYPE_PS4: #if SDL_VERSION_ATLEAST(2, 0, 14) case SDL_CONTROLLER_TYPE_PS5: #endif return GamepadLayout::PlayStation; case SDL_CONTROLLER_TYPE_XBOXONE: case SDL_CONTROLLER_TYPE_XBOX360: #if SDL_VERSION_ATLEAST(2, 0, 16) case SDL_CONTROLLER_TYPE_GOOGLE_STADIA: case SDL_CONTROLLER_TYPE_AMAZON_LUNA: #if SDL_VERSION_ATLEAST(2, 24, 0) case SDL_CONTROLLER_TYPE_NVIDIA_SHIELD: #endif #endif return GamepadLayout::Xbox; #if SDL_VERSION_ATLEAST(2, 0, 14) case SDL_CONTROLLER_TYPE_VIRTUAL: #endif case SDL_CONTROLLER_TYPE_UNKNOWN: #if SDL_VERSION_ATLEAST(2, 30, 0) case SDL_CONTROLLER_TYPE_MAX: #endif break; } #endif return GamepadLayout::Generic; #endif // !defined(DEVILUTIONX_GAMEPAD_TYPE) } } // namespace devilution ================================================ FILE: Source/controls/devices/game_controller.h ================================================ #pragma once #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include "controls/controller_buttons.h" #include "controls/game_controls.h" namespace devilution { class GameController { static std::vector controllers_; public: #ifdef USE_SDL3 static void Add(SDL_JoystickID joystickId); #else static void Add(int joystickIndex); #endif static void Remove(SDL_JoystickID instanceId); static GameController *Get(SDL_JoystickID instanceId); static GameController *Get(const SDL_Event &event); static const std::vector &All(); static bool IsPressedOnAnyController(ControllerButton button, SDL_JoystickID *which = nullptr); // Must be called exactly once at the start of each SDL input event. void UnlockTriggerState(); ControllerButton ToControllerButton(const SDL_Event &event); bool IsPressed(ControllerButton button) const; static bool ProcessAxisMotion(const SDL_Event &event); #ifdef USE_SDL3 static SDL_GamepadButton ToSdlGameControllerButton(ControllerButton button); #else static SDL_GameControllerButton ToSdlGameControllerButton(ControllerButton button); #endif static GamepadLayout getLayout(const SDL_Event &event); private: #ifdef USE_SDL3 SDL_Gamepad *sdl_game_controller_ = nullptr; #else SDL_GameController *sdl_game_controller_ = nullptr; #endif SDL_JoystickID instance_id_ = -1; ControllerButton trigger_left_state_ = ControllerButton_NONE; ControllerButton trigger_right_state_ = ControllerButton_NONE; bool trigger_left_is_down_ = false; bool trigger_right_is_down_ = false; }; } // namespace devilution ================================================ FILE: Source/controls/devices/joystick.cpp ================================================ #include "controls/devices/joystick.h" #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/controller_motion.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/stubs.h" namespace devilution { std::vector Joystick::joysticks_; StaticVector Joystick::ToControllerButtonEvents(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: { #ifdef USE_SDL3 const bool up = !event.jbutton.down; #else const bool up = (event.jbutton.state == SDL_RELEASED); #endif #if defined(JOY_BUTTON_A) || defined(JOY_BUTTON_B) || defined(JOY_BUTTON_X) || defined(JOY_BUTTON_Y) \ || defined(JOY_BUTTON_LEFTSTICK) || defined(JOY_BUTTON_RIGHTSTICK) || defined(JOY_BUTTON_LEFTSHOULDER) || defined(JOY_BUTTON_RIGHTSHOULDER) \ || defined(JOY_BUTTON_TRIGGERLEFT) || defined(JOY_BUTTON_TRIGGERRIGHT) || defined(JOY_BUTTON_START) || defined(JOY_BUTTON_BACK) \ || defined(JOY_BUTTON_DPAD_LEFT) || defined(JOY_BUTTON_DPAD_UP) || defined(JOY_BUTTON_DPAD_RIGHT) || defined(JOY_BUTTON_DPAD_DOWN) switch (event.jbutton.button) { #ifdef JOY_BUTTON_A case JOY_BUTTON_A: return { ControllerButtonEvent { ControllerButton_BUTTON_A, up } }; #endif #ifdef JOY_BUTTON_B case JOY_BUTTON_B: return { ControllerButtonEvent { ControllerButton_BUTTON_B, up } }; #endif #ifdef JOY_BUTTON_X case JOY_BUTTON_X: return { ControllerButtonEvent { ControllerButton_BUTTON_X, up } }; #endif #ifdef JOY_BUTTON_Y case JOY_BUTTON_Y: return { ControllerButtonEvent { ControllerButton_BUTTON_Y, up } }; #endif #ifdef JOY_BUTTON_LEFTSTICK case JOY_BUTTON_LEFTSTICK: return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSTICK, up } }; #endif #ifdef JOY_BUTTON_RIGHTSTICK case JOY_BUTTON_RIGHTSTICK: return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSTICK, up } }; #endif #ifdef JOY_BUTTON_LEFTSHOULDER case JOY_BUTTON_LEFTSHOULDER: return { ControllerButtonEvent { ControllerButton_BUTTON_LEFTSHOULDER, up } }; #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case JOY_BUTTON_RIGHTSHOULDER: return { ControllerButtonEvent { ControllerButton_BUTTON_RIGHTSHOULDER, up } }; #endif #ifdef JOY_BUTTON_TRIGGERLEFT case JOY_BUTTON_TRIGGERLEFT: return { ControllerButtonEvent { ControllerButton_AXIS_TRIGGERLEFT, up } }; #endif #ifdef JOY_BUTTON_TRIGGERRIGHT case JOY_BUTTON_TRIGGERRIGHT: return { ControllerButtonEvent { ControllerButton_AXIS_TRIGGERRIGHT, up } }; #endif #ifdef JOY_BUTTON_START case JOY_BUTTON_START: return { ControllerButtonEvent { ControllerButton_BUTTON_START, up } }; #endif #ifdef JOY_BUTTON_BACK case JOY_BUTTON_BACK: return { ControllerButtonEvent { ControllerButton_BUTTON_BACK, up } }; #endif #ifdef JOY_BUTTON_DPAD_LEFT case JOY_BUTTON_DPAD_LEFT: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_LEFT, up } }; #endif #ifdef JOY_BUTTON_DPAD_UP case JOY_BUTTON_DPAD_UP: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_UP, up } }; #endif #ifdef JOY_BUTTON_DPAD_RIGHT case JOY_BUTTON_DPAD_RIGHT: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_RIGHT, up } }; #endif #ifdef JOY_BUTTON_DPAD_DOWN case JOY_BUTTON_DPAD_DOWN: return { ControllerButtonEvent { ControllerButton_BUTTON_DPAD_DOWN, up } }; #endif default: return { ControllerButtonEvent { ControllerButton_IGNORE, up } }; } #else return { ControllerButtonEvent { ControllerButton_IGNORE, up } }; #endif } #ifdef USE_SDL3 case SDL_EVENT_JOYSTICK_HAT_MOTION: #else case SDL_JOYHATMOTION: #endif { Joystick *joystick = Get(event); if (joystick == nullptr) return { ControllerButtonEvent { ControllerButton_IGNORE, false } }; joystick->UpdateHatState(event.jhat); return joystick->GetHatEvents(); } #ifdef USE_SDL3 case SDL_EVENT_JOYSTICK_AXIS_MOTION: case SDL_EVENT_JOYSTICK_BALL_MOTION: #else case SDL_JOYAXISMOTION: case SDL_JOYBALLMOTION: #endif // ProcessAxisMotion() requires a ControllerButtonEvent parameter // so provide one here using ControllerButton_NONE return { ControllerButtonEvent { ControllerButton_NONE, false } }; default: return {}; } } StaticVector Joystick::GetHatEvents() { StaticVector hatEvents; if (hatState_[0].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_UP, !hatState_[0].pressed); if (hatState_[1].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_DOWN, !hatState_[1].pressed); if (hatState_[2].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_LEFT, !hatState_[2].pressed); if (hatState_[3].didStateChange) hatEvents.emplace_back(ControllerButton_BUTTON_DPAD_RIGHT, !hatState_[3].pressed); if (hatEvents.size() == 0) hatEvents.emplace_back(ControllerButton_IGNORE, false); return hatEvents; } void Joystick::UpdateHatState(const SDL_JoyHatEvent &event) { if (lockHatState_) return; #if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) if (event.hat == JOY_HAT_DPAD_UP_HAT) { HatState &hatState = hatState_[0]; bool pressed = (event.value & JOY_HAT_DPAD_UP) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) if (event.hat == JOY_HAT_DPAD_DOWN_HAT) { HatState &hatState = hatState_[1]; bool pressed = (event.value & JOY_HAT_DPAD_DOWN) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) if (event.hat == JOY_HAT_DPAD_LEFT_HAT) { HatState &hatState = hatState_[2]; bool pressed = (event.value & JOY_HAT_DPAD_LEFT) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif #if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) if (event.hat == JOY_HAT_DPAD_RIGHT_HAT) { HatState &hatState = hatState_[3]; bool pressed = (event.value & JOY_HAT_DPAD_RIGHT) != 0; hatState.didStateChange = (pressed != hatState.pressed); hatState.pressed = pressed; } #endif lockHatState_ = true; } void Joystick::UnlockHatState() { lockHatState_ = false; for (HatState &hatState : hatState_) hatState.didStateChange = false; } int Joystick::ToSdlJoyButton(ControllerButton button) { #if defined(JOY_BUTTON_A) || defined(JOY_BUTTON_B) || defined(JOY_BUTTON_X) || defined(JOY_BUTTON_Y) \ || defined(JOY_BUTTON_BACK) || defined(JOY_BUTTON_START) || defined(JOY_BUTTON_LEFTSTICK) || defined(JOY_BUTTON_RIGHTSTICK) \ || defined(JOY_BUTTON_LEFTSHOULDER) || defined(JOY_BUTTON_RIGHTSHOULDER) || defined(JOY_BUTTON_TRIGGERLEFT) || defined(JOY_BUTTON_TRIGGERRIGHT) \ || defined(JOY_BUTTON_DPAD_LEFT) || defined(JOY_BUTTON_DPAD_UP) || defined(JOY_BUTTON_DPAD_RIGHT) || defined(JOY_BUTTON_DPAD_DOWN) switch (button) { #ifdef JOY_BUTTON_A case ControllerButton_BUTTON_A: return JOY_BUTTON_A; #endif #ifdef JOY_BUTTON_B case ControllerButton_BUTTON_B: return JOY_BUTTON_B; #endif #ifdef JOY_BUTTON_X case ControllerButton_BUTTON_X: return JOY_BUTTON_X; #endif #ifdef JOY_BUTTON_Y case ControllerButton_BUTTON_Y: return JOY_BUTTON_Y; #endif #ifdef JOY_BUTTON_BACK case ControllerButton_BUTTON_BACK: return JOY_BUTTON_BACK; #endif #ifdef JOY_BUTTON_START case ControllerButton_BUTTON_START: return JOY_BUTTON_START; #endif #ifdef JOY_BUTTON_LEFTSTICK case ControllerButton_BUTTON_LEFTSTICK: return JOY_BUTTON_LEFTSTICK; #endif #ifdef JOY_BUTTON_RIGHTSTICK case ControllerButton_BUTTON_RIGHTSTICK: return JOY_BUTTON_RIGHTSTICK; #endif #ifdef JOY_BUTTON_LEFTSHOULDER case ControllerButton_BUTTON_LEFTSHOULDER: return JOY_BUTTON_LEFTSHOULDER; #endif #ifdef JOY_BUTTON_RIGHTSHOULDER case ControllerButton_BUTTON_RIGHTSHOULDER: return JOY_BUTTON_RIGHTSHOULDER; #endif #ifdef JOY_BUTTON_TRIGGERLEFT case ControllerButton_AXIS_TRIGGERLEFT: return JOY_BUTTON_TRIGGERLEFT; #endif #ifdef JOY_BUTTON_TRIGGERRIGHT case ControllerButton_AXIS_TRIGGERRIGHT: return JOY_BUTTON_TRIGGERRIGHT; #endif #ifdef JOY_BUTTON_DPAD_UP case ControllerButton_BUTTON_DPAD_UP: return JOY_BUTTON_DPAD_UP; #endif #ifdef JOY_BUTTON_DPAD_DOWN case ControllerButton_BUTTON_DPAD_DOWN: return JOY_BUTTON_DPAD_DOWN; #endif #ifdef JOY_BUTTON_DPAD_LEFT case ControllerButton_BUTTON_DPAD_LEFT: return JOY_BUTTON_DPAD_LEFT; #endif #ifdef JOY_BUTTON_DPAD_RIGHT case ControllerButton_BUTTON_DPAD_RIGHT: return JOY_BUTTON_DPAD_RIGHT; #endif default: return -1; } #else return -1; #endif } // NOLINTNEXTLINE(readability-convert-member-functions-to-static): Not static if joystick mappings are defined. bool Joystick::IsHatButtonPressed(ControllerButton button) const { #if (defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP)) || (defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN)) || (defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT)) || (defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT)) switch (button) { #if defined(JOY_HAT_DPAD_UP_HAT) && defined(JOY_HAT_DPAD_UP) case ControllerButton_BUTTON_DPAD_UP: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_UP_HAT) & JOY_HAT_DPAD_UP) != 0; #endif #if defined(JOY_HAT_DPAD_DOWN_HAT) && defined(JOY_HAT_DPAD_DOWN) case ControllerButton_BUTTON_DPAD_DOWN: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_DOWN_HAT) & JOY_HAT_DPAD_DOWN) != 0; #endif #if defined(JOY_HAT_DPAD_LEFT_HAT) && defined(JOY_HAT_DPAD_LEFT) case ControllerButton_BUTTON_DPAD_LEFT: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_LEFT_HAT) & JOY_HAT_DPAD_LEFT) != 0; #endif #if defined(JOY_HAT_DPAD_RIGHT_HAT) && defined(JOY_HAT_DPAD_RIGHT) case ControllerButton_BUTTON_DPAD_RIGHT: return (SDL_JoystickGetHat(sdl_joystick_, JOY_HAT_DPAD_RIGHT_HAT) & JOY_HAT_DPAD_RIGHT) != 0; #endif default: return false; } #else return false; #endif } bool Joystick::IsPressed(ControllerButton button) const { if (sdl_joystick_ == nullptr) return false; if (IsHatButtonPressed(button)) return true; const int joyButton = ToSdlJoyButton(button); if (joyButton == -1) return false; #ifdef USE_SDL3 const int numButtons = SDL_GetNumJoystickButtons(sdl_joystick_); return joyButton < numButtons && SDL_GetJoystickButton(sdl_joystick_, joyButton); #else const int numButtons = SDL_JoystickNumButtons(sdl_joystick_); return joyButton < numButtons && SDL_JoystickGetButton(sdl_joystick_, joyButton) != 0; #endif } bool Joystick::ProcessAxisMotion(const SDL_Event &event) { if (event.type != SDL_EVENT_JOYSTICK_AXIS_MOTION) return false; #if defined(JOY_AXIS_LEFTX) || defined(JOY_AXIS_LEFTY) || defined(JOY_AXIS_RIGHTX) || defined(JOY_AXIS_RIGHTY) switch (event.jaxis.axis) { #ifdef JOY_AXIS_LEFTX case JOY_AXIS_LEFTX: leftStickXUnscaled = event.jaxis.value; leftStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_LEFTY case JOY_AXIS_LEFTY: leftStickYUnscaled = -event.jaxis.value; leftStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_RIGHTX case JOY_AXIS_RIGHTX: rightStickXUnscaled = event.jaxis.value; rightStickNeedsScaling = true; return true; #endif #ifdef JOY_AXIS_RIGHTY case JOY_AXIS_RIGHTY: rightStickYUnscaled = -event.jaxis.value; rightStickNeedsScaling = true; return true; #endif default: return false; } #else return false; #endif } #ifdef USE_SDL3 void Joystick::Add(SDL_JoystickID id) { Joystick result; const char *name = SDL_GetJoystickNameForID(id); if (name == nullptr) { LogWarn("Error getting name for joystick {}", id, SDL_GetError()); SDL_ClearError(); } else { Log("Adding joystick {}: {}", id, name); } result.sdl_joystick_ = SDL_OpenJoystick(id); if (result.sdl_joystick_ == nullptr) { LogError("{}", SDL_GetError()); SDL_ClearError(); return; } result.instance_id_ = id; joysticks_.push_back(result); } #else void Joystick::Add(int deviceIndex) { if (SDL_NumJoysticks() <= deviceIndex) return; Joystick result; Log("Adding joystick {}: {}", deviceIndex, SDL_JoystickNameForIndex(deviceIndex)); result.sdl_joystick_ = SDL_JoystickOpen(deviceIndex); if (result.sdl_joystick_ == nullptr) { LogError("{}", SDL_GetError()); SDL_ClearError(); return; } #ifndef USE_SDL1 result.instance_id_ = SDL_JoystickInstanceID(result.sdl_joystick_); #endif joysticks_.push_back(result); } #endif void Joystick::Remove(SDL_JoystickID instanceId) { #ifndef USE_SDL1 Log("Removing joystick (instance id: {})", instanceId); for (std::size_t i = 0; i < joysticks_.size(); ++i) { const Joystick &joystick = joysticks_[i]; if (joystick.instance_id_ != instanceId) continue; joysticks_.erase(joysticks_.begin() + i); return; } Log("Joystick not found with instance id: {}", instanceId); #endif } const std::vector &Joystick::All() { return joysticks_; } Joystick *Joystick::Get(SDL_JoystickID instanceId) { for (auto &joystick : joysticks_) { if (joystick.instance_id_ == instanceId) return &joystick; } return nullptr; } Joystick *Joystick::Get(const SDL_Event &event) { switch (event.type) { #ifndef USE_SDL1 case SDL_EVENT_JOYSTICK_AXIS_MOTION: return Get(event.jaxis.which); case SDL_EVENT_JOYSTICK_BALL_MOTION: return Get(event.jball.which); case SDL_EVENT_JOYSTICK_HAT_MOTION: return Get(event.jhat.which); case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: return Get(event.jbutton.which); default: return nullptr; #else case SDL_JOYAXISMOTION: case SDL_JOYBALLMOTION: case SDL_JOYHATMOTION: case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: return joysticks_.empty() ? nullptr : &joysticks_[0]; default: return nullptr; #endif } } bool Joystick::IsPressedOnAnyJoystick(ControllerButton button) { for (auto &joystick : joysticks_) if (joystick.IsPressed(button)) return true; return false; } } // namespace devilution ================================================ FILE: Source/controls/devices/joystick.h ================================================ #pragma once // Joystick mappings for SDL1 and additional buttons on SDL2. #include #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/controller.h" #include "controls/controller_buttons.h" #include "utils/static_vector.hpp" namespace devilution { class Joystick { static std::vector joysticks_; public: #ifdef USE_SDL3 static void Add(SDL_JoystickID deviceIndex); #else static void Add(int deviceIndex); #endif static void Remove(SDL_JoystickID instanceId); static Joystick *Get(SDL_JoystickID instanceId); static Joystick *Get(const SDL_Event &event); static const std::vector &All(); static bool IsPressedOnAnyJoystick(ControllerButton button); // Must be called exactly once at the start of each SDL input event. void UnlockHatState(); static StaticVector ToControllerButtonEvents(const SDL_Event &event); bool IsPressed(ControllerButton button) const; static bool ProcessAxisMotion(const SDL_Event &event); SDL_JoystickID instance_id() const { return instance_id_; } private: struct HatState { bool pressed; bool didStateChange; }; static int ToSdlJoyButton(ControllerButton button); bool IsHatButtonPressed(ControllerButton button) const; StaticVector GetHatEvents(); void UpdateHatState(const SDL_JoyHatEvent &event); SDL_Joystick *sdl_joystick_ = NULL; SDL_JoystickID instance_id_ = -1; std::array hatState_; bool lockHatState_ = false; }; } // namespace devilution ================================================ FILE: Source/controls/devices/kbcontroller.cpp ================================================ #include "controls/devices/kbcontroller.h" #if HAS_KBCTRL == 1 #ifdef USE_SDL3 #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/controller_motion.h" #include "utils/sdl_compat.h" #include "utils/stubs.h" namespace devilution { ControllerButton KbCtrlToControllerButton(const SDL_Event &event) { switch (event.type) { #ifdef USE_SDL3 case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_DOWN: switch (event.key.key) #else case SDL_KEYDOWN: case SDL_KEYUP: switch (event.key.keysym.sym) #endif { #ifdef KBCTRL_IGNORE_1 case KBCTRL_IGNORE_1: return ControllerButton_IGNORE; #endif #ifdef KBCTRL_BUTTON_A case KBCTRL_BUTTON_A: return ControllerButton_BUTTON_A; #endif #ifdef KBCTRL_BUTTON_B case KBCTRL_BUTTON_B: // Right button return ControllerButton_BUTTON_B; #endif #ifdef KBCTRL_BUTTON_X case KBCTRL_BUTTON_X: // Left button return ControllerButton_BUTTON_X; #endif #ifdef KBCTRL_BUTTON_Y case KBCTRL_BUTTON_Y: return ControllerButton_BUTTON_Y; #endif #ifdef KBCTRL_BUTTON_LEFTSTICK case KBCTRL_BUTTON_LEFTSTICK: return ControllerButton_BUTTON_LEFTSTICK; #endif #ifdef KBCTRL_BUTTON_RIGHTSTICK case KBCTRL_BUTTON_RIGHTSTICK: return ControllerButton_BUTTON_RIGHTSTICK; #endif #ifdef KBCTRL_BUTTON_LEFTSHOULDER case KBCTRL_BUTTON_LEFTSHOULDER: return ControllerButton_BUTTON_LEFTSHOULDER; #endif #ifdef KBCTRL_BUTTON_RIGHTSHOULDER case KBCTRL_BUTTON_RIGHTSHOULDER: return ControllerButton_BUTTON_RIGHTSHOULDER; #endif #ifdef KBCTRL_BUTTON_TRIGGERLEFT case KBCTRL_BUTTON_TRIGGERLEFT: return ControllerButton_AXIS_TRIGGERLEFT; #endif #ifdef KBCTRL_BUTTON_TRIGGERRIGHT case KBCTRL_BUTTON_TRIGGERRIGHT: return ControllerButton_AXIS_TRIGGERRIGHT; #endif #ifdef KBCTRL_BUTTON_START case KBCTRL_BUTTON_START: return ControllerButton_BUTTON_START; #endif #ifdef KBCTRL_BUTTON_BACK case KBCTRL_BUTTON_BACK: return ControllerButton_BUTTON_BACK; #endif #ifdef KBCTRL_BUTTON_DPAD_UP case KBCTRL_BUTTON_DPAD_UP: return ControllerButton_BUTTON_DPAD_UP; #endif #ifdef KBCTRL_BUTTON_DPAD_DOWN case KBCTRL_BUTTON_DPAD_DOWN: return ControllerButton_BUTTON_DPAD_DOWN; #endif #ifdef KBCTRL_BUTTON_DPAD_LEFT case KBCTRL_BUTTON_DPAD_LEFT: return ControllerButton_BUTTON_DPAD_LEFT; #endif #ifdef KBCTRL_BUTTON_DPAD_RIGHT case KBCTRL_BUTTON_DPAD_RIGHT: return ControllerButton_BUTTON_DPAD_RIGHT; #endif default: return ControllerButton_NONE; } default: return ControllerButton_NONE; } } SDL_Keycode ControllerButtonToKbCtrlKeyCode(ControllerButton button) { switch (button) { #ifdef KBCTRL_BUTTON_A case ControllerButton_BUTTON_A: return KBCTRL_BUTTON_A; #endif #ifdef KBCTRL_BUTTON_B case ControllerButton_BUTTON_B: return KBCTRL_BUTTON_B; #endif #ifdef KBCTRL_BUTTON_X case ControllerButton_BUTTON_X: return KBCTRL_BUTTON_X; #endif #ifdef KBCTRL_BUTTON_Y case ControllerButton_BUTTON_Y: return KBCTRL_BUTTON_Y; #endif #ifdef KBCTRL_BUTTON_BACK case ControllerButton_BUTTON_BACK: return KBCTRL_BUTTON_BACK; #endif #ifdef KBCTRL_BUTTON_START case ControllerButton_BUTTON_START: return KBCTRL_BUTTON_START; #endif #ifdef KBCTRL_BUTTON_LEFTSTICK case ControllerButton_BUTTON_LEFTSTICK: return KBCTRL_BUTTON_LEFTSTICK; #endif #ifdef KBCTRL_BUTTON_RIGHTSTICK case ControllerButton_BUTTON_RIGHTSTICK: return KBCTRL_BUTTON_RIGHTSTICK; #endif #ifdef KBCTRL_BUTTON_LEFTSHOULDER case ControllerButton_BUTTON_LEFTSHOULDER: return KBCTRL_BUTTON_LEFTSHOULDER; #endif #ifdef KBCTRL_BUTTON_RIGHTSHOULDER case ControllerButton_BUTTON_RIGHTSHOULDER: return KBCTRL_BUTTON_RIGHTSHOULDER; #endif #ifdef KBCTRL_BUTTON_TRIGGERLEFT case ControllerButton_AXIS_TRIGGERLEFT: return KBCTRL_BUTTON_TRIGGERLEFT; #endif #ifdef KBCTRL_BUTTON_TRIGGERRIGHT case ControllerButton_AXIS_TRIGGERRIGHT: return KBCTRL_BUTTON_TRIGGERRIGHT; #endif #ifdef KBCTRL_BUTTON_DPAD_UP case ControllerButton_BUTTON_DPAD_UP: return KBCTRL_BUTTON_DPAD_UP; #endif #ifdef KBCTRL_BUTTON_DPAD_DOWN case ControllerButton_BUTTON_DPAD_DOWN: return KBCTRL_BUTTON_DPAD_DOWN; #endif #ifdef KBCTRL_BUTTON_DPAD_LEFT case ControllerButton_BUTTON_DPAD_LEFT: return KBCTRL_BUTTON_DPAD_LEFT; #endif #ifdef KBCTRL_BUTTON_DPAD_RIGHT case ControllerButton_BUTTON_DPAD_RIGHT: return KBCTRL_BUTTON_DPAD_RIGHT; #endif default: return SDLK_UNKNOWN; } } bool IsKbCtrlButtonPressed(ControllerButton button) { SDL_Keycode key_code = ControllerButtonToKbCtrlKeyCode(button); if (key_code == SDLK_UNKNOWN) return false; #ifdef USE_SDL3 return SDL_GetKeyboardState(nullptr)[SDL_GetScancodeFromKey(key_code, nullptr)]; #elif !defined(USE_SDL1) return SDL_GetKeyboardState(nullptr)[SDL_GetScancodeFromKey(key_code)]; #else return SDL_GetKeyState(nullptr)[key_code]; #endif } } // namespace devilution #endif ================================================ FILE: Source/controls/devices/kbcontroller.h ================================================ #pragma once // Keyboard keys acting like gamepad buttons #ifndef HAS_KBCTRL #define HAS_KBCTRL 0 #endif #if HAS_KBCTRL == 1 #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/controller_buttons.h" namespace devilution { ControllerButton KbCtrlToControllerButton(const SDL_Event &event); SDL_Keycode ControllerButtonToKbCtrlKeyCode(ControllerButton button); bool IsKbCtrlButtonPressed(ControllerButton button); } // namespace devilution #endif ================================================ FILE: Source/controls/game_controls.cpp ================================================ #include "controls/game_controls.h" #include #ifdef USE_SDL3 #include #else #include #endif #include "controls/control_mode.hpp" #include "controls/controller_motion.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" #include "controls/padmapper.hpp" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "doom.h" #include "gamemenu.h" #include "gmenu.h" #include "options.h" #include "panels/spell_list.hpp" #include "qol/stash.h" #include "stores.h" #include "utils/is_of.hpp" namespace devilution { bool PadMenuNavigatorActive = false; bool PadHotspellMenuActive = false; ControllerButton SuppressedButton = ControllerButton_NONE; namespace { SDL_Keycode TranslateControllerButtonToGameMenuKey(ControllerButton controllerButton) { switch (TranslateTo(GamepadType, controllerButton)) { case ControllerButton_BUTTON_A: case ControllerButton_BUTTON_Y: return SDLK_RETURN; case ControllerButton_BUTTON_B: case ControllerButton_BUTTON_BACK: case ControllerButton_BUTTON_START: return SDLK_ESCAPE; case ControllerButton_BUTTON_LEFTSTICK: return SDLK_TAB; // Map default: return SDLK_UNKNOWN; } } SDL_Keycode TranslateControllerButtonToMenuKey(ControllerButton controllerButton) { switch (TranslateTo(GamepadType, controllerButton)) { case ControllerButton_BUTTON_A: return SDLK_SPACE; case ControllerButton_BUTTON_B: case ControllerButton_BUTTON_BACK: case ControllerButton_BUTTON_START: return SDLK_ESCAPE; case ControllerButton_BUTTON_Y: return SDLK_RETURN; case ControllerButton_BUTTON_LEFTSTICK: return SDLK_TAB; // Map case ControllerButton_BUTTON_DPAD_LEFT: return SDLK_LEFT; case ControllerButton_BUTTON_DPAD_RIGHT: return SDLK_RIGHT; case ControllerButton_BUTTON_DPAD_UP: return SDLK_UP; case ControllerButton_BUTTON_DPAD_DOWN: return SDLK_DOWN; default: return SDLK_UNKNOWN; } } SDL_Keycode TranslateControllerButtonToQuestLogKey(ControllerButton controllerButton) { switch (TranslateTo(GamepadType, controllerButton)) { case ControllerButton_BUTTON_A: case ControllerButton_BUTTON_Y: return SDLK_RETURN; case ControllerButton_BUTTON_B: return SDLK_SPACE; case ControllerButton_BUTTON_LEFTSTICK: return SDLK_TAB; // Map default: return SDLK_UNKNOWN; } } bool GetGameAction(const SDL_Event &event, ControllerButtonEvent ctrlEvent, GameAction *action) { const bool inGameMenu = InGameMenu(); #ifndef USE_SDL1 if (ControlMode == ControlTypes::VirtualGamepad) { switch (event.type) { #ifdef USE_SDL3 case SDL_EVENT_FINGER_DOWN: #else case SDL_FINGERDOWN: #endif if (VirtualGamepadState.menuPanel.charButton.isHeld && VirtualGamepadState.menuPanel.charButton.didStateChange) { *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); return true; } if (VirtualGamepadState.menuPanel.questsButton.isHeld && VirtualGamepadState.menuPanel.questsButton.didStateChange) { *action = GameAction(GameActionType_TOGGLE_QUEST_LOG); return true; } if (VirtualGamepadState.menuPanel.inventoryButton.isHeld && VirtualGamepadState.menuPanel.inventoryButton.didStateChange) { *action = GameAction(GameActionType_TOGGLE_INVENTORY); return true; } if (VirtualGamepadState.menuPanel.mapButton.isHeld && VirtualGamepadState.menuPanel.mapButton.didStateChange) { *action = GameActionSendKey { SDLK_TAB, false }; return true; } if (VirtualGamepadState.primaryActionButton.isHeld && VirtualGamepadState.primaryActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !SpellbookFlag) { *action = GameAction(GameActionType_PRIMARY_ACTION); if (ControllerActionHeld == GameActionType_NONE) { ControllerActionHeld = GameActionType_PRIMARY_ACTION; } } else if (sgpCurrentMenu != nullptr || IsPlayerInStore() || QuestLogIsOpen) { *action = GameActionSendKey { SDLK_RETURN, false }; } else { *action = GameActionSendKey { SDLK_SPACE, false }; } return true; } if (VirtualGamepadState.secondaryActionButton.isHeld && VirtualGamepadState.secondaryActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !SpellbookFlag) { *action = GameAction(GameActionType_SECONDARY_ACTION); if (ControllerActionHeld == GameActionType_NONE) ControllerActionHeld = GameActionType_SECONDARY_ACTION; } return true; } if (VirtualGamepadState.spellActionButton.isHeld && VirtualGamepadState.spellActionButton.didStateChange) { if (!inGameMenu && !QuestLogIsOpen && !SpellbookFlag) { *action = GameAction(GameActionType_CAST_SPELL); if (ControllerActionHeld == GameActionType_NONE) ControllerActionHeld = GameActionType_CAST_SPELL; } return true; } if (VirtualGamepadState.cancelButton.isHeld && VirtualGamepadState.cancelButton.didStateChange) { if (inGameMenu || DoomFlag || SpellSelectFlag) *action = GameActionSendKey { SDLK_ESCAPE, false }; else if (invflag) *action = GameAction(GameActionType_TOGGLE_INVENTORY); else if (SpellbookFlag) *action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); else if (QuestLogIsOpen) *action = GameAction(GameActionType_TOGGLE_QUEST_LOG); else if (CharFlag) *action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); return true; } if (VirtualGamepadState.healthButton.isHeld && VirtualGamepadState.healthButton.didStateChange) { if (!QuestLogIsOpen && !SpellbookFlag && !IsPlayerInStore()) *action = GameAction(GameActionType_USE_HEALTH_POTION); return true; } if (VirtualGamepadState.manaButton.isHeld && VirtualGamepadState.manaButton.didStateChange) { if (!QuestLogIsOpen && !SpellbookFlag && !IsPlayerInStore()) *action = GameAction(GameActionType_USE_MANA_POTION); return true; } break; #ifdef USE_SDL3 case SDL_EVENT_FINGER_UP: #else case SDL_FINGERUP: #endif if ((!VirtualGamepadState.primaryActionButton.isHeld && ControllerActionHeld == GameActionType_PRIMARY_ACTION) || (!VirtualGamepadState.secondaryActionButton.isHeld && ControllerActionHeld == GameActionType_SECONDARY_ACTION) || (!VirtualGamepadState.spellActionButton.isHeld && ControllerActionHeld == GameActionType_CAST_SPELL)) { ControllerActionHeld = GameActionType_NONE; LastPlayerAction = PlayerActionType::None; } break; } } #endif if (PadMenuNavigatorActive || PadHotspellMenuActive) return false; SDL_Keycode translation = SDLK_UNKNOWN; if (gmenu_is_active() || IsPlayerInStore()) translation = TranslateControllerButtonToGameMenuKey(ctrlEvent.button); else if (inGameMenu) translation = TranslateControllerButtonToMenuKey(ctrlEvent.button); else if (QuestLogIsOpen) translation = TranslateControllerButtonToQuestLogKey(ctrlEvent.button); if (translation != SDLK_UNKNOWN) { *action = GameActionSendKey { static_cast(translation), ctrlEvent.up }; return true; } return false; } bool CanDeferToMovementHandler(const PadmapperOptions::Action &action) { if (action.boundInput.modifier != ControllerButton_NONE) return false; if (SpellSelectFlag) { const std::string_view prefix { "QuickSpell" }; const std::string_view key { action.key }; if (key.size() >= prefix.size()) { const std::string_view truncated { key.data(), prefix.size() }; if (truncated == prefix) return false; } } return IsAnyOf(action.boundInput.button, ControllerButton_BUTTON_DPAD_UP, ControllerButton_BUTTON_DPAD_DOWN, ControllerButton_BUTTON_DPAD_LEFT, ControllerButton_BUTTON_DPAD_RIGHT); } void PressControllerButton(ControllerButton button) { if (IsStashOpen) { switch (button) { case ControllerButton_BUTTON_BACK: StartGoldWithdraw(); return; case ControllerButton_BUTTON_LEFTSHOULDER: Stash.PreviousPage(); return; case ControllerButton_BUTTON_RIGHTSHOULDER: Stash.NextPage(); return; default: break; } } if (PadHotspellMenuActive) { auto quickSpellAction = [](size_t slot) { if (SpellSelectFlag) { SetSpeedSpell(slot); return; } if (!*GetOptions().Gameplay.quickCast) ToggleSpell(slot); else QuickCast(slot); }; switch (button) { case devilution::ControllerButton_BUTTON_A: quickSpellAction(2); return; case devilution::ControllerButton_BUTTON_B: quickSpellAction(3); return; case devilution::ControllerButton_BUTTON_X: quickSpellAction(0); return; case devilution::ControllerButton_BUTTON_Y: quickSpellAction(1); return; default: break; } } if (PadMenuNavigatorActive) { switch (button) { case devilution::ControllerButton_BUTTON_DPAD_UP: PressEscKey(); LastPlayerAction = PlayerActionType::None; PadHotspellMenuActive = false; PadMenuNavigatorActive = false; gamemenu_on(); return; case devilution::ControllerButton_BUTTON_DPAD_DOWN: CycleAutomapType(); return; case devilution::ControllerButton_BUTTON_DPAD_LEFT: ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); return; case devilution::ControllerButton_BUTTON_DPAD_RIGHT: ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); return; case devilution::ControllerButton_BUTTON_A: ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); return; case devilution::ControllerButton_BUTTON_B: return; case devilution::ControllerButton_BUTTON_X: ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); return; case devilution::ControllerButton_BUTTON_Y: #ifdef __3DS__ GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); CalcViewportGeometry(); #endif return; default: break; } } const PadmapperOptions::Action *action = GetOptions().Padmapper.findAction(button, IsControllerButtonPressed); if (action == nullptr) return; if (IsMovementHandlerActive() && CanDeferToMovementHandler(*action)) return; PadmapperPress(button, *action); } } // namespace ControllerButton TranslateTo(GamepadLayout layout, ControllerButton button) { if (layout != GamepadLayout::Nintendo) return button; switch (button) { case ControllerButton_BUTTON_A: return ControllerButton_BUTTON_B; case ControllerButton_BUTTON_B: return ControllerButton_BUTTON_A; case ControllerButton_BUTTON_X: return ControllerButton_BUTTON_Y; case ControllerButton_BUTTON_Y: return ControllerButton_BUTTON_X; default: return button; } } bool SkipsMovie(ControllerButtonEvent ctrlEvent) { return IsAnyOf(ctrlEvent.button, ControllerButton_BUTTON_A, ControllerButton_BUTTON_B, ControllerButton_BUTTON_START, ControllerButton_BUTTON_BACK); } bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent) { if (ctrlEvent.button == ControllerButton_NONE) return false; if (!ctrlEvent.up && ctrlEvent.button == SuppressedButton) return false; const std::string_view actionName = PadmapperActionNameTriggeredByButtonEvent(ctrlEvent); return IsAnyOf(actionName, "LeftMouseClick1", "LeftMouseClick2", "RightMouseClick1", "RightMouseClick2"); } AxisDirection GetMoveDirection() { return GetLeftStickOrDpadDirection(true); } bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonEvent ctrlEvent, GameAction &action) { if (ctrlEvent.button == ControllerButton_IGNORE) { return false; } struct ButtonReleaser { ~ButtonReleaser() { if (ctrlEvent.up) PadmapperRelease(ctrlEvent.button, /*invokeAction=*/false); } ControllerButtonEvent ctrlEvent; }; const ButtonReleaser buttonReleaser { ctrlEvent }; const bool isGamepadMotion = IsControllerMotion(event); if (!isGamepadMotion) { SimulateRightStickWithPadmapper(ctrlEvent); } DetectInputMethod(event, ctrlEvent); if (isGamepadMotion) { return true; } if (ctrlEvent.button != ControllerButton_NONE && ctrlEvent.button == SuppressedButton) { if (!ctrlEvent.up) return true; SuppressedButton = ControllerButton_NONE; } if (ctrlEvent.up && !PadmapperActionNameTriggeredByButtonEvent(ctrlEvent).empty()) { // Button press may have brought up a menu; // don't confuse release of that button with intent to interact with the menu PadmapperRelease(ctrlEvent.button, /*invokeAction=*/true); return true; } else if (GetGameAction(event, ctrlEvent, &action)) { ProcessGameAction(action); return true; } else if (ctrlEvent.button != ControllerButton_NONE) { if (!ctrlEvent.up) PressControllerButton(ctrlEvent.button); return true; } return false; } } // namespace devilution ================================================ FILE: Source/controls/game_controls.h ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif #include "controls/axis_direction.h" #include "controls/controller.h" namespace devilution { enum GameActionType : uint8_t { GameActionType_NONE, GameActionType_USE_HEALTH_POTION, GameActionType_USE_MANA_POTION, GameActionType_PRIMARY_ACTION, // Talk to towners, click on inv items, attack, etc. GameActionType_SECONDARY_ACTION, // Open chests, doors, pickup items. GameActionType_CAST_SPELL, GameActionType_TOGGLE_INVENTORY, GameActionType_TOGGLE_CHARACTER_INFO, GameActionType_TOGGLE_QUICK_SPELL_MENU, GameActionType_TOGGLE_SPELL_BOOK, GameActionType_TOGGLE_QUEST_LOG, GameActionType_SEND_KEY, }; struct GameActionSendKey { uint32_t vk_code; bool up; }; struct GameAction { GameActionType type; GameAction() : type(GameActionType_NONE) { } explicit GameAction(GameActionType type) : type(type) { } GameAction(GameActionSendKey send_key) : type(GameActionType_SEND_KEY) , send_key(send_key) { } union { GameActionSendKey send_key; }; }; ControllerButton TranslateTo(GamepadLayout layout, ControllerButton button); bool SkipsMovie(ControllerButtonEvent ctrlEvent); bool IsSimulatedMouseClickBinding(ControllerButtonEvent ctrlEvent); AxisDirection GetMoveDirection(); bool HandleControllerButtonEvent(const SDL_Event &event, const ControllerButtonEvent ctrlEvent, GameAction &action); extern bool PadMenuNavigatorActive; extern bool PadHotspellMenuActive; // Tracks the button most recently used as a modifier for another button. // // If two buttons are pressed simultaneously, SDL sends two events for which both buttons are in the pressed state. // The event processor may interpret the second event's button as a modifier for the action taken when processing the first event. // The code for the modifier will be stored here, and the event processor can check this value when processing the second event to suppress it. extern ControllerButton SuppressedButton; } // namespace devilution ================================================ FILE: Source/controls/input.h ================================================ #pragma once #ifdef USE_SDL3 #include #else #include #endif #include "controls/controller.h" #include "controls/controller_motion.h" namespace devilution { inline bool PollEvent(SDL_Event *event) { #ifdef USE_SDL3 const bool result = SDL_PollEvent(event); #else const bool result = SDL_PollEvent(event) != 0; #endif if (result) { UnlockControllerState(*event); ProcessControllerMotion(*event); } return result; } } // namespace devilution ================================================ FILE: Source/controls/keymapper.cpp ================================================ #include "controls/keymapper.hpp" #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "control/control.hpp" #include "options.h" #include "utils/is_of.hpp" namespace devilution { namespace { bool IsTextEntryKey(SDL_Keycode vkey) { return IsAnyOf(vkey, SDLK_ESCAPE, SDLK_RETURN, SDLK_KP_ENTER, SDLK_BACKSPACE, SDLK_DOWN, SDLK_UP) #ifdef USE_SDL3 || (vkey >= SDLK_SPACE && vkey <= SDLK_Z); #else || (vkey >= SDLK_SPACE && vkey <= SDLK_z); #endif } bool IsNumberEntryKey(SDL_Keycode vkey) { return ((vkey >= SDLK_0 && vkey <= SDLK_9) || vkey == SDLK_BACKSPACE); } SDL_Keycode ToAsciiUpper(SDL_Keycode key) { if ( #ifdef USE_SDL3 key >= SDLK_A && key <= SDLK_Z #else key >= SDLK_a && key <= SDLK_z #endif ) { return static_cast(static_cast(key) - ('a' - 'A')); } return key; } } // namespace void KeymapperPress(SDL_Keycode key) { key = ToAsciiUpper(key); const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(key)); if (action == nullptr || !action->actionPressed || !action->isEnabled()) return; // TODO: This should be handled outside of the keymapper. if (ChatFlag) return; action->actionPressed(); } void KeymapperRelease(SDL_Keycode key) { key = ToAsciiUpper(key); const KeymapperOptions::Action *action = GetOptions().Keymapper.findAction(static_cast(key)); if (action == nullptr || !action->actionReleased || !action->isEnabled()) return; // TODO: This should be handled outside of the keymapper. if ((ChatFlag && IsTextEntryKey(key)) || (DropGoldFlag && IsNumberEntryKey(key))) return; action->actionReleased(); } } // namespace devilution ================================================ FILE: Source/controls/keymapper.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif namespace devilution { void KeymapperPress(SDL_Keycode key); void KeymapperRelease(SDL_Keycode key); } // namespace devilution ================================================ FILE: Source/controls/menu_controls.cpp ================================================ #include "controls/menu_controls.h" #ifdef USE_SDL3 #include #include #include #else #include #endif #include "DiabloUI/diabloui.h" #include "controls/axis_direction.h" #include "controls/control_mode.hpp" #include "controls/controller.h" #include "controls/controller_motion.h" #include "controls/plrctrls.h" #include "controls/remap_keyboard.h" #include "utils/sdl_compat.h" namespace devilution { MenuAction GetMenuHeldUpDownAction() { static AxisDirectionRepeater repeater; const AxisDirection dir = repeater.Get(GetLeftStickOrDpadDirection(false)); switch (dir.y) { case AxisDirectionY_UP: return MenuAction_UP; case AxisDirectionY_DOWN: return MenuAction_DOWN; default: return MenuAction_NONE; } } std::vector GetMenuActions(const SDL_Event &event) { std::vector menuActions; for (const ControllerButtonEvent ctrlEvent : ToControllerButtonEvents(event)) { if (ctrlEvent.button == ControllerButton_IGNORE) { continue; } const bool isGamepadMotion = IsControllerMotion(event); DetectInputMethod(event, ctrlEvent); if (isGamepadMotion) { menuActions.push_back(GetMenuHeldUpDownAction()); continue; } if (!ctrlEvent.up) { switch (TranslateTo(GamepadType, ctrlEvent.button)) { case ControllerButton_BUTTON_A: case ControllerButton_BUTTON_START: menuActions.push_back(MenuAction_SELECT); break; case ControllerButton_BUTTON_BACK: case ControllerButton_BUTTON_B: menuActions.push_back(MenuAction_BACK); break; case ControllerButton_BUTTON_X: menuActions.push_back(MenuAction_DELETE); break; case ControllerButton_BUTTON_DPAD_UP: case ControllerButton_BUTTON_DPAD_DOWN: menuActions.push_back(GetMenuHeldUpDownAction()); break; case ControllerButton_BUTTON_DPAD_LEFT: menuActions.push_back(MenuAction_LEFT); break; case ControllerButton_BUTTON_DPAD_RIGHT: menuActions.push_back(MenuAction_RIGHT); break; case ControllerButton_BUTTON_LEFTSHOULDER: menuActions.push_back(MenuAction_PAGE_UP); break; case ControllerButton_BUTTON_RIGHTSHOULDER: menuActions.push_back(MenuAction_PAGE_DOWN); break; default: break; } } } if (!menuActions.empty()) { return menuActions; } if (event.type == #ifdef USE_SDL3 SDL_EVENT_MOUSE_BUTTON_DOWN #else SDL_MOUSEBUTTONDOWN #endif ) { switch (event.button.button) { case SDL_BUTTON_X1: #if !SDL_VERSION_ATLEAST(2, 0, 0) case 8: #endif return { MenuAction_BACK }; } } #if HAS_KBCTRL == 0 if (event.type == SDL_EVENT_KEY_DOWN) { SDL_Keycode sym = SDLC_EventKey(event); remap_keyboard_key(&sym); switch (sym) { case SDLK_UP: return { MenuAction_UP }; case SDLK_DOWN: return { MenuAction_DOWN }; case SDLK_TAB: if ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { return { MenuAction_UP }; } return { MenuAction_DOWN }; case SDLK_PAGEUP: return { MenuAction_PAGE_UP }; case SDLK_PAGEDOWN: return { MenuAction_PAGE_DOWN }; case SDLK_RETURN: if ((SDL_GetModState() & SDL_KMOD_ALT) == 0) { return { MenuAction_SELECT }; } break; case SDLK_KP_ENTER: return { MenuAction_SELECT }; case SDLK_SPACE: if (!IsTextInputActive()) { return { MenuAction_SELECT }; } break; case SDLK_DELETE: if (!IsTextInputActive()) { return { MenuAction_DELETE }; } break; case SDLK_LEFT: if (!IsTextInputActive()) { return { MenuAction_LEFT }; } break; case SDLK_RIGHT: if (!IsTextInputActive()) { return { MenuAction_RIGHT }; } break; case SDLK_ESCAPE: return { MenuAction_BACK }; default: break; } } #endif return {}; } // namespace devilution } // namespace devilution ================================================ FILE: Source/controls/menu_controls.h ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { enum MenuAction : uint8_t { MenuAction_NONE, MenuAction_SELECT, MenuAction_BACK, MenuAction_DELETE, MenuAction_UP, MenuAction_DOWN, MenuAction_LEFT, MenuAction_RIGHT, MenuAction_PAGE_UP, MenuAction_PAGE_DOWN, }; std::vector GetMenuActions(const SDL_Event &event); /** Menu action from holding the left stick or DPad. */ MenuAction GetMenuHeldUpDownAction(); } // namespace devilution ================================================ FILE: Source/controls/modifier_hints.cpp ================================================ #include "controls/modifier_hints.h" #include #include #include "DiabloUI/ui_flags.hpp" #include "control/control.hpp" #include "controls/controller_motion.h" #include "controls/game_controls.h" #include "controls/plrctrls.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "options.h" #include "panels/spell_book.hpp" #include "panels/spell_icons.hpp" #include "utils/language.h" namespace devilution { namespace { /** Vertical distance between text lines. */ constexpr int LineHeight = 25; /** Horizontal margin of the hints circle from panel edge. */ constexpr int CircleMarginX = 16; /** Distance between the panel top and the circle top. */ constexpr int CircleTop = 101; /** Spell icon side size. */ constexpr int IconSize = 37; constexpr int HintBoxSize = 39; constexpr int HintBoxMargin = 5; OptionalOwnedClxSpriteList hintBox; OptionalOwnedClxSpriteList hintBoxBackground; OptionalOwnedClxSpriteList hintIcons; enum HintIcon : uint8_t { IconChar, IconInv, IconQuests, IconSpells, IconMap, IconMenu, IconNull }; struct CircleMenuHint { CircleMenuHint(HintIcon top, HintIcon right, HintIcon bottom, HintIcon left) : top(top) , right(right) , bottom(bottom) , left(left) { } HintIcon top; HintIcon right; HintIcon bottom; HintIcon left; }; /** * @brief Draws hint text for a four button layout with the top/left edge of the bounding box at the position given by origin. * @param out The output buffer to draw on. * @param hint Struct describing the icon to draw. * @param origin Top left corner of the layout (relative to the output buffer). */ void DrawCircleMenuHint(const Surface &out, const CircleMenuHint &hint, const Point &origin) { const Displacement backgroundDisplacement = { (HintBoxSize - IconSize) / 2 + 1, (HintBoxSize - IconSize) / 2 - 1 }; const Point hintBoxPositions[4] = { origin + Displacement { 0, LineHeight - HintBoxSize }, origin + Displacement { HintBoxSize + HintBoxMargin, LineHeight - HintBoxSize * 2 - HintBoxMargin }, origin + Displacement { HintBoxSize + HintBoxMargin, LineHeight + HintBoxMargin }, origin + Displacement { HintBoxSize * 2 + HintBoxMargin * 2, LineHeight - HintBoxSize } }; const Point iconPositions[4] = { hintBoxPositions[0] + backgroundDisplacement, hintBoxPositions[1] + backgroundDisplacement, hintBoxPositions[2] + backgroundDisplacement, hintBoxPositions[3] + backgroundDisplacement, }; const uint8_t iconIndices[4] { hint.left, hint.top, hint.bottom, hint.right }; for (int slot = 0; slot < 4; ++slot) { if (iconIndices[slot] == HintIcon::IconNull) continue; RenderClxSprite(out, (*hintBoxBackground)[0], iconPositions[slot]); RenderClxSprite(out.subregion(iconPositions[slot].x, iconPositions[slot].y, 37, 38), (*hintIcons)[iconIndices[slot]], { 0, 0 }); RenderClxSprite(out, (*hintBox)[0], hintBoxPositions[slot]); } } /** * @brief Draws hint text for a four button layout with the top/left edge of the bounding box at the position given by origin plus the icon for the spell mapped to that entry. * @param out The output buffer to draw on. * @param origin Top left corner of the layout (relative to the output buffer). */ void DrawSpellsCircleMenuHint(const Surface &out, const Point &origin) { const Player &myPlayer = *MyPlayer; const Displacement spellIconDisplacement = { (HintBoxSize - IconSize) / 2 + 1, HintBoxSize - (HintBoxSize - IconSize) / 2 - 1 }; const Point hintBoxPositions[4] = { origin + Displacement { 0, LineHeight - HintBoxSize }, origin + Displacement { HintBoxSize + HintBoxMargin, LineHeight - HintBoxSize * 2 - HintBoxMargin }, origin + Displacement { HintBoxSize + HintBoxMargin, LineHeight + HintBoxMargin }, origin + Displacement { HintBoxSize * 2 + HintBoxMargin * 2, LineHeight - HintBoxSize } }; const Point spellIconPositions[4] = { hintBoxPositions[0] + spellIconDisplacement, hintBoxPositions[1] + spellIconDisplacement, hintBoxPositions[2] + spellIconDisplacement, hintBoxPositions[3] + spellIconDisplacement, }; const uint64_t spells = myPlayer._pAblSpells | myPlayer._pMemSpells | myPlayer._pScrlSpells | myPlayer._pISpells; SpellID splId; SpellType splType; for (int slot = 0; slot < 4; ++slot) { splId = myPlayer._pSplHotKey[slot]; if (IsValidSpell(splId) && (spells & GetSpellBitmask(splId)) != 0) splType = (leveltype == DTYPE_TOWN && !GetSpellData(splId).isAllowedInTown()) ? SpellType::Invalid : myPlayer._pSplTHotKey[slot]; else { splType = SpellType::Invalid; splId = SpellID::Null; } SetSpellTrans(splType); DrawSmallSpellIcon(out, spellIconPositions[slot], splId); RenderClxSprite(out, (*hintBox)[0], hintBoxPositions[slot]); } } void DrawGamepadMenuNavigator(const Surface &out) { if (!PadMenuNavigatorActive || SimulatingMouseWithPadmapper) return; static const CircleMenuHint DPad(/*top=*/HintIcon::IconMenu, /*right=*/HintIcon::IconInv, /*bottom=*/HintIcon::IconMap, /*left=*/HintIcon::IconChar); static const CircleMenuHint Buttons(/*top=*/HintIcon::IconNull, /*right=*/HintIcon::IconNull, /*bottom=*/HintIcon::IconSpells, /*left=*/HintIcon::IconQuests); const Rectangle &mainPanel = GetMainPanel(); DrawCircleMenuHint(out, DPad, { mainPanel.position.x + CircleMarginX, mainPanel.position.y - CircleTop }); DrawCircleMenuHint(out, Buttons, { mainPanel.position.x + mainPanel.size.width - HintBoxSize * 3 - CircleMarginX - HintBoxMargin * 2, mainPanel.position.y - CircleTop }); } void DrawGamepadHotspellMenu(const Surface &out) { if (!PadHotspellMenuActive || SimulatingMouseWithPadmapper) return; const Rectangle &mainPanel = GetMainPanel(); DrawSpellsCircleMenuHint(out, { mainPanel.position.x + mainPanel.size.width - HintBoxSize * 3 - CircleMarginX - HintBoxMargin * 2, mainPanel.position.y - CircleTop }); } } // namespace void InitModifierHints() { hintBox = LoadClx("data\\hintbox.clx"); hintBoxBackground = LoadClx("data\\hintboxbackground.clx"); hintIcons = LoadClx("data\\hinticons.clx"); } void FreeModifierHints() { hintIcons = std::nullopt; hintBoxBackground = std::nullopt; hintBox = std::nullopt; } void DrawControllerModifierHints(const Surface &out) { DrawGamepadMenuNavigator(out); DrawGamepadHotspellMenu(out); } } // namespace devilution ================================================ FILE: Source/controls/modifier_hints.h ================================================ #pragma once #include "engine/surface.hpp" namespace devilution { void DrawControllerModifierHints(const Surface &out); void InitModifierHints(); void FreeModifierHints(); } // namespace devilution ================================================ FILE: Source/controls/padmapper.cpp ================================================ #include "controls/padmapper.hpp" #include #include "controller.h" #include "game_controls.h" #include "options.h" namespace devilution { namespace { std::array::value> ButtonToReleaseAction; } // namespace void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action) { if (action.actionPressed) action.actionPressed(); SuppressedButton = action.boundInput.modifier; ButtonToReleaseAction[static_cast(button)] = &action; } void PadmapperRelease(ControllerButton button, bool invokeAction) { if (invokeAction) { const PadmapperOptions::Action *action = ButtonToReleaseAction[static_cast(button)]; if (action == nullptr) return; // Ignore unmapped buttons. // Check that the action can be triggered. if (action->actionReleased && action->isEnabled()) action->actionReleased(); } ButtonToReleaseAction[static_cast(button)] = nullptr; } bool PadmapperIsActionActive(std::string_view actionName) { for (const PadmapperOptions::Action &action : GetOptions().Padmapper.actions) { if (action.key != actionName) continue; const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast(action.boundInput.button)]; return releaseAction != nullptr && releaseAction->key == actionName; } return false; } void PadmapperReleaseAllActiveButtons() { for (const PadmapperOptions::Action *action : ButtonToReleaseAction) { if (action != nullptr) { PadmapperRelease(action->boundInput.button, /*invokeAction=*/true); } } } std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent) { if (!ctrlEvent.up) { const PadmapperOptions::Action *pressAction = GetOptions().Padmapper.findAction(ctrlEvent.button, IsControllerButtonPressed); if (pressAction == nullptr) return {}; return pressAction->key; } const PadmapperOptions::Action *releaseAction = ButtonToReleaseAction[static_cast(ctrlEvent.button)]; if (releaseAction == nullptr) return {}; return releaseAction->key; } } // namespace devilution ================================================ FILE: Source/controls/padmapper.hpp ================================================ #pragma once #include #include "controls/controller_buttons.h" #include "options.h" namespace devilution { void PadmapperPress(ControllerButton button, const PadmapperOptions::Action &action); void PadmapperRelease(ControllerButton button, bool invokeAction); void PadmapperReleaseAllActiveButtons(); [[nodiscard]] bool PadmapperIsActionActive(std::string_view actionName); [[nodiscard]] std::string_view PadmapperActionNameTriggeredByButtonEvent(ControllerButtonEvent ctrlEvent); } // namespace devilution ================================================ FILE: Source/controls/plrctrls.cpp ================================================ #include "controls/plrctrls.h" #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "automap.h" #include "control/control.hpp" #include "controls/controller_motion.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/control_mode.hpp" #include "controls/game_controls.h" #include "controls/touch/gamepad.h" #include "cursor.h" #include "doom.h" #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "game_mode.hpp" #include "gmenu.h" #include "help.h" #include "hwcursor.hpp" #include "inv.h" #include "items.h" #include "levels/tile_properties.hpp" #include "levels/town.h" #include "levels/trigs.h" #include "minitext.h" #include "missiles.h" #include "panels/spell_icons.hpp" #include "panels/spell_list.hpp" #include "panels/ui_panels.hpp" #include "qol/chatlog.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" #include "track.h" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" namespace devilution { GameActionType ControllerActionHeld = GameActionType_NONE; bool StandToggle = false; int pcurstrig = -1; Missile *pcursmissile = nullptr; quest_id pcursquest = Q_INVALID; /** * Native game menu, controlled by simulating a keyboard. */ bool InGameMenu() { return IsPlayerInStore() || HelpFlag || ChatLogFlag || ChatFlag || qtextflag || gmenu_is_active() || PauseMode == 2 || (MyPlayer != nullptr && MyPlayer->_pInvincible && MyPlayer->hasNoLife()); } namespace { int Slot = SLOTXY_INV_FIRST; Point ActiveStashSlot = InvalidStashPoint; int PreviousInventoryColumn = -1; bool BeltReturnsToStash = false; const Direction FaceDir[3][3] = { // NONE UP DOWN { Direction::South, Direction::North, Direction::South }, // NONE { Direction::West, Direction::NorthWest, Direction::SouthWest }, // LEFT { Direction::East, Direction::NorthEast, Direction::SouthEast }, // RIGHT }; /** * Number of angles to turn to face the coordinate * @param destination Tile coordinates * @return -1 == down */ int GetRotaryDistance(Point destination) { const Player &myPlayer = *MyPlayer; if (myPlayer.position.future == destination) return -1; const int d1 = static_cast(myPlayer._pdir); const int d2 = static_cast(GetDirection(myPlayer.position.future, destination)); const int d = std::abs(d1 - d2); if (d > 4) return 4 - (d % 4); return d; } /** * @brief Get the best case walking steps to coordinates * @param position Tile coordinates */ int GetMinDistance(Point position) { return MyPlayer->position.future.WalkingDistance(position); } /** * @brief Get walking steps to coordinate * @param destination Tile coordinates * @param maxDistance the max number of steps to search * @return number of steps, or 0 if not reachable */ int GetDistance(Point destination, int maxDistance) { if (GetMinDistance(destination) > maxDistance) { return 0; } int8_t walkpath[MaxPathLengthPlayer]; Player &myPlayer = *MyPlayer; const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, myPlayer.position.future, destination, walkpath, std::min(maxDistance, MaxPathLengthPlayer)); if (steps > maxDistance) return 0; return steps; } /** * @brief Get distance to coordinate * @param destination Tile coordinates */ int GetDistanceRanged(Point destination) { return MyPlayer->position.future.ExactDistance(destination); } void FindItemOrObject() { const WorldTilePosition futurePosition = MyPlayer->position.future; int rotations = 5; auto searchArea = PointsInRectangleColMajor(WorldTileRectangle { futurePosition, 1 }); for (const WorldTilePosition targetPosition : searchArea) { // As the player can not stand on the edge of the map this is safe from OOB const int8_t itemId = dItem[targetPosition.x][targetPosition.y] - 1; if (itemId < 0) { // there shouldn't be any items that occupy multiple ground tiles, but just in case only considering positive indexes here continue; } const Item &item = Items[itemId]; if (item.isEmpty() || item.selectionRegion == SelectionRegion::None) { continue; } const int newRotations = GetRotaryDistance(targetPosition); if (rotations < newRotations) { continue; } if (targetPosition != futurePosition && GetDistance(targetPosition, 1) == 0) { // Don't check the tile we're leaving if the player is walking continue; } rotations = newRotations; pcursitem = itemId; cursPosition = targetPosition; } if (leveltype == DTYPE_TOWN || pcursitem != -1) { return; // Don't look for objects in town } for (const WorldTilePosition targetPosition : searchArea) { Object *object = FindObjectAtPosition(targetPosition); if (object == nullptr || !object->canInteractWith()) { // No object or non-interactive object continue; } if (targetPosition == futurePosition && object->_oDoorFlag) { continue; // Ignore doorway so we don't get stuck behind barrels } const int newRotations = GetRotaryDistance(targetPosition); if (rotations < newRotations) { continue; } if (targetPosition != futurePosition && GetDistance(targetPosition, 1) == 0) { // Don't check the tile we're leaving if the player is walking continue; } if (object->IsDisabled()) { continue; } rotations = newRotations; ObjectUnderCursor = object; cursPosition = targetPosition; } } void CheckTownersNearby() { for (size_t i = 0; i < GetNumTowners(); i++) { const int distance = GetDistance(Towners[i].position, 2); if (distance == 0) continue; if (!IsTownerPresent(Towners[i]._ttype)) continue; pcursmonst = static_cast(i); } } bool HasRangedSpell() { const SpellID spl = MyPlayer->_pRSpell; return spl != SpellID::Invalid && spl != SpellID::TownPortal && spl != SpellID::Teleport && GetSpellData(spl).isTargeted() && !GetSpellData(spl).isAllowedInTown(); } bool CanTargetMonster(const Monster &monster) { if ((monster.flags & MFLAG_HIDDEN) != 0) return false; if (monster.isPlayerMinion()) return false; if (monster.hasNoLife()) // dead return false; if (!IsTileLit(monster.position.tile)) // not visible return false; const int mx = monster.position.tile.x; const int my = monster.position.tile.y; if (dMonster[mx][my] == 0) return false; return true; } void FindRangedTarget() { int rotations = 0; int distance = 0; bool canTalk = false; for (size_t i = 0; i < ActiveMonsterCount; i++) { const int mi = ActiveMonsters[i]; const Monster &monster = Monsters[mi]; if (!CanTargetMonster(monster)) continue; const bool newCanTalk = CanTalkToMonst(monster); if (pcursmonst != -1 && !canTalk && newCanTalk) continue; const int newDdistance = GetDistanceRanged(monster.position.future); const int newRotations = GetRotaryDistance(monster.position.future); if (pcursmonst != -1 && canTalk == newCanTalk) { if (distance < newDdistance) continue; if (distance == newDdistance && rotations < newRotations) continue; } distance = newDdistance; rotations = newRotations; canTalk = newCanTalk; pcursmonst = mi; } } void FindMeleeTarget() { bool visited[MAXDUNX][MAXDUNY] = { {} }; int maxSteps = 25; // Max steps for FindPath is 25 int rotations = 0; bool canTalk = false; struct SearchNode { int x, y; int steps; }; std::list queue; const Player &myPlayer = *MyPlayer; { const int startX = myPlayer.position.future.x; const int startY = myPlayer.position.future.y; visited[startX][startY] = true; queue.push_back({ startX, startY, 0 }); } while (!queue.empty()) { const SearchNode node = queue.front(); queue.pop_front(); for (auto pathDir : PathDirs) { const int dx = node.x + pathDir.deltaX; const int dy = node.y + pathDir.deltaY; if (visited[dx][dy]) continue; // already visisted if (node.steps > maxSteps) { visited[dx][dy] = true; continue; } if (!PosOkPlayer(myPlayer, { dx, dy })) { visited[dx][dy] = true; if (dMonster[dx][dy] != 0) { const int mi = std::abs(dMonster[dx][dy]) - 1; const Monster &monster = Monsters[mi]; if (CanTargetMonster(monster)) { const bool newCanTalk = CanTalkToMonst(monster); if (pcursmonst != -1 && !canTalk && newCanTalk) continue; const int newRotations = GetRotaryDistance({ dx, dy }); if (pcursmonst != -1 && canTalk == newCanTalk && rotations < newRotations) continue; rotations = newRotations; canTalk = newCanTalk; pcursmonst = mi; if (!canTalk) maxSteps = node.steps; // Monsters found, cap search to current steps } } continue; } if (CanStep({ node.x, node.y }, { dx, dy })) { queue.push_back({ dx, dy, node.steps + 1 }); visited[dx][dy] = true; } } } } void CheckMonstersNearby() { if (MyPlayer->UsesRangedWeapon() || HasRangedSpell()) { FindRangedTarget(); return; } FindMeleeTarget(); } void CheckPlayerNearby() { int newDdistance; int rotations = 0; int distance = 0; if (pcursmonst != -1) return; const Player &myPlayer = *MyPlayer; const SpellID spl = myPlayer._pRSpell; if (myPlayer.friendlyMode && spl != SpellID::Resurrect && spl != SpellID::HealOther) return; for (const Player &player : Players) { if (&player == MyPlayer) continue; const int mx = player.position.future.x; const int my = player.position.future.y; if (dPlayer[mx][my] == 0 || !IsTileLit(player.position.future) || (player.hasNoLife() && spl != SpellID::Resurrect)) continue; if (myPlayer.UsesRangedWeapon() || HasRangedSpell() || spl == SpellID::HealOther) { newDdistance = GetDistanceRanged(player.position.future); } else { newDdistance = GetDistance(player.position.future, distance); if (newDdistance == 0) continue; } if (PlayerUnderCursor != nullptr && distance < newDdistance) continue; const int newRotations = GetRotaryDistance(player.position.future); if (PlayerUnderCursor != nullptr && distance == newDdistance && rotations < newRotations) continue; distance = newDdistance; rotations = newRotations; PlayerUnderCursor = &player; } } void FindActor() { if (leveltype != DTYPE_TOWN) CheckMonstersNearby(); else CheckTownersNearby(); if (gbIsMultiplayer) CheckPlayerNearby(); } void FindTrigger() { int rotations = 0; int distance = 0; if (pcursitem != -1 || ObjectUnderCursor != nullptr) return; // Prefer showing items/objects over triggers (use of cursm* conflicts) for (auto &missile : Missiles) { if (missile._mitype == MissileID::TownPortal || missile._mitype == MissileID::RedPortal) { const int newDistance = GetDistance(missile.position.tile, 2); if (newDistance == 0) continue; if (pcursmissile != nullptr && distance < newDistance) continue; const int newRotations = GetRotaryDistance(missile.position.tile); if (pcursmissile != nullptr && distance == newDistance && rotations < newRotations) continue; cursPosition = missile.position.tile; pcursmissile = &missile; distance = newDistance; rotations = newRotations; } } if (pcursmissile == nullptr) { for (int i = 0; i < numtrigs; i++) { const int tx = trigs[i].position.x; int ty = trigs[i].position.y; if (trigs[i]._tlvl == 13) ty -= 1; const int newDistance = GetDistance({ tx, ty }, 2); if (newDistance == 0) continue; cursPosition = { tx, ty }; pcurstrig = i; } if (pcurstrig == -1) { for (auto &quest : Quests) { if (quest._qidx == Q_BETRAYER || currlevel != quest._qlevel || quest._qslvl == 0) continue; const int newDistance = GetDistance(quest.position, 2); if (newDistance == 0) continue; cursPosition = quest.position; pcursquest = quest._qidx; } } } if (pcursmonst != -1 || PlayerUnderCursor != nullptr || cursPosition.x == -1 || cursPosition.y == -1) return; // Prefer monster/player info text CheckTrigForce(); CheckTown(); CheckRportal(); } bool IsStandingGround() { if (ControlMode == ControlTypes::Gamepad) { const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); return StandToggle || IsControllerButtonComboPressed(standGroundCombo); } #ifndef USE_SDL1 if (ControlMode == ControlTypes::VirtualGamepad) { return VirtualGamepadState.standButton.isHeld; } #endif return false; } void Interact() { if (leveltype == DTYPE_TOWN && pcursmonst != -1) { NetSendCmdLocParam1(true, CMD_TALKXY, Towners[pcursmonst].position, pcursmonst); return; } const Player &myPlayer = *MyPlayer; if (leveltype != DTYPE_TOWN && IsStandingGround()) { Direction pdir = myPlayer._pdir; const AxisDirection moveDir = GetMoveDirection(); const bool motion = moveDir.x != AxisDirectionX_NONE || moveDir.y != AxisDirectionY_NONE; if (motion) { pdir = FaceDir[static_cast(moveDir.x)][static_cast(moveDir.y)]; } Point position = myPlayer.position.tile + pdir; if (pcursmonst != -1 && !motion) { position = Monsters[pcursmonst].position.tile; } NetSendCmdLoc(MyPlayerId, true, myPlayer.UsesRangedWeapon() ? CMD_RATTACKXY : CMD_SATTACKXY, position); LastPlayerAction = PlayerActionType::Attack; return; } if (pcursmonst != -1) { if (!myPlayer.UsesRangedWeapon() || CanTalkToMonst(Monsters[pcursmonst])) { NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); } else { NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); } LastPlayerAction = PlayerActionType::AttackMonsterTarget; return; } if (leveltype != DTYPE_TOWN && PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { NetSendCmdParam1(true, myPlayer.UsesRangedWeapon() ? CMD_RATTACKPID : CMD_ATTACKPID, PlayerUnderCursor->getId()); LastPlayerAction = PlayerActionType::AttackPlayerTarget; return; } if (ObjectUnderCursor != nullptr) { NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJXY, cursPosition); LastPlayerAction = PlayerActionType::OperateObject; return; } } void AttrIncBtnSnap(AxisDirection dir) { static AxisDirectionRepeater repeater; dir = repeater.Get(dir); if (dir.y == AxisDirectionY_NONE) return; if (CharPanelButtonActive && MyPlayer->_pStatPts <= 0) return; // first, find our cursor location int slot = 0; Rectangle button; for (int i = 0; i < 4; i++) { button = CharPanelButtonRect[i]; button.position = GetPanelPosition(UiPanels::Character, button.position); if (button.contains(MousePosition)) { slot = i; break; } } if (dir.y == AxisDirectionY_UP) { if (slot > 0) --slot; } else if (dir.y == AxisDirectionY_DOWN) { if (slot < 3) ++slot; } // move cursor to our new location button = CharPanelButtonRect[slot]; button.position = GetPanelPosition(UiPanels::Character, button.position); SetCursorPos(button.Center()); } Point InvGetEquipSlotCoord(const inv_body_loc invSlot) { Point result = GetPanelPosition(UiPanels::Inventory); switch (invSlot) { case INVLOC_HEAD: result.x += InvRect[SLOTXY_HEAD].Center().x; result.y += InvRect[SLOTXY_HEAD].Center().y; break; case INVLOC_RING_LEFT: result.x += InvRect[SLOTXY_RING_LEFT].Center().x; result.y += InvRect[SLOTXY_RING_LEFT].Center().y; break; case INVLOC_RING_RIGHT: result.x += InvRect[SLOTXY_RING_RIGHT].Center().x; result.y += InvRect[SLOTXY_RING_RIGHT].Center().y; break; case INVLOC_AMULET: result.x += InvRect[SLOTXY_AMULET].Center().x; result.y += InvRect[SLOTXY_AMULET].Center().y; break; case INVLOC_HAND_LEFT: result.x += InvRect[SLOTXY_HAND_LEFT].Center().x; result.y += InvRect[SLOTXY_HAND_LEFT].Center().y; break; case INVLOC_HAND_RIGHT: result.x += InvRect[SLOTXY_HAND_RIGHT].Center().x; result.y += InvRect[SLOTXY_HAND_RIGHT].Center().y; break; case INVLOC_CHEST: result.x += InvRect[SLOTXY_CHEST].Center().x; result.y += InvRect[SLOTXY_CHEST].Center().y; break; default: break; } return result; } Point InvGetEquipSlotCoordFromInvSlot(const inv_xy_slot slot) { if (slot == SLOTXY_HEAD) { return InvGetEquipSlotCoord(INVLOC_HEAD); } if (slot == SLOTXY_RING_LEFT) { return InvGetEquipSlotCoord(INVLOC_RING_LEFT); } if (slot == SLOTXY_RING_RIGHT) { return InvGetEquipSlotCoord(INVLOC_RING_RIGHT); } if (slot == SLOTXY_AMULET) { return InvGetEquipSlotCoord(INVLOC_AMULET); } if (slot == SLOTXY_HAND_LEFT) { return InvGetEquipSlotCoord(INVLOC_HAND_LEFT); } if (slot == SLOTXY_HAND_RIGHT) { return InvGetEquipSlotCoord(INVLOC_HAND_RIGHT); } if (slot == SLOTXY_CHEST) { return InvGetEquipSlotCoord(INVLOC_CHEST); } return {}; } /** * Get coordinates for a given slot */ Point GetSlotCoord(int slot) { if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) { return GetPanelPosition(UiPanels::Main, InvRect[slot].Center()); } return GetPanelPosition(UiPanels::Inventory, InvRect[slot].Center()); } /** * Return the item id of the current slot */ int GetItemIdOnSlot(int slot) { if (slot >= SLOTXY_INV_FIRST && slot <= SLOTXY_INV_LAST) { return std::abs(MyPlayer->InvGrid[slot - SLOTXY_INV_FIRST]); } return 0; } /** * Get item size (grid size) on the slot specified. Returns 1x1 if none exists. */ Size GetItemSizeOnSlot(int slot) { if (slot >= SLOTXY_INV_FIRST && slot <= SLOTXY_INV_LAST) { const int8_t ii = GetItemIdOnSlot(slot); if (ii != 0) { const Item &item = MyPlayer->InvList[ii - 1]; if (!item.isEmpty()) { return GetInventorySize(item); } } } return { 1, 1 }; } /** * Get item size (grid size) on the slot specified. Returns 1x1 if none exists. */ Size GetItemSizeOnSlot(Point slot) { if (Rectangle { { 0, 0 }, { 10, 10 } }.contains(slot)) { const StashStruct::StashCell ii = Stash.GetItemIdAtPosition(slot); if (ii != StashStruct::EmptyCell) { const Item &item = Stash.stashList[ii]; if (!item.isEmpty()) { return GetInventorySize(item); } } } return { 1, 1 }; } /** * Search for the first slot occupied by an item in the inventory. */ int FindFirstSlotOnItem(int8_t itemInvId) { if (itemInvId == 0) return -1; for (int s = SLOTXY_INV_FIRST; s <= SLOTXY_INV_LAST; s++) { if (GetItemIdOnSlot(s) == itemInvId) return s; } return -1; } Point FindFirstStashSlotOnItem(StashStruct::StashCell itemInvId) { if (itemInvId == StashStruct::EmptyCell) return InvalidStashPoint; for (WorldTilePosition point : PointsInRectangle(WorldTileRectangle { { 0, 0 }, { 10, 10 } })) { if (Stash.GetItemIdAtPosition(point) == itemInvId) return point; } return InvalidStashPoint; } /** * Reset cursor position based on the current slot. */ void ResetInvCursorPosition() { Point mousePos {}; if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) { auto slot = Slot; Size itemSize = { 1, 1 }; if (MyPlayer->HoldItem.isEmpty()) { const int8_t itemInvId = GetItemIdOnSlot(Slot); if (itemInvId != 0) { slot = FindFirstSlotOnItem(itemInvId); itemSize = GetItemSizeOnSlot(Slot); } } else { itemSize = GetInventorySize(MyPlayer->HoldItem); } mousePos = GetSlotCoord(slot); mousePos.x += ((itemSize.width - 1) * InventorySlotSizeInPixels.width) / 2; mousePos.y += ((itemSize.height - 1) * InventorySlotSizeInPixels.height) / 2; } else if (Slot >= SLOTXY_BELT_FIRST && Slot <= SLOTXY_BELT_LAST) { mousePos = GetSlotCoord(Slot); } else { mousePos = InvGetEquipSlotCoordFromInvSlot((inv_xy_slot)Slot); } SetCursorPos(mousePos); } int FindClosestInventorySlot( Point mousePos, const Item &heldItem, tl::function_ref distanceFunction = [](Point mousePos, int slot) { return mousePos.ManhattanDistance(GetSlotCoord(slot)); }) { int shortestDistance = std::numeric_limits::max(); int bestSlot = 0; auto checkCandidateSlot = [&](int slot) { const int distance = distanceFunction(mousePos, slot); if (distance < shortestDistance) { shortestDistance = distance; bestSlot = slot; } }; if (heldItem.isEmpty()) { for (int i = SLOTXY_HEAD; i <= SLOTXY_CHEST; i++) { checkCandidateSlot(i); } } else { if (heldItem._itype == ItemType::Ring) { for (const int i : { SLOTXY_RING_LEFT, SLOTXY_RING_RIGHT }) { checkCandidateSlot(i); } } else if (heldItem.isWeapon()) { checkCandidateSlot(SLOTXY_HAND_LEFT); } else if (heldItem.isShield()) { checkCandidateSlot(SLOTXY_HAND_RIGHT); } else if (heldItem.isHelm()) { checkCandidateSlot(SLOTXY_HEAD); } else if (heldItem.isArmor()) { checkCandidateSlot(SLOTXY_CHEST); } else if (heldItem._itype == ItemType::Amulet) { checkCandidateSlot(SLOTXY_AMULET); } } for (int i = SLOTXY_INV_FIRST; i <= SLOTXY_INV_LAST; i++) { checkCandidateSlot(i); } return bestSlot; } Point FindClosestStashSlot(Point mousePos) { int shortestDistance = std::numeric_limits::max(); Point bestSlot = {}; for (const Point point : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10, 10 } })) { const int distance = mousePos.ManhattanDistance(GetStashSlotCoord(point)); if (distance < shortestDistance) { shortestDistance = distance; bestSlot = point; } } return bestSlot; } void LiftInventoryItem() { const int inventorySlot = (Slot >= 0) ? Slot : FindClosestInventorySlot(MousePosition, MyPlayer->HoldItem); int jumpSlot = inventorySlot; // If the cursor is over an inventory slot we may need to adjust it due to pasting items of different sizes over each other if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) { const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); // Find any item occupying a slot that is currently under the cursor const int8_t itemUnderCursor = [](int inventorySlot, Size cursorSizeInCells) { if (inventorySlot < SLOTXY_INV_FIRST || inventorySlot > SLOTXY_INV_LAST) return 0; for (int x = 0; x < cursorSizeInCells.width; x++) { for (int y = 0; y < cursorSizeInCells.height; y++) { const int slotUnderCursor = inventorySlot + x + y * INV_ROW_SLOT_SIZE; if (slotUnderCursor > SLOTXY_INV_LAST) continue; const int itemId = GetItemIdOnSlot(slotUnderCursor); if (itemId != 0) return itemId; } } return 0; }(inventorySlot, cursorSizeInCells); // Capture the first slot of the first item (if any) under the cursor if (itemUnderCursor > 0) jumpSlot = FindFirstSlotOnItem(itemUnderCursor); } CheckInvItem(); if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) { Point mousePos = GetSlotCoord(jumpSlot); Slot = jumpSlot; const Size newCursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? GetItemSizeOnSlot(jumpSlot) : GetInventorySize(MyPlayer->HoldItem); mousePos.x += ((newCursorSizeInCells.width - 1) * InventorySlotSizeInPixels.width) / 2; mousePos.y += ((newCursorSizeInCells.height - 1) * InventorySlotSizeInPixels.height) / 2; SetCursorPos(mousePos); } } void LiftStashItem() { const Point stashSlot = (ActiveStashSlot != InvalidStashPoint) ? ActiveStashSlot : FindClosestStashSlot(MousePosition); Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); // Find any item occupying a slot that is currently under the cursor const StashStruct::StashCell itemUnderCursor = [](Point stashSlot, Size cursorSizeInCells) -> StashStruct::StashCell { if (stashSlot == InvalidStashPoint) return StashStruct::EmptyCell; for (const Point slotUnderCursor : PointsInRectangle(Rectangle { stashSlot, cursorSizeInCells })) { if (slotUnderCursor.x >= 10 || slotUnderCursor.y >= 10) continue; const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slotUnderCursor); if (itemId != StashStruct::EmptyCell) return itemId; } return StashStruct::EmptyCell; }(stashSlot, cursorSizeInCells); const Point jumpSlot = itemUnderCursor == StashStruct::EmptyCell ? stashSlot : FindFirstStashSlotOnItem(itemUnderCursor); CheckStashItem(MousePosition); Point mousePos = GetStashSlotCoord(jumpSlot); ActiveStashSlot = jumpSlot; // Center the Cursor based on the item we just put down or we're holding. cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? GetItemSizeOnSlot(jumpSlot) : GetInventorySize(MyPlayer->HoldItem); mousePos.x += ((cursorSizeInCells.width) * InventorySlotSizeInPixels.width) / 2; mousePos.y += ((cursorSizeInCells.height) * InventorySlotSizeInPixels.height) / 2; SetCursorPos(mousePos); } /** * @brief Figures out where on the body to move when on the first row */ inv_xy_slot InventoryMoveToBody(int slot) { PreviousInventoryColumn = slot - SLOTXY_INV_ROW1_FIRST; if (slot <= SLOTXY_INV_ROW1_FIRST + 2) { // first 3 general slots return SLOTXY_RING_LEFT; } if (slot <= SLOTXY_INV_ROW1_FIRST + 6) { // middle 4 general slots return SLOTXY_CHEST; } // last 3 general slots return SLOTXY_RING_RIGHT; } void InventoryMove(AxisDirection dir) { Point mousePos = MousePosition; const Item &heldItem = MyPlayer->HoldItem; // normalize slots if (Slot < 0) Slot = FindClosestInventorySlot(mousePos, heldItem); else if (Slot > SLOTXY_BELT_LAST) Slot = SLOTXY_BELT_LAST; const int initialSlot = Slot; const bool isHoldingItem = !heldItem.isEmpty(); Size itemSize = isHoldingItem ? GetInventorySize(heldItem) : Size { 1 }; // when item is on cursor (pcurs > 1), this is the real cursor XY if (dir.x == AxisDirectionX_LEFT) { if (isHoldingItem) { if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) { if (IsNoneOf(Slot, SLOTXY_INV_ROW1_FIRST, SLOTXY_INV_ROW2_FIRST, SLOTXY_INV_ROW3_FIRST, SLOTXY_INV_ROW4_FIRST, SLOTXY_BELT_FIRST)) { Slot -= 1; } } else if (heldItem._itype == ItemType::Ring) { Slot = SLOTXY_RING_LEFT; } else if (heldItem.isWeapon() || heldItem.isShield()) { Slot = SLOTXY_HAND_LEFT; } } else { if (Slot == SLOTXY_HAND_RIGHT) { Slot = SLOTXY_CHEST; } else if (Slot == SLOTXY_CHEST) { Slot = SLOTXY_HAND_LEFT; } else if (Slot == SLOTXY_AMULET) { Slot = SLOTXY_HEAD; } else if (Slot == SLOTXY_RING_RIGHT) { Slot = SLOTXY_RING_LEFT; } else if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) { const int8_t itemId = GetItemIdOnSlot(Slot); if (itemId != 0) { for (int i = 1; i < INV_ROW_SLOT_SIZE && !IsAnyOf(Slot - i + 1, SLOTXY_INV_ROW1_FIRST, SLOTXY_INV_ROW2_FIRST, SLOTXY_INV_ROW3_FIRST, SLOTXY_INV_ROW4_FIRST, SLOTXY_BELT_FIRST); i++) { if (itemId != GetItemIdOnSlot(Slot - i)) { Slot -= i; break; } } } else if (IsNoneOf(Slot, SLOTXY_INV_ROW1_FIRST, SLOTXY_INV_ROW2_FIRST, SLOTXY_INV_ROW3_FIRST, SLOTXY_INV_ROW4_FIRST, SLOTXY_BELT_FIRST)) { Slot -= 1; } } } } else if (dir.x == AxisDirectionX_RIGHT) { if (isHoldingItem) { if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) { if (IsNoneOf(Slot + itemSize.width - 1, SLOTXY_INV_ROW1_LAST, SLOTXY_INV_ROW2_LAST, SLOTXY_INV_ROW3_LAST, SLOTXY_INV_ROW4_LAST, SLOTXY_BELT_LAST)) { Slot += 1; } } else if (heldItem._itype == ItemType::Ring) { Slot = SLOTXY_RING_RIGHT; } else if (heldItem.isWeapon() || heldItem.isShield()) { Slot = SLOTXY_HAND_RIGHT; } } else { if (Slot == SLOTXY_RING_LEFT) { Slot = SLOTXY_RING_RIGHT; } else if (Slot == SLOTXY_HAND_LEFT) { Slot = SLOTXY_CHEST; } else if (Slot == SLOTXY_CHEST) { Slot = SLOTXY_HAND_RIGHT; } else if (Slot == SLOTXY_HEAD) { Slot = SLOTXY_AMULET; } else if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) { const int8_t itemId = GetItemIdOnSlot(Slot); if (itemId != 0) { for (int i = 1; i < INV_ROW_SLOT_SIZE && !IsAnyOf(Slot + i - 1, SLOTXY_INV_ROW1_LAST, SLOTXY_INV_ROW2_LAST, SLOTXY_INV_ROW3_LAST, SLOTXY_INV_ROW4_LAST, SLOTXY_BELT_LAST); i++) { if (itemId != GetItemIdOnSlot(Slot + i)) { Slot += i; break; } } } else if (IsNoneOf(Slot, SLOTXY_INV_ROW1_LAST, SLOTXY_INV_ROW2_LAST, SLOTXY_INV_ROW3_LAST, SLOTXY_INV_ROW4_LAST, SLOTXY_BELT_LAST)) { Slot += 1; } } } } if (dir.y == AxisDirectionY_UP) { if (isHoldingItem) { if (Slot >= SLOTXY_INV_ROW2_FIRST) { // general inventory Slot -= INV_ROW_SLOT_SIZE; } else if (Slot >= SLOTXY_INV_FIRST) { if (heldItem._itype == ItemType::Ring) { if (Slot >= SLOTXY_INV_ROW1_FIRST && Slot <= SLOTXY_INV_ROW1_FIRST + (INV_ROW_SLOT_SIZE / 2) - 1) { Slot = SLOTXY_RING_LEFT; } else { Slot = SLOTXY_RING_RIGHT; } } else if (heldItem.isWeapon()) { Slot = SLOTXY_HAND_LEFT; } else if (heldItem.isShield()) { Slot = SLOTXY_HAND_RIGHT; } else if (heldItem.isHelm()) { Slot = SLOTXY_HEAD; } else if (heldItem.isArmor()) { Slot = SLOTXY_CHEST; } else if (heldItem._itype == ItemType::Amulet) { Slot = SLOTXY_AMULET; } } } else { if (Slot >= SLOTXY_INV_ROW1_FIRST && Slot <= SLOTXY_INV_ROW1_LAST) { Slot = InventoryMoveToBody(Slot); } else if (Slot == SLOTXY_CHEST || Slot == SLOTXY_HAND_LEFT) { Slot = SLOTXY_HEAD; } else if (Slot == SLOTXY_RING_LEFT) { Slot = SLOTXY_HAND_LEFT; } else if (Slot == SLOTXY_RING_RIGHT) { Slot = SLOTXY_HAND_RIGHT; } else if (Slot == SLOTXY_HAND_RIGHT) { Slot = SLOTXY_AMULET; } else if (Slot >= SLOTXY_INV_ROW2_FIRST) { const int8_t itemId = GetItemIdOnSlot(Slot); if (itemId != 0) { for (int i = 1; i < 5; i++) { if (Slot - i * INV_ROW_SLOT_SIZE < SLOTXY_INV_ROW1_FIRST) { Slot = InventoryMoveToBody(Slot - (i - 1) * INV_ROW_SLOT_SIZE); break; } if (itemId != GetItemIdOnSlot(Slot - i * INV_ROW_SLOT_SIZE)) { Slot -= i * INV_ROW_SLOT_SIZE; break; } } } else { Slot -= INV_ROW_SLOT_SIZE; } } } } else if (dir.y == AxisDirectionY_DOWN) { if (isHoldingItem) { if (Slot == SLOTXY_HEAD || Slot == SLOTXY_CHEST) { Slot = SLOTXY_INV_ROW1_FIRST + 4; } else if (Slot == SLOTXY_RING_LEFT || Slot == SLOTXY_HAND_LEFT) { Slot = SLOTXY_INV_ROW1_FIRST + (itemSize.width > 1 ? 0 : 1); } else if (Slot == SLOTXY_RING_RIGHT || Slot == SLOTXY_HAND_RIGHT || Slot == SLOTXY_AMULET) { Slot = SLOTXY_INV_ROW1_LAST - 1; } else if (Slot <= (SLOTXY_INV_ROW4_LAST - (itemSize.height * INV_ROW_SLOT_SIZE))) { Slot += INV_ROW_SLOT_SIZE; } else if (Slot <= SLOTXY_INV_LAST && heldItem._itype == ItemType::Misc && itemSize == Size { 1, 1 }) { // forcing only 1x1 misc items if (Slot + INV_ROW_SLOT_SIZE <= SLOTXY_BELT_LAST) Slot += INV_ROW_SLOT_SIZE; } } else { if (Slot == SLOTXY_HEAD) { Slot = SLOTXY_CHEST; } else if (Slot == SLOTXY_CHEST) { if (PreviousInventoryColumn >= 3 && PreviousInventoryColumn <= 6) Slot = SLOTXY_INV_ROW1_FIRST + PreviousInventoryColumn; else Slot = SLOTXY_INV_ROW1_FIRST + (INV_ROW_SLOT_SIZE / 2); } else if (Slot == SLOTXY_HAND_LEFT) { Slot = SLOTXY_RING_LEFT; } else if (Slot == SLOTXY_RING_LEFT) { if (PreviousInventoryColumn >= 0 && PreviousInventoryColumn <= 2) Slot = SLOTXY_INV_ROW1_FIRST + PreviousInventoryColumn; else Slot = SLOTXY_INV_ROW1_FIRST + 1; } else if (Slot == SLOTXY_RING_RIGHT) { if (PreviousInventoryColumn >= 7 && PreviousInventoryColumn <= 9) Slot = SLOTXY_INV_ROW1_FIRST + PreviousInventoryColumn; else Slot = SLOTXY_INV_ROW1_LAST - 1; } else if (Slot == SLOTXY_AMULET) { Slot = SLOTXY_HAND_RIGHT; } else if (Slot == SLOTXY_HAND_RIGHT) { Slot = SLOTXY_RING_RIGHT; } else if (Slot <= SLOTXY_INV_LAST) { const int8_t itemId = GetItemIdOnSlot(Slot); if (itemId != 0) { for (int i = 1; i < 5 && Slot + i * INV_ROW_SLOT_SIZE <= SLOTXY_BELT_LAST; i++) { if (itemId != GetItemIdOnSlot(Slot + i * INV_ROW_SLOT_SIZE)) { Slot += i * INV_ROW_SLOT_SIZE; break; } } } else if (Slot + INV_ROW_SLOT_SIZE <= SLOTXY_BELT_LAST) { Slot += INV_ROW_SLOT_SIZE; } } } } // no movement was made if (Slot == initialSlot) return; if (Slot < SLOTXY_INV_FIRST) { mousePos = InvGetEquipSlotCoordFromInvSlot(static_cast(Slot)); } else { mousePos = GetSlotCoord(Slot); } // If we're in the inventory we may need to move the cursor to an area that doesn't line up with the center of a cell if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) { if (!isHoldingItem) { // If we're not holding an item const int8_t itemInvId = GetItemIdOnSlot(Slot); if (itemInvId != 0) { // but the cursor moved over an item int itemSlot = FindFirstSlotOnItem(itemInvId); if (itemSlot < 0) itemSlot = Slot; // then we need to offset the cursor so it shows over the center of the item mousePos = GetSlotCoord(itemSlot); itemSize = GetItemSizeOnSlot(itemSlot); } } // At this point itemSize is either the size of the cell/item the hand cursor is over, or the size of the item we're currently holding. // mousePos is the center of the top left cell of the item under the hand cursor, or the top left cell of the region that could fit the item we're holding. // either way we need to offset the mouse position to account for items (we're holding or hovering over) with a dimension larger than a single cell. mousePos.x += ((itemSize.width - 1) * InventorySlotSizeInPixels.width) / 2; mousePos.y += ((itemSize.height - 1) * InventorySlotSizeInPixels.height) / 2; } if (mousePos == MousePosition) { return; // Avoid wobbling when scaled } SetCursorPos(mousePos); } /** * Move the cursor around in the inventory * If mouse coords are at SLOTXY_CHEST_LAST, consider this center of equipment * small inventory squares are 29x29 (roughly) */ void CheckInventoryMove(AxisDirection dir) { static AxisDirectionRepeater repeater(/*min_interval_ms=*/150); dir = repeater.Get(dir); if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) return; InventoryMove(dir); } /** * @brief Try to clean the inventory related cursor states. * @return True if it is safe to close the inventory */ bool BlurInventory() { if (!MyPlayer->HoldItem.isEmpty()) { if (!TryDropItem()) { MyPlayer->Say(HeroSpeech::WhereWouldIPutThis); return false; } } CloseInventory(); if (pcurs > CURSOR_HAND) NewCursor(CURSOR_HAND); if (CharFlag) FocusOnCharInfo(); return true; } void StashMove(AxisDirection dir) { static AxisDirectionRepeater repeater(/*min_interval_ms=*/150); dir = repeater.Get(dir); if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) return; const Item &holdItem = MyPlayer->HoldItem; if (Slot < 0 && ActiveStashSlot == InvalidStashPoint) { const int invSlot = FindClosestInventorySlot(MousePosition, holdItem); const Point invSlotCoord = GetSlotCoord(invSlot); const int invDistance = MousePosition.ManhattanDistance(invSlotCoord); const Point stashSlot = FindClosestStashSlot(MousePosition); const Point stashSlotCoord = GetStashSlotCoord(stashSlot); const int stashDistance = MousePosition.ManhattanDistance(stashSlotCoord); if (invDistance < stashDistance) { BeltReturnsToStash = false; InventoryMove(dir); return; } ActiveStashSlot = stashSlot; } Size itemSize = holdItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(holdItem); if (dir.y == AxisDirectionY_UP) { // Check if we need to jump from belt to stash if (BeltReturnsToStash && Slot >= SLOTXY_BELT_FIRST && Slot <= SLOTXY_BELT_LAST) { const int beltSlot = Slot - SLOTXY_BELT_FIRST; InvalidateInventorySlot(); ActiveStashSlot = { 2 + beltSlot, 10 - itemSize.height }; dir.y = AxisDirectionY_NONE; } } if (dir.x == AxisDirectionX_LEFT) { // Check if we need to jump from general inventory to stash int firstSlot = Slot; if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) { if (MyPlayer->HoldItem.isEmpty()) { const int8_t itemId = GetItemIdOnSlot(Slot); if (itemId != 0) { firstSlot = FindFirstSlotOnItem(itemId); } } } // If we're in the leftmost column (or hovering over an item on the left side of the inventory) or // left side of the body and we're moving left we need to move into the closest stash column if (IsAnyOf(firstSlot, SLOTXY_HEAD, SLOTXY_HAND_LEFT, SLOTXY_RING_LEFT, SLOTXY_AMULET, SLOTXY_CHEST, SLOTXY_INV_ROW1_FIRST, SLOTXY_INV_ROW2_FIRST, SLOTXY_INV_ROW3_FIRST, SLOTXY_INV_ROW4_FIRST)) { const Point slotCoord = GetSlotCoord(Slot); InvalidateInventorySlot(); ActiveStashSlot = FindClosestStashSlot(slotCoord) - Displacement { itemSize.width - 1, 0 }; dir.x = AxisDirectionX_NONE; } } if (Slot >= 0) { InventoryMove(dir); return; } if (dir.x == AxisDirectionX_LEFT) { if (ActiveStashSlot.x > 0) { const StashStruct::StashCell itemIdAtActiveStashSlot = Stash.GetItemIdAtPosition(ActiveStashSlot); ActiveStashSlot.x--; if (holdItem.isEmpty() && itemIdAtActiveStashSlot != StashStruct::EmptyCell) { while (ActiveStashSlot.x > 0 && itemIdAtActiveStashSlot == Stash.GetItemIdAtPosition(ActiveStashSlot)) { ActiveStashSlot.x--; } } } } else if (dir.x == AxisDirectionX_RIGHT) { // If we're empty-handed and trying to move right while hovering over an item we may not // have a free stash column to move to. If the item we're hovering over occupies the last // column then we want to jump to the inventory instead of just moving one column over. const Size itemUnderCursorSize = holdItem.isEmpty() ? GetItemSizeOnSlot(ActiveStashSlot) : itemSize; if (ActiveStashSlot.x < 10 - itemUnderCursorSize.width) { const StashStruct::StashCell itemIdAtActiveStashSlot = Stash.GetItemIdAtPosition(ActiveStashSlot); ActiveStashSlot.x++; if (holdItem.isEmpty() && itemIdAtActiveStashSlot != StashStruct::EmptyCell) { while (ActiveStashSlot.x < 10 - itemSize.width && itemIdAtActiveStashSlot == Stash.GetItemIdAtPosition(ActiveStashSlot)) { ActiveStashSlot.x++; } } } else { const Point stashSlotCoord = GetStashSlotCoord(ActiveStashSlot); const Point rightPanelCoord = { GetRightPanel().position.x, stashSlotCoord.y }; Slot = FindClosestInventorySlot(rightPanelCoord, holdItem, [](Point mousePos, int slot) { const Point slotPos = GetSlotCoord(slot); // Exaggerate the vertical difference so that moving from the top 6 rows of the // stash is more likely to land on a body slot. The value 3 was found by trial and // error, this allows moving from the top row of the stash to the head while // empty-handed while 4 causes the amulet to be preferenced (due to less vertical // distance) and 2 causes the left hand to be preferenced (due to less horizontal // distance). return std::abs(mousePos.y - slotPos.y) * 3 + std::abs(mousePos.x - slotPos.x); }); ActiveStashSlot = InvalidStashPoint; BeltReturnsToStash = false; } } if (dir.y == AxisDirectionY_UP) { if (ActiveStashSlot.y > 0) { const StashStruct::StashCell itemIdAtActiveStashSlot = Stash.GetItemIdAtPosition(ActiveStashSlot); ActiveStashSlot.y--; if (holdItem.isEmpty() && itemIdAtActiveStashSlot != StashStruct::EmptyCell) { while (ActiveStashSlot.y > 0 && itemIdAtActiveStashSlot == Stash.GetItemIdAtPosition(ActiveStashSlot)) { ActiveStashSlot.y--; } } } } else if (dir.y == AxisDirectionY_DOWN) { if (ActiveStashSlot.y < 10 - itemSize.height) { const StashStruct::StashCell itemIdAtActiveStashSlot = Stash.GetItemIdAtPosition(ActiveStashSlot); ActiveStashSlot.y++; if (holdItem.isEmpty() && itemIdAtActiveStashSlot != StashStruct::EmptyCell) { while (ActiveStashSlot.y < 10 - itemSize.height && itemIdAtActiveStashSlot == Stash.GetItemIdAtPosition(ActiveStashSlot)) { ActiveStashSlot.y++; } } } else if ((holdItem.isEmpty() || CanBePlacedOnBelt(*MyPlayer, holdItem)) && ActiveStashSlot.x > 1) { const int beltSlot = ActiveStashSlot.x - 2; Slot = SLOTXY_BELT_FIRST + beltSlot; ActiveStashSlot = InvalidStashPoint; BeltReturnsToStash = true; } } if (Slot >= 0) { ResetInvCursorPosition(); return; } if (ActiveStashSlot != InvalidStashPoint) { Point mousePos = GetStashSlotCoord(ActiveStashSlot); // At this point itemSize is the size of the item we're currently holding. // We need to offset the mouse position to account for items (we're holding or hovering over) with a dimension larger than a single cell. if (holdItem.isEmpty()) { const StashStruct::StashCell itemIdAtActiveStashSlot = Stash.GetItemIdAtPosition(ActiveStashSlot); if (itemIdAtActiveStashSlot != StashStruct::EmptyCell) { const Item stashItem = Stash.stashList[itemIdAtActiveStashSlot]; const Point firstSlotOnItem = FindFirstStashSlotOnItem(itemIdAtActiveStashSlot); itemSize = GetInventorySize(stashItem); mousePos = GetStashSlotCoord(firstSlotOnItem); } } mousePos += Displacement { itemSize.width * INV_SLOT_HALF_SIZE_PX, itemSize.height * INV_SLOT_HALF_SIZE_PX }; SetCursorPos(mousePos); return; } FocusOnInventory(); } void HotSpellMove(AxisDirection dir) { static AxisDirectionRepeater repeater; dir = repeater.Get(dir); if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) return; auto spellListItems = GetSpellListItems(); Point position = MousePosition; int shortestDistance = std::numeric_limits::max(); for (auto &spellListItem : spellListItems) { const Point center = spellListItem.location + Displacement { SPLICONLENGTH / 2, -SPLICONLENGTH / 2 }; const int distance = MousePosition.ManhattanDistance(center); if (distance < shortestDistance) { position = center; shortestDistance = distance; } } const auto search = [&](AxisDirection dir, bool searchForward) { if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) return; for (size_t i = 0; i < spellListItems.size(); i++) { const size_t index = searchForward ? spellListItems.size() - i - 1 : i; auto &spellListItem = spellListItems[index]; if (spellListItem.isSelected) continue; const Point center = spellListItem.location + Displacement { SPLICONLENGTH / 2, -SPLICONLENGTH / 2 }; if (dir.x == AxisDirectionX_LEFT && center.x >= MousePosition.x) continue; if (dir.x == AxisDirectionX_RIGHT && center.x <= MousePosition.x) continue; if (dir.x == AxisDirectionX_NONE && center.x != position.x) continue; if (dir.y == AxisDirectionY_UP && center.y >= MousePosition.y) continue; if (dir.y == AxisDirectionY_DOWN && center.y <= MousePosition.y) continue; if (dir.y == AxisDirectionY_NONE && center.y != position.y) continue; position = center; break; } }; search({ AxisDirectionX_NONE, dir.y }, dir.y == AxisDirectionY_DOWN); search({ dir.x, AxisDirectionY_NONE }, dir.x == AxisDirectionX_RIGHT); if (position != MousePosition) { SetCursorPos(position); } } void SpellBookMove(AxisDirection dir) { static AxisDirectionRepeater repeater; dir = repeater.Get(dir); if (dir.x == AxisDirectionX_LEFT) { if (SpellbookTab > 0) SpellbookTab--; } else if (dir.x == AxisDirectionX_RIGHT) { if ((gbIsHellfire && SpellbookTab < 4) || (!gbIsHellfire && SpellbookTab < 3)) SpellbookTab++; } } /** * @brief check if stepping in direction (dir) from position is blocked. * * If you step from A to B, at least one of the Xs need to be clear: * * AX * XB * * @return true if step is blocked */ bool IsPathBlocked(Point position, Direction dir) { if (IsNoneOf(dir, Direction::North, Direction::East, Direction::South, Direction::West)) return false; // Steps along a major axis don't need to check corners auto leftStep { position + Left(dir) }; auto rightStep { position + Right(dir) }; if (IsTileNotSolid(leftStep) && IsTileNotSolid(rightStep)) return false; const Player &myPlayer = *MyPlayer; return !PosOkPlayer(myPlayer, leftStep) && !PosOkPlayer(myPlayer, rightStep); } void WalkInDir(Player &player, AxisDirection dir) { if (dir.x == AxisDirectionX_NONE && dir.y == AxisDirectionY_NONE) { if (ControlMode != ControlTypes::KeyboardAndMouse && player.walkpath[0] != WALK_NONE && player.destAction == ACTION_NONE) NetSendCmdLoc(player.getId(), true, CMD_WALKXY, player.position.future); // Stop walking return; } const Direction pdir = FaceDir[static_cast(dir.x)][static_cast(dir.y)]; const auto delta = player.position.future + pdir; if (!player.isWalking() && player.CanChangeAction()) player._pdir = pdir; if (IsStandingGround()) { if (player._pmode == PM_STAND) StartStand(player, pdir); return; } if (PosOkPlayer(player, delta) && IsPathBlocked(player.position.future, pdir)) { if (player._pmode == PM_STAND) StartStand(player, pdir); return; // Don't start backtrack around obstacles } NetSendCmdLoc(player.getId(), true, CMD_WALKXY, delta); } void QuestLogMove(AxisDirection moveDir) { static AxisDirectionRepeater repeater; moveDir = repeater.Get(moveDir); if (moveDir.y == AxisDirectionY_UP) QuestlogUp(); else if (moveDir.y == AxisDirectionY_DOWN) QuestlogDown(); } void StoreMove(AxisDirection moveDir) { static AxisDirectionRepeater repeater; moveDir = repeater.Get(moveDir); if (moveDir.y == AxisDirectionY_UP) StoreUp(); else if (moveDir.y == AxisDirectionY_DOWN) StoreDown(); } using HandleLeftStickOrDPadFn = void (*)(devilution::AxisDirection); HandleLeftStickOrDPadFn GetLeftStickOrDPadGameUIHandler() { if (SpellSelectFlag) { return &HotSpellMove; } if (IsStashOpen) { return &StashMove; } if (invflag) { return &CheckInventoryMove; } if (CharFlag && MyPlayer->_pStatPts > 0) { return &AttrIncBtnSnap; } if (QuestLogIsOpen) { return &QuestLogMove; } if (SpellbookFlag) { return &SpellBookMove; } if (IsPlayerInStore()) { return &StoreMove; } return nullptr; } void ProcessLeftStickOrDPadGameUI() { HandleLeftStickOrDPadFn handler = GetLeftStickOrDPadGameUIHandler(); if (handler != nullptr) handler(GetLeftStickOrDpadDirection(false)); } void ProcessAutomapMovementGamepad() { if (!AutomapActive) return; const auto &padmapper = GetOptions().Padmapper; if (IsControllerButtonComboPressed(padmapper.ButtonComboForAction("AutomapMoveUp"))) AutomapUp(); if (IsControllerButtonComboPressed(padmapper.ButtonComboForAction("AutomapMoveDown"))) AutomapDown(); if (IsControllerButtonComboPressed(padmapper.ButtonComboForAction("AutomapMoveLeft"))) AutomapLeft(); if (IsControllerButtonComboPressed(padmapper.ButtonComboForAction("AutomapMoveRight"))) AutomapRight(); } void Movement(Player &player) { if (PadMenuNavigatorActive || PadHotspellMenuActive || InGameMenu()) return; if (GetLeftStickOrDPadGameUIHandler() == nullptr) { WalkInDir(player, GetMoveDirection()); } } struct RightStickAccumulator { RightStickAccumulator() { lastTc = SDL_GetTicks(); hiresDX = 0; hiresDY = 0; } void Pool(int *x, int *y, int slowdown) { const Uint32 tc = SDL_GetTicks(); const int dtc = tc - lastTc; hiresDX += rightStickX * dtc; hiresDY += rightStickY * dtc; const int dx = static_cast(hiresDX / slowdown); const int dy = static_cast(hiresDY / slowdown); *x += dx; *y -= dy; lastTc = tc; // keep track of remainder for sub-pixel motion hiresDX -= dx * slowdown; hiresDY -= dy * slowdown; } void Clear() { lastTc = SDL_GetTicks(); } uint32_t lastTc; float hiresDX; float hiresDY; }; bool IsStickMovementSignificant() { // avoid sqrt() by comparing squared magnitudes const float leftStickMagnitudeSquared = leftStickX * leftStickX + leftStickY * leftStickY; const float thresholdSquared = StickDirectionThreshold * StickDirectionThreshold; return leftStickMagnitudeSquared >= thresholdSquared || rightStickX != 0 || rightStickY != 0; } ControlTypes GetInputTypeFromEvent(const SDL_Event &event) { switch (event.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: return ControlTypes::KeyboardAndMouse; #ifdef USE_SDL1 case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: case SDL_MOUSEMOTION: return ControlTypes::KeyboardAndMouse; #else // SDL 2/3-only events (touch / gamepad): case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: return event.button.which == SDL_TOUCH_MOUSEID ? ControlTypes::VirtualGamepad : ControlTypes::KeyboardAndMouse; case SDL_EVENT_MOUSE_MOTION: return event.motion.which == SDL_TOUCH_MOUSEID ? ControlTypes::VirtualGamepad : ControlTypes::KeyboardAndMouse; case SDL_EVENT_MOUSE_WHEEL: return event.wheel.which == SDL_TOUCH_MOUSEID ? ControlTypes::VirtualGamepad : ControlTypes::KeyboardAndMouse; case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_UP: case SDL_EVENT_FINGER_MOTION: return ControlTypes::VirtualGamepad; case SDL_EVENT_GAMEPAD_AXIS_MOTION: if (IsAnyOf(SDLC_EventGamepadAxis(event).axis, SDL_GAMEPAD_AXIS_LEFT_TRIGGER, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) || IsStickMovementSignificant()) { return ControlTypes::Gamepad; } break; #endif // !USE_SDL1 #ifndef USE_SDL1 case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_GAMEPAD_ADDED: #endif case SDL_EVENT_JOYSTICK_BALL_MOTION: case SDL_EVENT_JOYSTICK_HAT_MOTION: case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: return ControlTypes::Gamepad; case SDL_EVENT_JOYSTICK_AXIS_MOTION: #ifndef USE_SDL1 case SDL_EVENT_JOYSTICK_ADDED: #endif if (IsStickMovementSignificant()) return ControlTypes::Gamepad; break; default: break; } return ControlTypes::None; } float rightStickLastMove = 0; bool ContinueSimulatedMouseEvent(const SDL_Event &event, const ControllerButtonEvent &gamepadEvent) { if (!gbRunGame || AutomapActive) return false; #if !defined(USE_SDL1) && !defined(JOY_AXIS_RIGHTX) && !defined(JOY_AXIS_RIGHTY) if (IsAnyOf(event.type, SDL_EVENT_JOYSTICK_AXIS_MOTION, SDL_EVENT_JOYSTICK_HAT_MOTION, SDL_EVENT_JOYSTICK_BUTTON_DOWN, SDL_EVENT_JOYSTICK_BUTTON_UP) && !GameController::All().empty()) { return true; } #endif if (rightStickX != 0 || rightStickY != 0 || rightStickLastMove != 0) { rightStickLastMove = rightStickX + rightStickY; // Allow stick to come to a rest with out breaking simulation return true; } return SimulatingMouseWithPadmapper || IsSimulatedMouseClickBinding(gamepadEvent); } std::string_view ControlTypeToString(ControlTypes controlType) { switch (controlType) { case ControlTypes::None: return "None"; case ControlTypes::KeyboardAndMouse: return "KeyboardAndMouse"; case ControlTypes::Gamepad: return "Gamepad"; case ControlTypes::VirtualGamepad: return "VirtualGamepad"; } return "Invalid"; } void LogControlDeviceAndModeChange(ControlTypes newControlDevice, ControlTypes newControlMode) { if (!IsLogLevel(LogCategory::Application, SDL_LOG_PRIORITY_VERBOSE)) return; if (newControlDevice == ControlDevice && newControlMode == ControlMode) return; constexpr auto DebugChange = [](ControlTypes before, ControlTypes after) -> std::string { if (before == after) return std::string { ControlTypeToString(before) }; return StrCat(ControlTypeToString(before), " -> ", ControlTypeToString(after)); }; LogVerbose("Control: device {}, mode {}", DebugChange(ControlDevice, newControlDevice), DebugChange(ControlMode, newControlMode)); } #ifndef USE_SDL1 std::string_view GamepadTypeToString(GamepadLayout gamepadLayout) { switch (gamepadLayout) { case GamepadLayout::Nintendo: return "Nintendo"; case GamepadLayout::PlayStation: return "PlayStation"; case GamepadLayout::Xbox: return "Xbox"; case GamepadLayout::Generic: return "Unknown"; } return "Invalid"; } void LogGamepadChange(GamepadLayout newGamepad) { if (!IsLogLevel(LogCategory::Application, SDL_LOG_PRIORITY_VERBOSE)) return; constexpr auto DebugChange = [](GamepadLayout before, GamepadLayout after) -> std::string { if (before == after) return std::string { GamepadTypeToString(before) }; return StrCat(GamepadTypeToString(before), " -> ", GamepadTypeToString(after)); }; LogVerbose("Control: gamepad {}", DebugChange(GamepadType, newGamepad)); } #endif } // namespace void DetectInputMethod(const SDL_Event &event, const ControllerButtonEvent &gamepadEvent) { ControlTypes inputType = GetInputTypeFromEvent(event); if (inputType == ControlTypes::None) return; #ifdef __vita__ if (inputType == ControlTypes::VirtualGamepad) { inputType = ControlTypes::Gamepad; } #endif #if HAS_KBCTRL == 1 if (inputType == ControlTypes::KeyboardAndMouse && gamepadEvent.button != ControllerButton_NONE) { inputType = ControlTypes::Gamepad; } #endif const ControlTypes newControlDevice = inputType; ControlTypes newControlMode = inputType; if (ContinueSimulatedMouseEvent(event, gamepadEvent)) { newControlMode = ControlMode; } LogControlDeviceAndModeChange(newControlDevice, newControlMode); if (newControlDevice != ControlDevice) { ControlDevice = newControlDevice; #ifndef USE_SDL1 if (ControlDevice != ControlTypes::KeyboardAndMouse) { if (IsHardwareCursor()) SetHardwareCursor(CursorInfo::UnknownCursor()); } else { ResetCursor(); } if (ControlDevice == ControlTypes::Gamepad) { const GamepadLayout newGamepadLayout = GameController::getLayout(event); if (newGamepadLayout != GamepadType) { LogGamepadChange(newGamepadLayout); GamepadType = newGamepadLayout; } } #endif } if (newControlMode != ControlMode) { ControlMode = newControlMode; CalculatePanelAreas(); } } void ProcessGameAction(const GameAction &action) { switch (action.type) { case GameActionType_NONE: case GameActionType_SEND_KEY: break; case GameActionType_USE_HEALTH_POTION: UseBeltItem(BeltItemType::Healing); break; case GameActionType_USE_MANA_POTION: UseBeltItem(BeltItemType::Mana); break; case GameActionType_PRIMARY_ACTION: PerformPrimaryAction(); break; case GameActionType_SECONDARY_ACTION: PerformSecondaryAction(); break; case GameActionType_CAST_SPELL: if (!InGameMenu()) PerformSpellAction(); break; case GameActionType_TOGGLE_QUICK_SPELL_MENU: if (!invflag || BlurInventory()) { if (!SpellSelectFlag) DoSpeedBook(); else SpellSelectFlag = false; CloseCharPanel(); QuestLogIsOpen = false; SpellbookFlag = false; CloseGoldWithdraw(); CloseStash(); } break; case GameActionType_TOGGLE_CHARACTER_INFO: ToggleCharPanel(); if (CharFlag) { SpellSelectFlag = false; if (pcurs == CURSOR_DISARM) NewCursor(CURSOR_HAND); FocusOnCharInfo(); } break; case GameActionType_TOGGLE_QUEST_LOG: if (!QuestLogIsOpen) { StartQuestlog(); CloseCharPanel(); CloseGoldWithdraw(); CloseStash(); SpellSelectFlag = false; } else { QuestLogIsOpen = false; } break; case GameActionType_TOGGLE_INVENTORY: if (invflag) { BlurInventory(); } else { SpellbookFlag = false; SpellSelectFlag = false; invflag = true; if (pcurs == CURSOR_DISARM) NewCursor(CURSOR_HAND); FocusOnInventory(); } break; case GameActionType_TOGGLE_SPELL_BOOK: if (BlurInventory()) { CloseInventory(); SpellSelectFlag = false; SpellbookFlag = !SpellbookFlag; } break; } } void HandleRightStickMotion() { static RightStickAccumulator acc; // deadzone is handled in ScaleJoystickAxes() already if (rightStickX == 0 && rightStickY == 0) { acc.Clear(); return; } { // move cursor InvalidateInventorySlot(); int x = MousePosition.x; int y = MousePosition.y; acc.Pool(&x, &y, 2); x = std::min(std::max(x, 0), gnScreenWidth - 1); y = std::min(std::max(y, 0), gnScreenHeight - 1); // We avoid calling `SetCursorPos` within the same SDL tick because // that can cause all stick motion events to arrive before all // cursor position events. static int lastMouseSetTick = 0; const int now = SDL_GetTicks(); if (now - lastMouseSetTick > 0) { ResetCursor(); SetCursorPos({ x, y }); LogControlDeviceAndModeChange(ControlDevice, ControlTypes::KeyboardAndMouse); ControlMode = ControlTypes::KeyboardAndMouse; lastMouseSetTick = now; } } } void InvalidateInventorySlot() { Slot = -1; ActiveStashSlot = InvalidStashPoint; } /** * @brief Moves the mouse to the first inventory slot. */ void FocusOnInventory() { Slot = SLOTXY_INV_FIRST; ResetInvCursorPosition(); } bool PointAndClickState = false; void SetPointAndClick(bool value) { PointAndClickState = value; } bool IsPointAndClick() { return PointAndClickState; } bool IsMovementHandlerActive() { return GetLeftStickOrDPadGameUIHandler() != nullptr; } void plrctrls_after_check_curs_move() { // check for monsters first, then items, then towners. if (ControlMode == ControlTypes::KeyboardAndMouse || IsPointAndClick()) { return; } // While holding the button down we should retain target (but potentially lose it if it dies, goes out of view, etc) if (ControllerActionHeld != GameActionType_NONE && IsNoneOf(LastPlayerAction, PlayerActionType::None, PlayerActionType::Attack, PlayerActionType::Spell)) { InvalidateTargets(); if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && PlayerUnderCursor == nullptr) { FindTrigger(); } return; } // Clear focus set by cursor PlayerUnderCursor = nullptr; pcursmonst = -1; pcursitem = -1; ObjectUnderCursor = nullptr; pcursmissile = nullptr; pcurstrig = -1; pcursquest = Q_INVALID; cursPosition = { -1, -1 }; if (MyPlayer->_pInvincible) { return; } if (DoomFlag) { return; } if (!invflag) { InfoString = StringOrView {}; FindActor(); FindItemOrObject(); FindTrigger(); } } void plrctrls_every_frame() { ProcessLeftStickOrDPadGameUI(); HandleRightStickMotion(); ProcessAutomapMovementGamepad(); } void plrctrls_after_game_logic() { Movement(*MyPlayer); } void UseBeltItem(BeltItemType type) { for (int i = 0; i < MaxBeltItems; i++) { const Item &item = MyPlayer->SpdList[i]; if (item.isEmpty()) { continue; } const bool isRejuvenation = IsAnyOf(item._iMiscId, IMISC_REJUV, IMISC_FULLREJUV) || (item._iMiscId == IMISC_ARENAPOT && MyPlayer->isOnArenaLevel()); const bool isHealing = isRejuvenation || IsAnyOf(item._iMiscId, IMISC_HEAL, IMISC_FULLHEAL) || item.isScrollOf(SpellID::Healing); const bool isMana = isRejuvenation || IsAnyOf(item._iMiscId, IMISC_MANA, IMISC_FULLMANA); if ((type == BeltItemType::Healing && isHealing) || (type == BeltItemType::Mana && isMana)) { UseInvItem(INVITEM_BELT_FIRST + i); break; } } } void PerformPrimaryAction() { if (SpellSelectFlag) { SetSpell(); return; } if (invflag) { // inventory is open if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { if (pcurs == CURSOR_HOURGLASS) return; TryIconCurs(); NewCursor(CURSOR_HAND); } else if (GetRightPanel().contains(MousePosition) || GetMainPanel().contains(MousePosition)) { LiftInventoryItem(); } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { LiftStashItem(); } return; } if (CharFlag && !CharPanelButtonActive && MyPlayer->_pStatPts > 0) { CheckChrBtns(); if (CharPanelButtonActive) ReleaseChrBtns(false); return; } Interact(); } bool SpellHasActorTarget() { const SpellID spl = MyPlayer->_pRSpell; if (spl == SpellID::TownPortal || spl == SpellID::Teleport) return false; if (IsWallSpell(spl) && pcursmonst != -1) { cursPosition = Monsters[pcursmonst].position.tile; } return PlayerUnderCursor != nullptr || pcursmonst != -1; } void UpdateSpellTarget(SpellID spell) { if (SpellHasActorTarget()) return; PlayerUnderCursor = nullptr; pcursmonst = -1; const Player &myPlayer = *MyPlayer; const int range = spell == SpellID::Teleport ? 4 : 1; cursPosition = myPlayer.position.future + Displacement(myPlayer._pdir) * range; } /** * @brief Try dropping item in all 9 possible places */ bool TryDropItem() { Player &myPlayer = *MyPlayer; if (myPlayer.HoldItem.isEmpty()) { return false; } if (leveltype == DTYPE_TOWN) { if (UseItemOpensHive(myPlayer.HoldItem, myPlayer.position.tile)) { OpenHive(); NewCursor(CURSOR_HAND); return true; } if (UseItemOpensGrave(myPlayer.HoldItem, myPlayer.position.tile)) { OpenGrave(); NewCursor(CURSOR_HAND); return true; } } std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); if (!itemTile) { myPlayer.Say(HeroSpeech::WhereWouldIPutThis); return false; } NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); myPlayer.HoldItem.clear(); NewCursor(CURSOR_HAND); return true; } void PerformSpellAction() { if (SpellSelectFlag) { SetSpell(); return; } if (QuestLogIsOpen) return; if (invflag) { if (!MyPlayer->HoldItem.isEmpty()) TryDropItem(); else if (pcurs > CURSOR_HAND) { TryIconCurs(); NewCursor(CURSOR_HAND); } else if (pcursinvitem != -1) { const int itemId = GetItemIdOnSlot(Slot); CheckInvItem(true, false); if (itemId != GetItemIdOnSlot(Slot)) ResetInvCursorPosition(); } else if (pcursstashitem != StashStruct::EmptyCell) { CheckStashItem(MousePosition, true, false); } return; } if (!MyPlayer->HoldItem.isEmpty() && !TryDropItem()) return; if (pcurs > CURSOR_HAND) NewCursor(CURSOR_HAND); const Player &myPlayer = *MyPlayer; const SpellID spl = myPlayer._pRSpell; if ((PlayerUnderCursor == nullptr && (spl == SpellID::Resurrect || spl == SpellID::HealOther)) || (ObjectUnderCursor == nullptr && spl == SpellID::TrapDisarm)) { myPlayer.Say(HeroSpeech::ICantCastThatHere); return; } UpdateSpellTarget(myPlayer._pRSpell); CheckPlrSpell(false); if (PlayerUnderCursor != nullptr) LastPlayerAction = PlayerActionType::SpellPlayerTarget; else if (pcursmonst != -1) LastPlayerAction = PlayerActionType::SpellMonsterTarget; else LastPlayerAction = PlayerActionType::Spell; } void CtrlUseInvItem() { if (pcursinvitem == -1) { return; } Player &myPlayer = *MyPlayer; const Item &item = GetInventoryItem(myPlayer, pcursinvitem); if (item.isScroll()) { if (TargetsMonster(item._iSpell)) { return; } if (GetSpellData(item._iSpell).isTargeted()) { UpdateSpellTarget(item._iSpell); } } const int itemId = GetItemIdOnSlot(Slot); if (item.isEquipment()) { CheckInvItem(true, false); // auto-equip if it's an equipment } else { UseInvItem(pcursinvitem); } if (itemId != GetItemIdOnSlot(Slot)) { ResetInvCursorPosition(); } } void CtrlUseStashItem() { if (pcursstashitem == StashStruct::EmptyCell) { return; } const Item &item = Stash.stashList[pcursstashitem]; if (item.isScroll()) { if (TargetsMonster(item._iSpell)) { return; } if (GetSpellData(item._iSpell).isTargeted()) { UpdateSpellTarget(item._iSpell); } } if (item.isEquipment()) { CheckStashItem(MousePosition, true, false); // Auto-equip if it's equipment } else { UseStashItem(pcursstashitem); } // Todo reset cursor position if item is moved } void PerformSecondaryAction() { Player &myPlayer = *MyPlayer; if (invflag) { if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { TryIconCurs(); NewCursor(CURSOR_HAND); } else if (IsStashOpen) { if (pcursstashitem != StashStruct::EmptyCell) { TransferItemToInventory(myPlayer, pcursstashitem); } else if (pcursinvitem != -1) { TransferItemToStash(myPlayer, pcursinvitem); } } else { CtrlUseInvItem(); } return; } if (!MyPlayer->HoldItem.isEmpty() && !TryDropItem()) return; if (pcurs > CURSOR_HAND) NewCursor(CURSOR_HAND); if (pcursitem != -1) { NetSendCmdLocParam1(true, CMD_GOTOAGETITEM, cursPosition, pcursitem); } else if (ObjectUnderCursor != nullptr) { NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJXY, cursPosition); LastPlayerAction = PlayerActionType::OperateObject; } else { if (pcursmissile != nullptr) { MakePlrPath(myPlayer, pcursmissile->position.tile, true); myPlayer.destAction = ACTION_WALK; } else if (pcurstrig != -1) { MakePlrPath(myPlayer, trigs[pcurstrig].position, true); myPlayer.destAction = ACTION_WALK; } else if (pcursquest != Q_INVALID) { MakePlrPath(myPlayer, Quests[pcursquest].position, true); myPlayer.destAction = ACTION_WALK; } } } void QuickCast(size_t slot) { const PlayerActionType prevMouseButtonAction = LastPlayerAction; const Player &myPlayer = *MyPlayer; const SpellID spell = myPlayer._pSplHotKey[slot]; const SpellType spellType = myPlayer._pSplTHotKey[slot]; if (ControlMode != ControlTypes::KeyboardAndMouse) { UpdateSpellTarget(spell); } CheckPlrSpell(false, spell, spellType); LastPlayerAction = prevMouseButtonAction; } } // namespace devilution ================================================ FILE: Source/controls/plrctrls.h ================================================ #pragma once // Controller actions implementation #include #include #ifdef USE_SDL3 #include #else #include #endif #include "controls/controller.h" #include "controls/game_controls.h" #include "player.h" namespace devilution { enum class BeltItemType : uint8_t { Healing, Mana, }; extern GameActionType ControllerActionHeld; extern bool StandToggle; // Runs every frame. // Handles menu movement. void plrctrls_every_frame(); // Run after every game logic iteration. // Handles player movement. void plrctrls_after_game_logic(); // Runs at the end of CheckCursMove() // Handles item, object, and monster auto-aim. void plrctrls_after_check_curs_move(); // Moves the map if active, the cursor otherwise. void HandleRightStickMotion(); // Whether we're in a dialog menu that the game handles natively with keyboard controls. bool InGameMenu(); void SetPointAndClick(bool value); bool IsPointAndClick(); bool IsMovementHandlerActive(); void DetectInputMethod(const SDL_Event &event, const ControllerButtonEvent &gamepadEvent); void ProcessGameAction(const GameAction &action); void UseBeltItem(BeltItemType type); // Talk to towners, click on inv items, attack, etc. void PerformPrimaryAction(); // Open chests, doors, pickup items. void PerformSecondaryAction(); void UpdateSpellTarget(SpellID spell); bool TryDropItem(); void InvalidateInventorySlot(); void FocusOnInventory(); void PerformSpellAction(); void QuickCast(size_t slot); extern int speedspellcount; } // namespace devilution ================================================ FILE: Source/controls/remap_keyboard.h ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif namespace devilution { // Re-maps a keyboard key as per the REMAP_KEYBOARD_KEYS define. inline void remap_keyboard_key(SDL_Keycode *sym) { #ifdef REMAP_KEYBOARD_KEYS struct Mapping { SDL_Keycode from; SDL_Keycode to; }; constexpr Mapping kMappings[] = { REMAP_KEYBOARD_KEYS }; for (std::size_t i = 0; i < sizeof(kMappings) / sizeof(kMappings[0]); ++i) { if (*sym == kMappings[i].from) { *sym = kMappings[i].to; return; } } #endif } } // namespace devilution ================================================ FILE: Source/controls/touch/event_handlers.cpp ================================================ #include "controls/touch/event_handlers.h" #ifdef USE_SDL3 #include #include #else #include #endif #include "control/control.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "diablo.h" #include "engine/render/primitive_render.hpp" #include "engine/render/scrollrt.h" #include "game_mode.hpp" #include "gmenu.h" #include "inv.h" #include "panels/spell_book.hpp" #include "panels/spell_list.hpp" #include "qol/stash.h" #include "stores.h" #include "utils/is_of.hpp" #include "utils/sdl_compat.h" #include "utils/ui_fwd.h" namespace devilution { namespace { #if SDL_VERSION_ATLEAST(2, 0, 0) using SdlEventType = uint16_t; #else using SdlEventType = uint8_t; #endif VirtualGamepadEventHandler Handler(&VirtualGamepadState); Point ScaleToScreenCoordinates(float x, float y) { return Point { (int)round(x * gnScreenWidth), (int)round(y * gnScreenHeight) }; } constexpr bool IsFingerDown(const SDL_Event &event) { return event.type == SDL_EVENT_FINGER_DOWN; } constexpr bool IsFingerUp(const SDL_Event &event) { return event.type == SDL_EVENT_FINGER_UP; } constexpr bool IsFingerMotion(const SDL_Event &event) { return event.type == SDL_EVENT_FINGER_MOTION; } constexpr SDL_FingerID FingerId(const SDL_TouchFingerEvent &event) { #ifdef USE_SDL3 return event.fingerID; #else return event.fingerId; #endif } void SimulateMouseMovement(const SDL_Event &event) { const Point position = ScaleToScreenCoordinates(event.tfinger.x, event.tfinger.y); const bool isInMainPanel = GetMainPanel().contains(position); const bool isInLeftPanel = GetLeftPanel().contains(position); const bool isInRightPanel = GetRightPanel().contains(position); if (IsStashOpen) { if (!SpellSelectFlag && !isInMainPanel && !isInLeftPanel && !isInRightPanel) return; } else if (invflag) { if (!SpellSelectFlag && !isInMainPanel && !isInRightPanel) return; } MousePosition = position; SetPointAndClick(true); InvalidateInventorySlot(); } bool HandleGameMenuInteraction(const SDL_Event &event) { if (!gmenu_is_active()) return false; if (IsFingerDown(event) && gmenu_left_mouse(true)) return true; if (IsFingerMotion(event) && gmenu_on_mouse_move()) return true; return IsFingerUp(event) && gmenu_left_mouse(false); } bool HandleStoreInteraction(const SDL_Event &event) { if (!IsPlayerInStore()) return false; if (IsFingerDown(event)) CheckStoreBtn(); return true; } void HandleSpellBookInteraction(const SDL_Event &event) { if (!SpellbookFlag) return; if (IsFingerUp(event)) CheckSBook(); } bool HandleSpeedBookInteraction(const SDL_Event &event) { if (!SpellSelectFlag) return false; if (IsFingerUp(event)) SetSpell(); return true; } void HandleBottomPanelInteraction(const SDL_Event &event) { if (!gbRunGame || !MyPlayer->HoldItem.isEmpty()) return; ResetMainPanelButtons(); if (!IsFingerUp(event)) { SpellSelectFlag = true; CheckMainPanelButton(); SpellSelectFlag = false; } else { CheckMainPanelButton(); if (MainPanelButtonDown) CheckMainPanelButtonUp(); } } void HandleCharacterPanelInteraction(const SDL_Event &event) { if (!CharFlag) return; if (IsFingerDown(event)) CheckChrBtns(); else if (IsFingerUp(event) && CharPanelButtonActive) ReleaseChrBtns(false); } void HandleStashPanelInteraction(const SDL_Event &event) { if (!IsStashOpen || !MyPlayer->HoldItem.isEmpty()) return; if (!IsFingerUp(event)) { CheckStashButtonPress(MousePosition); } else { CheckStashButtonRelease(MousePosition); } } SdlEventType GetDeactivateEventType() { static const SdlEventType customEventType = SDL_RegisterEvents(1); return customEventType; } bool IsDeactivateEvent(const SDL_Event &event) { return event.type == GetDeactivateEventType(); } } // namespace void HandleTouchEvent(const SDL_Event &event) { SetPointAndClick(false); if (Handler.Handle(event)) { return; } if (!IsFingerDown(event) && !IsFingerUp(event) && !IsFingerMotion(event)) { return; } SimulateMouseMovement(event); if (HandleGameMenuInteraction(event)) return; if (HandleStoreInteraction(event)) return; if (HandleSpeedBookInteraction(event)) return; HandleSpellBookInteraction(event); HandleBottomPanelInteraction(event); HandleCharacterPanelInteraction(event); HandleStashPanelInteraction(event); } void DeactivateTouchEventHandlers() { SDL_Event event; event.type = GetDeactivateEventType(); HandleTouchEvent(event); } bool VirtualGamepadEventHandler::Handle(const SDL_Event &event) { if (!IsDeactivateEvent(event)) { if (!VirtualGamepadState.isActive || (!IsFingerDown(event) && !IsFingerUp(event) && !IsFingerMotion(event))) { VirtualGamepadState.primaryActionButton.didStateChange = false; VirtualGamepadState.secondaryActionButton.didStateChange = false; VirtualGamepadState.spellActionButton.didStateChange = false; VirtualGamepadState.cancelButton.didStateChange = false; return false; } } if (charMenuButtonEventHandler.Handle(event)) return true; if (questsMenuButtonEventHandler.Handle(event)) return true; if (inventoryMenuButtonEventHandler.Handle(event)) return true; if (mapMenuButtonEventHandler.Handle(event)) return true; if (directionPadEventHandler.Handle(event)) return true; if (leveltype != DTYPE_TOWN && standButtonEventHandler.Handle(event)) return true; if (primaryActionButtonEventHandler.Handle(event)) return true; if (secondaryActionButtonEventHandler.Handle(event)) return true; if (spellActionButtonEventHandler.Handle(event)) return true; if (cancelButtonEventHandler.Handle(event)) return true; if (healthButtonEventHandler.Handle(event)) return true; if (manaButtonEventHandler.Handle(event)) return true; return false; } bool VirtualDirectionPadEventHandler::Handle(const SDL_Event &event) { if (IsDeactivateEvent(event)) { isActive = false; return false; } if (IsFingerDown(event)) return HandleFingerDown(event.tfinger); if (IsFingerUp(event)) return HandleFingerUp(event.tfinger); if (IsFingerMotion(event)) return HandleFingerMotion(event.tfinger); return false; } bool VirtualDirectionPadEventHandler::HandleFingerDown(const SDL_TouchFingerEvent &event) { if (isActive) return false; const float x = event.x; const float y = event.y; const Point touchCoordinates = ScaleToScreenCoordinates(x, y); if (!virtualDirectionPad->area.contains(touchCoordinates)) return false; virtualDirectionPad->UpdatePosition(touchCoordinates); activeFinger = FingerId(event); isActive = true; return true; } bool VirtualDirectionPadEventHandler::HandleFingerUp(const SDL_TouchFingerEvent &event) { if (!isActive || FingerId(event) != activeFinger) return false; const Point position = virtualDirectionPad->area.position; virtualDirectionPad->UpdatePosition(position); isActive = false; return true; } bool VirtualDirectionPadEventHandler::HandleFingerMotion(const SDL_TouchFingerEvent &event) { if (!isActive || FingerId(event) != activeFinger) return false; const float x = event.x; const float y = event.y; const Point touchCoordinates = ScaleToScreenCoordinates(x, y); virtualDirectionPad->UpdatePosition(touchCoordinates); return true; } bool VirtualButtonEventHandler::Handle(const SDL_Event &event) { if (IsDeactivateEvent(event)) { isActive = false; return false; } if (!virtualButton->isUsable()) { virtualButton->didStateChange = virtualButton->isHeld; virtualButton->isHeld = false; return false; } virtualButton->didStateChange = false; if (IsFingerDown(event)) return HandleFingerDown(event.tfinger); if (IsFingerUp(event)) return HandleFingerUp(event.tfinger); if (IsFingerMotion(event)) return HandleFingerMotion(event.tfinger); return false; } bool VirtualButtonEventHandler::HandleFingerDown(const SDL_TouchFingerEvent &event) { if (isActive) return false; const float x = event.x; const float y = event.y; const Point touchCoordinates = ScaleToScreenCoordinates(x, y); if (!virtualButton->contains(touchCoordinates)) return false; if (toggles) virtualButton->isHeld = !virtualButton->isHeld; else virtualButton->isHeld = true; virtualButton->didStateChange = true; activeFinger = FingerId(event); isActive = true; return true; } bool VirtualButtonEventHandler::HandleFingerUp(const SDL_TouchFingerEvent &event) { if (!isActive || FingerId(event) != activeFinger) return false; if (!toggles) { if (virtualButton->isHeld) virtualButton->didStateChange = true; virtualButton->isHeld = false; } isActive = false; return true; } bool VirtualButtonEventHandler::HandleFingerMotion(const SDL_TouchFingerEvent &event) { if (!isActive || FingerId(event) != activeFinger) return false; if (toggles) return true; const float x = event.x; const float y = event.y; const Point touchCoordinates = ScaleToScreenCoordinates(x, y); const bool wasHeld = virtualButton->isHeld; virtualButton->isHeld = virtualButton->contains(touchCoordinates); virtualButton->didStateChange = virtualButton->isHeld != wasHeld; return true; } } // namespace devilution ================================================ FILE: Source/controls/touch/event_handlers.h ================================================ #pragma once #ifdef USE_SDL3 #include #else #include #endif #include "controls/touch/gamepad.h" namespace devilution { class VirtualDirectionPadEventHandler { public: VirtualDirectionPadEventHandler(VirtualDirectionPad *virtualDirectionPad) : virtualDirectionPad(virtualDirectionPad) , activeFinger(0) , isActive(false) { } bool Handle(const SDL_Event &event); private: VirtualDirectionPad *virtualDirectionPad; SDL_FingerID activeFinger; bool isActive; bool HandleFingerDown(const SDL_TouchFingerEvent &event); bool HandleFingerUp(const SDL_TouchFingerEvent &event); bool HandleFingerMotion(const SDL_TouchFingerEvent &event); }; class VirtualButtonEventHandler { public: VirtualButtonEventHandler(VirtualButton *virtualButton, bool toggles = false) : virtualButton(virtualButton) , activeFinger(0) , isActive(false) , toggles(toggles) { } bool Handle(const SDL_Event &event); private: VirtualButton *virtualButton; SDL_FingerID activeFinger; bool isActive; bool toggles; bool HandleFingerDown(const SDL_TouchFingerEvent &event); bool HandleFingerUp(const SDL_TouchFingerEvent &event); bool HandleFingerMotion(const SDL_TouchFingerEvent &event); }; class VirtualGamepadEventHandler { public: VirtualGamepadEventHandler(VirtualGamepad *virtualGamepad) : charMenuButtonEventHandler(&virtualGamepad->menuPanel.charButton) , questsMenuButtonEventHandler(&virtualGamepad->menuPanel.questsButton) , inventoryMenuButtonEventHandler(&virtualGamepad->menuPanel.inventoryButton) , mapMenuButtonEventHandler(&virtualGamepad->menuPanel.mapButton) , directionPadEventHandler(&virtualGamepad->directionPad) , standButtonEventHandler(&virtualGamepad->standButton, true) , primaryActionButtonEventHandler(&virtualGamepad->primaryActionButton) , secondaryActionButtonEventHandler(&virtualGamepad->secondaryActionButton) , spellActionButtonEventHandler(&virtualGamepad->spellActionButton) , cancelButtonEventHandler(&virtualGamepad->cancelButton) , healthButtonEventHandler(&virtualGamepad->healthButton) , manaButtonEventHandler(&virtualGamepad->manaButton) { } bool Handle(const SDL_Event &event); private: VirtualButtonEventHandler charMenuButtonEventHandler; VirtualButtonEventHandler questsMenuButtonEventHandler; VirtualButtonEventHandler inventoryMenuButtonEventHandler; VirtualButtonEventHandler mapMenuButtonEventHandler; VirtualDirectionPadEventHandler directionPadEventHandler; VirtualButtonEventHandler standButtonEventHandler; VirtualButtonEventHandler primaryActionButtonEventHandler; VirtualButtonEventHandler secondaryActionButtonEventHandler; VirtualButtonEventHandler spellActionButtonEventHandler; VirtualButtonEventHandler cancelButtonEventHandler; VirtualButtonEventHandler healthButtonEventHandler; VirtualButtonEventHandler manaButtonEventHandler; }; void HandleTouchEvent(const SDL_Event &event); void DeactivateTouchEventHandlers(); } // namespace devilution ================================================ FILE: Source/controls/touch/gamepad.cpp ================================================ #include #ifdef USE_SDL3 #include #else #include #endif #include "control/control.hpp" #include "controls/touch/event_handlers.h" #include "controls/touch/gamepad.h" #include "quests.h" #include "utils/display.h" #include "utils/ui_fwd.h" namespace devilution { VirtualGamepad VirtualGamepadState; namespace { constexpr float Pi = 3.141592653589793F; int roundToInt(float value) { return static_cast(round(value)); } constexpr bool PointsUp(float angle) { constexpr float UpAngle = Pi / 2; constexpr float MinAngle = UpAngle - 3 * Pi / 8; constexpr float MaxAngle = UpAngle + 3 * Pi / 8; return MinAngle <= angle && angle <= MaxAngle; } constexpr bool PointsDown(float angle) { constexpr float DownAngle = -Pi / 2; constexpr float MinAngle = DownAngle - 3 * Pi / 8; constexpr float MaxAngle = DownAngle + 3 * Pi / 8; return MinAngle <= angle && angle <= MaxAngle; } constexpr bool PointsLeft(float angle) { constexpr float MaxAngle = Pi - 3 * Pi / 8; constexpr float MinAngle = -Pi + 3 * Pi / 8; return !(MinAngle < angle && angle < MaxAngle); } constexpr bool PointsRight(float angle) { constexpr float MinAngle = -3 * Pi / 8; constexpr float MaxAngle = 3 * Pi / 8; return MinAngle <= angle && angle <= MaxAngle; } } // namespace void InitializeVirtualGamepad() { const float sqrt2 = sqrtf(2.0F); const int screenPixels = std::min(gnScreenWidth, gnScreenHeight); int inputMargin = screenPixels / 10; int menuButtonWidth = screenPixels / 10; int directionPadSize = screenPixels / 4; int padButtonSize = roundToInt(1.1F * screenPixels / 10); int padButtonSpacing = inputMargin / 3; #ifdef USE_SDL3 const float dpi = SDL_GetWindowDisplayScale(ghMainWnd); #else float dpi = 0.0F; float hdpi; float vdpi; const int displayIndex = SDL_GetWindowDisplayIndex(ghMainWnd); if (SDL_GetDisplayDPI(displayIndex, nullptr, &hdpi, &vdpi) == 0) { int clientWidth; int clientHeight; if (renderer != nullptr) SDL_GetRendererOutputSize(renderer, &clientWidth, &clientHeight); else SDL_GetWindowSize(ghMainWnd, &clientWidth, &clientHeight); hdpi *= static_cast(gnScreenWidth) / clientWidth; vdpi *= static_cast(gnScreenHeight) / clientHeight; dpi = std::min(hdpi, vdpi); } #endif if (dpi != 0.0F) { inputMargin = roundToInt(0.25F * dpi); menuButtonWidth = roundToInt(0.2F * dpi); directionPadSize = roundToInt(dpi); padButtonSize = roundToInt(0.3F * dpi); padButtonSpacing = roundToInt(0.1F * dpi); } const int menuPanelTopMargin = 30; const int menuPanelButtonSpacing = 4; const Size menuPanelButtonSize = { 64, 62 }; const int rightMarginMenuButton4 = menuPanelButtonSpacing + menuPanelButtonSize.width; const int rightMarginMenuButton3 = rightMarginMenuButton4 + menuPanelButtonSpacing + menuPanelButtonSize.width; const int rightMarginMenuButton2 = rightMarginMenuButton3 + menuPanelButtonSpacing + menuPanelButtonSize.width; const int rightMarginMenuButton1 = rightMarginMenuButton2 + menuPanelButtonSpacing + menuPanelButtonSize.width; const int padButtonAreaWidth = roundToInt(sqrt2 * (padButtonSize + padButtonSpacing)); const int padButtonRight = gnScreenWidth - inputMargin - padButtonSize / 2; const int padButtonLeft = padButtonRight - padButtonAreaWidth; const int padButtonBottom = gnScreenHeight - inputMargin - padButtonSize / 2; const int padButtonTop = padButtonBottom - padButtonAreaWidth; Rectangle &charButtonArea = VirtualGamepadState.menuPanel.charButton.area; charButtonArea.position.x = gnScreenWidth - rightMarginMenuButton1 * menuButtonWidth / menuPanelButtonSize.width; charButtonArea.position.y = menuPanelTopMargin * menuButtonWidth / menuPanelButtonSize.width; charButtonArea.size.width = menuButtonWidth; charButtonArea.size.height = menuPanelButtonSize.height * menuButtonWidth / menuPanelButtonSize.width; Rectangle &questsButtonArea = VirtualGamepadState.menuPanel.questsButton.area; questsButtonArea.position.x = gnScreenWidth - rightMarginMenuButton2 * menuButtonWidth / menuPanelButtonSize.width; questsButtonArea.position.y = menuPanelTopMargin * menuButtonWidth / menuPanelButtonSize.width; questsButtonArea.size.width = menuButtonWidth; questsButtonArea.size.height = menuPanelButtonSize.height * menuButtonWidth / menuPanelButtonSize.width; Rectangle &inventoryButtonArea = VirtualGamepadState.menuPanel.inventoryButton.area; inventoryButtonArea.position.x = gnScreenWidth - rightMarginMenuButton3 * menuButtonWidth / menuPanelButtonSize.width; inventoryButtonArea.position.y = menuPanelTopMargin * menuButtonWidth / menuPanelButtonSize.width; inventoryButtonArea.size.width = menuButtonWidth; inventoryButtonArea.size.height = menuPanelButtonSize.height * menuButtonWidth / menuPanelButtonSize.width; Rectangle &mapButtonArea = VirtualGamepadState.menuPanel.mapButton.area; mapButtonArea.position.x = gnScreenWidth - rightMarginMenuButton4 * menuButtonWidth / menuPanelButtonSize.width; mapButtonArea.position.y = menuPanelTopMargin * menuButtonWidth / menuPanelButtonSize.width; mapButtonArea.size.width = menuButtonWidth; mapButtonArea.size.height = menuPanelButtonSize.height * menuButtonWidth / menuPanelButtonSize.width; Rectangle &menuPanelArea = VirtualGamepadState.menuPanel.area; menuPanelArea.position.x = gnScreenWidth - 399 * menuButtonWidth / menuPanelButtonSize.width; menuPanelArea.position.y = 0; menuPanelArea.size.width = 399 * menuButtonWidth / menuPanelButtonSize.width; menuPanelArea.size.height = 162 * menuButtonWidth / menuPanelButtonSize.width; VirtualDirectionPad &directionPad = VirtualGamepadState.directionPad; Circle &directionPadArea = directionPad.area; directionPadArea.position.x = inputMargin + directionPadSize / 2; directionPadArea.position.y = gnScreenHeight - inputMargin - directionPadSize / 2; directionPadArea.radius = directionPadSize / 2; directionPad.position = directionPadArea.position; const int standButtonDiagonalOffset = directionPadArea.radius + padButtonSpacing / 2 + padButtonSize / 2; const int standButtonOffset = roundToInt(standButtonDiagonalOffset / sqrt2); Circle &standButtonArea = VirtualGamepadState.standButton.area; standButtonArea.position.x = directionPadArea.position.x - standButtonOffset; standButtonArea.position.y = directionPadArea.position.y + standButtonOffset; standButtonArea.radius = padButtonSize / 2; Circle &primaryActionButtonArea = VirtualGamepadState.primaryActionButton.area; primaryActionButtonArea.position.x = padButtonRight; primaryActionButtonArea.position.y = (padButtonTop + padButtonBottom) / 2; primaryActionButtonArea.radius = padButtonSize / 2; Circle &secondaryActionButtonArea = VirtualGamepadState.secondaryActionButton.area; secondaryActionButtonArea.position.x = (padButtonLeft + padButtonRight) / 2; secondaryActionButtonArea.position.y = padButtonTop; secondaryActionButtonArea.radius = padButtonSize / 2; Circle &spellActionButtonArea = VirtualGamepadState.spellActionButton.area; spellActionButtonArea.position.x = padButtonLeft; spellActionButtonArea.position.y = (padButtonTop + padButtonBottom) / 2; spellActionButtonArea.radius = padButtonSize / 2; Circle &cancelButtonArea = VirtualGamepadState.cancelButton.area; cancelButtonArea.position.x = (padButtonLeft + padButtonRight) / 2; cancelButtonArea.position.y = padButtonBottom; cancelButtonArea.radius = padButtonSize / 2; VirtualPadButton &healthButton = VirtualGamepadState.healthButton; Circle &healthButtonArea = healthButton.area; healthButtonArea.position.x = directionPad.area.position.x - (padButtonSize + padButtonSpacing) / 2; healthButtonArea.position.y = directionPad.area.position.y - (directionPadSize + padButtonSize + padButtonSpacing) / 2; healthButtonArea.radius = padButtonSize / 2; healthButton.isUsable = []() { return !CharFlag && !QuestLogIsOpen; }; VirtualPadButton &manaButton = VirtualGamepadState.manaButton; Circle &manaButtonArea = manaButton.area; manaButtonArea.position.x = directionPad.area.position.x + (padButtonSize + padButtonSpacing) / 2; manaButtonArea.position.y = directionPad.area.position.y - (directionPadSize + padButtonSize + padButtonSpacing) / 2; manaButtonArea.radius = padButtonSize / 2; manaButton.isUsable = []() { return !CharFlag && !QuestLogIsOpen; }; } void ActivateVirtualGamepad() { VirtualGamepadState.isActive = true; } void DeactivateVirtualGamepad() { VirtualGamepadState.Deactivate(); DeactivateTouchEventHandlers(); } void VirtualGamepad::Deactivate() { isActive = false; menuPanel.Deactivate(); directionPad.Deactivate(); standButton.Deactivate(); primaryActionButton.Deactivate(); secondaryActionButton.Deactivate(); spellActionButton.Deactivate(); cancelButton.Deactivate(); healthButton.Deactivate(); manaButton.Deactivate(); } void VirtualMenuPanel::Deactivate() { charButton.Deactivate(); questsButton.Deactivate(); inventoryButton.Deactivate(); mapButton.Deactivate(); } void VirtualDirectionPad::UpdatePosition(Point touchCoordinates) { position = touchCoordinates; const Displacement diff = position - area.position; if (diff == Displacement { 0, 0 }) { isUpPressed = false; isDownPressed = false; isLeftPressed = false; isRightPressed = false; return; } if (!area.contains(position)) { int x = diff.deltaX; int y = diff.deltaY; const float dist = sqrtf(static_cast(x * x + y * y)); x = roundToInt(x * area.radius / dist); y = roundToInt(y * area.radius / dist); position.x = area.position.x + x; position.y = area.position.y + y; } const float angle = atan2f(static_cast(-diff.deltaY), static_cast(diff.deltaX)); isUpPressed = PointsUp(angle); isDownPressed = PointsDown(angle); isLeftPressed = PointsLeft(angle); isRightPressed = PointsRight(angle); } void VirtualDirectionPad::Deactivate() { position = area.position; isUpPressed = false; isDownPressed = false; isLeftPressed = false; isRightPressed = false; } void VirtualButton::Deactivate() { isHeld = false; didStateChange = false; } } // namespace devilution ================================================ FILE: Source/controls/touch/gamepad.h ================================================ #pragma once #include #include "controls/controller_buttons.h" #include "engine/circle.hpp" #include "engine/point.hpp" #include "engine/rectangle.hpp" namespace devilution { struct VirtualDirectionPad { Circle area; Point position; bool isUpPressed { false }; bool isDownPressed { false }; bool isLeftPressed { false }; bool isRightPressed { false }; VirtualDirectionPad() : area({ { 0, 0 }, 0 }) , position({ 0, 0 }) { } void UpdatePosition(Point touchCoordinates); void Deactivate(); }; struct VirtualButton { bool isHeld { false }; bool didStateChange { false }; std::function isUsable; VirtualButton() : isUsable([]() { return true; }) { } virtual bool contains(Point point) = 0; void Deactivate(); }; struct VirtualMenuButton : VirtualButton { Rectangle area; VirtualMenuButton() : area({ { 0, 0 }, { 0, 0 } }) { } bool contains(Point point) override { return area.contains(point); } }; struct VirtualPadButton : VirtualButton { Circle area; VirtualPadButton() : area({ { 0, 0 }, 0 }) { } bool contains(Point point) override { return area.contains(point); } }; struct VirtualMenuPanel { VirtualMenuButton charButton; VirtualMenuButton questsButton; VirtualMenuButton inventoryButton; VirtualMenuButton mapButton; Rectangle area; VirtualMenuPanel() : area({ { 0, 0 }, { 0, 0 } }) { } void Deactivate(); }; struct VirtualGamepad { VirtualMenuPanel menuPanel; VirtualDirectionPad directionPad; VirtualPadButton standButton; VirtualPadButton primaryActionButton; VirtualPadButton secondaryActionButton; VirtualPadButton spellActionButton; VirtualPadButton cancelButton; VirtualPadButton healthButton; VirtualPadButton manaButton; bool isActive { false }; VirtualGamepad() = default; void Deactivate(); }; void InitializeVirtualGamepad(); void ActivateVirtualGamepad(); void DeactivateVirtualGamepad(); extern VirtualGamepad VirtualGamepadState; } // namespace devilution ================================================ FILE: Source/controls/touch/renderers.cpp ================================================ #include "controls/touch/renderers.h" #ifdef USE_SDL3 #include #else #include #endif #include "control/control.hpp" #include "cursor.h" #include "diablo.h" #include "doom.h" #include "engine/events.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" #include "init.hpp" #include "inv.h" #include "levels/gendung.h" #include "minitext.h" #include "panels/ui_panels.hpp" #include "qol/stash.h" #include "stores.h" #include "towners.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/sdl_wrap.h" namespace devilution { namespace { VirtualGamepadRenderer Renderer(&VirtualGamepadState); VirtualGamepadButtonType GetAttackButtonType(bool isPressed) { return isPressed ? GAMEPAD_ATTACKDOWN : GAMEPAD_ATTACK; } VirtualGamepadButtonType GetTalkButtonType(bool isPressed) { return isPressed ? GAMEPAD_TALKDOWN : GAMEPAD_TALK; } VirtualGamepadButtonType GetItemButtonType(bool isPressed) { return isPressed ? GAMEPAD_ITEMDOWN : GAMEPAD_ITEM; } VirtualGamepadButtonType GetObjectButtonType(bool isPressed) { return isPressed ? GAMEPAD_OBJECTDOWN : GAMEPAD_OBJECT; } VirtualGamepadButtonType GetCastButtonType(bool isPressed) { return isPressed ? GAMEPAD_CASTSPELLDOWN : GAMEPAD_CASTSPELL; } VirtualGamepadButtonType GetBackButtonType(bool isPressed) { return isPressed ? GAMEPAD_BACKDOWN : GAMEPAD_BACK; } VirtualGamepadButtonType GetBlankButtonType(bool isPressed) { return isPressed ? GAMEPAD_BLANKDOWN : GAMEPAD_BLANK; } VirtualGamepadButtonType GetPotionButtonType(bool isPressed) { return isPressed ? GAMEPAD_POTIONDOWN : GAMEPAD_POTION; } VirtualGamepadButtonType GetApplyButtonType(bool isPressed) { return isPressed ? GAMEPAD_APPLYDOWN : GAMEPAD_APPLY; } VirtualGamepadButtonType GetEquipButtonType(bool isPressed) { return isPressed ? GAMEPAD_EQUIPDOWN : GAMEPAD_EQUIP; } VirtualGamepadButtonType GetDropButtonType(bool isPressed) { return isPressed ? GAMEPAD_DROPDOWN : GAMEPAD_DROP; } VirtualGamepadButtonType GetStairsButtonType(bool isPressed) { return isPressed ? GAMEPAD_STAIRSDOWN : GAMEPAD_STAIRS; } VirtualGamepadButtonType GetStandButtonType(bool isPressed) { return isPressed ? GAMEPAD_STANDDOWN : GAMEPAD_STAND; } void LoadButtonArt(ButtonTexture *buttonArt) { constexpr unsigned Sprites = 13; constexpr unsigned Frames = 2; buttonArt->surface.reset(LoadPNG("ui_art\\button.png")); if (buttonArt->surface == nullptr) return; buttonArt->numSprites = Sprites; buttonArt->numFrames = Frames; } void LoadPotionArt(ButtonTexture *potionArt) { const item_cursor_graphic potionGraphics[] { ICURS_POTION_OF_HEALING, ICURS_POTION_OF_MANA, ICURS_POTION_OF_REJUVENATION, ICURS_POTION_OF_FULL_HEALING, ICURS_POTION_OF_FULL_MANA, ICURS_POTION_OF_FULL_REJUVENATION, ICURS_ARENA_POTION, ICURS_SCROLL_OF }; const int potionFrame = static_cast(CURSOR_FIRSTITEM) + static_cast(ICURS_POTION_OF_HEALING); const Size potionSize = GetInvItemSize(potionFrame); auto surface = SDLWrap::CreateRGBSurfaceWithFormat( /*flags=*/0, /*width=*/potionSize.width, /*height=*/potionSize.height * sizeof(potionGraphics), /*depth=*/8, SDL_PIXELFORMAT_INDEX8); auto palette = SDLWrap::AllocPalette(); if (!SDLC_SetSurfaceAndPaletteColors(surface.get(), palette.get(), logical_palette.data(), 0, 256)) ErrSdl(); #ifdef USE_SDL3 const Uint32 bgColor = SDL_MapRGB(SDL_GetPixelFormatDetails(surface->format), SDL_GetSurfacePalette(&*surface), logical_palette[1].r, logical_palette[1].g, logical_palette[1].b); if (!SDL_FillSurfaceRect(surface.get(), nullptr, bgColor)) ErrSdl(); if (!SDL_SetSurfaceColorKey(surface.get(), true, bgColor)) ErrSdl(); #else const Uint32 bgColor = SDL_MapRGB(surface->format, logical_palette[1].r, logical_palette[1].g, logical_palette[1].b); if (SDL_FillRect(surface.get(), nullptr, bgColor) < 0) ErrSdl(); if (SDL_SetColorKey(surface.get(), SDL_TRUE, bgColor) < 0) ErrSdl(); #endif Point position { 0, 0 }; for (const item_cursor_graphic graphic : potionGraphics) { const int cursorID = static_cast(CURSOR_FIRSTITEM) + graphic; position.y += potionSize.height; ClxDraw(Surface(surface.get()), position, GetInvItemSprite(cursorID)); } potionArt->numFrames = sizeof(potionGraphics); potionArt->surface.reset( #ifdef USE_SDL3 SDL_ConvertSurface(surface.get(), SDL_PIXELFORMAT_ARGB8888) #else SDL_ConvertSurfaceFormat(surface.get(), SDL_PIXELFORMAT_ARGB8888, 0) #endif ); } bool InteractsWithCharButton(Point point) { const Player &myPlayer = *MyPlayer; if (myPlayer._pStatPts == 0) return false; for (auto attribute : enum_values()) { if (myPlayer.GetBaseAttributeValue(attribute) >= myPlayer.GetMaximumAttributeValue(attribute)) continue; auto buttonId = static_cast(attribute); Rectangle button = CharPanelButtonRect[buttonId]; button.position = GetPanelPosition(UiPanels::Character, button.position); if (button.contains(point)) { return true; } } return false; } } // namespace Size ButtonTexture::size() const { int w, h; if (surface != nullptr) { w = surface->w; h = surface->h; } else { #ifdef USE_SDL3 float fw, fh; SDL_GetTextureSize(texture.get(), &fw, &fh); w = fw; h = fh; #else SDL_QueryTexture(texture.get(), /*format=*/nullptr, /*access=*/nullptr, &w, &h); #endif } w /= numSprites; h /= numFrames; return Size { w, h }; } void RenderVirtualGamepad(SDL_Renderer *renderer) { if (!gbRunGame) return; const RenderFunction renderFunction = [renderer](const ButtonTexture &art, SDL_Rect *src, SDL_Rect *dst) { if (art.texture == nullptr) return; #ifdef USE_SDL3 SDL_FRect fsrc, fdst; SDL_RectToFRect(src, &fsrc); SDL_RectToFRect(dst, &fdst); if (!SDL_RenderTexture(renderer, art.texture.get(), &fsrc, &fdst)) ErrSdl(); #else if (SDL_RenderCopy(renderer, art.texture.get(), src, dst) <= -1) ErrSdl(); #endif }; Renderer.Render(renderFunction); } void RenderVirtualGamepad(SDL_Surface *surface) { if (!gbRunGame) return; const RenderFunction renderFunction = [surface](const ButtonTexture &art, SDL_Rect *src, SDL_Rect *dst) { if (art.surface == nullptr) return; #ifdef USE_SDL3 if (!SDL_BlitSurfaceScaled(art.surface.get(), src, surface, dst, SDL_SCALEMODE_LINEAR)) #else if (SDL_BlitScaled(art.surface.get(), src, surface, dst) <= -1) #endif ErrSdl(); }; Renderer.Render(renderFunction); } void VirtualGamepadRenderer::LoadArt() { menuPanelRenderer.LoadArt(); directionPadRenderer.LoadArt(); LoadButtonArt(&buttonArt); LoadPotionArt(&potionArt); } void VirtualMenuPanelRenderer::LoadArt() { menuArt.surface.reset(LoadPNG("ui_art\\menu.png")); menuArtLevelUp.surface.reset(LoadPNG("ui_art\\menu-levelup.png")); } void VirtualDirectionPadRenderer::LoadArt() { padArt.surface.reset(LoadPNG("ui_art\\directions.png")); knobArt.surface.reset(LoadPNG("ui_art\\directions2.png")); } void VirtualGamepadRenderer::Render(RenderFunction renderFunction) { if (CurrentEventHandler == DisableInputEventHandler) return; primaryActionButtonRenderer.Render(renderFunction, buttonArt); secondaryActionButtonRenderer.Render(renderFunction, buttonArt); spellActionButtonRenderer.Render(renderFunction, buttonArt); cancelButtonRenderer.Render(renderFunction, buttonArt); healthButtonRenderer.Render(renderFunction, buttonArt); manaButtonRenderer.Render(renderFunction, buttonArt); healthButtonRenderer.RenderPotion(renderFunction, potionArt); manaButtonRenderer.RenderPotion(renderFunction, potionArt); if (leveltype != DTYPE_TOWN) standButtonRenderer.Render(renderFunction, buttonArt); directionPadRenderer.Render(renderFunction); menuPanelRenderer.Render(renderFunction); } void VirtualMenuPanelRenderer::Render(RenderFunction renderFunction) { const int x = virtualMenuPanel->area.position.x; const int y = virtualMenuPanel->area.position.y; const int width = virtualMenuPanel->area.size.width; const int height = virtualMenuPanel->area.size.height; SDL_Rect rect { x, y, width, height }; renderFunction(MyPlayer->_pStatPts == 0 ? menuArt : menuArtLevelUp, nullptr, &rect); } void VirtualDirectionPadRenderer::Render(RenderFunction renderFunction) { RenderPad(renderFunction); RenderKnob(renderFunction); } void VirtualDirectionPadRenderer::RenderPad(RenderFunction renderFunction) { auto center = virtualDirectionPad->area.position; auto radius = virtualDirectionPad->area.radius; const int diameter = 2 * radius; const int x = center.x - radius; const int y = center.y - radius; const int width = diameter; const int height = diameter; SDL_Rect rect { x, y, width, height }; renderFunction(padArt, nullptr, &rect); } void VirtualDirectionPadRenderer::RenderKnob(RenderFunction renderFunction) { auto center = virtualDirectionPad->position; auto radius = virtualDirectionPad->area.radius / 3; const int diameter = 2 * radius; const int x = center.x - radius; const int y = center.y - radius; const int width = diameter; const int height = diameter; SDL_Rect rect { x, y, width, height }; renderFunction(knobArt, nullptr, &rect); } void VirtualPadButtonRenderer::Render(RenderFunction renderFunction, const ButtonTexture &buttonArt) { if (!virtualPadButton->isUsable()) return; const VirtualGamepadButtonType buttonType = GetButtonType(); const Size size = buttonArt.size(); const auto index = static_cast(buttonType); const int xOffset = size.width * (index / buttonArt.numFrames); const int yOffset = size.height * (index % buttonArt.numFrames); auto center = virtualPadButton->area.position; auto radius = virtualPadButton->area.radius; const int diameter = 2 * radius; const int x = center.x - radius; const int y = center.y - radius; const int width = diameter; const int height = diameter; SDL_Rect src = MakeSdlRect(xOffset, yOffset, size.width, size.height); SDL_Rect dst = MakeSdlRect(x, y, width, height); renderFunction(buttonArt, &src, &dst); } void PotionButtonRenderer::RenderPotion(RenderFunction renderFunction, const ButtonTexture &potionArt) { if (!virtualPadButton->isUsable()) return; std::optional potionType = GetPotionType(); if (potionType == std::nullopt) return; const int frame = *potionType; const Size size = potionArt.size(); const int offset = size.height * frame; auto center = virtualPadButton->area.position; auto radius = virtualPadButton->area.radius * 8 / 10; const int diameter = 2 * radius; const int x = center.x - radius; const int y = center.y - radius; const int width = diameter; const int height = diameter; SDL_Rect src = MakeSdlRect(0, offset, size.width, size.height); SDL_Rect dst = MakeSdlRect(x, y, width, height); renderFunction(potionArt, &src, &dst); } std::optional PotionButtonRenderer::GetPotionType() { for (const Item &item : InspectPlayer->SpdList) { if (item.isEmpty()) { continue; } if (potionType == BeltItemType::Healing) { if (item._iMiscId == IMISC_HEAL) return GAMEPAD_HEALING; if (item._iMiscId == IMISC_FULLHEAL) return GAMEPAD_FULL_HEALING; if (item.isScrollOf(SpellID::Healing)) return GAMEPAD_SCROLL_OF_HEALING; } if (potionType == BeltItemType::Mana) { if (item._iMiscId == IMISC_MANA) return GAMEPAD_MANA; if (item._iMiscId == IMISC_FULLMANA) return GAMEPAD_FULL_MANA; } if (item._iMiscId == IMISC_REJUV) return GAMEPAD_REJUVENATION; if (item._iMiscId == IMISC_FULLREJUV) return GAMEPAD_FULL_REJUVENATION; if (item._iMiscId == IMISC_ARENAPOT && MyPlayer->isOnArenaLevel()) return GAMEPAD_ARENA_POTION; } return std::nullopt; } VirtualGamepadButtonType StandButtonRenderer::GetButtonType() { return GetStandButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType PrimaryActionButtonRenderer::GetButtonType() { // NEED: Confirm surface if (qtextflag) return GetTalkButtonType(virtualPadButton->isHeld); if (CharFlag && InteractsWithCharButton(MousePosition)) return GetApplyButtonType(virtualPadButton->isHeld); if (invflag) return GetInventoryButtonType(); if (leveltype == DTYPE_TOWN) return GetTownButtonType(); return GetDungeonButtonType(); } VirtualGamepadButtonType PrimaryActionButtonRenderer::GetTownButtonType() { if (IsPlayerInStore() || pcursmonst != -1) return GetTalkButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType PrimaryActionButtonRenderer::GetDungeonButtonType() { if (pcursmonst != -1) { const Monster &monster = Monsters[pcursmonst]; if (M_Talker(monster) || monster.talkMsg != TEXT_NONE) return GetTalkButtonType(virtualPadButton->isHeld); } return GetAttackButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType PrimaryActionButtonRenderer::GetInventoryButtonType() { if (pcursinvitem != -1 || pcursstashitem != StashStruct::EmptyCell || pcurs > CURSOR_HAND) return GetItemButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } extern int pcurstrig; extern Missile *pcursmissile; extern quest_id pcursquest; VirtualGamepadButtonType SecondaryActionButtonRenderer::GetButtonType() { if (pcursmissile != nullptr || pcurstrig != -1 || pcursquest != Q_INVALID) { return GetStairsButtonType(virtualPadButton->isHeld); } if (InGameMenu() || QuestLogIsOpen || SpellbookFlag) return GetBlankButtonType(virtualPadButton->isHeld); if (ObjectUnderCursor != nullptr) return GetObjectButtonType(virtualPadButton->isHeld); if (pcursitem != -1) return GetItemButtonType(virtualPadButton->isHeld); if (invflag) { if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) return GetApplyButtonType(virtualPadButton->isHeld); if (pcursinvitem != -1) { const Item &item = GetInventoryItem(*MyPlayer, pcursinvitem); if (!item.isScroll() || !TargetsMonster(item._iSpell)) { if (!item.isEquipment()) { return GetApplyButtonType(virtualPadButton->isHeld); } } } } return GetBlankButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType SpellActionButtonRenderer::GetButtonType() { if (!MyPlayer->HoldItem.isEmpty()) return GetDropButtonType(virtualPadButton->isHeld); if (invflag && pcursinvitem != -1 && pcurs == CURSOR_HAND) { return GetEquipButtonType(virtualPadButton->isHeld); } if (!invflag && !InGameMenu() && !QuestLogIsOpen && !SpellbookFlag) return GetCastButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType CancelButtonRenderer::GetButtonType() { if (InGameMenu()) return GetBackButtonType(virtualPadButton->isHeld); if (DoomFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag) return GetBackButtonType(virtualPadButton->isHeld); return GetBlankButtonType(virtualPadButton->isHeld); } VirtualGamepadButtonType PotionButtonRenderer::GetButtonType() { return GetPotionButtonType(virtualPadButton->isHeld); } void VirtualGamepadRenderer::UnloadArt() { menuPanelRenderer.UnloadArt(); directionPadRenderer.UnloadArt(); buttonArt.clearSurface(); potionArt.clearSurface(); } void VirtualMenuPanelRenderer::UnloadArt() { menuArt.clearSurface(); menuArtLevelUp.clearSurface(); } void VirtualDirectionPadRenderer::UnloadArt() { padArt.clearSurface(); knobArt.clearSurface(); } void InitVirtualGamepadGFX() { Renderer.LoadArt(); } void FreeVirtualGamepadGFX() { Renderer.UnloadArt(); } void VirtualGamepadRenderer::createTextures(SDL_Renderer &renderer) { menuPanelRenderer.createTextures(renderer); directionPadRenderer.createTextures(renderer); if (buttonArt.surface != nullptr) { buttonArt.texture.reset(SDL_CreateTextureFromSurface(&renderer, buttonArt.surface.get())); buttonArt.surface = nullptr; } if (potionArt.surface != nullptr) { potionArt.texture.reset(SDL_CreateTextureFromSurface(&renderer, potionArt.surface.get())); potionArt.surface = nullptr; } } void VirtualMenuPanelRenderer::createTextures(SDL_Renderer &renderer) { if (menuArt.surface != nullptr) { menuArt.texture.reset(SDL_CreateTextureFromSurface(&renderer, menuArt.surface.get())); menuArt.surface = nullptr; } if (menuArtLevelUp.surface != nullptr) { menuArtLevelUp.texture.reset(SDL_CreateTextureFromSurface(&renderer, menuArtLevelUp.surface.get())); menuArtLevelUp.surface = nullptr; } } void VirtualDirectionPadRenderer::createTextures(SDL_Renderer &renderer) { if (padArt.surface != nullptr) { padArt.texture.reset(SDL_CreateTextureFromSurface(&renderer, padArt.surface.get())); padArt.surface = nullptr; } if (knobArt.surface != nullptr) { knobArt.texture.reset(SDL_CreateTextureFromSurface(&renderer, knobArt.surface.get())); knobArt.surface = nullptr; } } void VirtualGamepadRenderer::destroyTextures() { menuPanelRenderer.destroyTextures(); directionPadRenderer.destroyTextures(); buttonArt.destroyTexture(); potionArt.destroyTexture(); } void VirtualMenuPanelRenderer::destroyTextures() { menuArt.destroyTexture(); menuArtLevelUp.destroyTexture(); } void VirtualDirectionPadRenderer::destroyTextures() { padArt.destroyTexture(); knobArt.destroyTexture(); } void InitVirtualGamepadTextures(SDL_Renderer &renderer) { Renderer.createTextures(renderer); } void FreeVirtualGamepadTextures() { Renderer.destroyTextures(); } } // namespace devilution ================================================ FILE: Source/controls/touch/renderers.h ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #else #include #endif #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "engine/surface.hpp" #include "utils/png.h" #include "utils/sdl_ptrs.h" namespace devilution { enum VirtualGamepadButtonType : uint8_t { GAMEPAD_ATTACK, GAMEPAD_ATTACKDOWN, GAMEPAD_TALK, GAMEPAD_TALKDOWN, GAMEPAD_ITEM, GAMEPAD_ITEMDOWN, GAMEPAD_OBJECT, GAMEPAD_OBJECTDOWN, GAMEPAD_CASTSPELL, GAMEPAD_CASTSPELLDOWN, GAMEPAD_BACK, GAMEPAD_BACKDOWN, GAMEPAD_BLANK, GAMEPAD_BLANKDOWN, GAMEPAD_APPLY, GAMEPAD_APPLYDOWN, GAMEPAD_EQUIP, GAMEPAD_EQUIPDOWN, GAMEPAD_DROP, GAMEPAD_DROPDOWN, GAMEPAD_STAIRS, GAMEPAD_STAIRSDOWN, GAMEPAD_STAND, GAMEPAD_STANDDOWN, GAMEPAD_POTION, GAMEPAD_POTIONDOWN, }; enum VirtualGamepadPotionType : uint8_t { GAMEPAD_HEALING, GAMEPAD_MANA, GAMEPAD_REJUVENATION, GAMEPAD_FULL_HEALING, GAMEPAD_FULL_MANA, GAMEPAD_FULL_REJUVENATION, GAMEPAD_ARENA_POTION, GAMEPAD_SCROLL_OF_HEALING, }; struct ButtonTexture { SDLSurfaceUniquePtr surface; SDLTextureUniquePtr texture; unsigned numSprites = 1; unsigned numFrames = 1; Size size() const; void clearSurface() { surface = nullptr; numFrames = 1; } void destroyTexture() { texture = nullptr; } }; typedef std::function RenderFunction; class VirtualMenuPanelRenderer { public: VirtualMenuPanelRenderer(VirtualMenuPanel *virtualMenuPanel) : virtualMenuPanel(virtualMenuPanel) { } void LoadArt(); /** * @brief Converts surfaces to textures. * * Must be called from the main thread. * * Per https://wiki.libsdl.org/SDL3/CategoryRender: * > These functions must be called from the main thread. See this bug for details: https://github.com/libsdl-org/SDL/issues/986 */ void createTextures(SDL_Renderer &renderer); /** * @brief Must be called from the main thread. */ void destroyTextures(); void Render(RenderFunction renderFunction); void UnloadArt(); private: VirtualMenuPanel *virtualMenuPanel; ButtonTexture menuArt; ButtonTexture menuArtLevelUp; }; class VirtualDirectionPadRenderer { public: VirtualDirectionPadRenderer(VirtualDirectionPad *virtualDirectionPad) : virtualDirectionPad(virtualDirectionPad) { } void LoadArt(); /** * @brief Converts surfaces to textures. * * Must be called from the main thread. * * Per https://wiki.libsdl.org/SDL3/CategoryRender: * > These functions must be called from the main thread. See this bug for details: https://github.com/libsdl-org/SDL/issues/986 */ void createTextures(SDL_Renderer &renderer); /** * @brief Must be called from the main thread. */ void destroyTextures(); void Render(RenderFunction renderFunction); void UnloadArt(); private: VirtualDirectionPad *virtualDirectionPad; ButtonTexture padArt; ButtonTexture knobArt; void RenderPad(RenderFunction renderFunction); void RenderKnob(RenderFunction renderFunction); }; class VirtualPadButtonRenderer { public: VirtualPadButtonRenderer(VirtualPadButton *virtualPadButton) : virtualPadButton(virtualPadButton) { } void Render(RenderFunction renderFunction, const ButtonTexture &buttonArt); protected: VirtualPadButton *virtualPadButton; virtual VirtualGamepadButtonType GetButtonType() = 0; }; class StandButtonRenderer : public VirtualPadButtonRenderer { public: StandButtonRenderer(VirtualPadButton *standButton) : VirtualPadButtonRenderer(standButton) { } private: VirtualGamepadButtonType GetButtonType(); }; class PrimaryActionButtonRenderer : public VirtualPadButtonRenderer { public: PrimaryActionButtonRenderer(VirtualPadButton *primaryActionButton) : VirtualPadButtonRenderer(primaryActionButton) { } private: VirtualGamepadButtonType GetButtonType(); VirtualGamepadButtonType GetTownButtonType(); VirtualGamepadButtonType GetDungeonButtonType(); VirtualGamepadButtonType GetInventoryButtonType(); }; class SecondaryActionButtonRenderer : public VirtualPadButtonRenderer { public: SecondaryActionButtonRenderer(VirtualPadButton *secondaryActionButton) : VirtualPadButtonRenderer(secondaryActionButton) { } private: VirtualGamepadButtonType GetButtonType(); }; class SpellActionButtonRenderer : public VirtualPadButtonRenderer { public: SpellActionButtonRenderer(VirtualPadButton *spellActionButton) : VirtualPadButtonRenderer(spellActionButton) { } private: VirtualGamepadButtonType GetButtonType(); }; class CancelButtonRenderer : public VirtualPadButtonRenderer { public: CancelButtonRenderer(VirtualPadButton *cancelButton) : VirtualPadButtonRenderer(cancelButton) { } private: VirtualGamepadButtonType GetButtonType(); }; class PotionButtonRenderer : public VirtualPadButtonRenderer { public: PotionButtonRenderer(VirtualPadButton *potionButton, BeltItemType potionType) : VirtualPadButtonRenderer(potionButton) , potionType(potionType) { } void RenderPotion(RenderFunction renderFunction, const ButtonTexture &potionArt); private: BeltItemType potionType; VirtualGamepadButtonType GetButtonType(); std::optional GetPotionType(); }; class VirtualGamepadRenderer { public: VirtualGamepadRenderer(VirtualGamepad *virtualGamepad) : menuPanelRenderer(&virtualGamepad->menuPanel) , directionPadRenderer(&virtualGamepad->directionPad) , standButtonRenderer(&virtualGamepad->standButton) , primaryActionButtonRenderer(&virtualGamepad->primaryActionButton) , secondaryActionButtonRenderer(&virtualGamepad->secondaryActionButton) , spellActionButtonRenderer(&virtualGamepad->spellActionButton) , cancelButtonRenderer(&virtualGamepad->cancelButton) , healthButtonRenderer(&virtualGamepad->healthButton, BeltItemType::Healing) , manaButtonRenderer(&virtualGamepad->manaButton, BeltItemType::Mana) { } void LoadArt(); /** * @brief Converts surfaces to textures. * * Must be called from the main thread. * * Per https://wiki.libsdl.org/SDL3/CategoryRender: * > These functions must be called from the main thread. See this bug for details: https://github.com/libsdl-org/SDL/issues/986 */ void createTextures(SDL_Renderer &renderer); /** * @brief Must be called from the main thread. */ void destroyTextures(); void Render(RenderFunction renderFunction); void UnloadArt(); private: VirtualMenuPanelRenderer menuPanelRenderer; VirtualDirectionPadRenderer directionPadRenderer; StandButtonRenderer standButtonRenderer; PrimaryActionButtonRenderer primaryActionButtonRenderer; SecondaryActionButtonRenderer secondaryActionButtonRenderer; SpellActionButtonRenderer spellActionButtonRenderer; CancelButtonRenderer cancelButtonRenderer; PotionButtonRenderer healthButtonRenderer; PotionButtonRenderer manaButtonRenderer; ButtonTexture buttonArt; ButtonTexture potionArt; }; void InitVirtualGamepadGFX(); /** * @brief Creates textures for the virtual gamepad. * * Must be called after `InitVirtualGamepadGFX`. * Must be called from the main thread. * * Per https://wiki.libsdl.org/SDL3/CategoryRender: * > These functions must be called from the main thread. See this bug for details: https://github.com/libsdl-org/SDL/issues/986 */ void InitVirtualGamepadTextures(SDL_Renderer &renderer); /** @brief Must be called from the main thread. */ void FreeVirtualGamepadTextures(); void RenderVirtualGamepad(SDL_Renderer *renderer); void RenderVirtualGamepad(SDL_Surface *surface); void FreeVirtualGamepadGFX(); } // namespace devilution ================================================ FILE: Source/cpp.hint ================================================ // Hint files help the Visual Studio IDE interpret Visual C++ identifiers // such as names of functions and macros. // For more information see https://go.microsoft.com/fwlink/?linkid=865984 #define DVL_ALWAYS_INLINE #define DVL_ATTRIBUTE_HOT #define DVL_API_FOR_TEST #define DVL_REINITIALIZES ================================================ FILE: Source/crawl.cpp ================================================ #include "crawl.hpp" #include #include #include "engine/displacement.hpp" namespace devilution { bool DoCrawl(unsigned radius, tl::function_ref function) { return DoCrawl(radius, radius, function); } bool DoCrawl(unsigned minRadius, unsigned maxRadius, tl::function_ref function) { for (int r = static_cast(minRadius); r <= static_cast(maxRadius); ++r) { if (!function(Displacement { 0, r })) return false; if (r == 0) continue; if (!function(Displacement { 0, -r })) return false; for (int x = 1; x < r; ++x) { if (!function(Displacement { -x, r })) return false; if (!function(Displacement { x, r })) return false; if (!function(Displacement { -x, -r })) return false; if (!function(Displacement { x, -r })) return false; } if (r > 1) { const int d = r - 1; if (!function(Displacement { -d, d })) return false; if (!function(Displacement { d, d })) return false; if (!function(Displacement { -d, -d })) return false; if (!function(Displacement { d, -d })) return false; } if (!function(Displacement { -r, 0 })) return false; if (!function(Displacement { r, 0 })) return false; for (int y = 1; y < r; ++y) { if (!function(Displacement { -r, y })) return false; if (!function(Displacement { r, y })) return false; if (!function(Displacement { -r, -y })) return false; if (!function(Displacement { r, -y })) return false; } } return true; } } // namespace devilution ================================================ FILE: Source/crawl.hpp ================================================ #include #include #include "engine/displacement.hpp" namespace devilution { /** * CrawlTable specifies X- and Y-coordinate deltas from a missile target coordinate. * * n=4 * * y * ^ * | 1 * | 3#4 * | 2 * +-----> x * * n=16 * * y * ^ * | 314 * | B7 8C * | F # G * | D9 AE * | 526 * +-------> x */ bool DoCrawl(unsigned radius, tl::function_ref function); bool DoCrawl(unsigned minRadius, unsigned maxRadius, tl::function_ref function); template auto Crawl(unsigned radius, F function) -> std::invoke_result_t { std::invoke_result_t result; DoCrawl(radius, [&result, &function](Displacement displacement) -> bool { result = function(displacement); return !result; }); return result; } template auto Crawl(unsigned minRadius, unsigned maxRadius, F function) -> std::invoke_result_t { std::invoke_result_t result; DoCrawl(minRadius, maxRadius, [&result, &function](Displacement displacement) -> bool { result = function(displacement); return !result; }); return result; } } // namespace devilution ================================================ FILE: Source/cursor.cpp ================================================ /** * @file cursor.cpp * * Implementation of cursor tracking functionality. */ #include "cursor.h" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "doom.h" #include "engine/backbuffer_state.hpp" #include "engine/demomode.h" #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/trn.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "inv.h" #include "levels/trigs.h" #include "missiles.h" #include "options.h" #include "qol/itemlabels.h" #include "qol/stash.h" #include "towners.h" #include "track.h" #include "utils/attributes.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/palette_blending.hpp" #include "utils/sdl_bilinear_scale.hpp" #include "utils/surface_to_clx.hpp" #include "utils/utf8.hpp" #ifdef UNPACKED_MPQS #include "engine/load_clx.hpp" #else #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "utils/parse_int.hpp" #endif namespace devilution { namespace { /** Cursor images CEL */ OptionalOwnedClxSpriteList pCursCels; OptionalOwnedClxSpriteList pCursCels2; OptionalOwnedClxSpriteList *HalfSizeItemSprites; OptionalOwnedClxSpriteList *HalfSizeItemSpritesRed; bool IsValidMonsterForSelection(const Monster &monster) { if (monster.hasNoLife()) return false; if ((monster.flags & MFLAG_HIDDEN) != 0) return false; if (monster.isPlayerMinion()) return false; return true; } bool TrySelectMonster(bool flipflag, Point tile, tl::function_ref isValidMonster) { auto checkPosition = [&](SelectionRegion selectionRegion, Displacement displacement) { const Point posToCheck = tile + displacement; if (!InDungeonBounds(posToCheck) || dMonster[posToCheck.x][posToCheck.y] == 0) return; const uint16_t monsterId = std::abs(dMonster[posToCheck.x][posToCheck.y]) - 1; const Monster &monster = Monsters[monsterId]; if (IsTileLit(posToCheck) && HasAnyOf(monster.data().selectionRegion, selectionRegion) && isValidMonster(monster)) { cursPosition = posToCheck; pcursmonst = monsterId; } }; if (!flipflag) checkPosition(SelectionRegion::Top, { 2, 1 }); if (flipflag) checkPosition(SelectionRegion::Top, { 1, 2 }); checkPosition(SelectionRegion::Top, { 2, 2 }); if (!flipflag) checkPosition(SelectionRegion::Middle, { 1, 0 }); if (flipflag) checkPosition(SelectionRegion::Middle, { 0, 1 }); checkPosition(SelectionRegion::Bottom, { 0, 0 }); checkPosition(SelectionRegion::Middle, { 1, 1 }); return pcursmonst != -1; } bool TrySelectTowner(bool flipflag, Point tile) { auto checkPosition = [&](Displacement displacement) { const Point posToCheck = tile + displacement; if (!InDungeonBounds(posToCheck) || dMonster[posToCheck.x][posToCheck.y] == 0) return; const uint16_t monsterId = std::abs(dMonster[posToCheck.x][posToCheck.y]) - 1; cursPosition = posToCheck; pcursmonst = monsterId; }; if (!flipflag) checkPosition({ 1, 0 }); if (flipflag) checkPosition({ 0, 1 }); checkPosition({ 0, 0 }); checkPosition({ 1, 1 }); return pcursmonst != -1; } bool TrySelectPlayer(bool flipflag, const Point tile) { if (!flipflag && tile.x + 1 < MAXDUNX && dPlayer[tile.x + 1][tile.y] != 0) { const uint8_t playerId = std::abs(dPlayer[tile.x + 1][tile.y]) - 1; Player &player = Players[playerId]; if (&player != MyPlayer && !player.hasNoLife()) { cursPosition = tile + Displacement { 1, 0 }; PlayerUnderCursor = &player; } } if (flipflag && tile.y + 1 < MAXDUNY && dPlayer[tile.x][tile.y + 1] != 0) { const uint8_t playerId = std::abs(dPlayer[tile.x][tile.y + 1]) - 1; Player &player = Players[playerId]; if (&player != MyPlayer && !player.hasNoLife()) { cursPosition = tile + Displacement { 0, 1 }; PlayerUnderCursor = &player; } } if (dPlayer[tile.x][tile.y] != 0) { const uint8_t playerId = std::abs(dPlayer[tile.x][tile.y]) - 1; Player &player = Players[playerId]; if (&player != MyPlayer) { cursPosition = tile; PlayerUnderCursor = &player; } } if (TileContainsDeadPlayer(tile)) { for (const Player &player : Players) { if (player.position.tile == tile && &player != MyPlayer) { cursPosition = tile; PlayerUnderCursor = &player; } } } if (pcurs == CURSOR_RESURRECT) { for (int xx = -1; xx < 2; xx++) { for (int yy = -1; yy < 2; yy++) { if (TileContainsDeadPlayer(tile + Displacement { xx, yy })) { for (const Player &player : Players) { if (player.position.tile == tile + Displacement { xx, yy } && &player != MyPlayer) { cursPosition = tile + Displacement { xx, yy }; PlayerUnderCursor = &player; } } } } } } if (tile.x + 1 < MAXDUNX && tile.y + 1 < MAXDUNY && dPlayer[tile.x + 1][tile.y + 1] != 0) { const uint8_t playerId = std::abs(dPlayer[tile.x + 1][tile.y + 1]) - 1; const Player &player = Players[playerId]; if (&player != MyPlayer && !player.hasNoLife()) { cursPosition = tile + Displacement { 1, 1 }; PlayerUnderCursor = &player; } } return PlayerUnderCursor != nullptr; } /** * @brief Try find an object starting with the tile below the current tile (tall objects like doors) */ bool TrySelectObject(bool flipflag, Point tile) { Point testPosition = tile + Direction::South; Object *object = FindObjectAtPosition(testPosition); if (object == nullptr || HasNoneOf(object->selectionRegion, SelectionRegion::Middle)) { // Either no object or can't interact from the test position, try the current tile testPosition = tile; object = FindObjectAtPosition(testPosition); if (object == nullptr || HasNoneOf(object->selectionRegion, SelectionRegion::Bottom)) { // Still no object (that could be activated from this position), try the tile to the bottom left or right // (whichever is closest to the cursor as determined when we set flipflag earlier) testPosition = tile + (flipflag ? Direction::SouthWest : Direction::SouthEast); object = FindObjectAtPosition(testPosition); if (object != nullptr && HasNoneOf(object->selectionRegion, SelectionRegion::Middle)) { // Found an object but it's not in range, clear the pointer object = nullptr; } } } if (object == nullptr) return false; // found object that can be activated with the given cursor position cursPosition = testPosition; ObjectUnderCursor = object; return true; } bool TrySelectItem(bool flipflag, Point tile) { if (!flipflag && tile.x + 1 < MAXDUNX && dItem[tile.x + 1][tile.y] > 0) { const uint8_t itemId = dItem[tile.x + 1][tile.y] - 1; if (HasAnyOf(Items[itemId].selectionRegion, SelectionRegion::Middle)) { cursPosition = tile + Displacement { 1, 0 }; pcursitem = static_cast(itemId); } } if (flipflag && tile.y + 1 < MAXDUNY && dItem[tile.x][tile.y + 1] > 0) { const uint8_t itemId = dItem[tile.x][tile.y + 1] - 1; if (HasAnyOf(Items[itemId].selectionRegion, SelectionRegion::Middle)) { cursPosition = tile + Displacement { 0, 1 }; pcursitem = static_cast(itemId); } } if (dItem[tile.x][tile.y] > 0) { const uint8_t itemId = dItem[tile.x][tile.y] - 1; if (HasAnyOf(Items[itemId].selectionRegion, SelectionRegion::Bottom)) { cursPosition = tile; pcursitem = static_cast(itemId); } } if (tile.x + 1 < MAXDUNX && tile.y + 1 < MAXDUNY && dItem[tile.x + 1][tile.y + 1] > 0) { const uint8_t itemId = dItem[tile.x + 1][tile.y + 1] - 1; if (HasAnyOf(Items[itemId].selectionRegion, SelectionRegion::Middle)) { cursPosition = tile + Displacement { 1, 1 }; pcursitem = static_cast(itemId); } } return pcursitem != -1; } bool TrySelectPixelBased(Point tile) { if (demo::IsRunning() || demo::IsRecording() || HeadlessMode) { // Recorded demos can run headless, but headless mode doesn't support loading sprites that are needed for pixel perfect selection // => Ensure demos are always compatible // => Never use sprites for selection when handling demos return false; } auto checkSprite = [](Point renderingTile, const ClxSprite sprite, Displacement renderingOffset) { const Point renderPosition = GetScreenPosition(renderingTile) + renderingOffset; Point spriteTopLeft = renderPosition - Displacement { 0, sprite.height() }; Size spriteSize = { sprite.width(), sprite.height() }; if (*GetOptions().Graphics.zoom) { spriteSize *= 2; spriteTopLeft *= 2; } const Rectangle spriteCoords = Rectangle(spriteTopLeft, spriteSize); if (!spriteCoords.contains(MousePosition)) return false; Point pointInSprite = Point { 0, 0 } + (MousePosition - spriteCoords.position); if (*GetOptions().Graphics.zoom) pointInSprite /= 2; return IsPointWithinClx(pointInSprite, sprite); }; auto convertFromRenderingToWorldTile = [](Point renderingPoint) { // Columns Displacement ret = Displacement(Direction::East) * renderingPoint.x; // Rows ret += Displacement(Direction::South) * renderingPoint.y / 2; if ((renderingPoint.y & 1) == 1) ret.deltaY += 1; return ret; }; // Try to find the selected entity from rendered pixels. // We search the rendered rows/columns backwards, because the last rendered tile overrides previous rendered pixels. auto searchArea = PointsInRectangle(Rectangle { { -1, -1 }, { 3, 8 } }); for (auto it = searchArea.rbegin(); it != searchArea.rend(); ++it) { const Point renderingColumnRaw = *it; const Point adjacentTile = tile + convertFromRenderingToWorldTile(renderingColumnRaw); if (!InDungeonBounds(adjacentTile)) continue; int monsterId = dMonster[adjacentTile.x][adjacentTile.y]; // Never select a monster if a target-player-only spell is selected if (monsterId != 0 && IsNoneOf(pcurs, CURSOR_HEALOTHER, CURSOR_RESURRECT)) { monsterId = std::abs(monsterId) - 1; if (leveltype == DTYPE_TOWN) { const Towner &towner = Towners[monsterId]; const ClxSprite sprite = towner.currentSprite(); const Displacement renderingOffset = towner.getRenderingOffset(); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; pcursmonst = monsterId; return true; } } else { const Monster &monster = Monsters[monsterId]; if (IsTileLit(adjacentTile) && IsValidMonsterForSelection(monster)) { const ClxSprite sprite = monster.animInfo.currentSprite(); const Displacement renderingOffset = monster.getRenderingOffset(sprite); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; pcursmonst = monsterId; return true; } } } } const int8_t dPlayerValue = dPlayer[adjacentTile.x][adjacentTile.y]; if (dPlayerValue != 0) { const uint8_t playerId = std::abs(dPlayerValue) - 1; if (playerId != MyPlayerId) { const Player &player = Players[playerId]; const ClxSprite sprite = player.currentSprite(); const Displacement renderingOffset = player.getRenderingOffset(sprite); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; PlayerUnderCursor = &player; return true; } } } if (TileContainsDeadPlayer(adjacentTile)) { for (const Player &player : Players) { if (player.position.tile == adjacentTile && &player != MyPlayer) { const ClxSprite sprite = player.currentSprite(); const Displacement renderingOffset = player.getRenderingOffset(sprite); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; PlayerUnderCursor = &player; return true; } } } } Object *object = FindObjectAtPosition(adjacentTile); if (object != nullptr && object->canInteractWith()) { const ClxSprite sprite = object->currentSprite(); const Displacement renderingOffset = object->getRenderingOffset(sprite, adjacentTile); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; ObjectUnderCursor = object; return true; } } uint8_t itemId = dItem[adjacentTile.x][adjacentTile.y]; if (itemId != 0) { itemId = itemId - 1; const Item &item = Items[itemId]; const ClxSprite sprite = item.AnimInfo.currentSprite(); const Displacement renderingOffset = item.getRenderingOffset(sprite); if (checkSprite(adjacentTile, sprite, renderingOffset)) { cursPosition = adjacentTile; pcursitem = static_cast(itemId); return true; } } } return false; } #ifndef UNPACKED_MPQS std::vector ReadWidths(AssetRef &&ref) { const size_t len = ref.size(); if (len == 0) { app_fatal("Missing widths"); } const std::unique_ptr data { new char[len] }; AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok() || !handle.read(data.get(), len)) { app_fatal("Failed to load widths"); } std::string_view str { data.get(), len }; std::vector result; while (!str.empty()) { const char *end; const ParseIntResult parseResult = ParseInt(str, std::numeric_limits::min(), std::numeric_limits::max(), &end); if (!parseResult.has_value()) { app_fatal(StrCat("Failed to parse width value from: [", str, "]")); } result.push_back(parseResult.value()); str.remove_prefix(end - str.data()); while (!str.empty() && (str[0] == '\r' || str[0] == '\n')) { str.remove_prefix(1); } } return result; } #endif } // namespace /** Current highlighted monster */ int pcursmonst = -1; /** inv_item value */ int8_t pcursinvitem; /** StashItem value */ uint16_t pcursstashitem; /** Current highlighted item */ int8_t pcursitem; /** Current highlighted object */ Object *ObjectUnderCursor; /** Current highlighted player */ const Player *PlayerUnderCursor; /** Current highlighted tile position */ Point cursPosition; /** Previously highlighted monster */ int pcurstemp; /** Index of current cursor image */ int pcurs; void InitCursor() { assert(!pCursCels); #ifdef UNPACKED_MPQS pCursCels = LoadClx("data\\inv\\objcurs.clx"); pCursCels2 = LoadOptionalClx("data\\inv\\objcurs2.clx"); #else pCursCels = LoadCel("data\\inv\\objcurs", ReadWidths(FindAsset("data\\inv\\objcurs-widths.txt")).data()); AssetRef ref = FindAsset("data\\inv\\objcurs2-widths.txt"); if (ref.ok()) { pCursCels2 = LoadOptionalCel("data\\inv\\objcurs2", ReadWidths(std::move(ref)).data()); } #endif ClearCursor(); } void FreeCursor() { pCursCels = std::nullopt; pCursCels2 = std::nullopt; ClearCursor(); } ClxSprite GetInvItemSprite(int cursId) { assert(cursId > 0); const size_t numSprites = pCursCels->numSprites(); if (static_cast(cursId) <= numSprites) { return (*pCursCels)[cursId - 1]; } assert(pCursCels2.has_value()); assert(cursId - numSprites <= pCursCels2->numSprites()); return (*pCursCels2)[cursId - numSprites - 1]; } Size GetInvItemSize(int cursId) { const ClxSprite sprite = GetInvItemSprite(cursId); return { sprite.width(), sprite.height() }; } ClxSprite GetHalfSizeItemSprite(int cursId) { return (*HalfSizeItemSprites[cursId])[0]; } ClxSprite GetHalfSizeItemSpriteRed(int cursId) { return (*HalfSizeItemSpritesRed[cursId])[0]; } void CreateHalfSizeItemSprites() { if (HalfSizeItemSprites != nullptr) return; const uint32_t numInvItems = pCursCels->numSprites() - (static_cast(CURSOR_FIRSTITEM) - 1) + (pCursCels2.has_value() ? pCursCels2->numSprites() : 0); HalfSizeItemSprites = new OptionalOwnedClxSpriteList[numInvItems]; HalfSizeItemSpritesRed = new OptionalOwnedClxSpriteList[numInvItems]; const uint8_t *redTrn = GetInfravisionTRN(); constexpr int MaxWidth = 28 * 3; constexpr int MaxHeight = 28 * 3; OwnedSurface ownedItemSurface { MaxWidth, MaxHeight }; OwnedSurface ownedHalfSurface { MaxWidth / 2, MaxHeight / 2 }; const auto createHalfSize = [&, redTrn](const ClxSprite itemSprite, size_t outputIndex) { if (itemSprite.width() <= 28 && itemSprite.height() <= 28) { // Skip creating half-size sprites for 1x1 items because we always render them at full size anyway. return; } const Surface itemSurface = ownedItemSurface.subregion(0, 0, itemSprite.width(), itemSprite.height()); const SDL_Rect itemSurfaceRect = MakeSdlRect(0, 0, itemSurface.w(), itemSurface.h()); SDL_SetSurfaceClipRect(itemSurface.surface, &itemSurfaceRect); SDL_FillSurfaceRect(itemSurface.surface, nullptr, 1); ClxDraw(itemSurface, { 0, itemSurface.h() }, itemSprite); const Surface halfSurface = ownedHalfSurface.subregion(0, 0, itemSurface.w() / 2, itemSurface.h() / 2); const SDL_Rect halfSurfaceRect = MakeSdlRect(0, 0, halfSurface.w(), halfSurface.h()); SDL_SetSurfaceClipRect(halfSurface.surface, &halfSurfaceRect); BilinearDownscaleByHalf8(itemSurface.surface, paletteTransparencyLookup, halfSurface.surface, 1); HalfSizeItemSprites[outputIndex].emplace(SurfaceToClx(halfSurface, 1, 1)); SDL_FillSurfaceRect(itemSurface.surface, nullptr, 1); ClxDrawTRN(itemSurface, { 0, itemSurface.h() }, itemSprite, redTrn); BilinearDownscaleByHalf8(itemSurface.surface, paletteTransparencyLookup, halfSurface.surface, 1); HalfSizeItemSpritesRed[outputIndex].emplace(SurfaceToClx(halfSurface, 1, 1)); }; size_t outputIndex = 0; for (size_t i = static_cast(CURSOR_FIRSTITEM) - 1, n = pCursCels->numSprites(); i < n; ++i, ++outputIndex) { createHalfSize((*pCursCels)[i], outputIndex); } if (pCursCels2.has_value()) { for (size_t i = 0, n = pCursCels2->numSprites(); i < n; ++i, ++outputIndex) { createHalfSize((*pCursCels2)[i], outputIndex); } } } void FreeHalfSizeItemSprites() { if (HalfSizeItemSprites != nullptr) { delete[] HalfSizeItemSprites; HalfSizeItemSprites = nullptr; delete[] HalfSizeItemSpritesRed; HalfSizeItemSpritesRed = nullptr; } } void DrawItem(const Item &item, const Surface &out, Point position, ClxSprite clx) { const bool usable = !IsInspectingPlayer() ? item._iStatFlag : InspectPlayer->CanUseItem(item); if (usable) { ClxDraw(out, position, clx); } else { ClxDrawTRN(out, position, clx, GetInfravisionTRN()); } } void ResetCursor() { NewCursor(pcurs); } void NewCursor(const Item &item) { if (item.isEmpty()) { NewCursor(CURSOR_HAND); } else { NewCursor(item._iCurs + CURSOR_FIRSTITEM); } } void NewCursor(int cursId) { if (pcurs >= CURSOR_FIRSTITEM && cursId > CURSOR_HAND && cursId < CURSOR_HOURGLASS) { if (!TryDropItem()) { return; } } if (cursId < CURSOR_HOURGLASS && MyPlayer != nullptr) { MyPlayer->HoldItem.clear(); } pcurs = cursId; if (IsHardwareCursorEnabled() && ControlDevice == ControlTypes::KeyboardAndMouse) { if (!ArtCursor && cursId == CURSOR_NONE) return; const CursorInfo newCursor = ArtCursor ? CursorInfo::UserInterfaceCursor() : CursorInfo::GameCursor(cursId); if (newCursor != GetCurrentCursorInfo()) SetHardwareCursor(newCursor); } } void DrawSoftwareCursor(const Surface &out, Point position, int cursId) { const ClxSprite sprite = GetInvItemSprite(cursId); if (cursId >= CURSOR_FIRSTITEM && !MyPlayer->HoldItem.isEmpty()) { const auto &heldItem = MyPlayer->HoldItem; ClxDrawOutline(out, GetOutlineColor(heldItem, true), position, sprite); DrawItem(heldItem, out, position, sprite); } else { ClxDraw(out, position, sprite); } } void InitLevelCursor() { NewCursor(CURSOR_HAND); cursPosition = ViewPosition; pcurstemp = -1; pcursmonst = -1; ObjectUnderCursor = nullptr; pcursitem = -1; pcursstashitem = StashStruct::EmptyCell; PlayerUnderCursor = nullptr; ClearCursor(); } void CheckTown() { for (auto &missile : Missiles) { if (missile._mitype == MissileID::TownPortal) { if (EntranceBoundaryContains(missile.position.tile, cursPosition)) { trigflag = true; InfoString = _("Town Portal"); AddInfoBoxString(fmt::format(fmt::runtime(_("from {:s}")), Players[missile._misource]._pName)); cursPosition = missile.position.tile; } } } } void CheckRportal() { for (auto &missile : Missiles) { if (missile._mitype == MissileID::RedPortal) { if (EntranceBoundaryContains(missile.position.tile, cursPosition)) { trigflag = true; InfoString = _("Portal to"); AddInfoBoxString(!setlevel ? _("The Unholy Altar") : _("level 15")); cursPosition = missile.position.tile; } } } } void DisplayTriggerInfo() { CheckTrigForce(); CheckTown(); CheckRportal(); } /** * @brief Adjusts mouse position based on panels */ void AlterMousePositionViaPanels(Point &screenPosition) { if (CanPanelsCoverView()) { if (IsLeftPanelOpen()) { screenPosition.x -= GetScreenWidth() / 4; } else if (IsRightPanelOpen()) { screenPosition.x += GetScreenWidth() / 4; } } } /** * @brief If scrolling, offset the mousepos */ void AlterMousePositionViaScrolling(Point &screenPosition, Rectangle mainPanel) { if (mainPanel.contains(MousePosition) && track_isscrolling()) { screenPosition.y = mainPanel.position.y - 1; } } /** * @brief Adjust based on current zoom */ void AlterMousePositionViaZoom(Point &screenPosition) { if (*GetOptions().Graphics.zoom) { screenPosition.x /= 2; screenPosition.y /= 2; } } /** * @brief Adjust by player offset and tile grid alignment */ void AlterMousePositionViaPlayer(Point &screenPosition, const Player &myPlayer) { int xo = 0; int yo = 0; CalcTileOffset(&xo, &yo); screenPosition.x += xo; screenPosition.y += yo; // Adjust for player walking if (myPlayer.isWalking()) { const Displacement offset = GetOffsetForWalking(myPlayer.AnimInfo, myPlayer._pdir, true); screenPosition.x -= offset.deltaX; screenPosition.y -= offset.deltaY; // Predict the next frame when walking to avoid input jitter const DisplacementOf offset2 = myPlayer.position.CalculateWalkingOffsetShifted8(myPlayer._pdir, myPlayer.AnimInfo); const DisplacementOf velocity = myPlayer.position.GetWalkingVelocityShifted8(myPlayer._pdir, myPlayer.AnimInfo); int fx = offset2.deltaX / 256; int fy = offset2.deltaY / 256; fx -= (offset2.deltaX + velocity.deltaX) / 256; fy -= (offset2.deltaY + velocity.deltaY) / 256; screenPosition.x -= fx; screenPosition.y -= fy; } } Point ConvertToTileGrid(Point &screenPosition) { int columns = 0; int rows = 0; TilesInView(&columns, &rows); const int lrow = rows - RowsCoveredByPanel(); // Center player tile on screen Point currentTile = ViewPosition; ShiftGrid(¤tTile, -columns / 2, -lrow / 2); // Align grid if ((columns % 2) == 0 && (lrow % 2) == 0) { screenPosition.y += TILE_HEIGHT / 2; } else if ((columns % 2) != 0 && (lrow % 2) != 0) { screenPosition.x -= TILE_WIDTH / 2; } else if ((columns % 2) != 0 && (lrow % 2) == 0) { currentTile.y++; } if (*GetOptions().Graphics.zoom) { screenPosition.y -= TILE_HEIGHT / 4; } const int tx = screenPosition.x / TILE_WIDTH; const int ty = screenPosition.y / TILE_HEIGHT; ShiftGrid(¤tTile, tx, ty); return currentTile; } /** * @brief Shift position to match diamond grid alignment */ void ShiftToDiamondGridAlignment(Point screenPosition, Point &tile, bool &flipflag) { const int px = screenPosition.x % TILE_WIDTH; const int py = screenPosition.y % TILE_HEIGHT; const bool flipy = py < (px / 2); if (flipy) { tile.y--; } const bool flipx = py >= TILE_HEIGHT - (px / 2); if (flipx) { tile.x++; } tile.x = std::clamp(tile.x, 0, MAXDUNX - 1); tile.y = std::clamp(tile.y, 0, MAXDUNY - 1); flipflag = (flipy && flipx) || ((flipy || flipx) && px < TILE_WIDTH / 2); } /** * @brief While holding the button down we should retain target (but potentially lose it if it dies, goes out of view, etc) */ bool CheckMouseHold(const Point currentTile) { if ((sgbMouseDown != CLICK_NONE || ControllerActionHeld != GameActionType_NONE) && IsNoneOf(LastPlayerAction, PlayerActionType::None, PlayerActionType::Attack, PlayerActionType::Spell)) { InvalidateTargets(); if (pcursmonst == -1 && ObjectUnderCursor == nullptr && pcursitem == -1 && pcursinvitem == -1 && pcursstashitem == StashStruct::EmptyCell && PlayerUnderCursor == nullptr) { cursPosition = currentTile; DisplayTriggerInfo(); } return true; } return false; } void ResetCursorInfo() { pcurstemp = pcursmonst; pcursmonst = -1; ObjectUnderCursor = nullptr; pcursitem = -1; if (pcursinvitem != -1) { RedrawComponent(PanelDrawComponent::Belt); } pcursinvitem = -1; pcursstashitem = StashStruct::EmptyCell; PlayerUnderCursor = nullptr; ShowUniqueItemInfoBox = false; MainPanelFlag = false; trigflag = false; } bool CheckPlayerState(const Point currentTile, const Player &myPlayer) { if (myPlayer._pInvincible) { return true; } if (!myPlayer.HoldItem.isEmpty() || SpellSelectFlag) { cursPosition = currentTile; return true; } return false; } bool CheckPanelsAndFlags(Rectangle mainPanel) { if (mainPanel.contains(MousePosition)) { CheckPanelInfo(); return true; } if (DoomFlag) { return true; } if (invflag && GetRightPanel().contains(MousePosition)) { pcursinvitem = CheckInvHLight(); return true; } if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { pcursstashitem = CheckStashHLight(MousePosition); } if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { return true; } if (IsLeftPanelOpen() && GetLeftPanel().contains(MousePosition)) { return true; } return false; } bool CheckCursorActions(const Point currentTile, bool flipflag) { if (pcurs == CURSOR_IDENTIFY) { ObjectUnderCursor = nullptr; pcursmonst = -1; pcursitem = -1; cursPosition = currentTile; return true; } if (TrySelectPixelBased(currentTile)) return true; if (leveltype != DTYPE_TOWN) { // Never select a monster if a target-player-only spell is selected if (IsNoneOf(pcurs, CURSOR_HEALOTHER, CURSOR_RESURRECT)) { if (pcurstemp != -1 && TrySelectMonster(flipflag, currentTile, [](const Monster &monster) { if (!IsValidMonsterForSelection(monster)) return false; if (monster.getId() != static_cast(pcurstemp)) return false; return true; })) { // found a valid previous selected monster return true; } if (TrySelectMonster(flipflag, currentTile, IsValidMonsterForSelection)) { // found a valid monster return true; } } } else { if (TrySelectTowner(flipflag, currentTile)) { // found a towner return true; } } return TrySelectPlayer(flipflag, currentTile) || TrySelectObject(flipflag, currentTile) || TrySelectItem(flipflag, currentTile); } /** * @brief Checks for early return if an item is highlighted */ void CheckCursMove() { if (IsItemLabelHighlighted()) return; Point screenPosition = MousePosition; const Rectangle &mainPanel = GetMainPanel(); AlterMousePositionViaPanels(screenPosition); AlterMousePositionViaScrolling(screenPosition, mainPanel); AlterMousePositionViaZoom(screenPosition); const Player &myPlayer = *MyPlayer; AlterMousePositionViaPlayer(screenPosition, myPlayer); bool flipflag = false; Point currentTile = ConvertToTileGrid(screenPosition); ShiftToDiamondGridAlignment(screenPosition, currentTile, flipflag); if (CheckMouseHold(currentTile)) return; ResetCursorInfo(); if (CheckPlayerState(currentTile, myPlayer) || CheckPanelsAndFlags(mainPanel) || CheckCursorActions(currentTile, flipflag)) return; // update cursor position cursPosition = currentTile; DisplayTriggerInfo(); } } // namespace devilution ================================================ FILE: Source/cursor.h ================================================ /** * @file cursor.h * * Interface of cursor tracking functionality. */ #pragma once #include #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" #include "utils/attributes.h" #include "utils/enum_traits.h" namespace devilution { enum class SelectionRegion : uint8_t { None = 0, Bottom = 1U << 0, Middle = 1U << 1, Top = 1U << 2, }; use_enum_as_flags(SelectionRegion); enum cursor_id : uint8_t { CURSOR_NONE, CURSOR_HAND, CURSOR_IDENTIFY, CURSOR_REPAIR, CURSOR_RECHARGE, CURSOR_DISARM, CURSOR_OIL, CURSOR_TELEKINESIS, CURSOR_RESURRECT, CURSOR_TELEPORT, CURSOR_HEALOTHER, CURSOR_HOURGLASS, CURSOR_FIRSTITEM, }; extern int pcursmonst; extern int8_t pcursinvitem; extern uint16_t pcursstashitem; extern int8_t pcursitem; struct Object; // Defined in objects.h extern Object *ObjectUnderCursor; struct Player; // Defined in player.h extern const Player *PlayerUnderCursor; extern Point cursPosition; extern DVL_API_FOR_TEST int pcurs; void InitCursor(); void FreeCursor(); void ResetCursor(); struct Item; /** * @brief Use the item sprite as the cursor (or show the default hand cursor if the item isEmpty) */ void NewCursor(const Item &item); void NewCursor(int cursId); void InitLevelCursor(); void CheckRportal(); void CheckTown(); void CheckCursMove(); void DrawSoftwareCursor(const Surface &out, Point position, int cursId); void DrawItem(const Item &item, const Surface &out, Point position, ClxSprite clx); /** Returns the sprite for the given inventory index. */ ClxSprite GetInvItemSprite(int cursId); ClxSprite GetHalfSizeItemSprite(int cursId); ClxSprite GetHalfSizeItemSpriteRed(int cursId); void CreateHalfSizeItemSprites(); void FreeHalfSizeItemSprites(); /** Returns the width and height for an inventory index. */ Size GetInvItemSize(int cursId); } // namespace devilution ================================================ FILE: Source/data/file.cpp ================================================ #include "file.hpp" #include #include #include #include #include #include #include #include "engine/assets.hpp" #include "utils/algorithm/container.hpp" #include "utils/language.h" namespace devilution { tl::expected DataFile::load(std::string_view path) { AssetRef ref = FindAsset(path); if (!ref.ok()) return tl::unexpected { Error::NotFound }; const size_t size = ref.size(); // TODO: It should be possible to stream the data file contents instead of copying the whole thing into memory std::unique_ptr data { new char[size] }; { AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok()) return tl::unexpected { Error::OpenFailed }; if (size > 0 && !handle.read(data.get(), size)) return tl::unexpected { Error::BadRead }; } return DataFile { std::move(data), size }; } DataFile DataFile::loadOrDie(std::string_view path) { tl::expected dataFileResult = DataFile::load(path); if (!dataFileResult.has_value()) { DataFile::reportFatalError(dataFileResult.error(), path); } return *std::move(dataFileResult); } void DataFile::reportFatalError(Error code, std::string_view fileName) { switch (code) { case Error::NotFound: case Error::OpenFailed: case Error::BadRead: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} */ "Unable to load data from file {0}")), fileName)); case Error::NoContent: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} */ "{0} is incomplete, please check the file contents.")), fileName)); case Error::NotEnoughColumns: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} */ "Your {0} file doesn't have the expected columns, please make sure it matches the documented format.")), fileName)); } } void DataFile::reportFatalFieldError(DataFileField::Error code, std::string_view fileName, std::string_view fieldName, const DataFileField &field, std::string_view details) { std::string detailsStr; if (!details.empty()) { detailsStr = StrCat("\n", details); } switch (code) { case DataFileField::Error::NotANumber: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ "Non-numeric value {0} for {1} in {2} at row {3} and column {4}")), field.currentValue(), fieldName, fileName, field.row(), field.column()) .append(detailsStr)); case DataFileField::Error::OutOfRange: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ "Out of range value {0} for {1} in {2} at row {3} and column {4}")), field.currentValue(), fieldName, fileName, field.row(), field.column()) .append(detailsStr)); case DataFileField::Error::InvalidValue: app_fatal(fmt::format(fmt::runtime(_( /* TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} */ "Invalid value {0} for {1} in {2} at row {3} and column {4}")), field.currentValue(), fieldName, fileName, field.row(), field.column()) .append(detailsStr)); } } tl::expected DataFile::parseHeader(ColumnDefinition *begin, ColumnDefinition *end, tl::function_ref(std::string_view)> mapper) { std::bitset::max()> seenColumns; unsigned lastColumn = 0; RecordIterator firstRecord { data(), data() + size(), false }; for (DataFileField field : *firstRecord) { if (begin == end) { // All key columns have been identified break; } auto mapResult = mapper(*field); if (!mapResult.has_value()) { // not a key column continue; } const uint8_t columnType = mapResult.value(); if (seenColumns.test(columnType)) { // Repeated column? unusual, maybe this should be an error continue; } seenColumns.set(columnType); unsigned skipColumns = 0; if (field.column() > lastColumn) skipColumns = field.column() - lastColumn - 1; lastColumn = field.column(); *begin = { columnType, skipColumns }; ++begin; } // Incrementing the iterator causes it to read to the end of the record in case we broke early (maybe there were extra columns) ++firstRecord; if (firstRecord == this->end()) { return tl::unexpected { Error::NoContent }; } body_ = firstRecord.data(); if (begin != end) { return tl::unexpected { Error::NotEnoughColumns }; } return {}; } tl::expected DataFile::skipHeader() { RecordIterator it { data(), data() + size(), false }; ++it; if (it == this->end()) { return tl::unexpected { Error::NoContent }; } body_ = it.data(); return {}; } void DataFile::skipHeaderOrDie(std::string_view path) { if (tl::expected result = skipHeader(); !result.has_value()) { DataFile::reportFatalError(result.error(), path); } } [[nodiscard]] size_t DataFile::numRecords() const { if (content_.empty()) return 0; const auto numNewlines = static_cast(c_count(content_, '\n') + (content_.back() == '\n' ? 0 : 1)); if (numNewlines < 2) return 0; return static_cast(numNewlines - 1); } } // namespace devilution ================================================ FILE: Source/data/file.hpp ================================================ #pragma once #include #include #include #include #include #include #include "iterators.hpp" namespace devilution { struct ColumnDefinition { enum class Error { UnknownColumn }; uint8_t type = std::numeric_limits::max(); // The number of fields between this column and the last one identified as important (or from start of the record if this is the first column we care about) unsigned skipLength = 0; bool operator==(const ColumnDefinition &other) const = default; template explicit operator T() const { return static_cast(type); } }; /** * @brief Container for a tab-delimited file following the TSV-like format described in txtdata/Readme.md */ class DataFile { std::unique_ptr data_; std::string_view content_; const char *body_; DataFile() = delete; /** * @brief Creates a view over a sequence of utf8 code units, skipping over the BOM if present * @param data pointer to the raw data backing the view (this container will take ownership to ensure the lifetime of the view) * @param size total number of bytes/code units including the BOM if present */ DataFile(std::unique_ptr &&data, size_t size) : data_(std::move(data)) , content_(data_.get(), size) { constexpr std::string_view utf8BOM = "\xef\xbb\xbf"; if (this->content_.starts_with(utf8BOM)) this->content_.remove_prefix(utf8BOM.size()); body_ = this->content_.data(); } public: enum class Error { NotFound, OpenFailed, BadRead, NoContent, NotEnoughColumns }; /** * @brief Attempts to load a data file (using the same mechanism as other runtime assets) * * @param path file to load including the /txtdata/ prefix * @return an object containing an owned pointer to an in-memory copy of the file * or an error code describing the reason for failure. */ static tl::expected load(std::string_view path); static DataFile loadOrDie(std::string_view path); static void reportFatalError(Error code, std::string_view fileName); static void reportFatalFieldError(DataFileField::Error code, std::string_view fileName, std::string_view fieldName, const DataFileField &field, std::string_view details = {}); void resetHeader() { body_ = content_.data(); } /** * @brief Attempts to parse the first row/record in the file, populating the range defined by [begin, end) using the provided mapping function * * This method will also set an internal marker so that future uses of the begin iterator skip the header line. * @param begin Start of the destination range * @param end End of the destination range * @param mapper Function that maps from a string_view to a unique numeric identifier for the column * @return If the file ends after the header or not enough columns were defined this function returns an error code describing the failure. */ [[nodiscard]] tl::expected parseHeader(ColumnDefinition *begin, ColumnDefinition *end, tl::function_ref(std::string_view)> mapper); /** * @brief Templated version of parseHeader(uint8_t) to allow using directly with enum definitions of columns * @tparam T An enum or any type that defines operator uint8_t() * @param begin Start of the destination range * @param end End of the destination range * @param typedMapper Function that maps from a string_view to a unique T value * @return A void success result or an error code as described above */ template [[nodiscard]] tl::expected parseHeader(ColumnDefinition *begin, ColumnDefinition *end, std::function(std::string_view)> typedMapper) { return parseHeader(begin, end, [typedMapper](std::string_view label) { return typedMapper(label).transform([](T value) { return static_cast(value); }); }); } [[nodiscard]] tl::expected skipHeader(); void skipHeaderOrDie(std::string_view path); [[nodiscard]] RecordIterator begin() const { return { body_, data() + size(), body_ != data() }; } [[nodiscard]] RecordIterator end() const { return {}; } // Assumes a header [[nodiscard]] size_t numRecords() const; [[nodiscard]] const char *data() const { return content_.data(); } [[nodiscard]] size_t size() const { return content_.size(); } }; } // namespace devilution ================================================ FILE: Source/data/iterators.hpp ================================================ #pragma once #include #include #include #include #include "parser.hpp" #include "utils/parse_int.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution { class DataFileField { GetFieldResult *state_; const char *end_; unsigned row_; unsigned column_; public: enum class Error { NotANumber, OutOfRange, InvalidValue }; static tl::expected mapError(std::errc ec) { if (ec == std::errc()) return {}; switch (ec) { case std::errc::result_out_of_range: return tl::unexpected { Error::OutOfRange }; case std::errc::invalid_argument: return tl::unexpected { Error::NotANumber }; default: return tl::unexpected { Error::InvalidValue }; } } static tl::expected mapError(ParseIntError ec) { switch (ec) { case ParseIntError::OutOfRange: return tl::unexpected { Error::OutOfRange }; case ParseIntError::ParseError: return tl::unexpected { Error::NotANumber }; default: return tl::unexpected { Error::InvalidValue }; } } DataFileField(GetFieldResult *state, const char *end, unsigned row, unsigned column) : state_(state) , end_(end) , row_(row) , column_(column) { } /** * @brief Returns a view of the current field * * This method scans the current field if this is the first value access since the last * advance. If you expect the field to contain a numeric value then calling parseInt first * is more efficient, but calling the methods in either order is supported. * @return The current field value (may be an empty string) or a zero length string_view */ [[nodiscard]] std::string_view value() { if (state_->status == GetFieldResult::Status::ReadyToRead) { *state_ = GetNextField(state_->next, end_); } return state_->value; } /** * Convenience function to let DataFileField instances be used like other single-value STL containers */ [[nodiscard]] std::string_view operator*() { return this->value(); } /** * @brief Attempts to parse the current field as a numeric value using std::from_chars * * You can freely interleave this method with calls to operator*. If this is the first value * access since the last advance this will scan the current field and store it for later * use with operator* or repeated calls to parseInt (even with different types). * @tparam T an Integral type supported by std::from_chars * @param destination value to store the result of successful parsing * @return an error code corresponding to the from_chars result if parsing failed */ template [[nodiscard]] tl::expected parseInt(T &destination) { std::from_chars_result result {}; if (state_->status == GetFieldResult::Status::ReadyToRead) { const char *begin = state_->next; result = std::from_chars(begin, end_, destination); if (result.ec != std::errc::invalid_argument) { // from_chars was able to consume at least one character, consume the rest of the field *state_ = GetNextField(result.ptr, end_); // and prepend what was already parsed state_->value = { begin, (state_->value.data() - begin) + state_->value.size() }; } } else { result = std::from_chars(state_->value.data(), end_, destination); } return mapError(result.ec); } [[nodiscard]] tl::expected parseBool(bool &destination) { const std::string_view str = value(); if (str == "true") { destination = true; return {}; } if (str == "false") { destination = false; return {}; } return tl::make_unexpected(DataFileField::Error::InvalidValue); } template [[nodiscard]] tl::expected parseIntArray(T *destination, size_t n) { size_t i = 0; for (const std::string_view part : SplitByChar(value(), ',')) { if (i == n) return tl::make_unexpected(Error::InvalidValue); const std::from_chars_result result = std::from_chars(part.data(), part.data() + part.size(), destination[i]); if (result.ec != std::errc()) return mapError(result.ec); ++i; } if (i != n) return tl::make_unexpected(Error::InvalidValue); return {}; } template [[nodiscard]] tl::expected parseIntArray(T (&destination)[N]) { return parseIntArray(destination, N); } template [[nodiscard]] tl::expected parseIntArray(std::array &destination) { return parseIntArray(destination.data(), N); } template [[nodiscard]] tl::expected parseEnumArray(T *destination, size_t n, std::optional fillMissing, ParseFn &&parseFn) { size_t i = 0; const std::string_view str = value(); if (!str.empty()) { for (const std::string_view part : SplitByChar(str, ',')) { if (i == n) return tl::make_unexpected(StrCat("Too many values, max: ", n)); auto result = parseFn(part); if (!result.has_value()) { return tl::make_unexpected(std::move(result).error()); } destination[i++] = *result; } } if (i != n) { if (!fillMissing.has_value()) { return tl::make_unexpected(StrCat("Too few values, expected ", n, " got ", i)); } while (i < n) { destination[i++] = *fillMissing; } } return {}; } template [[nodiscard]] tl::expected parseEnumArray(T (&destination)[N], std::optional fillMissing, ParseFn &&parseFn) { return parseEnumArray(destination, N, std::move(fillMissing), std::forward(parseFn)); } template [[nodiscard]] tl::expected parseIntArray(std::array &destination, std::optional fillMissing, ParseFn &&parseFn) { return parseEnumArray(destination.data(), N, std::move(fillMissing), std::forward(parseFn)); } template [[nodiscard]] tl::expected parseEnumList(T &destination, ParseFn &&parseFn) { destination = {}; const std::string_view str = value(); if (str.empty()) return {}; for (const std::string_view part : SplitByChar(str, ',')) { auto result = parseFn(part); if (!result.has_value()) return tl::make_unexpected(std::move(result).error()); destination |= result.value(); } return {}; } template [[nodiscard]] tl::expected asInt() { T value = 0; return parseInt(value).map([value]() { return value; }); } /** * @brief Attempts to parse the current field as a fixed point value with 6 bits for the fraction * * You can freely interleave this method with calls to operator*. If this is the first value * access since the last advance this will scan the current field and store it for later * use with operator* or repeated calls to parseInt/Fixed6 (even with different types). * @tparam T an Integral type supported by std::from_chars * @param destination value to store the result of successful parsing * @return an error code equivalent to what you'd get from from_chars if parsing failed */ template [[nodiscard]] tl::expected parseFixed6(T &destination) { ParseIntResult parseResult; if (state_->status == GetFieldResult::Status::ReadyToRead) { const char *begin = state_->next; // first read, consume digits parseResult = ParseFixed6({ begin, static_cast(end_ - begin) }, &state_->next); // then read the remainder of the field *state_ = GetNextField(state_->next, end_); // and prepend what was already parsed state_->value = { begin, (state_->value.data() - begin) + state_->value.size() }; } else { parseResult = ParseFixed6(state_->value); } if (parseResult.has_value()) { destination = parseResult.value(); return {}; } else { return mapError(parseResult.error()); } } template [[nodiscard]] tl::expected asFixed6() { T value = 0; return parseFixed6(value).map([value]() { return value; }); } /** * Returns the current row number */ [[nodiscard]] unsigned row() const { return row_; } /** * Returns the current column/field number (from the start of the row/record) */ [[nodiscard]] unsigned column() const { return column_; } /** * Allows accessing the value of this field in a const context * * This requires an actual non-const value access to happen first before it returns * any useful results, intended for use in error reporting (or test output). */ [[nodiscard]] std::string_view currentValue() const { return state_->value; } }; /** * @brief Show the field value along with the row/column number (mainly used in test failure messages) * @param stream output stream, expected to have overloads for unsigned, std::string_view, and char* * @param field Object to display * @return the stream, to allow chaining */ inline std::ostream &operator<<(std::ostream &stream, const DataFileField &field) { return stream << "\"" << field.currentValue() << "\" (at row " << field.row() << ", column " << field.column() << ")"; } class FieldIterator { GetFieldResult *state_; const char *const end_; const unsigned row_; unsigned column_ = 0; public: using iterator_category = std::input_iterator_tag; using value_type = DataFileField; FieldIterator() : state_(nullptr) , end_(nullptr) , row_(0) { } FieldIterator(GetFieldResult *state, const char *end, unsigned row) : state_(state) , end_(end) , row_(row) { state_->status = GetFieldResult::Status::ReadyToRead; } [[nodiscard]] bool operator==(const FieldIterator &rhs) const { if (state_ == nullptr && rhs.state_ == nullptr) return true; return state_ != nullptr && rhs.state_ != nullptr && state_->next == rhs.state_->next; } [[nodiscard]] bool operator!=(const FieldIterator &rhs) const { return !(*this == rhs); } /** * Advances to the next field in the current record */ FieldIterator &operator++() { return *this += 1; } /** * @brief Advances by the specified number of fields * * if a non-zero increment is provided and advancing the iterator causes it to reach the end * of the record the iterator is invalidated. It will compare equal to an end iterator and * cannot be used for value access or any further parsing * @param increment how many fields to advance (can be 0) * @return self-reference */ FieldIterator &operator+=(unsigned increment) { if (increment == 0) return *this; if (state_->status == GetFieldResult::Status::ReadyToRead) { // We never read the value and no longer need it, discard it so that we end up // advancing past the field delimiter (as if a value access had happened) *state_ = DiscardField(state_->next, end_); } if (state_->endOfRecord()) { state_ = nullptr; } else { unsigned fieldsSkipped = 0; // By this point we've already advanced past the end of this field (either because the // last value access found the end of the field by necessity or we discarded it a few // lines up), so we only need to advance further if an increment greater than 1 was // provided. *state_ = DiscardMultipleFields(state_->next, end_, increment - 1, &fieldsSkipped); // As we've consumed the current field by this point we need to increment the internal // column counter one extra time so we have an accurate value. column_ += fieldsSkipped + 1; // We use Status::ReadyToRead as a marker so we only read the next value on the next // value access, this allows consumers to choose the most efficient method (e.g. if // they want the value as an int) or even repeated advances without using a value. state_->status = GetFieldResult::Status::ReadyToRead; } return *this; } /** * @brief Returns a view of the current field * * The returned value is a thin wrapper over the current state of this iterator (or last * successful read if incrementing this iterator would result in it reaching the end state). */ [[nodiscard]] value_type operator*() { return { state_, end_, row_, column_ }; } /** * @brief Returns the current row number */ [[nodiscard]] unsigned row() const { return row_; } /** * @brief Returns the current column/field number (from the start of the row/record) */ [[nodiscard]] unsigned column() const { return column_; } }; class DataFileRecord { GetFieldResult *state_; const char *const end_; const unsigned row_; public: DataFileRecord(GetFieldResult *state, const char *end, unsigned row) : state_(state) , end_(end) , row_(row) { } [[nodiscard]] FieldIterator begin() { return { state_, end_, row_ }; } [[nodiscard]] FieldIterator end() const { return {}; } [[nodiscard]] unsigned row() const { return row_; } }; class RecordIterator { GetFieldResult state_; const char *const end_; unsigned row_ = 0; public: using iterator_category = std::forward_iterator_tag; using value_type = DataFileRecord; RecordIterator() : state_(nullptr, GetFieldResult::Status::EndOfFile) , end_(nullptr) { } RecordIterator(const char *begin, const char *end, bool skippedHeader) : state_(begin) , end_(end) , row_(skippedHeader ? 1 : 0) { } [[nodiscard]] bool operator==(const RecordIterator &rhs) const { return state_.next == rhs.state_.next; } [[nodiscard]] bool operator!=(const RecordIterator &rhs) const { return !(*this == rhs); } RecordIterator &operator++() { return *this += 1; } RecordIterator &operator+=(unsigned increment) { if (increment == 0) return *this; if (!state_.endOfRecord()) { // The field iterator either hasn't been used or hasn't consumed the entire record state_ = DiscardRemainingFields(state_.next, end_); } if (state_.endOfFile()) { state_.next = nullptr; } else { unsigned recordsSkipped = 0; // By this point we've already advanced past the end of this record (either because the // last value access found the end of the record by necessity or we discarded any // leftovers a few lines up), so we only need to advance further if an increment // greater than 1 was provided. state_ = DiscardMultipleRecords(state_.next, end_, increment - 1, &recordsSkipped); // As we've consumed the current record by this point we need to increment the internal // row counter one extra time so we have an accurate value. row_ += recordsSkipped + 1; // We use Status::ReadyToRead as a marker in case the DataFileField iterator is never // used, so the next call to operator+= will advance past the current record state_.status = GetFieldResult::Status::ReadyToRead; } return *this; } [[nodiscard]] DataFileRecord operator*() { return { &state_, end_, row_ }; } /** * @brief Exposes the current location of this input iterator. * * This is only expected to be used internally so the DataFile instance knows where the header * ends and the body begins. You probably don't want to use this directly. */ [[nodiscard]] const char *data() const { return state_.next; } /** * @brief Returns the current row/record number (from the start of the file) * * The header row is always considered row 0, however if you've called DataFile.parseHeader() * before calling DataFile.begin() then you'll get row 1 as the first record of the range. */ [[nodiscard]] unsigned row() const { return row_; } }; } // namespace devilution ================================================ FILE: Source/data/parser.cpp ================================================ #include "parser.hpp" namespace devilution { GetFieldResult HandleRecordTerminator(const char *begin, const char *end) { if (begin == end) { return { end, GetFieldResult::Status::NoFinalTerminator }; } if (*begin == '\r') { ++begin; if (begin == end) { return { end, GetFieldResult::Status::FileTruncated }; } // carriage returns should be followed by a newline, so let's let the following checks handle it } if (*begin == '\n') { ++begin; if (begin == end) { return { end, GetFieldResult::Status::EndOfFile }; } return { begin, GetFieldResult::Status::EndOfRecord }; } return { begin, GetFieldResult::Status::BadRecordTerminator }; } GetFieldResult DiscardMultipleFields(const char *begin, const char *end, unsigned skipLength, unsigned *fieldsSkipped) { GetFieldResult result { begin }; unsigned skipCount = 0; while (skipCount < skipLength) { ++skipCount; result = DiscardField(result.next, end); if (result.endOfRecord()) { // Found the end of record early break; } } if (fieldsSkipped != nullptr) { *fieldsSkipped = skipCount; } return result; } GetFieldResult DiscardMultipleRecords(const char *begin, const char *end, unsigned skipLength, unsigned *recordsSkipped) { GetFieldResult result { begin }; unsigned skipCount = 0; while (skipCount < skipLength) { ++skipCount; result = DiscardRemainingFields(result.next, end); if (result.endOfFile()) { // Found the end of file early break; } } if (recordsSkipped != nullptr) { *recordsSkipped = skipCount; } return result; } } // namespace devilution ================================================ FILE: Source/data/parser.hpp ================================================ #pragma once #include #include #include "utils/is_of.hpp" namespace devilution { struct GetFieldResult { std::string_view value; const char *next = nullptr; enum class Status { ReadyToRead, EndOfField, EndOfRecord, BadRecordTerminator, EndOfFile, FileTruncated, NoFinalTerminator } status = Status::ReadyToRead; GetFieldResult() = default; GetFieldResult(const char *next) : value() , next(next) , status(Status::ReadyToRead) { } GetFieldResult(const char *next, const Status &status) : value() , next(next) , status(status) { } /** * @brief Recreates a GetFieldResult with a new value */ GetFieldResult(std::string_view value, const GetFieldResult &result) : value(value) , next(result.next) , status(result.status) { } /** * @brief Returns true if the last read reached the end of the current record */ [[nodiscard]] bool endOfRecord() const { return IsAnyOf(status, Status::EndOfRecord, Status::BadRecordTerminator) || endOfFile(); } /** * @brief Returns true if the last read reached the end of the file/stream */ [[nodiscard]] bool endOfFile() const { return IsAnyOf(status, Status::EndOfFile, Status::FileTruncated, Status::NoFinalTerminator); } }; /** * @brief Checks if this character is potentially part of a record terminator sequence * @param c character to check * @return true if it's a record terminator (lf) or carriage return (cr, accepted as the start of a crlf pair) */ constexpr bool IsRecordTerminator(char c) { return c == '\r' || c == '\n'; } /** * @brief Checks if this character is a field separator * * Note that record terminator sequences also act to separate fields * @param c character to check * @return true if it's a field separator (tab) or part of a record terminator sequence */ constexpr bool IsFieldSeparator(char c) { return c == '\t' || IsRecordTerminator(c); } /** * @brief Consumes the current record terminator sequence and returns a result describing whether at least one more record is available. * * Assumes that begin points to a record terminator (lf or crlf) or the last read reached the end * of the final record (in which case the terminator is optional). If we reached the end of the * stream (`begin == end`) then `status` will compare equal to Status::NoFinalTerminator. If we * found a carriage return (cr) character just before the end of the stream then it's likely the * file was truncated, `status` will contain Status::FileTruncated. If we found a carriage return * that is followed by any character other than a newline (lf), or `begin` didn't point to a record * terminator, then `status` will be Status::BadRecordTerminator and the file has probably been * mangled. Otherwise `status` will be Status::EndOfRecord at least one more record is available or * Status::EndOfFile if this was the end of the last record. * @param begin start of a stream (expected to be pointing to a record terminator) * @param end one past the last character of the stream * @return a struct containing a pointer to the start of the next record (if more characters are * available) or a copy of end, and a status code describing the terminator. */ GetFieldResult HandleRecordTerminator(const char *begin, const char *end); /** * @brief Consumes the current field (or record) separator and returns a result describing whether at least one more field is available. * * Assumes that begin points to a field or record separator (tab, cr, lf, or EOF). If there are * more fields in the current record then the return value will have a pointer to the start of the * next field and `status` will be GetFieldResult::Status::EndOfField. Otherwise refer to * HandleRecordTerminator for a description of the different codes. * @param begin start of a stream (expected to be pointing to a record separator) * @param end one past the last character of the stream * @return a struct containing a pointer to the start of the next field (if more characters are * available) or a copy of end, and optionally an error code describing what type of * separator was found. */ inline GetFieldResult HandleFieldSeparator(const char *begin, const char *end) { if (begin != end && *begin == '\t') { return { begin + 1, GetFieldResult::Status::EndOfField }; } return HandleRecordTerminator(begin, end); } /** * @brief Advances to the next field separator without saving any characters * @param begin first character of the stream * @param end one past the last character in the stream * @return a GetFieldResult struct containing an empty value, a pointer to the start of the next * field/record, and a status code describing what type of separator was found */ inline GetFieldResult DiscardField(const char *begin, const char *end) { const char *nextSeparator = std::find_if(begin, end, IsFieldSeparator); return HandleFieldSeparator(nextSeparator, end); } /** * @brief Advances by the specified number of fields or until the end of the record, whichever occurs first * @param begin first character of the stream * @param end one past the last character in the stream * @param skipLength how many fields to skip (specifying 0 will cause the method to return without advancing) * @param fieldsSkipped optional output parameter, will be filled with a count of how many fields * were skipped in case the end of record was reached early * @return a GetFieldResult struct containing an empty value, a pointer to the start of the next * field/record, and a status code describing what type of separator was found */ GetFieldResult DiscardMultipleFields(const char *begin, const char *end, unsigned skipLength, unsigned *fieldsSkipped = nullptr); /** * @brief Advances by the specified number of records or until the end of the file, whichever occurs first * @param begin first character of the stream * @param end one past the last character in the stream * @param skipLength how many records to skip (specifying 0 will cause the method to return without advancing) * @param recordsSkipped optional output parameter, will be filled with a count of how many records * were skipped in case the end of file was reached early * @return a GetFieldResult struct containing an empty value, a pointer to the start of the next * record, and a status code describing what type of separator was found */ GetFieldResult DiscardMultipleRecords(const char *begin, const char *end, unsigned skipLength, unsigned *recordsSkipped = nullptr); /** * @brief Discard any remaining fields in the current record * @param begin pointer to the current character in the stream * @param end one past the last character in the stream * @return a GetFieldResult struct containing an empty value, the start of the next record (or * `end`), and a status describing whether more records are available */ inline GetFieldResult DiscardRemainingFields(const char *begin, const char *end) { const char *nextSeparator = std::find_if(begin, end, IsRecordTerminator); return HandleRecordTerminator(nextSeparator, end); } /** * @brief Returns a view of the next field from a tab-delimited stream. * * Note that the result *always* contains a value after calling this function as a zero-length * field is a valid value. This function consumes the field separator whenever possible, the * `next` member of the returned type will be either the start of the next field/record or `end`. * The `status` member contains additional information to distinguish between the end of a field * and the end of a record. If there are additional fields in this record then `status` will be * GetFieldResult::Status::EndOfField, otherwise refer to HandleRecordTerminator for the meanings * associated with the remaining codes. * @param begin first character of the stream * @param end one past the last character in the stream * @return a GetFieldResult struct containing a string_view of the field, the start of the next * field/record, and a status code describing what type of separator was found */ inline GetFieldResult GetNextField(const char *begin, const char *end) { const char *nextSeparator = std::find_if(begin, end, IsFieldSeparator); // Can't use the string_view(It, It) constructor since that was only added in C++20... return { { begin, static_cast(nextSeparator - begin) }, HandleFieldSeparator(nextSeparator, end) }; } } // namespace devilution ================================================ FILE: Source/data/record_reader.cpp ================================================ #include "data/record_reader.hpp" namespace devilution { void RecordReader::advance() { if (needsIncrement_) { ++it_; } else { needsIncrement_ = true; } if (it_ == end_) { DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename_); } } } // namespace devilution ================================================ FILE: Source/data/record_reader.hpp ================================================ #pragma once #include #include #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" namespace devilution { /** * @brief A record reader that treats every error as fatal. */ class RecordReader { public: RecordReader(DataFileRecord &record, std::string_view filename) : it_(record.begin()) , end_(record.end()) , filename_(filename) { } template typename std::enable_if_t, void> readInt(std::string_view name, T &out) { DataFileField field = nextField(); failOnError(field.parseInt(out), name, field); } template typename std::enable_if_t, void> readOptionalInt(std::string_view name, T &out) { DataFileField field = nextField(); if (field.value().empty()) return; failOnError(field.parseInt(out), name, field); } template void readIntArray(std::string_view name, T (&out)[N]) { DataFileField field = nextField(); failOnError(field.parseIntArray(out), name, field); } template void readEnumArray(std::string_view name, std::optional fillMissing, T (&out)[N], F &&parseFn) { DataFileField field = nextField(); failOnError(field.parseEnumArray(out, fillMissing, parseFn), name, field, DataFileField::Error::InvalidValue); } template void readIntArray(std::string_view name, std::array &out) { DataFileField field = nextField(); failOnError(field.parseIntArray(out), name, field); } template typename std::enable_if_t, void> readFixed6(std::string_view name, T &out) { DataFileField field = nextField(); failOnError(field.parseFixed6(out), name, field); } void readBool(std::string_view name, bool &out) { DataFileField field = nextField(); failOnError(field.parseBool(out), name, field); } void readString(std::string_view name, std::string &out) { advance(); out = (*it_).value(); } template void read(std::string_view name, T &out, F &&parseFn) { DataFileField field = nextField(); tl::expected result = parseFn(field.value()); failOnError(result, name, field, DataFileField::Error::InvalidValue); out = *std::move(result); } template void readEnumList(std::string_view name, T &out, F &&parseFn) { DataFileField field = nextField(); failOnError(field.parseEnumList(out, std::forward(parseFn)), name, field, DataFileField::Error::InvalidValue); } std::string_view value() { advance(); needsIncrement_ = false; return (*it_).value(); } void advance(); DataFileField nextField() { advance(); return *it_; } private: template void failOnError(const tl::expected &result, std::string_view name, const DataFileField &field) { if (!result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename_, name, field); } } template void failOnError(const tl::expected &result, std::string_view name, const DataFileField &field, DataFileField::Error error) { if (!result.has_value()) { DataFile::reportFatalFieldError(error, filename_, name, field, result.error()); } } FieldIterator it_; const FieldIterator end_; std::string_view filename_; bool needsIncrement_ = false; }; } // namespace devilution ================================================ FILE: Source/data/value_reader.cpp ================================================ #include "data/value_reader.hpp" #include #include "appfat.h" namespace devilution { ValueReader::ValueReader(DataFile &dataFile, std::string_view filename) : it_(dataFile.begin()) , end_(dataFile.end()) , filename_(filename) { } DataFileField ValueReader::getValueField(std::string_view expectedKey) { if (it_ == end_) { app_fatal(fmt::format("Missing field {} in {}", expectedKey, filename_)); } DataFileRecord record = *it_; FieldIterator fieldIt = record.begin(); const FieldIterator endField = record.end(); const std::string_view key = (*fieldIt).value(); if (key != expectedKey) { app_fatal(fmt::format("Unexpected field in {}: got {}, expected {}", filename_, key, expectedKey)); } ++fieldIt; if (fieldIt == endField) { DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename_); } return *fieldIt; } } // namespace devilution ================================================ FILE: Source/data/value_reader.hpp ================================================ #pragma once #include #include #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" namespace devilution { /** * @brief A value reader. */ class ValueReader { public: ValueReader(DataFile &dataFile, std::string_view filename); DataFileField getValueField(std::string_view expectedKey); template void readValue(std::string_view expectedKey, T &outValue, F &&readFn) { DataFileField valueField = getValueField(expectedKey); if (const tl::expected result = readFn(valueField, outValue); !result.has_value()) { DataFile::reportFatalFieldError(result.error(), filename_, "Value", valueField); } ++it_; } template void read(std::string_view expectedKey, T &outValue, F &&parseFn) { readValue(expectedKey, outValue, [&parseFn](DataFileField &valueField, T &outValue) -> tl::expected { const auto result = parseFn(valueField.value()); if (!result.has_value()) { return tl::make_unexpected(devilution::DataFileField::Error::InvalidValue); } outValue = result.value(); return {}; }); } template void readEnumList(std::string_view expectedKey, T &outValue, F &&parseFn) { readValue(expectedKey, outValue, [&parseFn](DataFileField &valueField, T &outValue) -> tl::expected { const auto result = valueField.parseEnumList(outValue, std::forward(parseFn)); if (!result.has_value()) { return tl::make_unexpected(devilution::DataFileField::Error::InvalidValue); } return {}; }); } template typename std::enable_if_t, void> readInt(std::string_view expectedKey, T &outValue) { readValue(expectedKey, outValue, [](DataFileField &valueField, T &outValue) { return valueField.parseInt(outValue); }); } template typename std::enable_if_t, void> readDecimal(std::string_view expectedKey, T &outValue) { readValue(expectedKey, outValue, [](DataFileField &valueField, T &outValue) { return valueField.parseFixed6(outValue); }); } void readString(std::string_view expectedKey, std::string &outValue) { readValue(expectedKey, outValue, [](DataFileField &valueField, std::string &outValue) { outValue = valueField.value(); return tl::expected {}; }); } void readChar(std::string_view expectedKey, char &outValue) { readValue(expectedKey, outValue, [](DataFileField &valueField, char &outValue) -> tl::expected { if (valueField.value().size() != 1) { return tl::make_unexpected(devilution::DataFileField::Error::InvalidValue); } outValue = valueField.value().at(0); return {}; }); } private: RecordIterator it_; const RecordIterator end_; std::string_view filename_; }; } // namespace devilution ================================================ FILE: Source/dead.cpp ================================================ /** * @file dead.cpp * * Implementation of functions for placing dead monsters. */ #include "dead.h" #include #include "diablo.h" #include "headless_mode.hpp" #include "levels/gendung.h" #include "lighting.h" #include "monster.h" #include "tables/misdat.h" namespace devilution { Corpse Corpses[MaxCorpses]; int8_t stonendx; namespace { void InitDeadAnimationFromMonster(Corpse &corpse, const CMonster &mon) { const AnimStruct &animData = mon.getAnimData(MonsterGraphic::Death); if (animData.sprites) { corpse.sprites.emplace(*animData.sprites); } else { corpse.sprites = std::nullopt; } corpse.frame = animData.frames - 1; corpse.width = animData.width; } void MoveLightToCorpse(Monster &monster) { for (int dx = 0; dx < MAXDUNX; dx++) { for (int dy = 0; dy < MAXDUNY; dy++) { if ((dCorpse[dx][dy] & 0x1F) == monster.corpseId) { ChangeLightXY(monster.lightId, { dx, dy }); return; } } } AddUnLight(monster.lightId); } } // namespace void InitCorpses() { int8_t mtypes[MaxMonsters] = {}; int8_t nd = 0; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { CMonster &monsterType = LevelMonsterTypes[i]; if (mtypes[monsterType.type] != 0) continue; InitDeadAnimationFromMonster(Corpses[nd], monsterType); Corpses[nd].translationPaletteIndex = 0; nd++; monsterType.corpseId = nd; mtypes[monsterType.type] = nd; } nd++; // Unused blood spatter if (!HeadlessMode) Corpses[nd].sprites.emplace(*GetMissileSpriteData(MissileGraphicID::StoneCurseShatter).sprites); Corpses[nd].frame = 11; Corpses[nd].width = 128; Corpses[nd].translationPaletteIndex = 0; nd++; stonendx = nd; for (size_t i = 0; i < ActiveMonsterCount; i++) { auto &monster = Monsters[ActiveMonsters[i]]; if (monster.isUnique()) { InitDeadAnimationFromMonster(Corpses[nd], monster.type()); Corpses[nd].translationPaletteIndex = ActiveMonsters[i] + 1; nd++; monster.corpseId = nd; } } assert(static_cast(nd) <= MaxCorpses); } void AddCorpse(Point tilePosition, int8_t dv, Direction ddir) { dCorpse[tilePosition.x][tilePosition.y] = (dv & 0x1F) + (static_cast(ddir) << 5); } void MoveLightsToCorpses() { for (size_t i = 0; i < ActiveMonsterCount; i++) { auto &monster = Monsters[ActiveMonsters[i]]; if (!monster.isUnique()) continue; MoveLightToCorpse(monster); } } } // namespace devilution ================================================ FILE: Source/dead.h ================================================ /** * @file dead.h * * Interface of functions for placing dead monsters. */ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "engine/direction.hpp" #include "engine/point.hpp" namespace devilution { static constexpr unsigned MaxCorpses = 31; struct Corpse { OptionalClxSpriteListOrSheet sprites; int frame; uint16_t width; uint8_t translationPaletteIndex; /** * @brief Returns the sprite list for a given direction. * * @param direction One of the 16 directions. Valid range: [0, 15]. * @return ClxSpriteList */ [[nodiscard]] ClxSpriteList spritesForDirection(Direction direction) const { return sprites->isSheet() ? sprites->sheet()[static_cast(direction)] : sprites->list(); } }; extern Corpse Corpses[MaxCorpses]; extern int8_t stonendx; void InitCorpses(); void AddCorpse(Point tilePosition, int8_t dv, Direction ddir); void MoveLightsToCorpses(); } // namespace devilution ================================================ FILE: Source/debug.cpp ================================================ /** * @file debug.cpp * * Implementation of debug functions. */ #ifdef _DEBUG #include #include #include #include #include "debug.h" #include "automap.h" #include "cursor.h" #include "engine/load_cel.hpp" #include "engine/point.hpp" #include "lighting.h" #include "missiles.h" #include "monster.h" #include "plrmsg.h" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" namespace devilution { std::string TestMapPath; OptionalOwnedClxSpriteList pSquareCel; bool DebugToggle = false; bool DebugGodMode = false; bool DebugInvisible = false; bool DebugVision = false; bool DebugPath = false; bool DebugGrid = false; ankerl::unordered_dense::map DebugCoordsMap; bool DebugScrollViewEnabled = false; std::string debugTRN; // Used for debugging level generation uint32_t glMid1Seed[NUMLEVELS]; uint32_t glMid2Seed[NUMLEVELS]; uint32_t glMid3Seed[NUMLEVELS]; uint32_t glEndSeed[NUMLEVELS]; namespace { DebugGridTextItem SelectedDebugGridTextItem; int DebugMonsterId; std::vector SearchMonsters; std::vector SearchItems; std::vector SearchObjects; void PrintDebugMonster(const Monster &monster) { EventPlrMsg(StrCat( "Monster ", static_cast(monster.getId()), " = ", monster.name(), "\nX = ", monster.position.tile.x, ", Y = ", monster.position.tile.y, "\nEnemy = ", monster.enemy, ", HP = ", monster.hitPoints, "\nMode = ", static_cast(monster.mode), ", Var1 = ", monster.var1), UiFlags::ColorWhite); bool bActive = false; for (size_t i = 0; i < ActiveMonsterCount; i++) { if (&Monsters[ActiveMonsters[i]] == &monster) { bActive = true; break; } } EventPlrMsg(StrCat("Active List = ", bActive ? 1 : 0, ", Squelch = ", monster.activeForTicks), UiFlags::ColorWhite); } } // namespace void LoadDebugGFX() { pSquareCel = LoadCel("data\\square", 64); } void FreeDebugGFX() { pSquareCel = std::nullopt; } void GetDebugMonster() { int monsterIndex = pcursmonst; if (monsterIndex == -1) monsterIndex = std::abs(dMonster[cursPosition.x][cursPosition.y]) - 1; if (monsterIndex == -1) monsterIndex = DebugMonsterId; PrintDebugMonster(Monsters[monsterIndex]); } void NextDebugMonster() { DebugMonsterId++; if (DebugMonsterId == MaxMonsters) DebugMonsterId = 0; EventPlrMsg(StrCat("Current debug monster = ", DebugMonsterId), UiFlags::ColorWhite); } void SetDebugLevelSeedInfos(uint32_t mid1Seed, uint32_t mid2Seed, uint32_t mid3Seed, uint32_t endSeed) { glMid1Seed[currlevel] = mid1Seed; glMid2Seed[currlevel] = mid2Seed; glMid3Seed[currlevel] = mid3Seed; glEndSeed[currlevel] = endSeed; } bool IsDebugGridTextNeeded() { return SelectedDebugGridTextItem != DebugGridTextItem::None; } bool IsDebugGridInMegatiles() { switch (SelectedDebugGridTextItem) { case DebugGridTextItem::AutomapView: case DebugGridTextItem::dungeon: case DebugGridTextItem::pdungeon: case DebugGridTextItem::Protected: return true; default: return false; } } DebugGridTextItem GetDebugGridTextType() { return SelectedDebugGridTextItem; } void SetDebugGridTextType(DebugGridTextItem value) { SelectedDebugGridTextItem = value; } bool GetDebugGridText(Point dungeonCoords, std::string &debugGridText) { int info = 0; int blankValue = 0; debugGridText.clear(); Point megaCoords = dungeonCoords.worldToMega(); switch (SelectedDebugGridTextItem) { case DebugGridTextItem::coords: StrAppend(debugGridText, dungeonCoords.x, ":", dungeonCoords.y); return true; case DebugGridTextItem::cursorcoords: if (dungeonCoords != cursPosition) return false; StrAppend(debugGridText, dungeonCoords.x, ":", dungeonCoords.y); return true; case DebugGridTextItem::objectindex: { info = 0; Object *object = FindObjectAtPosition(dungeonCoords); if (object != nullptr) { info = static_cast(object->_otype); } break; } case DebugGridTextItem::microTiles: { const MICROS µs = DPieceMicros[dPiece[dungeonCoords.x][dungeonCoords.y]]; for (const LevelCelBlock tile : micros.mt) { if (!tile.hasValue()) break; if (!debugGridText.empty()) debugGridText += '\n'; StrAppend(debugGridText, tile.frame(), " "); switch (tile.type()) { case TileType::Square: StrAppend(debugGridText, "S"); break; case TileType::TransparentSquare: StrAppend(debugGridText, "T"); break; case TileType::LeftTriangle: StrAppend(debugGridText, "<"); break; case TileType::RightTriangle: StrAppend(debugGridText, ">"); break; case TileType::LeftTrapezoid: StrAppend(debugGridText, "\\"); break; case TileType::RightTrapezoid: StrAppend(debugGridText, "/"); break; } } if (debugGridText.empty()) return false; return true; } break; case DebugGridTextItem::dPiece: info = dPiece[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dTransVal: info = dTransVal[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dLight: info = dLight[dungeonCoords.x][dungeonCoords.y]; blankValue = LightsMax; break; case DebugGridTextItem::dPreLight: info = dPreLight[dungeonCoords.x][dungeonCoords.y]; blankValue = LightsMax; break; case DebugGridTextItem::dFlags: info = static_cast(dFlags[dungeonCoords.x][dungeonCoords.y]); break; case DebugGridTextItem::dPlayer: info = dPlayer[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dMonster: info = dMonster[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::missiles: { for (auto &missile : Missiles) { if (missile.position.tile == dungeonCoords) { if (!debugGridText.empty()) debugGridText += '\n'; debugGridText.append(std::to_string((int)missile._mitype)); } } if (debugGridText.empty()) return false; return true; } break; case DebugGridTextItem::dCorpse: info = dCorpse[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dItem: info = dItem[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dSpecial: info = dSpecial[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::dObject: info = dObject[dungeonCoords.x][dungeonCoords.y]; break; case DebugGridTextItem::Solid: info = TileHasAny(dungeonCoords, TileProperties::Solid) << 0 | TileHasAny(dungeonCoords, TileProperties::BlockLight) << 1 | TileHasAny(dungeonCoords, TileProperties::BlockMissile) << 2; break; case DebugGridTextItem::Transparent: info = TileHasAny(dungeonCoords, TileProperties::Transparent) << 0 | TileHasAny(dungeonCoords, TileProperties::TransparentLeft) << 1 | TileHasAny(dungeonCoords, TileProperties::TransparentRight) << 2; break; case DebugGridTextItem::Trap: info = TileHasAny(dungeonCoords, TileProperties::Trap); break; case DebugGridTextItem::AutomapView: if (megaCoords.x >= 0 && megaCoords.x < DMAXX && megaCoords.y >= 0 && megaCoords.y < DMAXY) info = AutomapView[megaCoords.x][megaCoords.y]; break; case DebugGridTextItem::dungeon: if (megaCoords.x >= 0 && megaCoords.x < DMAXX && megaCoords.y >= 0 && megaCoords.y < DMAXY) info = dungeon[megaCoords.x][megaCoords.y]; break; case DebugGridTextItem::pdungeon: if (megaCoords.x >= 0 && megaCoords.x < DMAXX && megaCoords.y >= 0 && megaCoords.y < DMAXY) info = pdungeon[megaCoords.x][megaCoords.y]; break; case DebugGridTextItem::Protected: if (megaCoords.x >= 0 && megaCoords.x < DMAXX && megaCoords.y >= 0 && megaCoords.y < DMAXY) info = Protected.test(megaCoords.x, megaCoords.y); break; case DebugGridTextItem::None: return false; } if (info == blankValue) return false; StrAppend(debugGridText, info); return true; } bool IsDebugAutomapHighlightNeeded() { return SearchMonsters.size() > 0 || SearchItems.size() > 0 || SearchObjects.size() > 0; } bool ShouldHighlightDebugAutomapTile(Point position) { auto matchesSearched = [](const std::string_view name, const std::vector &searchedNames) { const std::string lowercaseName = AsciiStrToLower(name); for (const auto &searchedName : searchedNames) { if (lowercaseName.find(searchedName) != std::string::npos) { return true; } } return false; }; if (SearchMonsters.size() > 0 && dMonster[position.x][position.y] != 0) { const int mi = std::abs(dMonster[position.x][position.y]) - 1; const Monster &monster = Monsters[mi]; if (matchesSearched(monster.name(), SearchMonsters)) return true; } if (SearchItems.size() > 0 && dItem[position.x][position.y] != 0) { const int itemId = std::abs(dItem[position.x][position.y]) - 1; const Item &item = Items[itemId]; if (matchesSearched(item.getName(), SearchItems)) return true; } if (SearchObjects.size() > 0 && IsObjectAtPosition(position)) { const Object &object = ObjectAtPosition(position); if (matchesSearched(object.name(), SearchObjects)) return true; } return false; } void AddDebugAutomapMonsterHighlight(std::string_view name) { SearchMonsters.emplace_back(name); } void AddDebugAutomapItemHighlight(std::string_view name) { SearchItems.emplace_back(name); } void AddDebugAutomapObjectHighlight(std::string_view name) { SearchObjects.emplace_back(name); } void ClearDebugAutomapHighlights() { SearchMonsters.clear(); SearchItems.clear(); SearchObjects.clear(); } } // namespace devilution #endif ================================================ FILE: Source/debug.h ================================================ /** * @file debug.h * * Interface of debug functions. */ #pragma once #include #include #include #include #include "diablo.h" #include "engine/clx_sprite.hpp" #include "engine/point.hpp" namespace devilution { extern std::string TestMapPath; extern OptionalOwnedClxSpriteList pSquareCel; extern bool DebugToggle; extern bool DebugGodMode; extern bool DebugInvisible; extern bool DebugVision; extern bool DebugPath; extern bool DebugGrid; extern ankerl::unordered_dense::map DebugCoordsMap; extern bool DebugScrollViewEnabled; extern std::string debugTRN; extern uint32_t glMid1Seed[NUMLEVELS]; extern uint32_t glMid2Seed[NUMLEVELS]; extern uint32_t glMid3Seed[NUMLEVELS]; extern uint32_t glEndSeed[NUMLEVELS]; enum class DebugGridTextItem : uint16_t { None, microTiles, dPiece, dTransVal, dLight, dPreLight, dFlags, dPlayer, dMonster, missiles, dCorpse, dObject, dItem, dSpecial, coords, cursorcoords, objectindex, // take dPiece as index Solid, Transparent, Trap, // megatiles AutomapView, dungeon, pdungeon, Protected, }; void FreeDebugGFX(); void LoadDebugGFX(); void GetDebugMonster(); void NextDebugMonster(); void SetDebugLevelSeedInfos(uint32_t mid1Seed, uint32_t mid2Seed, uint32_t mid3Seed, uint32_t endSeed); bool IsDebugGridTextNeeded(); bool IsDebugGridInMegatiles(); DebugGridTextItem GetDebugGridTextType(); void SetDebugGridTextType(DebugGridTextItem value); bool GetDebugGridText(Point dungeonCoords, std::string &debugGridText); bool IsDebugAutomapHighlightNeeded(); bool ShouldHighlightDebugAutomapTile(Point position); void AddDebugAutomapMonsterHighlight(std::string_view name); void AddDebugAutomapItemHighlight(std::string_view name); void AddDebugAutomapObjectHighlight(std::string_view name); void ClearDebugAutomapHighlights(); } // namespace devilution ================================================ FILE: Source/diablo.cpp ================================================ /** * @file diablo.cpp * * Implementation of the main game initialization functions. */ #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include #include #include "DiabloUI/selstart.h" #include "appfat.h" #include "automap.h" #include "capture.h" #include "control/control.hpp" #include "cursor.h" #include "dead.h" #ifdef _DEBUG #include "debug.h" #endif #include "DiabloUI/diabloui.h" #include "controls/control_mode.hpp" #include "controls/keymapper.hpp" #include "controls/plrctrls.h" #include "controls/remap_keyboard.h" #include "diablo.h" #include "diablo_msg.hpp" #include "discord/discord.h" #include "doom.h" #include "encrypt.h" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/demomode.h" #include "engine/dx.h" #include "engine/events.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/sound.h" #include "game_mode.hpp" #include "gamemenu.h" #include "gmenu.h" #include "headless_mode.hpp" #include "help.h" #include "hwcursor.hpp" #include "init.hpp" #include "inv.h" #include "levels/drlg_l1.h" #include "levels/drlg_l2.h" #include "levels/drlg_l3.h" #include "levels/drlg_l4.h" #include "levels/gendung.h" #include "levels/setmaps.h" #include "levels/themes.h" #include "levels/town.h" #include "levels/trigs.h" #include "lighting.h" #include "loadsave.h" #include "lua/lua_event.hpp" #include "lua/lua_global.hpp" #include "menu.h" #include "minitext.h" #include "missiles.h" #include "movie.h" #include "multi.h" #include "nthread.h" #include "objects.h" #include "options.h" #include "panels/console.hpp" #include "panels/info_box.hpp" #include "panels/partypanel.hpp" #include "panels/spell_book.hpp" #include "panels/spell_list.hpp" #include "pfile.h" #include "plrmsg.h" #include "qol/chatlog.h" #include "qol/floatingnumbers.h" #include "qol/itemlabels.h" #include "qol/monhealthbar.h" #include "qol/stash.h" #include "qol/xpbar.h" #include "quick_messages.hpp" #include "restrict.h" #include "stores.h" #include "storm/storm_net.hpp" #include "storm/storm_svid.h" #include "tables/monstdat.h" #include "tables/playerdat.hpp" #include "towners.h" #include "track.h" #include "utils/console.h" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/parse_int.hpp" #include "utils/paths.h" #include "utils/screen_reader.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_thread.h" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" #ifndef USE_SDL1 #include "controls/touch/gamepad.h" #include "controls/touch/renderers.h" #endif #ifdef __vita__ #include "platform/vita/touch.h" #endif #ifdef GPERF_HEAP_FIRST_GAME_ITERATION #include #endif namespace devilution { uint32_t DungeonSeeds[NUMLEVELS]; std::optional LevelSeeds[NUMLEVELS]; Point MousePosition; bool gbRunGameResult; bool ReturnToMainMenu; /** Enable updating of player character, set to false once Diablo dies */ bool gbProcessPlayers; bool gbLoadGame; bool cineflag; int PauseMode; clicktype sgbMouseDown; uint16_t gnTickDelay = 50; char gszProductName[64] = "DevilutionX vUnknown"; #ifdef _DEBUG bool DebugDisableNetworkTimeout = false; std::vector DebugCmdsFromCommandLine; #endif GameLogicStep gGameLogicStep = GameLogicStep::None; /** This and the following mouse variables are for handling in-game click-and-hold actions */ PlayerActionType LastPlayerAction = PlayerActionType::None; // Controller support: Actions to run after updating the cursor state. // Defined in SourceX/controls/plctrls.cpp. extern void plrctrls_after_check_curs_move(); extern void plrctrls_every_frame(); extern void plrctrls_after_game_logic(); namespace { char gszVersionNumber[64] = "internal version unknown"; bool gbGameLoopStartup; bool forceSpawn; bool forceDiablo; int sgnTimeoutCurs; bool gbShowIntro = true; /** To know if these things have been done when we get to the diablo_deinit() function */ bool was_archives_init = false; /** To know if surfaces have been initialized or not */ bool was_window_init = false; bool was_ui_init = false; void StartGame(interface_mode uMsg) { CalcViewportGeometry(); cineflag = false; InitCursor(); #ifdef _DEBUG LoadDebugGFX(); #endif assert(HeadlessMode || ghMainWnd); music_stop(); InitMonsterHealthBar(); InitXPBar(); ShowProgress(uMsg); gmenu_init_menu(); InitLevelCursor(); sgnTimeoutCurs = CURSOR_NONE; sgbMouseDown = CLICK_NONE; LastPlayerAction = PlayerActionType::None; } void FreeGame() { FreeMonsterHealthBar(); FreeXPBar(); FreeControlPan(); FreeInvGFX(); FreeGMenu(); FreeQuestText(); FreeInfoBoxGfx(); FreeStoreMem(); for (Player &player : Players) ResetPlayerGFX(player); FreeCursor(); #ifdef _DEBUG FreeDebugGFX(); #endif FreeGameMem(); stream_stop(); music_stop(); } bool ProcessInput() { if (PauseMode == 2) { return false; } plrctrls_every_frame(); if (!gbIsMultiplayer && gmenu_is_active()) { RedrawViewport(); return false; } if (!gmenu_is_active() && sgnTimeoutCurs == CURSOR_NONE) { #ifdef __vita__ FinishSimulatedMouseClicks(MousePosition); #endif CheckCursMove(); plrctrls_after_check_curs_move(); RepeatPlayerAction(); } return true; } void LeftMouseCmd(bool bShift) { bool bNear; assert(!GetMainPanel().contains(MousePosition)); if (leveltype == DTYPE_TOWN) { CloseGoldWithdraw(); CloseStash(); if (pcursitem != -1 && pcurs == CURSOR_HAND) NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); if (pcursmonst != -1) NetSendCmdLocParam1(true, CMD_TALKXY, cursPosition, pcursmonst); if (pcursitem == -1 && pcursmonst == -1 && PlayerUnderCursor == nullptr) { LastPlayerAction = PlayerActionType::Walk; NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); } return; } const Player &myPlayer = *MyPlayer; bNear = myPlayer.position.tile.WalkingDistance(cursPosition) < 2; if (pcursitem != -1 && pcurs == CURSOR_HAND && !bShift) { NetSendCmdLocParam1(true, invflag ? CMD_GOTOGETITEM : CMD_GOTOAGETITEM, cursPosition, pcursitem); } else if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled() && (!bShift || (bNear && ObjectUnderCursor->_oBreak == 1))) { LastPlayerAction = PlayerActionType::OperateObject; NetSendCmdLoc(MyPlayerId, true, pcurs == CURSOR_DISARM ? CMD_DISARMXY : CMD_OPOBJXY, cursPosition); } else if (myPlayer.UsesRangedWeapon()) { if (bShift) { LastPlayerAction = PlayerActionType::Attack; NetSendCmdLoc(MyPlayerId, true, CMD_RATTACKXY, cursPosition); } else if (pcursmonst != -1) { if (CanTalkToMonst(Monsters[pcursmonst])) { NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); } else { LastPlayerAction = PlayerActionType::AttackMonsterTarget; NetSendCmdParam1(true, CMD_RATTACKID, pcursmonst); } } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { LastPlayerAction = PlayerActionType::AttackPlayerTarget; NetSendCmdParam1(true, CMD_RATTACKPID, PlayerUnderCursor->getId()); } } else { if (bShift) { if (pcursmonst != -1) { if (CanTalkToMonst(Monsters[pcursmonst])) { NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); } else { LastPlayerAction = PlayerActionType::Attack; NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); } } else { LastPlayerAction = PlayerActionType::Attack; NetSendCmdLoc(MyPlayerId, true, CMD_SATTACKXY, cursPosition); } } else if (pcursmonst != -1) { LastPlayerAction = PlayerActionType::AttackMonsterTarget; NetSendCmdParam1(true, CMD_ATTACKID, pcursmonst); } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { LastPlayerAction = PlayerActionType::AttackPlayerTarget; NetSendCmdParam1(true, CMD_ATTACKPID, PlayerUnderCursor->getId()); } } if (!bShift && pcursitem == -1 && ObjectUnderCursor == nullptr && pcursmonst == -1 && PlayerUnderCursor == nullptr) { LastPlayerAction = PlayerActionType::Walk; NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); } } bool TryOpenDungeonWithMouse() { if (leveltype != DTYPE_TOWN) return false; const Item &holdItem = MyPlayer->HoldItem; if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) OpenHive(); else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) OpenGrave(); else return false; NewCursor(CURSOR_HAND); return true; } void LeftMouseDown(uint16_t modState) { LastPlayerAction = PlayerActionType::None; if (gmenu_left_mouse(true)) return; if (CheckMuteButton()) return; if (sgnTimeoutCurs != CURSOR_NONE) return; if (MyPlayerIsDead) { CheckMainPanelButtonDead(); return; } if (PauseMode == 2) { return; } if (DoomFlag) { doom_close(); return; } if (SpellSelectFlag) { SetSpell(); return; } if (IsPlayerInStore()) { CheckStoreBtn(); return; } const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; const bool isCtrlHeld = (modState & SDL_KMOD_CTRL) != 0; if (!GetMainPanel().contains(MousePosition)) { if (!gmenu_is_active() && !TryIconCurs()) { if (QuestLogIsOpen && GetLeftPanel().contains(MousePosition)) { QuestlogESC(); } else if (qtextflag) { qtextflag = false; stream_stop(); } else if (CharFlag && GetLeftPanel().contains(MousePosition)) { CheckChrBtns(); } else if (invflag && GetRightPanel().contains(MousePosition)) { if (!DropGoldFlag) CheckInvItem(isShiftHeld, isCtrlHeld); } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { if (!IsWithdrawGoldOpen) CheckStashItem(MousePosition, isShiftHeld, isCtrlHeld); CheckStashButtonPress(MousePosition); } else if (SpellbookFlag && GetRightPanel().contains(MousePosition)) { CheckSBook(); } else if (!MyPlayer->HoldItem.isEmpty()) { if (!TryOpenDungeonWithMouse()) { const Point currentPosition = MyPlayer->position.tile; std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, cursPosition)); if (itemTile) { NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, MyPlayer->HoldItem); NewCursor(CURSOR_HAND); } } } else { CheckLevelButton(); if (!LevelButtonDown) LeftMouseCmd(isShiftHeld); } } } else { if (!ChatFlag && !DropGoldFlag && !IsWithdrawGoldOpen && !gmenu_is_active()) CheckInvScrn(isShiftHeld, isCtrlHeld); CheckMainPanelButton(); CheckStashButtonPress(MousePosition); if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) NewCursor(CURSOR_HAND); } } void LeftMouseUp(uint16_t modState) { gmenu_left_mouse(false); CheckMuteButtonUp(); if (MainPanelButtonDown) CheckMainPanelButtonUp(); CheckStashButtonRelease(MousePosition); if (CharPanelButtonActive) { const bool isShiftHeld = (modState & SDL_KMOD_SHIFT) != 0; ReleaseChrBtns(isShiftHeld); } if (LevelButtonDown) CheckLevelButtonUp(); if (IsPlayerInStore()) ReleaseStoreBtn(); } void RightMouseDown(bool isShiftHeld) { LastPlayerAction = PlayerActionType::None; if (gmenu_is_active() || sgnTimeoutCurs != CURSOR_NONE || PauseMode == 2 || MyPlayer->_pInvincible) { return; } if (qtextflag) { qtextflag = false; stream_stop(); return; } if (DoomFlag) { doom_close(); return; } if (IsPlayerInStore()) return; if (SpellSelectFlag) { SetSpell(); return; } if (SpellbookFlag && GetRightPanel().contains(MousePosition)) return; if (TryIconCurs()) return; if (pcursinvitem != -1 && UseInvItem(pcursinvitem)) return; if (pcursstashitem != StashStruct::EmptyCell && UseStashItem(pcursstashitem)) return; if (DidRightClickPartyPortrait()) return; if (pcurs == CURSOR_HAND) { CheckPlrSpell(isShiftHeld); } else if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { NewCursor(CURSOR_HAND); } } void ReleaseKey(SDL_Keycode vkey) { remap_keyboard_key(&vkey); if (sgnTimeoutCurs != CURSOR_NONE) return; KeymapperRelease(vkey); } void ClosePanels() { if (CanPanelsCoverView()) { if (!IsLeftPanelOpen() && IsRightPanelOpen() && MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition + Displacement { 160, 0 }); } else if (!IsRightPanelOpen() && IsLeftPanelOpen() && MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition - Displacement { 160, 0 }); } } CloseInventory(); CloseCharPanel(); SpellbookFlag = false; QuestLogIsOpen = false; } void PressKey(SDL_Keycode vkey, uint16_t modState) { Options &options = GetOptions(); remap_keyboard_key(&vkey); if (vkey == SDLK_UNKNOWN) return; if (gmenu_presskeys(vkey) || CheckKeypress(vkey)) { return; } if (MyPlayerIsDead) { if (vkey == SDLK_ESCAPE) { if (!gbIsMultiplayer) { if (gbValidSaveFile) gamemenu_load_game(false); else gamemenu_exit_game(false); } else { NetSendCmd(true, CMD_RETOWN); } return; } if (sgnTimeoutCurs != CURSOR_NONE) { return; } KeymapperPress(vkey); if (vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) { if ((modState & SDL_KMOD_ALT) != 0) { options.Graphics.fullscreen.SetValue(!IsFullScreen()); if (!demo::IsRunning()) SaveOptions(); } else { TypeChatMessage(); } } if (vkey != SDLK_ESCAPE) { return; } } // Disallow player from accessing escape menu during the frames before the death message appears if (vkey == SDLK_ESCAPE && MyPlayer->_pHitPoints > 0) { if (!PressEscKey()) { LastPlayerAction = PlayerActionType::None; gamemenu_on(); } return; } if (DropGoldFlag) { control_drop_gold(vkey); return; } if (IsWithdrawGoldOpen) { WithdrawGoldKeyPress(vkey); return; } if (sgnTimeoutCurs != CURSOR_NONE) { return; } KeymapperPress(vkey); if (PauseMode == 2) { if ((vkey == SDLK_RETURN || vkey == SDLK_KP_ENTER) && (modState & SDL_KMOD_ALT) != 0) { options.Graphics.fullscreen.SetValue(!IsFullScreen()); if (!demo::IsRunning()) SaveOptions(); } return; } if (DoomFlag) { doom_close(); return; } switch (vkey) { case SDLK_PLUS: case SDLK_KP_PLUS: case SDLK_EQUALS: case SDLK_KP_EQUALS: if (AutomapActive) { AutomapZoomIn(); } return; case SDLK_MINUS: case SDLK_KP_MINUS: case SDLK_UNDERSCORE: if (AutomapActive) { AutomapZoomOut(); } return; #ifdef _DEBUG case SDLK_V: if ((modState & SDL_KMOD_SHIFT) != 0) NextDebugMonster(); else GetDebugMonster(); return; #endif case SDLK_RETURN: case SDLK_KP_ENTER: if ((modState & SDL_KMOD_ALT) != 0) { options.Graphics.fullscreen.SetValue(!IsFullScreen()); if (!demo::IsRunning()) SaveOptions(); } else if (IsPlayerInStore()) { StoreEnter(); } else if (QuestLogIsOpen) { QuestlogEnter(); } else { TypeChatMessage(); } return; case SDLK_UP: if (IsPlayerInStore()) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); } else if (HelpFlag) { HelpScrollUp(); } else if (ChatLogFlag) { ChatLogScrollUp(); } else if (AutomapActive) { AutomapUp(); } else if (IsStashOpen) { Stash.PreviousPage(); } return; case SDLK_DOWN: if (IsPlayerInStore()) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); } else if (HelpFlag) { HelpScrollDown(); } else if (ChatLogFlag) { ChatLogScrollDown(); } else if (AutomapActive) { AutomapDown(); } else if (IsStashOpen) { Stash.NextPage(); } return; case SDLK_PAGEUP: if (IsPlayerInStore()) { StorePrior(); } else if (ChatLogFlag) { ChatLogScrollTop(); } return; case SDLK_PAGEDOWN: if (IsPlayerInStore()) { StoreNext(); } else if (ChatLogFlag) { ChatLogScrollBottom(); } return; case SDLK_LEFT: if (AutomapActive && !ChatFlag) AutomapLeft(); return; case SDLK_RIGHT: if (AutomapActive && !ChatFlag) AutomapRight(); return; default: break; } } void HandleMouseButtonDown(Uint8 button, uint16_t modState) { if (IsPlayerInStore() && (button == SDL_BUTTON_X1 #if !SDL_VERSION_ATLEAST(2, 0, 0) || button == 8 #endif )) { StoreESC(); return; } switch (button) { case SDL_BUTTON_LEFT: if (sgbMouseDown == CLICK_NONE) { sgbMouseDown = CLICK_LEFT; LeftMouseDown(modState); } break; case SDL_BUTTON_RIGHT: if (sgbMouseDown == CLICK_NONE) { sgbMouseDown = CLICK_RIGHT; RightMouseDown((modState & SDL_KMOD_SHIFT) != 0); } break; default: KeymapperPress(static_cast(button | KeymapperMouseButtonMask)); break; } } void HandleMouseButtonUp(Uint8 button, uint16_t modState) { if (sgbMouseDown == CLICK_LEFT && button == SDL_BUTTON_LEFT) { LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; LeftMouseUp(modState); } else if (sgbMouseDown == CLICK_RIGHT && button == SDL_BUTTON_RIGHT) { LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; } else { KeymapperRelease(static_cast(button | KeymapperMouseButtonMask)); } } [[maybe_unused]] void LogUnhandledEvent(const char *name, int value) { LogVerbose("Unhandled SDL event: {} {}", name, value); } void PrepareForFadeIn() { if (HeadlessMode) return; BlackPalette(); // Render the game to the buffer(s) with a fully black palette. // Palette fade-in will gradually make it visible. RedrawEverything(); while (IsRedrawEverything()) { DrawAndBlit(); } } void GameEventHandler(const SDL_Event &event, uint16_t modState) { [[maybe_unused]] const Options &options = GetOptions(); StaticVector ctrlEvents = ToControllerButtonEvents(event); for (const ControllerButtonEvent ctrlEvent : ctrlEvents) { GameAction action; if (HandleControllerButtonEvent(event, ctrlEvent, action) && action.type == GameActionType_SEND_KEY) { if ((action.send_key.vk_code & KeymapperMouseButtonMask) != 0) { const unsigned button = action.send_key.vk_code & ~KeymapperMouseButtonMask; if (!action.send_key.up) HandleMouseButtonDown(static_cast(button), modState); else HandleMouseButtonUp(static_cast(button), modState); } else { if (!action.send_key.up) PressKey(static_cast(action.send_key.vk_code), modState); else ReleaseKey(static_cast(action.send_key.vk_code)); } } } if (ctrlEvents.size() > 0 && ctrlEvents[0].button != ControllerButton_NONE) { return; } #ifdef _DEBUG if (ConsoleHandleEvent(event)) { return; } #endif if (IsChatActive() && HandleTalkTextInputEvent(event)) { return; } if (DropGoldFlag && HandleGoldDropTextInputEvent(event)) { return; } if (IsWithdrawGoldOpen && HandleGoldWithdrawTextInputEvent(event)) { return; } switch (event.type) { case SDL_EVENT_KEY_DOWN: PressKey(SDLC_EventKey(event), modState); return; case SDL_EVENT_KEY_UP: ReleaseKey(SDLC_EventKey(event)); return; case SDL_EVENT_MOUSE_MOTION: if (ControlMode == ControlTypes::KeyboardAndMouse && invflag) InvalidateInventorySlot(); MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; gmenu_on_mouse_move(); return; case SDL_EVENT_MOUSE_BUTTON_DOWN: MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; HandleMouseButtonDown(event.button.button, modState); return; case SDL_EVENT_MOUSE_BUTTON_UP: MousePosition = { SDLC_EventButtonIntX(event), SDLC_EventButtonIntY(event) }; HandleMouseButtonUp(event.button.button, modState); return; #if SDL_VERSION_ATLEAST(2, 0, 0) case SDL_EVENT_MOUSE_WHEEL: if (SDLC_EventWheelIntY(event) > 0) { // Up if (IsPlayerInStore()) { StoreUp(); } else if (QuestLogIsOpen) { QuestlogUp(); } else if (HelpFlag) { HelpScrollUp(); } else if (ChatLogFlag) { ChatLogScrollUp(); } else if (IsStashOpen) { Stash.PreviousPage(); } else if (SDL_GetModState() & SDL_KMOD_CTRL) { if (AutomapActive) { AutomapZoomIn(); } } else { KeymapperPress(MouseScrollUpButton); } } else if (SDLC_EventWheelIntY(event) < 0) { // down if (IsPlayerInStore()) { StoreDown(); } else if (QuestLogIsOpen) { QuestlogDown(); } else if (HelpFlag) { HelpScrollDown(); } else if (ChatLogFlag) { ChatLogScrollDown(); } else if (IsStashOpen) { Stash.NextPage(); } else if (SDL_GetModState() & SDL_KMOD_CTRL) { if (AutomapActive) { AutomapZoomOut(); } } else { KeymapperPress(MouseScrollDownButton); } } else if (SDLC_EventWheelIntX(event) > 0) { // left KeymapperPress(MouseScrollLeftButton); } else if (SDLC_EventWheelIntX(event) < 0) { // right KeymapperPress(MouseScrollRightButton); } break; #endif default: if (IsCustomEvent(event.type)) { if (gbIsMultiplayer) pfile_write_hero(); nthread_ignore_mutex(true); PaletteFadeOut(8); sound_stop(); ShowProgress(GetCustomEvent(event)); PrepareForFadeIn(); LoadPWaterPalette(); if (gbRunGame) PaletteFadeIn(8); nthread_ignore_mutex(false); gbGameLoopStartup = true; return; } MainWndProc(event); break; } } void RunGameLoop(interface_mode uMsg) { demo::NotifyGameLoopStart(); nthread_ignore_mutex(true); StartGame(uMsg); assert(HeadlessMode || ghMainWnd); EventHandler previousHandler = SetEventHandler(GameEventHandler); run_delta_info(); gbRunGame = true; gbProcessPlayers = IsDiabloAlive(true); gbRunGameResult = true; PrepareForFadeIn(); LoadPWaterPalette(); PaletteFadeIn(8); InitBackbufferState(); RedrawEverything(); gbGameLoopStartup = true; nthread_ignore_mutex(false); discord_manager::StartGame(); lua::GameStart(); #ifdef GPERF_HEAP_FIRST_GAME_ITERATION unsigned run_game_iteration = 0; #endif while (gbRunGame) { #ifdef _DEBUG if (!gbGameLoopStartup && !DebugCmdsFromCommandLine.empty()) { InitConsole(); for (const std::string &cmd : DebugCmdsFromCommandLine) { RunInConsole(cmd); } DebugCmdsFromCommandLine.clear(); } #endif SDL_Event event; uint16_t modState; while (FetchMessage(&event, &modState)) { if (event.type == SDL_EVENT_QUIT) { gbRunGameResult = false; gbRunGame = false; break; } HandleMessage(event, modState); } if (!gbRunGame) break; bool drawGame = true; bool processInput = true; const bool runGameLoop = demo::IsRunning() ? demo::GetRunGameLoop(drawGame, processInput) : nthread_has_500ms_passed(&drawGame); if (demo::IsRecording()) demo::RecordGameLoopResult(runGameLoop); discord_manager::UpdateGame(); if (!runGameLoop) { if (processInput) ProcessInput(); DvlNet_ProcessNetworkPackets(); if (!drawGame) continue; RedrawViewport(); DrawAndBlit(); continue; } ProcessGameMessagePackets(); if (game_loop(gbGameLoopStartup)) diablo_color_cyc_logic(); gbGameLoopStartup = false; if (drawGame) DrawAndBlit(); #ifdef GPERF_HEAP_FIRST_GAME_ITERATION if (run_game_iteration++ == 0) HeapProfilerDump("first_game_iteration"); #endif } demo::NotifyGameLoopEnd(); if (gbIsMultiplayer) { pfile_write_hero(/*writeGameData=*/false); sfile_write_stash(); } PaletteFadeOut(8); NewCursor(CURSOR_NONE); ClearScreenBuffer(); RedrawEverything(); scrollrt_draw_game_screen(); previousHandler = SetEventHandler(previousHandler); assert(HeadlessMode || previousHandler == GameEventHandler); FreeGame(); if (cineflag) { cineflag = false; DoEnding(); } } void PrintWithRightPadding(std::string_view str, size_t width) { printInConsole(str); if (str.size() >= width) return; printInConsole(std::string(width - str.size(), ' ')); } void PrintHelpOption(std::string_view flags, std::string_view description) { printInConsole(" "); PrintWithRightPadding(flags, 20); printInConsole(" "); PrintWithRightPadding(description, 30); printNewlineInConsole(); } #if SDL_VERSION_ATLEAST(2, 0, 0) FILE *SdlLogFile = nullptr; extern "C" void SdlLogToFile(void *userdata, int category, SDL_LogPriority priority, const char *message) { FILE *file = reinterpret_cast(userdata); static const char *const LogPriorityPrefixes[SDL_LOG_PRIORITY_COUNT] = { "", "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" }; std::fprintf(file, "%s: %s\n", LogPriorityPrefixes[priority], message); std::fflush(file); } #endif [[noreturn]] void PrintHelpAndExit() { printInConsole((/* TRANSLATORS: Commandline Option */ "Options:")); printNewlineInConsole(); PrintHelpOption("-h, --help", _(/* TRANSLATORS: Commandline Option */ "Print this message and exit")); PrintHelpOption("--version", _(/* TRANSLATORS: Commandline Option */ "Print the version and exit")); PrintHelpOption("--data-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of diabdat.mpq")); PrintHelpOption("--save-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the folder of save files")); PrintHelpOption("--config-dir", _(/* TRANSLATORS: Commandline Option */ "Specify the location of diablo.ini")); PrintHelpOption("--lang", _(/* TRANSLATORS: Commandline Option */ "Specify the language code (e.g. en or pt_BR)")); PrintHelpOption("-n", _(/* TRANSLATORS: Commandline Option */ "Skip startup videos")); PrintHelpOption("-f", _(/* TRANSLATORS: Commandline Option */ "Display frames per second")); PrintHelpOption("--verbose", _(/* TRANSLATORS: Commandline Option */ "Enable verbose logging")); #if SDL_VERSION_ATLEAST(2, 0, 0) PrintHelpOption("--log-to-file ", _(/* TRANSLATORS: Commandline Option */ "Log to a file instead of stderr")); #endif #ifndef DISABLE_DEMOMODE PrintHelpOption("--record <#>", _(/* TRANSLATORS: Commandline Option */ "Record a demo file")); PrintHelpOption("--demo <#>", _(/* TRANSLATORS: Commandline Option */ "Play a demo file")); PrintHelpOption("--timedemo", _(/* TRANSLATORS: Commandline Option */ "Disable all frame limiting during demo playback")); #endif printNewlineInConsole(); printInConsole(_(/* TRANSLATORS: Commandline Option */ "Game selection:")); printNewlineInConsole(); PrintHelpOption("--spawn", _(/* TRANSLATORS: Commandline Option */ "Force Shareware mode")); PrintHelpOption("--diablo", _(/* TRANSLATORS: Commandline Option */ "Force Diablo mode")); PrintHelpOption("--hellfire", _(/* TRANSLATORS: Commandline Option */ "Force Hellfire mode")); printInConsole(_(/* TRANSLATORS: Commandline Option */ "Hellfire options:")); printNewlineInConsole(); #ifdef _DEBUG printNewlineInConsole(); printInConsole("Debug options:"); printNewlineInConsole(); PrintHelpOption("-i", "Ignore network timeout"); PrintHelpOption("+", "Pass commands to the engine"); #endif printNewlineInConsole(); printInConsole(_("Report bugs at https://github.com/diasurgical/devilutionX/")); printNewlineInConsole(); diablo_quit(0); } void PrintFlagMessage(std::string_view flag, std::string_view message) { printInConsole(flag); printInConsole(message); printNewlineInConsole(); } void PrintFlagRequiresArgument(std::string_view flag) { PrintFlagMessage(flag, " requires an argument"); } void DiabloParseFlags(int argc, char **argv) { #ifdef _DEBUG int argumentIndexOfLastCommandPart = -1; std::string currentCommand; #endif #ifndef DISABLE_DEMOMODE bool timedemo = false; int demoNumber = -1; int recordNumber = -1; bool createDemoReference = false; #endif for (int i = 1; i < argc; i++) { const std::string_view arg = argv[i]; if (arg == "-h" || arg == "--help") { PrintHelpAndExit(); } else if (arg == "--version") { printInConsole(PROJECT_NAME); printInConsole(" v"); printInConsole(PROJECT_VERSION); printNewlineInConsole(); diablo_quit(0); } else if (arg == "--data-dir") { if (i + 1 == argc) { PrintFlagRequiresArgument("--data-dir"); diablo_quit(64); } paths::SetBasePath(argv[++i]); } else if (arg == "--save-dir") { if (i + 1 == argc) { PrintFlagRequiresArgument("--save-dir"); diablo_quit(64); } paths::SetPrefPath(argv[++i]); } else if (arg == "--config-dir") { if (i + 1 == argc) { PrintFlagRequiresArgument("--config-dir"); diablo_quit(64); } paths::SetConfigPath(argv[++i]); } else if (arg == "--lang") { if (i + 1 == argc) { PrintFlagRequiresArgument("--lang"); diablo_quit(64); } forceLocale = argv[++i]; #ifndef DISABLE_DEMOMODE } else if (arg == "--demo") { if (i + 1 == argc) { PrintFlagRequiresArgument("--demo"); diablo_quit(64); } ParseIntResult parsedParam = ParseInt(argv[++i]); if (!parsedParam.has_value()) { PrintFlagMessage("--demo", " must be a number"); diablo_quit(64); } demoNumber = parsedParam.value(); gbShowIntro = false; } else if (arg == "--timedemo") { timedemo = true; } else if (arg == "--record") { if (i + 1 == argc) { PrintFlagRequiresArgument("--record"); diablo_quit(64); } ParseIntResult parsedParam = ParseInt(argv[++i]); if (!parsedParam.has_value()) { PrintFlagMessage("--record", " must be a number"); diablo_quit(64); } recordNumber = parsedParam.value(); } else if (arg == "--create-reference") { createDemoReference = true; #else } else if (arg == "--demo" || arg == "--timedemo" || arg == "--record" || arg == "--create-reference") { printInConsole("Binary compiled without demo mode support."); printNewlineInConsole(); diablo_quit(1); #endif } else if (arg == "-n") { gbShowIntro = false; } else if (arg == "-f") { EnableFrameCount(); } else if (arg == "--spawn") { forceSpawn = true; } else if (arg == "--diablo") { forceDiablo = true; } else if (arg == "--hellfire") { forceHellfire = true; } else if (arg == "--vanilla") { gbVanilla = true; } else if (arg == "--verbose") { SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); #if SDL_VERSION_ATLEAST(2, 0, 0) } else if (arg == "--log-to-file") { if (i + 1 == argc) { PrintFlagRequiresArgument("--log-to-file"); diablo_quit(64); } SdlLogFile = OpenFile(argv[++i], "wb"); if (SdlLogFile == nullptr) { printInConsole("Failed to open log file for writing"); diablo_quit(64); } SDL_SetLogOutputFunction(&SdlLogToFile, /*userdata=*/SdlLogFile); #endif #ifdef _DEBUG } else if (arg == "-i") { DebugDisableNetworkTimeout = true; } else if (arg[0] == '+') { if (!currentCommand.empty()) DebugCmdsFromCommandLine.push_back(currentCommand); argumentIndexOfLastCommandPart = i; currentCommand = arg.substr(1); } else if (arg[0] != '-' && (argumentIndexOfLastCommandPart + 1) == i) { currentCommand.append(" "); currentCommand.append(arg); argumentIndexOfLastCommandPart = i; #endif } else { printInConsole("unrecognized option '"); printInConsole(argv[i]); printInConsole("'"); printNewlineInConsole(); PrintHelpAndExit(); } } #ifdef _DEBUG if (!currentCommand.empty()) DebugCmdsFromCommandLine.push_back(currentCommand); #endif #ifndef DISABLE_DEMOMODE if (demoNumber != -1) demo::InitPlayBack(demoNumber, timedemo); if (recordNumber != -1) demo::InitRecording(recordNumber, createDemoReference); #endif } void DiabloInitScreen() { MousePosition = { gnScreenWidth / 2, gnScreenHeight / 2 }; if (ControlMode == ControlTypes::KeyboardAndMouse) SetCursorPos(MousePosition); ClrDiabloMsg(); } void SetApplicationVersions() { *BufCopy(gszProductName, PROJECT_NAME, " v", PROJECT_VERSION) = '\0'; *BufCopy(gszVersionNumber, "version ", PROJECT_VERSION) = '\0'; } void CheckArchivesUpToDate() { const bool devilutionxMpqOutOfDate = IsDevilutionXMpqOutOfDate(); const bool fontsMpqOutOfDate = AreExtraFontsOutOfDate(); if (devilutionxMpqOutOfDate && fontsMpqOutOfDate) { app_fatal(_("Please update devilutionx.mpq and fonts.mpq to the latest version")); } else if (devilutionxMpqOutOfDate) { app_fatal(_("Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date.")); } else if (fontsMpqOutOfDate) { app_fatal(_("Please update fonts.mpq to the latest version")); } } void ApplicationInit() { if (*GetOptions().Graphics.showFPS) EnableFrameCount(); init_create_window(); was_window_init = true; InitializeScreenReader(); LanguageInitialize(); SetApplicationVersions(); ReadOnlyTest(); } void DiabloInit() { if (forceSpawn || *GetOptions().GameMode.shareware) gbIsSpawn = true; bool wasHellfireDiscovered = false; if (!forceDiablo && !forceHellfire) wasHellfireDiscovered = (HaveHellfire() && *GetOptions().GameMode.gameMode == StartUpGameMode::Ask); bool enableHellfire = forceHellfire || wasHellfireDiscovered; if (!forceDiablo && *GetOptions().GameMode.gameMode == StartUpGameMode::Hellfire) { // Migrate legacy options GetOptions().GameMode.gameMode.SetValue(StartUpGameMode::Diablo); enableHellfire = true; } if (forceDiablo || enableHellfire) { GetOptions().Mods.SetHellfireEnabled(enableHellfire); } gbIsHellfireSaveGame = gbIsHellfire; for (size_t i = 0; i < QuickMessages.size(); i++) { auto &messages = GetOptions().Chat.szHotKeyMsgs[i]; if (messages.empty()) { messages.emplace_back(_(QuickMessages[i].message)); } } #ifndef USE_SDL1 InitializeVirtualGamepad(); #endif UiInitialize(); was_ui_init = true; if (wasHellfireDiscovered) { UiSelStartUpGameOption(); if (!gbIsHellfire) { // Reinitialize the UI Elements because we changed the game UnloadUiGFX(); UiInitialize(); if (IsHardwareCursor()) SetHardwareCursor(CursorInfo::UnknownCursor()); } } DiabloInitScreen(); snd_init(); ui_sound_init(); // Item graphics are loaded early, they already get touched during hero selection. InitItemGFX(); // Always available. LoadSmallSelectionSpinner(); CheckArchivesUpToDate(); } void DiabloSplash() { if (!gbShowIntro) return; if (*GetOptions().StartUp.splash == StartUpSplash::LogoAndTitleDialog) play_movie("gendata\\logo.smk", true); auto &intro = gbIsHellfire ? GetOptions().StartUp.hellfireIntro : GetOptions().StartUp.diabloIntro; if (*intro != StartUpIntro::Off) { if (gbIsHellfire) play_movie("gendata\\Hellfire.smk", true); else play_movie("gendata\\diablo1.smk", true); if (*intro == StartUpIntro::Once) { intro.SetValue(StartUpIntro::Off); if (!demo::IsRunning()) SaveOptions(); } } if (IsAnyOf(*GetOptions().StartUp.splash, StartUpSplash::TitleDialog, StartUpSplash::LogoAndTitleDialog)) UiTitleDialog(); } void DiabloDeinit() { FreeItemGFX(); LuaShutdown(); ShutDownScreenReader(); if (gbSndInited) effects_cleanup_sfx(); snd_deinit(); if (was_ui_init) UiDestroy(); if (was_archives_init) init_cleanup(); if (was_window_init) dx_cleanup(); // Cleanup SDL surfaces stuff, so we have to do it before SDL_Quit(). UnloadFonts(); if (SDL_WasInit((~0U) & ~SDL_INIT_HAPTIC) != 0) SDL_Quit(); } tl::expected LoadLvlGFX() { assert(pDungeonCels == nullptr); constexpr int SpecialCelWidth = 64; const auto loadAll = [](const char *cel, const char *til, const char *special) -> tl::expected { ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus(cel)); ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus(til)); ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus(special, SpecialCelWidth)); return {}; }; switch (leveltype) { case DTYPE_TOWN: { auto cel = LoadFileInMemWithStatus("nlevels\\towndata\\town.cel"); if (!cel.has_value()) { ASSIGN_OR_RETURN(pDungeonCels, LoadFileInMemWithStatus("levels\\towndata\\town.cel")); } else { pDungeonCels = std::move(*cel); } auto til = LoadFileInMemWithStatus("nlevels\\towndata\\town.til"); if (!til.has_value()) { ASSIGN_OR_RETURN(pMegaTiles, LoadFileInMemWithStatus("levels\\towndata\\town.til")); } else { pMegaTiles = std::move(*til); } ASSIGN_OR_RETURN(pSpecialCels, LoadCelWithStatus("levels\\towndata\\towns", SpecialCelWidth)); return {}; } case DTYPE_CATHEDRAL: return loadAll( "levels\\l1data\\l1.cel", "levels\\l1data\\l1.til", "levels\\l1data\\l1s"); case DTYPE_CATACOMBS: return loadAll( "levels\\l2data\\l2.cel", "levels\\l2data\\l2.til", "levels\\l2data\\l2s"); case DTYPE_CAVES: return loadAll( "levels\\l3data\\l3.cel", "levels\\l3data\\l3.til", "levels\\l1data\\l1s"); case DTYPE_HELL: return loadAll( "levels\\l4data\\l4.cel", "levels\\l4data\\l4.til", "levels\\l2data\\l2s"); case DTYPE_NEST: return loadAll( "nlevels\\l6data\\l6.cel", "nlevels\\l6data\\l6.til", "levels\\l1data\\l1s"); case DTYPE_CRYPT: return loadAll( "nlevels\\l5data\\l5.cel", "nlevels\\l5data\\l5.til", "nlevels\\l5data\\l5s"); default: return tl::make_unexpected("LoadLvlGFX"); } } tl::expected LoadAllGFX() { IncProgress(); #if !defined(USE_SDL1) && !defined(__vita__) InitVirtualGamepadGFX(); #endif IncProgress(); RETURN_IF_ERROR(InitObjectGFX()); IncProgress(); RETURN_IF_ERROR(InitMissileGFX()); IncProgress(); return {}; } /** * @param entry Where is the player entering from */ void CreateLevel(lvl_entry entry) { CreateDungeon(DungeonSeeds[currlevel], entry); switch (leveltype) { case DTYPE_TOWN: InitTownTriggers(); break; case DTYPE_CATHEDRAL: InitL1Triggers(); break; case DTYPE_CATACOMBS: InitL2Triggers(); break; case DTYPE_CAVES: InitL3Triggers(); break; case DTYPE_HELL: InitL4Triggers(); break; case DTYPE_NEST: InitHiveTriggers(); break; case DTYPE_CRYPT: InitCryptTriggers(); break; default: app_fatal("CreateLevel"); } if (leveltype != DTYPE_TOWN) { Freeupstairs(); } LoadRndLvlPal(leveltype); } void UnstuckChargers() { if (gbIsMultiplayer) { for (Player &player : Players) { if (!player.plractive) continue; if (player._pLvlChanging) continue; if (!player.isOnActiveLevel()) continue; if (&player == MyPlayer) continue; return; } } for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; if (monster.mode == MonsterMode::Charge) monster.mode = MonsterMode::Stand; } } void UpdateMonsterLights() { for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; if ((monster.flags & MFLAG_BERSERK) != 0) { const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; monster.lightId = AddLight(monster.position.tile, lightRadius); } if (monster.lightId != NO_LIGHT) { if (monster.lightId == MyPlayer->lightId) { // Fix old saves where some monsters had 0 instead of NO_LIGHT monster.lightId = NO_LIGHT; continue; } const Light &light = Lights[monster.lightId]; if (monster.position.tile != light.position.tile) { ChangeLightXY(monster.lightId, monster.position.tile); } } } } void GameLogic() { if (!ProcessInput()) { return; } if (gbProcessPlayers) { gGameLogicStep = GameLogicStep::ProcessPlayers; ProcessPlayers(); } if (leveltype != DTYPE_TOWN) { gGameLogicStep = GameLogicStep::ProcessMonsters; #ifdef _DEBUG if (!DebugInvisible) #endif ProcessMonsters(); gGameLogicStep = GameLogicStep::ProcessObjects; ProcessObjects(); gGameLogicStep = GameLogicStep::ProcessMissiles; ProcessMissiles(); gGameLogicStep = GameLogicStep::ProcessItems; ProcessItems(); ProcessLightList(); ProcessVisionList(); } else { gGameLogicStep = GameLogicStep::ProcessTowners; ProcessTowners(); gGameLogicStep = GameLogicStep::ProcessItemsTown; ProcessItems(); gGameLogicStep = GameLogicStep::ProcessMissilesTown; ProcessMissiles(); } gGameLogicStep = GameLogicStep::None; #ifdef _DEBUG if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) { ScrollView(); } #endif sound_update(); CheckTriggers(); CheckQuests(); RedrawViewport(); pfile_update(false); plrctrls_after_game_logic(); } void TimeoutCursor(bool bTimeout) { if (bTimeout) { if (sgnTimeoutCurs == CURSOR_NONE && sgbMouseDown == CLICK_NONE) { sgnTimeoutCurs = pcurs; multi_net_ping(); InfoString = StringOrView {}; AddInfoBoxString(_("-- Network timeout --")); AddInfoBoxString(_("-- Waiting for players --")); for (uint8_t i = 0; i < Players.size(); i++) { bool isConnected = (player_state[i] & PS_CONNECTED) != 0; bool isActive = (player_state[i] & PS_ACTIVE) != 0; if (!(isConnected && !isActive)) continue; DvlNetLatencies latencies = DvlNet_GetLatencies(i); std::string ping = fmt::format( fmt::runtime(_(/* TRANSLATORS: {:s} means: Character Name */ "Player {:s} is timing out!")), Players[i].name()); StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Echo latency: {:d} ms")), latencies.echoLatency)); if (latencies.providerLatency) { if (latencies.isRelayed && *latencies.isRelayed) { StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms (Relayed)")), *latencies.providerLatency)); } else { StrAppend(ping, "\n ", fmt::format(fmt::runtime(_(/* TRANSLATORS: Network connectivity statistics */ "Provider latency: {:d} ms")), *latencies.providerLatency)); } } EventPlrMsg(ping); } NewCursor(CURSOR_HOURGLASS); RedrawEverything(); } scrollrt_draw_game_screen(); } else if (sgnTimeoutCurs != CURSOR_NONE) { // Timeout is gone, we should restore the previous cursor. // But the timeout cursor could already be changed by the now processed messages (for example item cursor from CMD_GETITEM). // Changing the item cursor back to the previous (hand) cursor could result in deleted items, because this resets Player.HoldItem (see NewCursor). if (pcurs == CURSOR_HOURGLASS) NewCursor(sgnTimeoutCurs); sgnTimeoutCurs = CURSOR_NONE; InfoString = StringOrView {}; RedrawEverything(); } } void HelpKeyPressed() { if (HelpFlag) { HelpFlag = false; } else if (IsPlayerInStore()) { InfoString = StringOrView {}; AddInfoBoxString(_("No help available")); /// BUGFIX: message isn't displayed AddInfoBoxString(_("while in stores")); LastPlayerAction = PlayerActionType::None; } else { CloseInventory(); CloseCharPanel(); SpellbookFlag = false; SpellSelectFlag = false; if (qtextflag && leveltype == DTYPE_TOWN) { qtextflag = false; stream_stop(); } QuestLogIsOpen = false; CancelCurrentDiabloMsg(); gamemenu_off(); DisplayHelp(); doom_close(); } } void InventoryKeyPressed() { if (IsPlayerInStore()) return; invflag = !invflag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { if (!invflag) { // We closed the inventory if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition + Displacement { 160, 0 }); } } else if (!SpellbookFlag) { // We opened the inventory if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition - Displacement { 160, 0 }); } } } SpellbookFlag = false; CloseGoldWithdraw(); CloseStash(); } void CharacterSheetKeyPressed() { if (IsPlayerInStore()) return; if (!IsRightPanelOpen() && CanPanelsCoverView()) { if (CharFlag) { // We are closing the character sheet if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition - Displacement { 160, 0 }); } } else if (!QuestLogIsOpen) { // We opened the character sheet if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition + Displacement { 160, 0 }); } } } ToggleCharPanel(); } void PartyPanelSideToggleKeyPressed() { PartySidePanelOpen = !PartySidePanelOpen; } void QuestLogKeyPressed() { if (IsPlayerInStore()) return; if (!QuestLogIsOpen) { StartQuestlog(); } else { QuestLogIsOpen = false; } if (!IsRightPanelOpen() && CanPanelsCoverView()) { if (!QuestLogIsOpen) { // We closed the quest log if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition - Displacement { 160, 0 }); } } else if (!CharFlag) { // We opened the character quest log if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition + Displacement { 160, 0 }); } } } CloseCharPanel(); CloseGoldWithdraw(); CloseStash(); } void DisplaySpellsKeyPressed() { if (IsPlayerInStore()) return; CloseCharPanel(); QuestLogIsOpen = false; CloseInventory(); SpellbookFlag = false; if (!SpellSelectFlag) { DoSpeedBook(); } else { SpellSelectFlag = false; } LastPlayerAction = PlayerActionType::None; } void SpellBookKeyPressed() { if (IsPlayerInStore()) return; SpellbookFlag = !SpellbookFlag; if (!IsLeftPanelOpen() && CanPanelsCoverView()) { if (!SpellbookFlag) { // We closed the inventory if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition + Displacement { 160, 0 }); } } else if (!invflag) { // We opened the inventory if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) { SetCursorPos(MousePosition - Displacement { 160, 0 }); } } } CloseInventory(); } void CycleSpellHotkeys(bool next) { StaticVector validHotKeyIndexes; std::optional currentIndex; for (size_t slot = 0; slot < NumHotkeys; slot++) { if (!IsValidSpeedSpell(slot)) continue; if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) { // found current currentIndex = validHotKeyIndexes.size(); } validHotKeyIndexes.emplace_back(slot); } if (validHotKeyIndexes.size() == 0) return; size_t newIndex; if (!currentIndex) { newIndex = next ? 0 : (validHotKeyIndexes.size() - 1); } else if (next) { newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1); } else { newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1); } ToggleSpell(validHotKeyIndexes[newIndex]); } bool IsPlayerDead() { return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead; } bool IsGameRunning() { return PauseMode != 2; } bool CanPlayerTakeAction() { return !IsPlayerDead() && IsGameRunning(); } bool CanAutomapBeToggledOff() { // check if every window is closed - if yes, automap can be toggled off if (!QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !CharFlag && !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag && !ChatLogFlag && !HelpFlag) return true; return false; } void OptionLanguageCodeChanged() { UnloadFonts(); LanguageInitialize(); LoadLanguageArchive(); effects_cleanup_sfx(false); if (gbRunGame) sound_init(); else ui_sound_init(); } const auto OptionChangeHandlerLanguage = (GetOptions().Language.code.SetValueChangedCallback(OptionLanguageCodeChanged), true); } // namespace void InitKeymapActions() { Options &options = GetOptions(); for (uint32_t i = 0; i < 8; ++i) { options.Keymapper.AddAction( "BeltItem{}", N_("Belt item {}"), N_("Use Belt item."), '1' + i, [i] { const Player &myPlayer = *MyPlayer; if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { UseInvItem(INVITEM_BELT_FIRST + i); } }, nullptr, CanPlayerTakeAction, i + 1); } for (uint32_t i = 0; i < NumHotkeys; ++i) { options.Keymapper.AddAction( "QuickSpell{}", N_("Quick spell {}"), N_("Hotkey for skill or spell."), i < 4 ? static_cast(SDLK_F5) + i : static_cast(SDLK_UNKNOWN), [i]() { if (SpellSelectFlag) { SetSpeedSpell(i); return; } if (!*GetOptions().Gameplay.quickCast) ToggleSpell(i); else QuickCast(i); }, nullptr, CanPlayerTakeAction, i + 1); } options.Keymapper.AddAction( "QuickSpellPrevious", N_("Previous quick spell"), N_("Selects the previous quick spell (cycles)."), MouseScrollUpButton, [] { CycleSpellHotkeys(false); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "QuickSpellNext", N_("Next quick spell"), N_("Selects the next quick spell (cycles)."), MouseScrollDownButton, [] { CycleSpellHotkeys(true); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "UseHealthPotion", N_("Use health potion"), N_("Use health potions from belt."), SDLK_UNKNOWN, [] { UseBeltItem(BeltItemType::Healing); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "UseManaPotion", N_("Use mana potion"), N_("Use mana potions from belt."), SDLK_UNKNOWN, [] { UseBeltItem(BeltItemType::Mana); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "DisplaySpells", N_("Speedbook"), N_("Open Speedbook."), 'S', DisplaySpellsKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "QuickSave", N_("Quick save"), N_("Saves the game."), SDLK_F2, [] { gamemenu_save_game(false); }, nullptr, [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); options.Keymapper.AddAction( "QuickLoad", N_("Quick load"), N_("Loads the game."), SDLK_F3, [] { gamemenu_load_game(false); }, nullptr, [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); #ifndef NOEXIT options.Keymapper.AddAction( "QuitGame", N_("Quit game"), N_("Closes the game."), SDLK_UNKNOWN, [] { gamemenu_quit_game(false); }); #endif options.Keymapper.AddAction( "StopHero", N_("Stop hero"), N_("Stops walking and cancel pending actions."), SDLK_UNKNOWN, [] { MyPlayer->Stop(); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "ItemHighlighting", N_("Item highlighting"), N_("Show/hide items on ground."), SDLK_LALT, [] { HighlightKeyPressed(true); }, [] { HighlightKeyPressed(false); }); options.Keymapper.AddAction( "ToggleItemHighlighting", N_("Toggle item highlighting"), N_("Permanent show/hide items on ground."), SDLK_RCTRL, nullptr, [] { ToggleItemLabelHighlight(); }); options.Keymapper.AddAction( "ToggleAutomap", N_("Toggle automap"), N_("Toggles if automap is displayed."), SDLK_TAB, DoAutoMap, nullptr, IsGameRunning); options.Keymapper.AddAction( "CycleAutomapType", N_("Cycle map type"), N_("Opaque -> Transparent -> Minimap -> None"), SDLK_M, CycleAutomapType, nullptr, IsGameRunning); options.Keymapper.AddAction( "Inventory", N_("Inventory"), N_("Open Inventory screen."), 'I', InventoryKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "Character", N_("Character"), N_("Open Character screen."), 'C', CharacterSheetKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "Party", N_("Party"), N_("Open side Party panel."), 'Y', PartyPanelSideToggleKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "QuestLog", N_("Quest log"), N_("Open Quest log."), 'Q', QuestLogKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "SpellBook", N_("Spellbook"), N_("Open Spellbook."), 'B', SpellBookKeyPressed, nullptr, CanPlayerTakeAction); for (uint32_t i = 0; i < QuickMessages.size(); ++i) { options.Keymapper.AddAction( "QuickMessage{}", N_("Quick Message {}"), N_("Use Quick Message in chat."), (i < 4) ? static_cast(SDLK_F9) + i : static_cast(SDLK_UNKNOWN), [i]() { DiabloHotkeyMsg(i); }, nullptr, nullptr, i + 1); } options.Keymapper.AddAction( "HideInfoScreens", N_("Hide Info Screens"), N_("Hide all info screens."), SDLK_SPACE, [] { if (CanAutomapBeToggledOff()) AutomapActive = false; ClosePanels(); HelpFlag = false; ChatLogFlag = false; SpellSelectFlag = false; if (qtextflag && leveltype == DTYPE_TOWN) { qtextflag = false; stream_stop(); } CancelCurrentDiabloMsg(); gamemenu_off(); doom_close(); }, nullptr, IsGameRunning); options.Keymapper.AddAction( "Zoom", N_("Zoom"), N_("Zoom Game Screen."), 'Z', [] { GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); CalcViewportGeometry(); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "PauseGame", N_("Pause Game"), N_("Pauses the game."), 'P', diablo_pause_game); options.Keymapper.AddAction( "PauseGameAlternate", N_("Pause Game (Alternate)"), N_("Pauses the game."), SDLK_PAUSE, diablo_pause_game); options.Keymapper.AddAction( "DecreaseBrightness", N_("Decrease Brightness"), N_("Reduce screen brightness."), 'F', DecreaseBrightness, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "IncreaseBrightness", N_("Increase Brightness"), N_("Increase screen brightness."), 'G', IncreaseBrightness, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "Help", N_("Help"), N_("Open Help Screen."), SDLK_F1, HelpKeyPressed, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "Screenshot", N_("Screenshot"), N_("Takes a screenshot."), SDLK_PRINTSCREEN, nullptr, CaptureScreen); options.Keymapper.AddAction( "GameInfo", N_("Game info"), N_("Displays game infos."), 'V', [] { EventPlrMsg(fmt::format( fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), PROJECT_NAME, PROJECT_VERSION), UiFlags::ColorWhite); }, nullptr, CanPlayerTakeAction); options.Keymapper.AddAction( "ChatLog", N_("Chat Log"), N_("Displays chat log."), 'L', [] { ToggleChatLog(); }); options.Keymapper.AddAction( "SortInv", N_("Sort Inventory"), N_("Sorts the inventory."), 'R', [] { ReorganizeInventory(*MyPlayer); }); #ifdef _DEBUG options.Keymapper.AddAction( "OpenConsole", N_("Console"), N_("Opens Lua console."), SDLK_GRAVE, OpenConsole); options.Keymapper.AddAction( "DebugToggle", "Debug toggle", "Programming is like magic.", 'X', [] { DebugToggle = !DebugToggle; }); #endif options.Keymapper.CommitActions(); } void InitPadmapActions() { Options &options = GetOptions(); for (int i = 0; i < 8; ++i) { options.Padmapper.AddAction( "BeltItem{}", N_("Belt item {}"), N_("Use Belt item."), ControllerButton_NONE, [i] { const Player &myPlayer = *MyPlayer; if (!myPlayer.SpdList[i].isEmpty() && myPlayer.SpdList[i]._itype != ItemType::Gold) { UseInvItem(INVITEM_BELT_FIRST + i); } }, nullptr, CanPlayerTakeAction, i + 1); } for (uint32_t i = 0; i < NumHotkeys; ++i) { options.Padmapper.AddAction( "QuickSpell{}", N_("Quick spell {}"), N_("Hotkey for skill or spell."), ControllerButton_NONE, [i]() { if (SpellSelectFlag) { SetSpeedSpell(i); return; } if (!*GetOptions().Gameplay.quickCast) ToggleSpell(i); else QuickCast(i); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }, i + 1); } options.Padmapper.AddAction( "PrimaryAction", N_("Primary action"), N_("Attack monsters, talk to towners, lift and place inventory items."), ControllerButton_BUTTON_B, [] { ControllerActionHeld = GameActionType_PRIMARY_ACTION; LastPlayerAction = PlayerActionType::None; PerformPrimaryAction(); }, [] { ControllerActionHeld = GameActionType_NONE; LastPlayerAction = PlayerActionType::None; }, CanPlayerTakeAction); options.Padmapper.AddAction( "SecondaryAction", N_("Secondary action"), N_("Open chests, interact with doors, pick up items."), ControllerButton_BUTTON_Y, [] { ControllerActionHeld = GameActionType_SECONDARY_ACTION; LastPlayerAction = PlayerActionType::None; PerformSecondaryAction(); }, [] { ControllerActionHeld = GameActionType_NONE; LastPlayerAction = PlayerActionType::None; }, CanPlayerTakeAction); options.Padmapper.AddAction( "SpellAction", N_("Spell action"), N_("Cast the active spell."), ControllerButton_BUTTON_X, [] { ControllerActionHeld = GameActionType_CAST_SPELL; LastPlayerAction = PlayerActionType::None; PerformSpellAction(); }, [] { ControllerActionHeld = GameActionType_NONE; LastPlayerAction = PlayerActionType::None; }, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "CancelAction", N_("Cancel action"), N_("Close menus."), ControllerButton_BUTTON_A, [] { if (DoomFlag) { doom_close(); return; } GameAction action; if (SpellSelectFlag) action = GameAction(GameActionType_TOGGLE_QUICK_SPELL_MENU); else if (invflag) action = GameAction(GameActionType_TOGGLE_INVENTORY); else if (SpellbookFlag) action = GameAction(GameActionType_TOGGLE_SPELL_BOOK); else if (QuestLogIsOpen) action = GameAction(GameActionType_TOGGLE_QUEST_LOG); else if (CharFlag) action = GameAction(GameActionType_TOGGLE_CHARACTER_INFO); ProcessGameAction(action); }, nullptr, [] { return DoomFlag || SpellSelectFlag || invflag || SpellbookFlag || QuestLogIsOpen || CharFlag; }); options.Padmapper.AddAction( "MoveUp", N_("Move up"), N_("Moves the player character up."), ControllerButton_BUTTON_DPAD_UP, [] {}); options.Padmapper.AddAction( "MoveDown", N_("Move down"), N_("Moves the player character down."), ControllerButton_BUTTON_DPAD_DOWN, [] {}); options.Padmapper.AddAction( "MoveLeft", N_("Move left"), N_("Moves the player character left."), ControllerButton_BUTTON_DPAD_LEFT, [] {}); options.Padmapper.AddAction( "MoveRight", N_("Move right"), N_("Moves the player character right."), ControllerButton_BUTTON_DPAD_RIGHT, [] {}); options.Padmapper.AddAction( "StandGround", N_("Stand ground"), N_("Hold to prevent the player from moving."), ControllerButton_NONE, [] {}); options.Padmapper.AddAction( "ToggleStandGround", N_("Toggle stand ground"), N_("Toggle whether the player moves."), ControllerButton_NONE, [] { StandToggle = !StandToggle; }, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "UseHealthPotion", N_("Use health potion"), N_("Use health potions from belt."), ControllerButton_BUTTON_LEFTSHOULDER, [] { UseBeltItem(BeltItemType::Healing); }, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "UseManaPotion", N_("Use mana potion"), N_("Use mana potions from belt."), ControllerButton_BUTTON_RIGHTSHOULDER, [] { UseBeltItem(BeltItemType::Mana); }, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "Character", N_("Character"), N_("Open Character screen."), ControllerButton_AXIS_TRIGGERLEFT, [] { ProcessGameAction(GameAction { GameActionType_TOGGLE_CHARACTER_INFO }); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "Inventory", N_("Inventory"), N_("Open Inventory screen."), ControllerButton_AXIS_TRIGGERRIGHT, [] { ProcessGameAction(GameAction { GameActionType_TOGGLE_INVENTORY }); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "QuestLog", N_("Quest log"), N_("Open Quest log."), { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERLEFT }, [] { ProcessGameAction(GameAction { GameActionType_TOGGLE_QUEST_LOG }); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "SpellBook", N_("Spellbook"), N_("Open Spellbook."), { ControllerButton_BUTTON_BACK, ControllerButton_AXIS_TRIGGERRIGHT }, [] { ProcessGameAction(GameAction { GameActionType_TOGGLE_SPELL_BOOK }); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "DisplaySpells", N_("Speedbook"), N_("Open Speedbook."), ControllerButton_BUTTON_A, [] { ProcessGameAction(GameAction { GameActionType_TOGGLE_QUICK_SPELL_MENU }); }, nullptr, []() { return CanPlayerTakeAction() && !InGameMenu(); }); options.Padmapper.AddAction( "ToggleAutomap", N_("Toggle automap"), N_("Toggles if automap is displayed."), ControllerButton_BUTTON_LEFTSTICK, DoAutoMap); options.Padmapper.AddAction( "AutomapMoveUp", N_("Automap Move Up"), N_("Moves the automap up when active."), ControllerButton_NONE, [] {}); options.Padmapper.AddAction( "AutomapMoveDown", N_("Automap Move Down"), N_("Moves the automap down when active."), ControllerButton_NONE, [] {}); options.Padmapper.AddAction( "AutomapMoveLeft", N_("Automap Move Left"), N_("Moves the automap left when active."), ControllerButton_NONE, [] {}); options.Padmapper.AddAction( "AutomapMoveRight", N_("Automap Move Right"), N_("Moves the automap right when active."), ControllerButton_NONE, [] {}); options.Padmapper.AddAction( "MouseUp", N_("Move mouse up"), N_("Simulates upward mouse movement."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_UP }, [] {}); options.Padmapper.AddAction( "MouseDown", N_("Move mouse down"), N_("Simulates downward mouse movement."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_DOWN }, [] {}); options.Padmapper.AddAction( "MouseLeft", N_("Move mouse left"), N_("Simulates leftward mouse movement."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_LEFT }, [] {}); options.Padmapper.AddAction( "MouseRight", N_("Move mouse right"), N_("Simulates rightward mouse movement."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_DPAD_RIGHT }, [] {}); auto leftMouseDown = [] { const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); sgbMouseDown = CLICK_LEFT; LeftMouseDown(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); }; auto leftMouseUp = [] { const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; LeftMouseUp(standGround ? SDL_KMOD_SHIFT : SDL_KMOD_NONE); }; options.Padmapper.AddAction( "LeftMouseClick1", N_("Left mouse click"), N_("Simulates the left mouse button."), ControllerButton_BUTTON_RIGHTSTICK, leftMouseDown, leftMouseUp); options.Padmapper.AddAction( "LeftMouseClick2", N_("Left mouse click"), N_("Simulates the left mouse button."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_LEFTSHOULDER }, leftMouseDown, leftMouseUp); auto rightMouseDown = [] { const ControllerButtonCombo standGroundCombo = GetOptions().Padmapper.ButtonComboForAction("StandGround"); const bool standGround = StandToggle || IsControllerButtonComboPressed(standGroundCombo); LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_RIGHT; RightMouseDown(standGround); }; auto rightMouseUp = [] { LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; }; options.Padmapper.AddAction( "RightMouseClick1", N_("Right mouse click"), N_("Simulates the right mouse button."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSTICK }, rightMouseDown, rightMouseUp); options.Padmapper.AddAction( "RightMouseClick2", N_("Right mouse click"), N_("Simulates the right mouse button."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_RIGHTSHOULDER }, rightMouseDown, rightMouseUp); options.Padmapper.AddAction( "PadHotspellMenu", N_("Gamepad hotspell menu"), N_("Hold to set or use spell hotkeys."), ControllerButton_BUTTON_BACK, [] { PadHotspellMenuActive = true; }, [] { PadHotspellMenuActive = false; }); options.Padmapper.AddAction( "PadMenuNavigator", N_("Gamepad menu navigator"), N_("Hold to access gamepad menu navigation."), ControllerButton_BUTTON_START, [] { PadMenuNavigatorActive = true; }, [] { PadMenuNavigatorActive = false; }); auto toggleGameMenu = [] { const bool inMenu = gmenu_is_active(); PressEscKey(); LastPlayerAction = PlayerActionType::None; PadHotspellMenuActive = false; PadMenuNavigatorActive = false; if (!inMenu) gamemenu_on(); }; options.Padmapper.AddAction( "ToggleGameMenu1", N_("Toggle game menu"), N_("Opens the game menu."), { ControllerButton_BUTTON_BACK, ControllerButton_BUTTON_START, }, toggleGameMenu); options.Padmapper.AddAction( "ToggleGameMenu2", N_("Toggle game menu"), N_("Opens the game menu."), { ControllerButton_BUTTON_START, ControllerButton_BUTTON_BACK, }, toggleGameMenu); options.Padmapper.AddAction( "QuickSave", N_("Quick save"), N_("Saves the game."), ControllerButton_NONE, [] { gamemenu_save_game(false); }, nullptr, [&]() { return !gbIsMultiplayer && CanPlayerTakeAction(); }); options.Padmapper.AddAction( "QuickLoad", N_("Quick load"), N_("Loads the game."), ControllerButton_NONE, [] { gamemenu_load_game(false); }, nullptr, [&]() { return !gbIsMultiplayer && gbValidSaveFile && !IsPlayerInStore() && IsGameRunning(); }); options.Padmapper.AddAction( "ItemHighlighting", N_("Item highlighting"), N_("Show/hide items on ground."), ControllerButton_NONE, [] { HighlightKeyPressed(true); }, [] { HighlightKeyPressed(false); }); options.Padmapper.AddAction( "ToggleItemHighlighting", N_("Toggle item highlighting"), N_("Permanent show/hide items on ground."), ControllerButton_NONE, nullptr, [] { ToggleItemLabelHighlight(); }); options.Padmapper.AddAction( "HideInfoScreens", N_("Hide Info Screens"), N_("Hide all info screens."), ControllerButton_NONE, [] { if (CanAutomapBeToggledOff()) AutomapActive = false; ClosePanels(); HelpFlag = false; ChatLogFlag = false; SpellSelectFlag = false; if (qtextflag && leveltype == DTYPE_TOWN) { qtextflag = false; stream_stop(); } CancelCurrentDiabloMsg(); gamemenu_off(); doom_close(); }, nullptr, IsGameRunning); options.Padmapper.AddAction( "Zoom", N_("Zoom"), N_("Zoom Game Screen."), ControllerButton_NONE, [] { GetOptions().Graphics.zoom.SetValue(!*GetOptions().Graphics.zoom); CalcViewportGeometry(); }, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "PauseGame", N_("Pause Game"), N_("Pauses the game."), ControllerButton_NONE, diablo_pause_game); options.Padmapper.AddAction( "DecreaseBrightness", N_("Decrease Brightness"), N_("Reduce screen brightness."), ControllerButton_NONE, DecreaseBrightness, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "IncreaseBrightness", N_("Increase Brightness"), N_("Increase screen brightness."), ControllerButton_NONE, IncreaseBrightness, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "Help", N_("Help"), N_("Open Help Screen."), ControllerButton_NONE, HelpKeyPressed, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "Screenshot", N_("Screenshot"), N_("Takes a screenshot."), ControllerButton_NONE, nullptr, CaptureScreen); options.Padmapper.AddAction( "GameInfo", N_("Game info"), N_("Displays game infos."), ControllerButton_NONE, [] { EventPlrMsg(fmt::format( fmt::runtime(_(/* TRANSLATORS: {:s} means: Project Name, Game Version. */ "{:s} {:s}")), PROJECT_NAME, PROJECT_VERSION), UiFlags::ColorWhite); }, nullptr, CanPlayerTakeAction); options.Padmapper.AddAction( "SortInv", N_("Sort Inventory"), N_("Sorts the inventory."), ControllerButton_NONE, [] { ReorganizeInventory(*MyPlayer); }); options.Padmapper.AddAction( "ChatLog", N_("Chat Log"), N_("Displays chat log."), ControllerButton_NONE, [] { ToggleChatLog(); }); options.Padmapper.CommitActions(); } void SetCursorPos(Point position) { if (ControlDevice != ControlTypes::KeyboardAndMouse) { MousePosition = position; return; } LogicalToOutput(&position.x, &position.y); if (!demo::IsRunning()) SDL_WarpMouseInWindow(ghMainWnd, position.x, position.y); } void FreeGameMem() { pDungeonCels = nullptr; pMegaTiles = nullptr; pSpecialCels = std::nullopt; FreeMonsters(); FreeMissileGFX(); FreeObjectGFX(); FreeTownerGFX(); FreeStashGFX(); #ifndef USE_SDL1 DeactivateVirtualGamepad(); FreeVirtualGamepadGFX(); #endif } bool StartGame(bool bNewGame, bool bSinglePlayer) { gbSelectProvider = true; ReturnToMainMenu = false; do { gbLoadGame = false; if (!NetInit(bSinglePlayer)) { gbRunGameResult = true; break; } // Save 2.8 MiB of RAM by freeing all main menu resources // before starting the game. UiDestroy(); gbSelectProvider = false; if (bNewGame || !gbValidSaveFile) { InitLevels(); InitQuests(); InitPortals(); InitDungMsgs(*MyPlayer); DeltaSyncJunk(); } giNumberOfLevels = gbIsHellfire ? 25 : 17; interface_mode uMsg = WM_DIABNEWGAME; if (gbValidSaveFile && gbLoadGame) { uMsg = WM_DIABLOADGAME; } RunGameLoop(uMsg); NetClose(); UnloadFonts(); // If the player left the game into the main menu, // initialize main menu resources. if (gbRunGameResult) UiInitialize(); if (ReturnToMainMenu) return true; } while (gbRunGameResult); SNetDestroy(); return gbRunGameResult; } void diablo_quit(int exitStatus) { FreeGameMem(); music_stop(); DiabloDeinit(); #if SDL_VERSION_ATLEAST(2, 0, 0) if (SdlLogFile != nullptr) std::fclose(SdlLogFile); #endif exit(exitStatus); } #ifdef __UWP__ void (*onInitialized)() = NULL; void setOnInitialized(void (*callback)()) { onInitialized = callback; } #endif int DiabloMain(int argc, char **argv) { #ifdef _DEBUG SDL_SetLogPriorities(SDL_LOG_PRIORITY_DEBUG); #endif DiabloParseFlags(argc, argv); InitKeymapActions(); InitPadmapActions(); // Need to ensure devilutionx.mpq (and fonts.mpq if available) are loaded before attempting to read translation settings LoadCoreArchives(); was_archives_init = true; // Read settings including translation next. This will use the presence of fonts.mpq and look for assets in devilutionx.mpq LoadOptions(); if (demo::IsRunning()) demo::OverrideOptions(); // Then look for a voice pack file based on the selected translation LoadLanguageArchive(); ApplicationInit(); LuaInitialize(); if (!demo::IsRunning()) SaveOptions(); // Finally load game data LoadGameArchives(); LoadTextData(); // Load dynamic data before we go into the menu as we need to initialise player characters in memory pretty early. LoadPlayerDataFiles(); // TODO: We can probably load this much later (when the game is starting). LoadSpellData(); LoadMissileData(); LoadMonsterData(); LoadItemData(); LoadObjectData(); LoadQuestData(); DiabloInit(); #ifdef __UWP__ onInitialized(); #endif if (!demo::IsRunning()) SaveOptions(); DiabloSplash(); mainmenu_loop(); DiabloDeinit(); return 0; } bool TryIconCurs() { if (pcurs == CURSOR_RESURRECT) { if (PlayerUnderCursor != nullptr) { NetSendCmdParam1(true, CMD_RESURRECT, PlayerUnderCursor->getId()); NewCursor(CURSOR_HAND); return true; } return false; } if (pcurs == CURSOR_HEALOTHER) { if (PlayerUnderCursor != nullptr) { NetSendCmdParam1(true, CMD_HEALOTHER, PlayerUnderCursor->getId()); NewCursor(CURSOR_HAND); return true; } return false; } if (pcurs == CURSOR_TELEKINESIS) { DoTelekinesis(); return true; } Player &myPlayer = *MyPlayer; if (pcurs == CURSOR_IDENTIFY) { if (pcursinvitem != -1 && !IsInspectingPlayer()) CheckIdentify(myPlayer, pcursinvitem); else if (pcursstashitem != StashStruct::EmptyCell) { Item &item = Stash.stashList[pcursstashitem]; item._iIdentified = true; } NewCursor(CURSOR_HAND); return true; } if (pcurs == CURSOR_REPAIR) { if (pcursinvitem != -1 && !IsInspectingPlayer()) DoRepair(myPlayer, pcursinvitem); else if (pcursstashitem != StashStruct::EmptyCell) { Item &item = Stash.stashList[pcursstashitem]; RepairItem(item, myPlayer.getCharacterLevel()); } NewCursor(CURSOR_HAND); return true; } if (pcurs == CURSOR_RECHARGE) { if (pcursinvitem != -1 && !IsInspectingPlayer()) DoRecharge(myPlayer, pcursinvitem); else if (pcursstashitem != StashStruct::EmptyCell) { Item &item = Stash.stashList[pcursstashitem]; RechargeItem(item, myPlayer); } NewCursor(CURSOR_HAND); return true; } if (pcurs == CURSOR_OIL) { bool changeCursor = true; if (pcursinvitem != -1 && !IsInspectingPlayer()) changeCursor = DoOil(myPlayer, pcursinvitem); else if (pcursstashitem != StashStruct::EmptyCell) { Item &item = Stash.stashList[pcursstashitem]; changeCursor = ApplyOilToItem(item, myPlayer); } if (changeCursor) NewCursor(CURSOR_HAND); return true; } if (pcurs == CURSOR_TELEPORT) { const SpellID spellID = myPlayer.inventorySpell; const SpellType spellType = SpellType::Scroll; const int spellFrom = myPlayer.spellFrom; if (IsWallSpell(spellID)) { const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); } else if (pcursmonst != -1 && leveltype != DTYPE_TOWN) { NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !myPlayer.friendlyMode) { NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); } else { NetSendCmdLocParam3(true, CMD_SPELLXY, cursPosition, static_cast(spellID), static_cast(spellType), spellFrom); } NewCursor(CURSOR_HAND); return true; } if (pcurs == CURSOR_DISARM && ObjectUnderCursor == nullptr) { NewCursor(CURSOR_HAND); return true; } return false; } void diablo_pause_game() { if (!gbIsMultiplayer) { if (PauseMode != 0) { PauseMode = 0; } else { PauseMode = 2; sound_stop(); qtextflag = false; LastPlayerAction = PlayerActionType::None; } RedrawEverything(); } } bool GameWasAlreadyPaused = false; bool MinimizePaused = false; bool diablo_is_focused() { #ifndef USE_SDL1 return SDL_GetKeyboardFocus() == ghMainWnd; #else Uint8 appState = SDL_GetAppState(); return (appState & SDL_APPINPUTFOCUS) != 0; #endif } void diablo_focus_pause() { if (!movie_playing && (gbIsMultiplayer || MinimizePaused)) { return; } GameWasAlreadyPaused = PauseMode != 0; if (!GameWasAlreadyPaused) { PauseMode = 2; sound_stop(); LastPlayerAction = PlayerActionType::None; } SVidMute(); music_mute(); MinimizePaused = true; } void diablo_focus_unpause() { if (!GameWasAlreadyPaused) { PauseMode = 0; } SVidUnmute(); music_unmute(); MinimizePaused = false; } bool PressEscKey() { bool rv = false; if (DoomFlag) { doom_close(); rv = true; } if (HelpFlag) { HelpFlag = false; rv = true; } if (ChatLogFlag) { ChatLogFlag = false; rv = true; } if (qtextflag) { qtextflag = false; stream_stop(); rv = true; } if (IsPlayerInStore()) { StoreESC(); rv = true; } if (IsDiabloMsgAvailable()) { CancelCurrentDiabloMsg(); rv = true; } if (ChatFlag) { ResetChat(); rv = true; } if (DropGoldFlag) { control_drop_gold(SDLK_ESCAPE); rv = true; } if (IsWithdrawGoldOpen) { WithdrawGoldKeyPress(SDLK_ESCAPE); rv = true; } if (SpellSelectFlag) { SpellSelectFlag = false; rv = true; } if (IsLeftPanelOpen() || IsRightPanelOpen()) { ClosePanels(); rv = true; } return rv; } void DisableInputEventHandler(const SDL_Event &event, uint16_t modState) { switch (event.type) { case SDL_EVENT_MOUSE_MOTION: MousePosition = { SDLC_EventMotionIntX(event), SDLC_EventMotionIntY(event) }; return; case SDL_EVENT_MOUSE_BUTTON_DOWN: if (sgbMouseDown != CLICK_NONE) return; switch (event.button.button) { case SDL_BUTTON_LEFT: sgbMouseDown = CLICK_LEFT; return; case SDL_BUTTON_RIGHT: sgbMouseDown = CLICK_RIGHT; return; default: return; } case SDL_EVENT_MOUSE_BUTTON_UP: sgbMouseDown = CLICK_NONE; return; } MainWndProc(event); } void LoadGameLevelStopMusic(_music_id neededTrack) { if (neededTrack != sgnMusicTrack) music_stop(); } void LoadGameLevelStartMusic(_music_id neededTrack) { if (sgnMusicTrack != neededTrack) music_start(neededTrack); if (MinimizePaused) { music_mute(); } } void LoadGameLevelResetCursor() { if (pcurs > CURSOR_HAND && pcurs < CURSOR_FIRSTITEM) { NewCursor(CURSOR_HAND); } } void SetRndSeedForDungeonLevel() { if (setlevel) { // Maps are not randomly generated, but the monsters max hitpoints are. // So we need to ensure that we have a stable seed when generating quest/set-maps. // For this purpose we reuse the normal dungeon seeds. SetRndSeed(DungeonSeeds[static_cast(setlvlnum)]); } else { SetRndSeed(DungeonSeeds[currlevel]); } } void LoadGameLevelFirstFlagEntry() { CloseInventory(); qtextflag = false; if (!HeadlessMode) { InitInv(); ClearUniqueItemFlags(); InitQuestText(); InitInfoBoxGfx(); InitHelp(); } InitStores(); InitAutomapOnce(); } void LoadGameLevelStores() { if (leveltype == DTYPE_TOWN) { SetupTownStores(); } else { FreeStoreMem(); } } void LoadGameLevelStash() { const bool isHellfireSaveGame = gbIsHellfireSaveGame; gbIsHellfireSaveGame = gbIsHellfire; LoadStash(); gbIsHellfireSaveGame = isHellfireSaveGame; } tl::expected LoadGameLevelDungeon(bool firstflag, lvl_entry lvldir, const Player &myPlayer) { if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pLvlVisited[currlevel] || gbIsMultiplayer) { HoldThemeRooms(); [[maybe_unused]] const uint32_t mid1Seed = GetLCGEngineState(); InitGolems(); InitObjects(); [[maybe_unused]] const uint32_t mid2Seed = GetLCGEngineState(); IncProgress(); RETURN_IF_ERROR(InitMonsters()); InitItems(); CreateThemeRooms(); IncProgress(); [[maybe_unused]] const uint32_t mid3Seed = GetLCGEngineState(); InitMissiles(); InitCorpses(); #ifdef _DEBUG SetDebugLevelSeedInfos(mid1Seed, mid2Seed, mid3Seed, GetLCGEngineState()); #endif SavePreLighting(); IncProgress(); if (gbIsMultiplayer) DeltaLoadLevel(); } else { HoldThemeRooms(); InitGolems(); RETURN_IF_ERROR(InitMonsters()); InitMissiles(); InitCorpses(); IncProgress(); RETURN_IF_ERROR(LoadLevel()); IncProgress(); } return {}; } void LoadGameLevelSyncPlayerEntry(lvl_entry lvldir) { for (Player &player : Players) { if (player.plractive && player.isOnActiveLevel() && (!player._pLvlChanging || &player == MyPlayer)) { if (player._pHitPoints > 0) { if (lvldir != ENTRY_LOAD) SyncInitPlrPos(player); } else { dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; } } } } void LoadGameLevelLightVision() { if (leveltype != DTYPE_TOWN) { memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh ProcessLightList(); ProcessVisionList(); } } void LoadGameLevelReturn() { ViewPosition = GetMapReturnPosition(); if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) Quests[Q_BETRAYER]._qvar2 = 2; } void LoadGameLevelInitPlayers(bool firstflag, lvl_entry lvldir) { for (Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) { InitPlayerGFX(player); if (lvldir != ENTRY_LOAD) InitPlayer(player, firstflag); } } } void LoadGameLevelSetVisited() { bool visited = false; for (const Player &player : Players) { if (player.plractive) visited = visited || player._pLvlVisited[currlevel]; } } tl::expected LoadGameLevelTown(bool firstflag, lvl_entry lvldir, const Player &myPlayer) { for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) for (int j = 0; j < MAXDUNY; j++) { dFlags[i][j] |= DungeonFlag::Lit; } } InitTowners(); InitStash(); InitItems(); InitMissiles(); IncProgress(); if (!firstflag && lvldir != ENTRY_LOAD && myPlayer._pLvlVisited[currlevel] && !gbIsMultiplayer) RETURN_IF_ERROR(LoadLevel()); if (gbIsMultiplayer) DeltaLoadLevel(); IncProgress(); for (int x = 0; x < DMAXX; x++) for (int y = 0; y < DMAXY; y++) UpdateAutomapExplorer({ x, y }, MAP_EXP_SELF); return {}; } tl::expected LoadGameLevelSetLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) { LoadSetMap(); IncProgress(); RETURN_IF_ERROR(GetLevelMTypes()); IncProgress(); InitGolems(); RETURN_IF_ERROR(InitMonsters()); IncProgress(); if (!HeadlessMode) { #if !defined(USE_SDL1) && !defined(__vita__) InitVirtualGamepadGFX(); #endif RETURN_IF_ERROR(InitMissileGFX()); IncProgress(); } InitCorpses(); IncProgress(); if (lvldir == ENTRY_WARPLVL) GetPortalLvlPos(); IncProgress(); for (Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) { InitPlayerGFX(player); if (lvldir != ENTRY_LOAD) InitPlayer(player, firstflag); } } IncProgress(); InitMultiView(); IncProgress(); if (firstflag || lvldir == ENTRY_LOAD || !myPlayer._pSLvlVisited[setlvlnum] || gbIsMultiplayer) { InitItems(); SavePreLighting(); } else { RETURN_IF_ERROR(LoadLevel()); } if (gbIsMultiplayer) { DeltaLoadLevel(); if (!UseMultiplayerQuests()) ResyncQuests(); } PlayDungMsgs(); InitMissiles(); IncProgress(); return {}; } tl::expected LoadGameLevelStandardLevel(bool firstflag, lvl_entry lvldir, const Player &myPlayer) { CreateLevel(lvldir); IncProgress(); SetRndSeedForDungeonLevel(); if (leveltype != DTYPE_TOWN) { RETURN_IF_ERROR(GetLevelMTypes()); InitThemes(); if (!HeadlessMode) RETURN_IF_ERROR(LoadAllGFX()); } else if (!HeadlessMode) { IncProgress(); #if !defined(USE_SDL1) && !defined(__vita__) InitVirtualGamepadGFX(); #endif IncProgress(); RETURN_IF_ERROR(InitMissileGFX()); IncProgress(); IncProgress(); } IncProgress(); if (lvldir == ENTRY_RTNLVL) { LoadGameLevelReturn(); } if (lvldir == ENTRY_WARPLVL) GetPortalLvlPos(); IncProgress(); LoadGameLevelInitPlayers(firstflag, lvldir); InitMultiView(); IncProgress(); LoadGameLevelSetVisited(); SetRndSeedForDungeonLevel(); if (leveltype == DTYPE_TOWN) { LoadGameLevelTown(firstflag, lvldir, myPlayer); } else { LoadGameLevelDungeon(firstflag, lvldir, myPlayer); } PlayDungMsgs(); if (UseMultiplayerQuests()) ResyncMPQuests(); else ResyncQuests(); return {}; } void LoadGameLevelCrypt() { if (CornerStone.isAvailable()) { CornerstoneLoad(CornerStone.position); } if (Quests[Q_NAKRUL]._qactive == QUEST_DONE && currlevel == 24) { SyncNakrulRoom(); } } void LoadGameLevelCalculateCursor() { // Recalculate mouse selection of entities after level change/load LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; ResetItemlabelHighlighted(); // level changed => item changed pcursmonst = -1; // ensure pcurstemp is set to a valid value CheckCursMove(); } tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir) { const _music_id neededTrack = GetLevelMusic(leveltype); ClearFloatingNumbers(); LoadGameLevelStopMusic(neededTrack); LoadGameLevelResetCursor(); SetRndSeedForDungeonLevel(); NaKrulTomeSequence = 0; IncProgress(); RETURN_IF_ERROR(LoadTrns()); MakeLightTable(); RETURN_IF_ERROR(LoadLevelSOLData()); IncProgress(); RETURN_IF_ERROR(LoadLvlGFX()); SetDungeonMicros(pDungeonCels, MicroTileLen); ClearClxDrawCache(); IncProgress(); if (firstflag) { LoadGameLevelFirstFlagEntry(); } SetRndSeedForDungeonLevel(); LoadGameLevelStores(); if (firstflag || lvldir == ENTRY_LOAD) { LoadGameLevelStash(); } IncProgress(); InitAutomap(); if (leveltype != DTYPE_TOWN && lvldir != ENTRY_LOAD) { InitLighting(); } InitLevelMonsters(); IncProgress(); const Player &myPlayer = *MyPlayer; if (setlevel) { RETURN_IF_ERROR(LoadGameLevelSetLevel(firstflag, lvldir, myPlayer)); } else { RETURN_IF_ERROR(LoadGameLevelStandardLevel(firstflag, lvldir, myPlayer)); } SyncPortals(); LoadGameLevelSyncPlayerEntry(lvldir); IncProgress(); IncProgress(); if (firstflag) { RETURN_IF_ERROR(InitMainPanel()); } IncProgress(); UpdateMonsterLights(); UnstuckChargers(); LoadGameLevelLightVision(); if (leveltype == DTYPE_CRYPT) { LoadGameLevelCrypt(); } #ifndef USE_SDL1 ActivateVirtualGamepad(); #endif LoadGameLevelStartMusic(neededTrack); CompleteProgress(); LoadGameLevelCalculateCursor(); return {}; } bool game_loop(bool bStartup) { const uint16_t wait = bStartup ? sgGameInitInfo.nTickRate * 3 : 3; for (unsigned i = 0; i < wait; i++) { if (!multi_handle_delta()) { TimeoutCursor(true); return false; } TimeoutCursor(false); GameLogic(); ClearLastSentPlayerCmd(); if (!gbRunGame || !gbIsMultiplayer || demo::IsRunning() || demo::IsRecording() || !nthread_has_500ms_passed()) break; } return true; } void diablo_color_cyc_logic() { if (!*GetOptions().Graphics.colorCycling) return; if (PauseMode != 0) return; if (leveltype == DTYPE_CAVES) { if (setlevel && setlvlnum == Quests[Q_PWATER]._qslvl) { UpdatePWaterPalette(); } else { palette_update_caves(); } } else if (leveltype == DTYPE_HELL) { lighting_color_cycling(); } else if (leveltype == DTYPE_NEST) { palette_update_hive(); } else if (leveltype == DTYPE_CRYPT) { palette_update_crypt(); } } bool IsDiabloAlive(bool playSFX) { if (Quests[Q_DIABLO]._qactive == QUEST_DONE && !gbIsMultiplayer) { if (playSFX) PlaySFX(SfxID::DiabloDeath); return false; } return true; } void PrintScreen(SDL_Keycode vkey) { ReleaseKey(vkey); } } // namespace devilution ================================================ FILE: Source/diablo.h ================================================ /** * @file diablo.h * * Interface of the main game initialization functions. */ #pragma once #include #ifdef USE_SDL3 #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #ifdef _DEBUG #include "tables/monstdat.h" #endif #include "levels/gendung.h" #include "utils/attributes.h" #include "utils/endian_read.hpp" namespace devilution { constexpr uint32_t GameIdDiabloFull = LoadBE32("DRTL"); constexpr uint32_t GameIdDiabloSpawn = LoadBE32("DSHR"); constexpr uint32_t GameIdHellfireFull = LoadBE32("HRTL"); constexpr uint32_t GameIdHellfireSpawn = LoadBE32("HSHR"); #define GAME_ID (gbIsHellfire ? (gbIsSpawn ? GameIdHellfireSpawn : GameIdHellfireFull) : (gbIsSpawn ? GameIdDiabloSpawn : GameIdDiabloFull)) #define NUMLEVELS 25 enum clicktype : int8_t { CLICK_NONE, CLICK_LEFT, CLICK_RIGHT, }; /** * @brief Specifies what game logic step is currently executed */ enum class GameLogicStep : uint8_t { None, ProcessPlayers, ProcessMonsters, ProcessObjects, ProcessMissiles, ProcessItems, ProcessTowners, ProcessItemsTown, ProcessMissilesTown, }; enum class PlayerActionType : uint8_t { None, Walk, Spell, SpellMonsterTarget, SpellPlayerTarget, Attack, AttackMonsterTarget, AttackPlayerTarget, OperateObject, }; extern uint32_t DungeonSeeds[NUMLEVELS]; extern DVL_API_FOR_TEST std::optional LevelSeeds[NUMLEVELS]; extern Point MousePosition; extern bool gbRunGameResult; extern bool ReturnToMainMenu; extern bool gbProcessPlayers; extern DVL_API_FOR_TEST bool gbLoadGame; extern bool cineflag; /* These are defined in fonts.h */ extern void FontsCleanup(); extern DVL_API_FOR_TEST int PauseMode; extern clicktype sgbMouseDown; extern uint16_t gnTickDelay; extern char gszProductName[64]; extern PlayerActionType LastPlayerAction; void InitKeymapActions(); void SetCursorPos(Point position); void FreeGameMem(); bool StartGame(bool bNewGame, bool bSinglePlayer); [[noreturn]] void diablo_quit(int exitStatus); int DiabloMain(int argc, char **argv); bool TryIconCurs(); void diablo_pause_game(); bool diablo_is_focused(); void diablo_focus_pause(); void diablo_focus_unpause(); bool PressEscKey(); void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); bool IsDiabloAlive(bool playSFX); void PrintScreen(SDL_Keycode vkey); /** * @param bStartup Process additional ticks before returning */ bool game_loop(bool bStartup); void diablo_color_cyc_logic(); /* rdata */ #ifdef _DEBUG extern bool DebugDisableNetworkTimeout; #endif /** * @brief Specifies what game logic step is currently executed */ extern GameLogicStep gGameLogicStep; #ifdef __UWP__ void setOnInitialized(void (*)()); #endif } // namespace devilution ================================================ FILE: Source/diablo_msg.cpp ================================================ /** * @file diablo_msg.cpp * * Implementation of in-game message functions. */ #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "diablo_msg.hpp" #include "DiabloUI/ui_flags.hpp" #include "engine/clx_sprite.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "panels/info_box.hpp" #include "utils/algorithm/container.hpp" #include "utils/language.h" #include "utils/str_split.hpp" namespace devilution { namespace { struct MessageEntry { std::string text; uint32_t duration; // Duration in milliseconds }; std::deque DiabloMessages; uint32_t msgStartTime; std::vector TextLines; int OuterHeight; int LineWidth; int LineHeight; void InitDiabloMsg() { TextLines.clear(); if (DiabloMessages.empty()) return; LineWidth = 418; const std::string_view text = DiabloMessages.front().text; const std::string wrapped = WordWrapString(text, LineWidth, GameFont12); for (const std::string_view line : SplitByChar(wrapped, '\n')) { LineWidth = std::max(LineWidth, GetLineWidth(text, GameFont12)); TextLines.emplace_back(line); } msgStartTime = SDL_GetTicks(); LineHeight = GetLineHeight(text, GameFont12); OuterHeight = static_cast((TextLines.size()) * LineHeight) + 42; } } // namespace /** Maps from error_id to error message. */ const char *const MsgStrings[] = { "", N_("Game saved"), N_("No multiplayer functions in demo"), N_("Direct Sound Creation Failed"), N_("Not available in shareware version"), N_("Not enough space to save"), N_("No Pause in town"), N_("Copying to a hard disk is recommended"), N_("Multiplayer sync problem"), N_("No pause in multiplayer"), N_("Loading..."), N_("Saving..."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Some are weakened as one grows strong"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "New strength is forged through destruction"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Those who defend seldom attack"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "The sword of justice is swift and sharp"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "While the spirit is vigilant the body thrives"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "The powers of mana refocused renews"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Time cannot diminish the power of steel"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Magic is not always what it seems to be"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "What once was opened now is closed"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Intensity comes at the cost of wisdom"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Arcane power brings destruction"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "That which cannot be held cannot be harmed"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Crimson and Azure become as the sun"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Knowledge and wisdom at the cost of self"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Drink and be refreshed"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Wherever you go, there you are"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Energy comes at the cost of wisdom"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Riches abound when least expected"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Where avarice fails, patience gains reward"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Blessed by a benevolent companion!"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "The hands of men may be guided by fate"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Strength is bolstered by heavenly faith"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "The essence of life flows from within"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "The way is made clear when viewed from above"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Salvation comes at the cost of wisdom"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Mysteries are revealed in the light of reason"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Those who are last may yet be first"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Generosity brings its own rewards"), N_("You must be at least level 8 to use this."), N_("You must be at least level 13 to use this."), N_("You must be at least level 17 to use this."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Arcane knowledge gained!"), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "That which does not kill you..."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Knowledge is power."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Give and you shall receive."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Some experience is gained by touch."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "There's no place like home."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "Spiritual energy is restored."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "You feel more agile."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "You feel stronger."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "You feel wiser."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "You feel refreshed."), N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "That which can break will."), }; void InitDiabloMsg(diablo_message e, uint32_t duration /*= 3500*/) { InitDiabloMsg(LanguageTranslate(MsgStrings[e]), duration); } void InitDiabloMsg(std::string_view msg, uint32_t duration /*= 3500*/) { if (c_find_if(DiabloMessages, [&msg](const MessageEntry &entry) { return entry.text == msg; }) != DiabloMessages.end()) return; DiabloMessages.push_back({ std::string(msg), duration }); if (DiabloMessages.size() == 1) { InitDiabloMsg(); } } bool IsDiabloMsgAvailable() { return !DiabloMessages.empty(); } void CancelCurrentDiabloMsg() { if (!DiabloMessages.empty()) { DiabloMessages.pop_front(); InitDiabloMsg(); } } void ClrDiabloMsg() { DiabloMessages.clear(); } void DrawDiabloMsg(const Surface &out) { const ClxSpriteList sprites = *pSTextSlidCels; const ClxSprite borderCornerTopLeftSprite = sprites[0]; const ClxSprite borderCornerBottomLeftSprite = sprites[1]; const ClxSprite borderCornerBottomRightSprite = sprites[2]; const ClxSprite borderCornerTopRightSprite = sprites[3]; const ClxSprite borderTopSprite = sprites[4]; const ClxSprite borderLeftSprite = sprites[5]; const ClxSprite borderBottomSprite = sprites[6]; const ClxSprite borderRightSprite = sprites[7]; const int borderPartWidth = 12; const int borderPartHeight = 12; const int textPaddingX = 5; const int borderThickness = 3; const int outerHeight = std::min(out.h(), OuterHeight); const int outerWidth = std::min(out.w(), LineWidth + 2 * borderThickness + textPaddingX); const int innerWidth = outerWidth - 2 * borderThickness; const int lineWidth = innerWidth - textPaddingX; const int innerHeight = outerHeight - 2 * borderThickness; const Point topLeft { (out.w() - outerWidth) / 2, (out.h() - outerHeight) / 2 }; const int innerXBegin = topLeft.x + borderThickness; const int innerXEnd = innerXBegin + innerWidth; const int innerYBegin = topLeft.y + borderThickness; const int innerYEnd = innerYBegin + innerHeight; const int borderRightX = innerXEnd - (borderPartWidth - borderThickness); const int borderBottomY = innerYEnd - (borderPartHeight - borderThickness); RenderClxSprite(out, borderCornerTopLeftSprite, topLeft); RenderClxSprite(out, borderCornerBottomLeftSprite, { topLeft.x, borderBottomY }); RenderClxSprite(out, borderCornerBottomRightSprite, { borderRightX, borderBottomY }); RenderClxSprite(out, borderCornerTopRightSprite, { borderRightX, topLeft.y }); const Surface horizontalBorderOut = out.subregionX(topLeft.x, outerWidth - borderPartWidth); for (int x = borderPartWidth; x < horizontalBorderOut.w(); x += borderPartWidth) { RenderClxSprite(horizontalBorderOut, borderTopSprite, { x, topLeft.y }); RenderClxSprite(horizontalBorderOut, borderBottomSprite, { x, borderBottomY }); } const Surface verticalBorderOut = out.subregionY(topLeft.y, outerHeight - borderPartHeight); for (int y = borderPartHeight; y < verticalBorderOut.h(); y += borderPartHeight) { RenderClxSprite(verticalBorderOut, borderLeftSprite, { topLeft.x, y }); RenderClxSprite(verticalBorderOut, borderRightSprite, { borderRightX, y }); } DrawHalfTransparentRectTo(out, innerXBegin, innerYBegin, innerWidth, innerHeight); const int textX = innerXBegin + textPaddingX; int textY = innerYBegin + (innerHeight - LineHeight * static_cast(TextLines.size())) / 2; for (const std::string &line : TextLines) { DrawString(out, line, { { textX, textY }, { lineWidth, LineHeight } }, { .flags = UiFlags::AlignCenter, .lineHeight = LineHeight }); textY += LineHeight; } // Calculate the time the current message has been displayed const uint32_t messageElapsedTime = SDL_GetTicks() - msgStartTime; // Check if the current message's duration has passed if (!DiabloMessages.empty() && messageElapsedTime >= DiabloMessages.front().duration) { DiabloMessages.pop_front(); InitDiabloMsg(); } } } // namespace devilution ================================================ FILE: Source/diablo_msg.hpp ================================================ /** * @file diablo_msg.hpp * * Interface of in-game message functions. */ #pragma once #include #include #include "engine/surface.hpp" namespace devilution { enum diablo_message : uint8_t { EMSG_NONE, EMSG_GAME_SAVED, EMSG_NO_MULTIPLAYER_IN_DEMO, EMSG_DIRECT_SOUND_FAILED, EMSG_NOT_IN_SHAREWARE, EMSG_NO_SPACE_TO_SAVE, EMSG_NO_PAUSE_IN_TOWN, EMSG_COPY_TO_HDD, EMSG_DESYNC, EMSG_NO_PAUSE_IN_MP, EMSG_LOADING, EMSG_SAVING, EMSG_SHRINE_MYSTERIOUS, EMSG_SHRINE_HIDDEN, EMSG_SHRINE_GLOOMY, EMSG_SHRINE_WEIRD, EMSG_SHRINE_MAGICAL, EMSG_SHRINE_STONE, EMSG_SHRINE_RELIGIOUS, EMSG_SHRINE_ENCHANTED, EMSG_SHRINE_THAUMATURGIC, EMSG_SHRINE_FASCINATING, EMSG_SHRINE_CRYPTIC, EMSG_SHRINE_UNUSED, EMSG_SHRINE_ELDRITCH, EMSG_SHRINE_EERIE, EMSG_SHRINE_DIVINE, EMSG_SHRINE_HOLY, EMSG_SHRINE_SACRED, EMSG_SHRINE_SPIRITUAL, EMSG_SHRINE_SPOOKY1, EMSG_SHRINE_SPOOKY2, EMSG_SHRINE_ABANDONED, EMSG_SHRINE_CREEPY, EMSG_SHRINE_QUIET, EMSG_SHRINE_SECLUDED, EMSG_SHRINE_ORNATE, EMSG_SHRINE_GLIMMERING, EMSG_SHRINE_TAINTED1, EMSG_SHRINE_TAINTED2, EMSG_REQUIRES_LVL_8, EMSG_REQUIRES_LVL_13, EMSG_REQUIRES_LVL_17, EMSG_BONECHAMB, EMSG_SHRINE_OILY, EMSG_SHRINE_GLOWING, EMSG_SHRINE_MENDICANT, EMSG_SHRINE_SPARKLING, EMSG_SHRINE_TOWN, EMSG_SHRINE_SHIMMERING, EMSG_SHRINE_SOLAR1, EMSG_SHRINE_SOLAR2, EMSG_SHRINE_SOLAR3, EMSG_SHRINE_SOLAR4, EMSG_SHRINE_MURPHYS, }; void InitDiabloMsg(diablo_message e, uint32_t duration = 3500); void InitDiabloMsg(std::string_view msg, uint32_t duration = 3500); bool IsDiabloMsgAvailable(); void CancelCurrentDiabloMsg(); void ClrDiabloMsg(); void DrawDiabloMsg(const Surface &out); } // namespace devilution ================================================ FILE: Source/discord/discord.cpp ================================================ #include "discord.h" #ifdef _WIN32 // On Windows, discordsrc-src/cpp/discord.h includes windows.h #define NOMINMAX 1 #define WIN32_LEAN_AND_MEAN #endif #include #include #include #include #include #include #include #include #include "config.h" #include "levels/gendung.h" #include "levels/setmaps.h" #include "lua/lua_global.hpp" #include "multi.h" #include "panels/charpanel.hpp" #include "player.h" #include "tables/playerdat.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { namespace { void ModChanged() { discord_manager::UpdateMenu(true); } const auto ModChangedHandler = (AddModsChangedHandler(ModChanged), true); } // namespace namespace discord_manager { // App ID used for DevilutionX's Diablo (classic Diablo's is 496571953147150354) constexpr discord::ClientId DiscordDevilutionxAppId = 795760213524742205; constexpr auto IgnoreResult = [](discord::Result result) {}; discord::Core *discord_core = []() -> discord::Core * { discord::Core *core; discord::Result result = discord::Core::Create(DiscordDevilutionxAppId, DiscordCreateFlags_NoRequireDiscord, &core); if (result != discord::Result::Ok) { core = nullptr; } return core; }(); struct PlayerData { dungeon_type dungeonArea; _setlevels questMap; Uint8 dungeonLevel; Uint8 playerLevel; int playerGfx; // Why??? This is POD bool operator!=(const PlayerData &other) const { return std::tie(dungeonArea, dungeonLevel, playerLevel, playerGfx) != std::tie(other.dungeonArea, other.dungeonLevel, other.playerLevel, other.playerGfx); } }; bool want_menu_update = true; PlayerData tracked_data; Sint64 start_time = 0; std::string GetLocationString() { // Quest Level Name if (setlevel) { return std::string(_(QuestLevelNames[setlvlnum])); } // Dungeon Name constexpr std::array DungeonStrs = { N_("Town"), N_("Cathedral"), N_("Catacombs"), N_("Caves"), N_("Hell"), N_("Nest"), N_("Crypt") }; std::string dungeonStr; if (tracked_data.dungeonArea != DTYPE_NONE) { dungeonStr = _(DungeonStrs[tracked_data.dungeonArea]); } else { dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); } // Dungeon Level if (tracked_data.dungeonLevel > 0) { int level = tracked_data.dungeonLevel; if (tracked_data.dungeonArea == DTYPE_NEST) level -= 16; else if (tracked_data.dungeonArea == DTYPE_CRYPT) level -= 20; return fmt::format(fmt::runtime(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}")), dungeonStr, level); } return dungeonStr; } std::string GetCharacterString() { return fmt::format(fmt::runtime(_(/* TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" */ "Lv {} {}")), tracked_data.playerLevel, MyPlayer->getClassName()); } std::string GetDetailString() { return StrCat(GetCharacterString(), " - ", GetLocationString()); } std::string GetStateString() { constexpr std::array DifficultyStrs = { N_("Normal"), N_("Nightmare"), N_("Hell") }; const std::string_view difficultyStr = _(DifficultyStrs[sgGameInitInfo.nDifficulty]); return fmt::format(fmt::runtime(_(/* TRANSLATORS: Discord state i.e. "Nightmare difficulty" */ "{} difficulty")), difficultyStr); } std::string GetTooltipString() { return StrCat(MyPlayer->_pName, " - ", GetCharacterString()); } std::string GetPlayerAssetString() { char chars[5] { GetPlayerSpriteDataForClass(MyPlayer->_pClass).classChar, ArmourChar[tracked_data.playerGfx >> 4], WepChar[tracked_data.playerGfx & 0xF], 'a', 's' }; return std::string(chars, 5); } void ResetStartTime() { start_time = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } const char *GetIconAsset() { return gbIsHellfire ? "hellfire" : "icon"; } void UpdateGame() { if (discord_core == nullptr) return; auto newData = PlayerData { leveltype, setlvlnum, currlevel, MyPlayer->getCharacterLevel(), MyPlayer->_pgfxnum }; if (newData != tracked_data) { tracked_data = newData; // Update status strings discord::Activity activity = {}; activity.SetName(PROJECT_NAME); activity.SetState(GetStateString().c_str()); activity.SetDetails(GetDetailString().c_str()); activity.SetInstance(true); activity.GetTimestamps().SetStart(start_time); // Set image assets activity.GetAssets().SetLargeImage(GetPlayerAssetString().c_str()); activity.GetAssets().SetLargeText(GetTooltipString().c_str()); activity.GetAssets().SetSmallImage(GetIconAsset()); activity.GetAssets().SetSmallText(gszProductName); discord_core->ActivityManager().UpdateActivity(activity, IgnoreResult); } discord_core->RunCallbacks(); } void StartGame() { tracked_data = PlayerData { dungeon_type::DTYPE_NONE, _setlevels::SL_NONE, 0, 0, 0 }; want_menu_update = true; ResetStartTime(); } void UpdateMenu(bool forced) { if (discord_core == nullptr) return; if (want_menu_update || forced) { if (!forced) { ResetStartTime(); } want_menu_update = false; discord::Activity activity = {}; activity.SetName(PROJECT_NAME); activity.SetState(_(/* TRANSLATORS: Discord activity, not in game */ "In Menu").data()); activity.GetTimestamps().SetStart(start_time); activity.GetAssets().SetLargeImage(GetIconAsset()); activity.GetAssets().SetLargeText(gszProductName); discord_core->ActivityManager().UpdateActivity(activity, IgnoreResult); } discord_core->RunCallbacks(); } } // namespace discord_manager } // namespace devilution ================================================ FILE: Source/discord/discord.h ================================================ #pragma once namespace devilution { namespace discord_manager { #ifdef DISCORD void UpdateGame(); void StartGame(); void UpdateMenu(bool forced = false); #else constexpr void UpdateGame() { } constexpr void StartGame() { } constexpr void UpdateMenu(bool forced = false) { } #endif } // namespace discord_manager } // namespace devilution ================================================ FILE: Source/doom.cpp ================================================ /** * @file doom.cpp * * Implementation of the map of the stars quest. */ #include "doom.h" #include #include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" namespace devilution { namespace { OptionalOwnedClxSpriteList DoomSprite; } // namespace bool DoomFlag; void doom_init() { DoomSprite = LoadCel("items\\map\\mapztown", 640); DoomFlag = true; } void doom_close() { DoomFlag = false; DoomSprite = std::nullopt; } void doom_draw(const Surface &out) { if (!DoomFlag) { return; } ClxDraw(out, GetUIRectangle().position + Displacement { 0, 352 }, (*DoomSprite)[0]); } } // namespace devilution ================================================ FILE: Source/doom.h ================================================ /** * @file doom.h * * Interface of the map of the stars quest. */ #pragma once #include "engine/surface.hpp" namespace devilution { extern bool DoomFlag; void doom_init(); void doom_close(); void doom_draw(const Surface &out); } // namespace devilution ================================================ FILE: Source/dvlnet/abstract_net.cpp ================================================ #include "dvlnet/abstract_net.h" #include "dvlnet/loopback.h" #include "utils/stubs.h" #ifndef NONET #include "dvlnet/cdwrap.h" #ifndef DISABLE_ZERO_TIER #include "dvlnet/base_protocol.h" #include "dvlnet/protocol_zt.h" #endif #ifndef DISABLE_TCP #include "dvlnet/tcp_client.h" #endif #endif namespace devilution::net { std::unique_ptr abstract_net::MakeNet(provider_t provider) { #ifdef NONET return std::make_unique(); #else switch (provider) { #ifndef DISABLE_TCP case SELCONN_TCP: return std::make_unique([]() { return std::make_unique(); }); #endif #ifndef DISABLE_ZERO_TIER case SELCONN_ZT: return std::make_unique([]() { return std::make_unique>(); }); #endif case SELCONN_LOOPBACK: return std::make_unique(); default: ABORT(); } #endif } } // namespace devilution::net ================================================ FILE: Source/dvlnet/abstract_net.h ================================================ #pragma once #include #include #include #include #include #include "multi.h" #include "storm/storm_net.hpp" namespace devilution::net { using buffer_t = std::vector; using provider_t = unsigned long; class abstract_net { public: virtual int create(std::string_view addrstr) = 0; virtual int join(std::string_view addrstr) = 0; virtual bool SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) = 0; virtual bool SNetSendMessage(uint8_t dest, void *data, size_t size) = 0; virtual bool SNetReceiveTurns(char **data, size_t *size, uint32_t *status) = 0; virtual bool SNetSendTurn(char *data, size_t size) = 0; virtual void SNetGetProviderCaps(struct _SNETCAPS *caps) = 0; virtual bool SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) = 0; virtual bool SNetUnregisterEventHandler(event_type evtype) = 0; virtual bool SNetLeaveGame(net::leaveinfo_t type) = 0; virtual bool SNetDropPlayer(int playerid, net::leaveinfo_t flags) = 0; virtual bool SNetGetOwnerTurnsWaiting(uint32_t *turns) = 0; virtual bool SNetGetTurnsInTransit(uint32_t *turns) = 0; virtual void setup_gameinfo(buffer_t info) = 0; virtual ~abstract_net() = default; virtual std::string make_default_gamename() = 0; virtual void process_network_packets() { } virtual void setup_password(std::string passwd) { } virtual void clear_password() { } virtual bool send_info_request() { return true; } virtual void clear_gamelist() { } virtual std::vector get_gamelist() { return std::vector(); } virtual DvlNetLatencies get_latencies(uint8_t playerid) { return {}; } static std::unique_ptr MakeNet(provider_t provider); }; } // namespace devilution::net ================================================ FILE: Source/dvlnet/base.cpp ================================================ #include "dvlnet/base.h" #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "player.h" namespace devilution { namespace net { void base::process_network_packets() { tl::expected result = poll(); if (!result.has_value()) { LogVerbose("Error polling network: {}", result.error().what()); } } void base::setup_gameinfo(buffer_t info) { game_init_info = std::move(info); } void base::setup_password(std::string pw) { pktfty = std::make_unique(pw); } void base::clear_password() { pktfty = std::make_unique(); } DvlNetLatencies base::get_latencies(uint8_t playerid) { DvlNetLatencies latencies = abstract_net::get_latencies(playerid); const PlayerState &playerState = playerStateTable_[playerid]; latencies.echoLatency = playerState.roundTripLatency; return latencies; } void base::RunEventHandler(_SNETEVENT &ev) { auto f = registered_handlers[static_cast(ev.eventid)]; if (f != nullptr) { f(&ev); } } void base::DisconnectNet(plr_t plr) { } tl::expected base::SendEchoRequest(plr_t player) { if (plr_self == PLR_BROADCAST) return {}; if (player == plr_self) return {}; const timestamp_t now = SDL_GetTicks(); tl::expected, PacketError> pkt = pktfty->make_packet(plr_self, player, now); if (!pkt.has_value()) { return tl::make_unexpected(pkt.error()); } return send(**pkt); } tl::expected base::HandleAccept(packet &pkt) { if (plr_self != PLR_BROADCAST) { return {}; // already have player id } if (pkt.Cookie() == cookie_self) { tl::expected newPlayerPkt = pkt.NewPlayer(); if (!newPlayerPkt.has_value()) return tl::make_unexpected(newPlayerPkt.error()); plr_self = *std::move(newPlayerPkt); Connect(plr_self); } tl::expected infoPkt = pkt.Info(); if (!infoPkt.has_value()) return tl::make_unexpected(infoPkt.error()); const buffer_t &info = **infoPkt; if (game_init_info != info) { if (info.size() != sizeof(GameData)) { ABORT(); } // we joined and did not create game_init_info = info; _SNETEVENT ev; ev.eventid = EVENT_TYPE_PLAYER_CREATE_GAME; ev.playerid = plr_self; ev.data = const_cast(info.data()); ev.databytes = info.size(); RunEventHandler(ev); } return {}; } tl::expected base::HandleConnect(packet &pkt) { return pkt.NewPlayer().transform([this](plr_t &&newPlayer) { Connect(newPlayer); }); } tl::expected base::HandleTurn(packet &pkt) { const plr_t src = pkt.Source(); PlayerState &playerState = playerStateTable_[src]; std::deque &turnQueue = playerState.turnQueue; return pkt.Turn().transform([&](turn_t &&turn) { turnQueue.push_back(turn); MakeReady(turn.SequenceNumber); }); } tl::expected base::HandleDisconnect(packet &pkt) { tl::expected newPlayer = pkt.NewPlayer(); if (!newPlayer.has_value()) return tl::make_unexpected(newPlayer.error()); if (*newPlayer == plr_self) return tl::make_unexpected("We were dropped by the owner?"); if (IsConnected(*newPlayer)) { tl::expected leaveinfo = pkt.LeaveInfo(); if (!leaveinfo.has_value()) return tl::make_unexpected(leaveinfo.error()); _SNETEVENT ev; ev.eventid = EVENT_TYPE_PLAYER_LEAVE_GAME; ev.playerid = *newPlayer; ev.data = reinterpret_cast(&*leaveinfo); ev.databytes = sizeof(leaveinfo_t); RunEventHandler(ev); DisconnectNet(*newPlayer); ClearMsg(*newPlayer); PlayerState &playerState = playerStateTable_[*newPlayer]; playerState.isConnected = false; playerState.turnQueue.clear(); } return {}; } tl::expected base::HandleEchoRequest(packet &pkt) { // If we have already left the game, // there is no need to respond to echoes if (plr_self == PLR_BROADCAST) return {}; return pkt.Time() .and_then([&](cookie_t &&pktTime) { return pktfty->make_packet(plr_self, pkt.Source(), pktTime); }) .and_then([&](std::unique_ptr &&pkt) { return send(*pkt); }); } tl::expected base::HandleEchoReply(packet &pkt) { const uint32_t now = SDL_GetTicks(); plr_t src = pkt.Source(); if (src >= MAX_PLRS) return {}; return pkt.Time().transform([&](cookie_t &&pktTime) { PlayerState &playerState = playerStateTable_[src]; playerState.roundTripLatency = now - pktTime; }); } void base::ClearMsg(plr_t plr) { message_queue.erase(std::remove_if(message_queue.begin(), message_queue.end(), [&](message_t &msg) { return msg.sender == plr; }), message_queue.end()); } tl::expected base::Connect(plr_t player) { PlayerState &playerState = playerStateTable_[player]; const bool wasConnected = playerState.isConnected; playerState.isConnected = true; if (!wasConnected) return SendFirstTurnIfReady(player); return {}; } bool base::IsConnected(plr_t player) const { const PlayerState &playerState = playerStateTable_[player]; return playerState.isConnected; } tl::expected base::RecvLocal(packet &pkt) { if (pkt.Source() < MAX_PLRS) { if (tl::expected result = Connect(pkt.Source()); !result.has_value()) { return result; } } switch (pkt.Type()) { case PT_MESSAGE: return pkt.Message().transform([&](const buffer_t *message) { message_queue.emplace_back(pkt.Source(), *message); }); case PT_TURN: return HandleTurn(pkt); case PT_JOIN_ACCEPT: return HandleAccept(pkt); case PT_CONNECT: return HandleConnect(pkt); case PT_DISCONNECT: return HandleDisconnect(pkt); case PT_ECHO_REQUEST: return HandleEchoRequest(pkt); case PT_ECHO_REPLY: return HandleEchoReply(pkt); default: return {}; // otherwise drop } } bool base::SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) { uint32_t now = SDL_GetTicks(); if (now == 0) now++; if (lastEchoTime == 0 || now - lastEchoTime > 5000) { for (plr_t i = 0; i < Players.size(); i++) SendEchoRequest(i); lastEchoTime = now; } process_network_packets(); if (message_queue.empty()) return false; message_last = message_queue.front(); message_queue.pop_front(); *sender = message_last.sender; *size = message_last.payload.size(); *data = message_last.payload.data(); return true; } bool base::SNetSendMessage(uint8_t playerId, void *data, size_t size) { if (playerId != SNPLAYER_OTHERS && playerId >= MAX_PLRS) abort(); auto *rawMessage = reinterpret_cast(data); const buffer_t message(rawMessage, rawMessage + size); if (playerId == plr_self) message_queue.emplace_back(plr_self, message); plr_t dest; if (playerId == SNPLAYER_OTHERS) dest = PLR_BROADCAST; else dest = playerId; if (dest != plr_self) { tl::expected, PacketError> pkt = pktfty->make_packet(plr_self, dest, message); if (!pkt.has_value()) { LogError("make_packet: {}", pkt.error().what()); return false; } tl::expected result = send(**pkt); if (!result.has_value()) { LogError("send: {}", result.error().what()); return false; } } return true; } bool base::AllTurnsArrived() { for (size_t i = 0; i < Players.size(); ++i) { const PlayerState &playerState = playerStateTable_[i]; if (!playerState.isConnected) continue; const std::deque &turnQueue = playerState.turnQueue; if (turnQueue.empty()) return false; } return true; } bool base::SNetReceiveTurns(char **data, size_t *size, uint32_t *status) { process_network_packets(); for (size_t i = 0; i < Players.size(); ++i) { status[i] = 0; PlayerState &playerState = playerStateTable_[i]; if (!playerState.isConnected) continue; status[i] |= PS_CONNECTED; std::deque &turnQueue = playerState.turnQueue; while (!turnQueue.empty()) { const turn_t &turn = turnQueue.front(); const seq_t diff = turn.SequenceNumber - current_turn; if (diff <= 0x7F) break; turnQueue.pop_front(); } } if (AllTurnsArrived()) { for (size_t i = 0; i < Players.size(); ++i) { PlayerState &playerState = playerStateTable_[i]; if (!playerState.isConnected) continue; std::deque &turnQueue = playerState.turnQueue; if (turnQueue.empty()) continue; const turn_t &turn = turnQueue.front(); if (turn.SequenceNumber != current_turn) continue; playerState.lastTurnValue = turn.Value; turnQueue.pop_front(); status[i] |= PS_ACTIVE; status[i] |= PS_TURN_ARRIVED; size[i] = sizeof(int32_t); data[i] = reinterpret_cast(&playerState.lastTurnValue); } current_turn++; return true; } for (size_t i = 0; i < Players.size(); ++i) { const PlayerState &playerState = playerStateTable_[i]; if (!playerState.isConnected) continue; const std::deque &turnQueue = playerState.turnQueue; if (turnQueue.empty()) continue; status[i] |= PS_ACTIVE; } return false; } bool base::SNetSendTurn(char *data, size_t size) { if (size != sizeof(int32_t)) ABORT(); turn_t turn; turn.SequenceNumber = next_turn; std::memcpy(&turn.Value, data, size); next_turn++; PlayerState &playerState = playerStateTable_[plr_self]; std::deque &turnQueue = playerState.turnQueue; turnQueue.push_back(turn); SendTurnIfReady(turn); return true; } tl::expected base::SendTurnIfReady(turn_t turn) { if (awaitingSequenceNumber_) awaitingSequenceNumber_ = !IsGameHost(); if (!awaitingSequenceNumber_) { tl::expected, PacketError> pkt = pktfty->make_packet(plr_self, PLR_BROADCAST, turn); if (!pkt.has_value()) { return tl::make_unexpected(pkt.error()); } return send(**pkt); } return {}; } tl::expected base::SendFirstTurnIfReady(plr_t player) { if (awaitingSequenceNumber_) return {}; const PlayerState &playerState = playerStateTable_[plr_self]; const std::deque &turnQueue = playerState.turnQueue; if (turnQueue.empty()) return {}; for (const turn_t turn : turnQueue) { tl::expected, PacketError> pkt = pktfty->make_packet(plr_self, player, turn); if (!pkt.has_value()) { return tl::make_unexpected(pkt.error()); } tl::expected result = send(**pkt); if (!result.has_value()) { return result; } } return {}; } tl::expected base::MakeReady(seq_t sequenceNumber) { if (!awaitingSequenceNumber_) return {}; current_turn = sequenceNumber; next_turn = sequenceNumber; awaitingSequenceNumber_ = false; PlayerState &playerState = playerStateTable_[plr_self]; std::deque &turnQueue = playerState.turnQueue; for (turn_t &turn : turnQueue) { turn.SequenceNumber = next_turn; next_turn++; if (tl::expected result = SendTurnIfReady(turn); !result.has_value()) { return result; } } return {}; } void base::SNetGetProviderCaps(struct _SNETCAPS *caps) { caps->size = 0; // engine writes only ?!? caps->flags = 0; // unused caps->maxmessagesize = 512; // capped to 512; underflow if < 24 caps->maxqueuesize = 0; // unused caps->maxplayers = MAX_PLRS; // capped to 4 caps->bytessec = 1000000; // ? caps->latencyms = 0; // unused caps->defaultturnssec = 10; // ? caps->defaultturnsintransit = 2; // maximum acceptable number // of turns in queue? } bool base::SNetUnregisterEventHandler(event_type evtype) { registered_handlers.erase(evtype); return true; } bool base::SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) { /* engine registers handler for: EVENT_TYPE_PLAYER_LEAVE_GAME EVENT_TYPE_PLAYER_CREATE_GAME (should be raised during SNetCreateGame for non-creating player) EVENT_TYPE_PLAYER_MESSAGE (for bnet? not implemented) (engine uses same function for all three) */ registered_handlers[evtype] = func; return true; } bool base::SNetLeaveGame(net::leaveinfo_t type) { tl::expected, PacketError> pkt = pktfty->make_packet( plr_self, PLR_BROADCAST, plr_self, type); if (!pkt.has_value()) { LogError("make_packet: {}", pkt.error().what()); return false; } tl::expected result = send(**pkt); if (!result.has_value()) { LogError("send: {}", result.error().what()); return false; } plr_self = PLR_BROADCAST; return true; } bool base::SNetDropPlayer(int playerid, net::leaveinfo_t flags) { const plr_t plr = static_cast(playerid); tl::expected, PacketError> pkt = pktfty->make_packet( plr_self, PLR_BROADCAST, plr, flags); if (!pkt.has_value()) { LogError("make_packet: {}", pkt.error().what()); return false; } // Disconnect at the network layer first so we // don't send players their own disconnect packet DisconnectNet(plr); tl::expected sendResult = send(**pkt); if (!sendResult.has_value()) { LogError("send: {}", sendResult.error().what()); return false; } tl::expected receiveResult = RecvLocal(**pkt); if (!receiveResult.has_value()) { LogError("SNetDropPlayer: {}", receiveResult.error().what()); return false; } return true; } plr_t base::GetOwner() { for (plr_t i = 0; i < Players.size(); ++i) { if (IsConnected(i)) { return i; } } return PLR_BROADCAST; // should be unreachable } bool base::SNetGetOwnerTurnsWaiting(uint32_t *turns) { process_network_packets(); const plr_t owner = GetOwner(); const PlayerState &playerState = playerStateTable_[owner]; const std::deque &turnQueue = playerState.turnQueue; *turns = static_cast(turnQueue.size()); return true; } bool base::SNetGetTurnsInTransit(uint32_t *turns) { const PlayerState &playerState = playerStateTable_[plr_self]; const std::deque &turnQueue = playerState.turnQueue; *turns = static_cast(turnQueue.size()); return true; } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/base.h ================================================ #pragma once #include #include #include #include #include #include #include "dvlnet/abstract_net.h" #include "dvlnet/packet.h" #include "multi.h" #include "storm/storm_net.hpp" namespace devilution { namespace net { class base : public abstract_net { public: bool SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) override; bool SNetSendMessage(uint8_t playerId, void *data, size_t size) override; bool SNetReceiveTurns(char **data, size_t *size, uint32_t *status) override; bool SNetSendTurn(char *data, size_t size) override; void SNetGetProviderCaps(struct _SNETCAPS *caps) override; bool SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) override; bool SNetUnregisterEventHandler(event_type evtype) override; bool SNetLeaveGame(net::leaveinfo_t type) override; bool SNetDropPlayer(int playerid, net::leaveinfo_t flags) override; bool SNetGetOwnerTurnsWaiting(uint32_t *turns) override; bool SNetGetTurnsInTransit(uint32_t *turns) override; virtual tl::expected poll() = 0; virtual tl::expected send(packet &pkt) = 0; virtual void DisconnectNet(plr_t plr); void process_network_packets() override; void setup_gameinfo(buffer_t info) override; void setup_password(std::string pw) override; void clear_password() override; DvlNetLatencies get_latencies(uint8_t playerid) override; ~base() override = default; protected: ankerl::unordered_dense::map registered_handlers; buffer_t game_init_info; struct message_t { uint8_t sender; buffer_t payload; message_t() : sender(-1) , payload({}) { } message_t(int s, buffer_t p) : sender(s) , payload(p) { } }; struct PlayerState { bool isConnected = {}; std::deque turnQueue; int32_t lastTurnValue = {}; uint32_t roundTripLatency = {}; }; seq_t current_turn = 0; seq_t next_turn = 0; message_t message_last; std::deque message_queue; plr_t plr_self = PLR_BROADCAST; cookie_t cookie_self = 0; std::unique_ptr pktfty; tl::expected Connect(plr_t player); tl::expected RecvLocal(packet &pkt); void RunEventHandler(_SNETEVENT &ev); tl::expected SendEchoRequest(plr_t player); [[nodiscard]] bool IsConnected(plr_t player) const; virtual bool IsGameHost() = 0; private: std::array playerStateTable_; bool awaitingSequenceNumber_ = true; uint32_t lastEchoTime = 0; plr_t GetOwner(); bool AllTurnsArrived(); tl::expected MakeReady(seq_t sequenceNumber); tl::expected SendTurnIfReady(turn_t turn); tl::expected SendFirstTurnIfReady(plr_t player); void ClearMsg(plr_t plr); tl::expected HandleAccept(packet &pkt); tl::expected HandleConnect(packet &pkt); tl::expected HandleTurn(packet &pkt); tl::expected HandleDisconnect(packet &pkt); tl::expected HandleEchoRequest(packet &pkt); tl::expected HandleEchoReply(packet &pkt); }; } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/base_protocol.h ================================================ #pragma once #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "dvlnet/base.h" #include "dvlnet/packet.h" #include "player.h" #include "utils/algorithm/container.hpp" #include "utils/is_of.hpp" #include "utils/log.hpp" namespace devilution::net { template class base_protocol : public base { public: int create(std::string_view addrstr) override; int join(std::string_view addrstr) override; tl::expected poll() override; tl::expected send(packet &pkt) override; void DisconnectNet(plr_t plr) override; bool SNetLeaveGame(net::leaveinfo_t type) override; std::string make_default_gamename() override; bool send_info_request() override; void clear_gamelist() override; std::vector get_gamelist() override; DvlNetLatencies get_latencies(uint8_t playerid) override; ~base_protocol() override = default; protected: bool IsGameHost() override; private: P proto; typedef typename P::endpoint endpoint_t; struct Peer { endpoint_t endpoint; std::unique_ptr> sendQueue; }; endpoint_t firstpeer; std::string gamename; struct GameListValue { GameData data; std::vector playerNames; endpoint_t peer; }; ankerl::unordered_dense::map game_list; std::array peers; bool isGameHost_; plr_t get_master(); tl::expected InitiateHandshake(plr_t player); tl::expected SendTo(plr_t player, packet &pkt); void DrainSendQueue(plr_t player); void recv(); tl::expected handle_join_request(packet &pkt, endpoint_t sender); tl::expected recv_decrypted(packet &pkt, endpoint_t sender); tl::expected recv_ingame(packet &pkt, endpoint_t sender); bool is_recognized(endpoint_t sender); tl::expected wait_network(); tl::expected wait_firstpeer(); tl::expected wait_join(); }; template plr_t base_protocol

::get_master() { plr_t ret = plr_self; for (plr_t i = 0; i < Players.size(); ++i) if (peers[i].endpoint) ret = std::min(ret, i); return ret; } template tl::expected base_protocol

::wait_network() { // wait for ZeroTier for 5 seconds for (auto i = 0; i < 500; ++i) { tl::expected status = proto.network_online(); if (!status.has_value()) return tl::make_unexpected(std::move(status).error()); if (*status) return {}; SDL_Delay(10); } return tl::make_unexpected("Timeout waiting for ZeroTier network initialization"); } template void base_protocol

::DisconnectNet(plr_t plr) { Peer &peer = peers[plr]; proto.disconnect(peer.endpoint); peer = {}; } template tl::expected base_protocol

::wait_firstpeer() { firstpeer = {}; // wait for peer for 5 seconds for (auto i = 0; i < 500; ++i) { auto it = game_list.find(gamename); if (it != game_list.end()) { firstpeer = it->second.peer; break; } send_info_request(); recv(); SDL_Delay(10); } if (!firstpeer) return tl::make_unexpected("Timeout waiting for response from game host"); return {}; } template bool base_protocol

::send_info_request() { tl::expected status = proto.peers_ready(); if (!status.has_value()) { LogError("peers_ready: {}", status.error().what()); return false; } if (!*status) return false; tl::expected, PacketError> pkt = pktfty->make_packet(PLR_BROADCAST, PLR_MASTER); if (!pkt.has_value()) { LogError("make_packet: {}", pkt.error().what()); return false; } proto.send_oob_mc((*pkt)->Data()); return true; } template tl::expected base_protocol

::wait_join() { cookie_self = packet_out::GenerateCookie(); tl::expected, PacketError> pkt = pktfty->make_packet(PLR_BROADCAST, PLR_MASTER, cookie_self, game_init_info); if (!pkt.has_value()) { return tl::make_unexpected(pkt.error()); } tl::expected result = proto.send(firstpeer, (*pkt)->Data()); if (!result.has_value()) { return result; } for (auto i = 0; i < 500; ++i) { recv(); if (plr_self != PLR_BROADCAST) return {}; // join successful SDL_Delay(10); } return tl::make_unexpected("Timeout waiting to join game"); } template int base_protocol

::create(std::string_view addrstr) { gamename = addrstr; isGameHost_ = true; tl::expected isReady = wait_network(); if (!isReady.has_value()) { const std::string_view message = isReady.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } plr_self = 0; if (tl::expected result = Connect(plr_self); !result.has_value()) { const std::string_view message = result.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } return plr_self; } template int base_protocol

::join(std::string_view addrstr) { gamename = addrstr; isGameHost_ = false; tl::expected isReady = wait_network(); if (!isReady.has_value()) { const std::string_view message = isReady.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } tl::expected isPeerReady = wait_firstpeer(); if (!isPeerReady.has_value()) { const std::string_view message = isPeerReady.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } tl::expected isJoined = wait_join(); if (!isJoined.has_value()) { const std::string_view message = isJoined.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } return (plr_self == PLR_BROADCAST ? -1 : plr_self); } template bool base_protocol

::IsGameHost() { return isGameHost_; } template tl::expected base_protocol

::poll() { recv(); return {}; } template tl::expected base_protocol

::InitiateHandshake(plr_t player) { Peer &peer = peers[player]; // The first packet sent will initiate the TCP connection over the ZeroTier network. // It will cause problems if both peers attempt to initiate the handshake simultaneously. // If the connection is already open, it should be safe to initiate from either end. // If not, only the player with the smaller player number should initiate the handshake. if (peer.endpoint && (plr_self < player || proto.is_peer_connected(peer.endpoint))) return SendEchoRequest(player); return {}; } template tl::expected base_protocol

::send(packet &pkt) { plr_t destination = pkt.Destination(); if (destination == PLR_BROADCAST) { for (plr_t player = 0; player < Players.size(); player++) { tl::expected result = SendTo(player, pkt); if (!result.has_value()) LogError("Failed to send packet {} to player {}: {}", static_cast(pkt.Type()), player, result.error().what()); } return {}; } if (destination >= MAX_PLRS) return tl::make_unexpected("Invalid player ID"); if (destination == plr_self) return {}; return SendTo(destination, pkt); } template tl::expected base_protocol

::SendTo(plr_t player, packet &pkt) { Peer &peer = peers[player]; if (!peer.endpoint) return {}; // The handshake uses echo packets so clients know // when they can safely drain their send queues if (peer.sendQueue && !IsAnyOf(pkt.Type(), PT_ECHO_REQUEST, PT_ECHO_REPLY)) { peer.sendQueue->push_back(pkt); return {}; } return proto.send(peer.endpoint, pkt.Data()); } template void base_protocol

::recv() { buffer_t pkt_buf; endpoint_t sender; while (proto.recv(sender, pkt_buf)) { // read until kernel buffer is empty? tl::expected result = pktfty->make_packet(pkt_buf) .and_then([&](std::unique_ptr &&pkt) { return recv_decrypted(*pkt, sender); }); if (!result.has_value()) { // drop packet proto.disconnect(sender); Log("{}", result.error().what()); } } while (proto.get_disconnected(sender)) { for (plr_t i = 0; i < Players.size(); ++i) { if (peers[i].endpoint == sender) { DisconnectNet(i); break; } } } } template tl::expected base_protocol

::handle_join_request(packet &inPkt, endpoint_t sender) { plr_t i; for (i = 0; i < Players.size(); ++i) { Peer &peer = peers[i]; if (i != plr_self && !peer.endpoint) { peer.endpoint = sender; peer.sendQueue = std::make_unique>(); if (tl::expected result = Connect(i); !result.has_value()) { return result; } break; } } if (i >= MAX_PLRS) { // already full return {}; } auto senderinfo = sender.serialize(); for (plr_t j = 0; j < Players.size(); ++j) { endpoint_t peer = peers[j].endpoint; if ((j != plr_self) && (j != i) && peer) { tl::expected result = pktfty->make_packet(PLR_MASTER, PLR_BROADCAST, i, senderinfo) .and_then([&](std::unique_ptr &&pkt) { return proto.send(peer, pkt->Data()); }) .and_then([&]() { return pktfty->make_packet(PLR_MASTER, PLR_BROADCAST, j, peer.serialize()); }) .and_then([&](std::unique_ptr &&pkt) { return proto.send(sender, pkt->Data()); }); if (!result.has_value()) return result; } } // PT_JOIN_ACCEPT must be sent after all PT_CONNECT packets so the new player does // not resume game logic until after having been notified of all existing players tl::expected cookie = inPkt.Cookie(); if (!cookie.has_value()) return tl::make_unexpected(cookie.error()); tl::expected, PacketError> pkt = pktfty->make_packet(plr_self, PLR_BROADCAST, *cookie, i, game_init_info); if (!pkt.has_value()) return tl::make_unexpected(pkt.error()); tl::expected result = proto.send(sender, (*pkt)->Data()); if (!result.has_value()) return result; DrainSendQueue(i); return {}; } template tl::expected base_protocol

::recv_decrypted(packet &pkt, endpoint_t sender) { if (pkt.Source() == PLR_BROADCAST && pkt.Destination() == PLR_MASTER && pkt.Type() == PT_INFO_REPLY) { size_t neededSize = sizeof(GameData) + (PlayerNameLength * MAX_PLRS); const tl::expected pktInfo = pkt.Info(); if (!pktInfo.has_value()) return tl::make_unexpected(pktInfo.error()); const buffer_t &infoBuffer = **pktInfo; if (infoBuffer.size() < neededSize) return {}; GameData gameData; std::memcpy(&gameData, infoBuffer.data(), sizeof(GameData)); gameData.swapLE(); if (gameData.size != sizeof(GameData)) return {}; std::vector playerNames; for (size_t i = 0; i < Players.size(); i++) { std::string_view playerNameBuffer { reinterpret_cast(infoBuffer.data() + sizeof(GameData) + (i * PlayerNameLength)), PlayerNameLength }; if (const size_t nullPos = playerNameBuffer.find('\0'); nullPos != std::string_view::npos) { playerNameBuffer.remove_suffix(playerNameBuffer.size() - nullPos); } if (!playerNameBuffer.empty()) { playerNames.emplace_back(playerNameBuffer); } } std::string gameName; size_t gameNameSize = infoBuffer.size() - neededSize; gameName.resize(gameNameSize); std::memcpy(&gameName[0], infoBuffer.data() + neededSize, gameNameSize); game_list[gameName] = GameListValue { gameData, std::move(playerNames), sender }; return {}; } return recv_ingame(pkt, sender); } template tl::expected base_protocol

::recv_ingame(packet &pkt, endpoint_t sender) { if (pkt.Source() == PLR_BROADCAST && pkt.Destination() == PLR_MASTER) { if (pkt.Type() == PT_JOIN_REQUEST) { if (tl::expected result = handle_join_request(pkt, sender); !result.has_value()) { return result; } } else if (pkt.Type() == PT_INFO_REQUEST) { if ((plr_self != PLR_BROADCAST) && (get_master() == plr_self)) { buffer_t buf; buf.resize(game_init_info.size() + (PlayerNameLength * MAX_PLRS) + gamename.size()); std::memcpy(buf.data(), &game_init_info[0], game_init_info.size()); for (size_t i = 0; i < Players.size(); i++) { if (Players[i].plractive) { std::memcpy(buf.data() + game_init_info.size() + (i * PlayerNameLength), &Players[i]._pName, PlayerNameLength); } else { std::memset(buf.data() + game_init_info.size() + (i * PlayerNameLength), '\0', PlayerNameLength); } } std::memcpy(buf.data() + game_init_info.size() + (PlayerNameLength * MAX_PLRS), &gamename[0], gamename.size()); tl::expected, PacketError> reply = pktfty->make_packet(PLR_BROADCAST, PLR_MASTER, buf); if (!reply.has_value()) { return tl::make_unexpected(reply.error()); } proto.send_oob(sender, (*reply)->Data()); } } return {}; } if (pkt.Source() == PLR_MASTER && pkt.Type() == PT_CONNECT) { if (!is_recognized(sender)) { LogDebug("Invalid packet: PT_CONNECT received from unrecognized endpoint"); return {}; } // addrinfo packets tl::expected newPlayer = pkt.NewPlayer(); if (!newPlayer.has_value()) return tl::make_unexpected(newPlayer.error()); Peer &peer = peers[*newPlayer]; tl::expected pktInfo = pkt.Info(); if (!pktInfo.has_value()) return tl::make_unexpected(pktInfo.error()); if (tl::expected result = peer.endpoint.unserialize(**pktInfo); !result.has_value()) { return result; } peer.sendQueue = std::make_unique>(); if (tl::expected result = Connect(*newPlayer); !result.has_value()) { return result; } if (plr_self != PLR_BROADCAST) return InitiateHandshake(*newPlayer); return {}; } if (pkt.Source() >= MAX_PLRS) { // normal packets LogDebug("Invalid packet: packet source ({}) >= MAX_PLRS", pkt.Source()); return {}; } if (sender == firstpeer && pkt.Type() == PT_JOIN_ACCEPT) { plr_t src = pkt.Source(); peers[src].endpoint = sender; if (tl::expected result = Connect(src); !result.has_value()) { return result; } firstpeer = {}; } else if (sender != peers[pkt.Source()].endpoint) { LogDebug("Invalid packet: packet source ({}) received from unrecognized endpoint", pkt.Source()); return {}; } if (pkt.Destination() != plr_self && pkt.Destination() != PLR_BROADCAST) return {}; // packet not for us, drop bool wasBroadcast = plr_self == PLR_BROADCAST; if (tl::expected result = RecvLocal(pkt); !result.has_value()) { return result; } if (plr_self != PLR_BROADCAST) { if (wasBroadcast) { // Send a handshake to everyone just after PT_JOIN_ACCEPT for (plr_t player = 0; player < Players.size(); player++) { if (tl::expected result = InitiateHandshake(player); !result.has_value()) { return result; } } } DrainSendQueue(pkt.Source()); } return {}; } template void base_protocol

::DrainSendQueue(plr_t player) { Peer &srcPeer = peers[player]; if (!srcPeer.sendQueue) return; std::deque &sendQueue = *srcPeer.sendQueue; while (!sendQueue.empty()) { packet &pkt = sendQueue.front(); tl::expected result = proto.send(srcPeer.endpoint, pkt.Data()); if (!result.has_value()) LogError("DrainSendQueue failed to send packet: {}", result.error().what()); sendQueue.pop_front(); } srcPeer.sendQueue = nullptr; } template bool base_protocol

::is_recognized(endpoint_t sender) { if (!sender) return false; if (sender == firstpeer) return true; for (auto player = 0; player <= MAX_PLRS; player++) { if (sender == peers[player].endpoint) return true; } return false; } template void base_protocol

::clear_gamelist() { game_list.clear(); } template std::vector base_protocol

::get_gamelist() { recv(); std::vector ret; ret.reserve(game_list.size()); for (const auto &[name, gameInfo] : game_list) { const auto &[gameData, players, endpoint] = gameInfo; std::optional latency = proto.get_latency_to(endpoint); std::optional isRelayed = proto.is_peer_relayed(endpoint); ret.push_back(GameInfo { name, gameData, players, latency, isRelayed }); } c_sort(ret, [](const GameInfo &a, const GameInfo &b) { return a.name < b.name; }); return ret; } template DvlNetLatencies base_protocol

::get_latencies(uint8_t playerid) { DvlNetLatencies latencies = base::get_latencies(playerid); Peer &srcPeer = peers[playerid]; latencies.providerLatency = proto.get_latency_to(srcPeer.endpoint); latencies.isRelayed = proto.is_peer_relayed(srcPeer.endpoint); return latencies; } template bool base_protocol

::SNetLeaveGame(net::leaveinfo_t type) { auto ret = base::SNetLeaveGame(type); recv(); return ret; } template std::string base_protocol

::make_default_gamename() { return proto.make_default_gamename(); } } // namespace devilution::net ================================================ FILE: Source/dvlnet/cdwrap.cpp ================================================ #include "dvlnet/cdwrap.h" namespace devilution::net { void cdwrap::reset() { dvlnet_wrap = make_net_fn_(); dvlnet_wrap->setup_gameinfo(game_init_info); if (game_pw != std::nullopt) { dvlnet_wrap->setup_password(*game_pw); } else { dvlnet_wrap->clear_password(); } for (const auto &[eventType, eventHandler] : registered_handlers) dvlnet_wrap->SNetRegisterEventHandler(eventType, eventHandler); } int cdwrap::create(std::string_view addrstr) { reset(); return dvlnet_wrap->create(addrstr); } int cdwrap::join(std::string_view addrstr) { game_init_info = buffer_t(); reset(); return dvlnet_wrap->join(addrstr); } void cdwrap::process_network_packets() { dvlnet_wrap->process_network_packets(); } void cdwrap::setup_gameinfo(buffer_t info) { game_init_info = std::move(info); if (dvlnet_wrap) dvlnet_wrap->setup_gameinfo(game_init_info); } bool cdwrap::SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) { return dvlnet_wrap->SNetReceiveMessage(sender, data, size); } bool cdwrap::SNetSendMessage(uint8_t playerID, void *data, size_t size) { return dvlnet_wrap->SNetSendMessage(playerID, data, size); } bool cdwrap::SNetReceiveTurns(char **data, size_t *size, uint32_t *status) { return dvlnet_wrap->SNetReceiveTurns(data, size, status); } bool cdwrap::SNetSendTurn(char *data, size_t size) { return dvlnet_wrap->SNetSendTurn(data, size); } void cdwrap::SNetGetProviderCaps(struct _SNETCAPS *caps) { dvlnet_wrap->SNetGetProviderCaps(caps); } bool cdwrap::SNetUnregisterEventHandler(event_type evtype) { registered_handlers.erase(evtype); if (dvlnet_wrap) return dvlnet_wrap->SNetUnregisterEventHandler(evtype); return true; } bool cdwrap::SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) { registered_handlers[evtype] = func; if (dvlnet_wrap) return dvlnet_wrap->SNetRegisterEventHandler(evtype, func); return true; } bool cdwrap::SNetLeaveGame(net::leaveinfo_t type) { return dvlnet_wrap->SNetLeaveGame(type); } bool cdwrap::SNetDropPlayer(int playerid, net::leaveinfo_t flags) { return dvlnet_wrap->SNetDropPlayer(playerid, flags); } bool cdwrap::SNetGetOwnerTurnsWaiting(uint32_t *turns) { return dvlnet_wrap->SNetGetOwnerTurnsWaiting(turns); } bool cdwrap::SNetGetTurnsInTransit(uint32_t *turns) { return dvlnet_wrap->SNetGetTurnsInTransit(turns); } std::string cdwrap::make_default_gamename() { return dvlnet_wrap->make_default_gamename(); } bool cdwrap::send_info_request() { return dvlnet_wrap->send_info_request(); } void cdwrap::clear_gamelist() { dvlnet_wrap->clear_gamelist(); } std::vector cdwrap::get_gamelist() { return dvlnet_wrap->get_gamelist(); } void cdwrap::setup_password(std::string pw) { game_pw = pw; return dvlnet_wrap->setup_password(pw); } void cdwrap::clear_password() { game_pw = std::nullopt; return dvlnet_wrap->clear_password(); } DvlNetLatencies cdwrap::get_latencies(uint8_t playerid) { return dvlnet_wrap->get_latencies(playerid); } } // namespace devilution::net ================================================ FILE: Source/dvlnet/cdwrap.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include "dvlnet/abstract_net.h" #include "storm/storm_net.hpp" namespace devilution::net { class cdwrap : public abstract_net { private: std::unique_ptr dvlnet_wrap; ankerl::unordered_dense::map registered_handlers; buffer_t game_init_info; std::optional game_pw; tl::function_ref()> make_net_fn_; void reset(); public: explicit cdwrap(tl::function_ref()> makeNetFn) : make_net_fn_(makeNetFn) { reset(); } int create(std::string_view addrstr) override; int join(std::string_view addrstr) override; bool SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) override; bool SNetSendMessage(uint8_t dest, void *data, size_t size) override; bool SNetReceiveTurns(char **data, size_t *size, uint32_t *status) override; bool SNetSendTurn(char *data, size_t size) override; void SNetGetProviderCaps(struct _SNETCAPS *caps) override; bool SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) override; bool SNetUnregisterEventHandler(event_type evtype) override; bool SNetLeaveGame(net::leaveinfo_t type) override; bool SNetDropPlayer(int playerid, net::leaveinfo_t flags) override; bool SNetGetOwnerTurnsWaiting(uint32_t *turns) override; bool SNetGetTurnsInTransit(uint32_t *turns) override; void process_network_packets() override; void setup_gameinfo(buffer_t info) override; std::string make_default_gamename() override; bool send_info_request() override; void clear_gamelist() override; std::vector get_gamelist() override; void setup_password(std::string pw) override; void clear_password() override; DvlNetLatencies get_latencies(uint8_t playerid) override; virtual ~cdwrap() = default; }; } // namespace devilution::net ================================================ FILE: Source/dvlnet/frame_queue.cpp ================================================ #include "dvlnet/frame_queue.h" #include #include "appfat.h" #include "dvlnet/packet.h" #include "utils/attributes.h" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" namespace devilution { namespace net { namespace { PacketError FrameQueueError() { return PacketError("Incorrect frame size"); } } // namespace framesize_t frame_queue::Size() const { return current_size; } tl::expected frame_queue::Read(framesize_t s) { if (current_size < s) return tl::make_unexpected(FrameQueueError()); buffer_t ret; while (s > 0 && s >= buffer_deque.front().size()) { const framesize_t bufferSize = static_cast(buffer_deque.front().size()); s -= bufferSize; current_size -= bufferSize; ret.insert(ret.end(), buffer_deque.front().begin(), buffer_deque.front().end()); buffer_deque.pop_front(); } if (s > 0) { ret.insert(ret.end(), buffer_deque.front().begin(), buffer_deque.front().begin() + s); buffer_deque.front().erase(buffer_deque.front().begin(), buffer_deque.front().begin() + s); current_size -= s; } return ret; } void frame_queue::Write(buffer_t buf) { current_size += static_cast(buf.size()); buffer_deque.push_back(std::move(buf)); } tl::expected frame_queue::PacketReady() { if (nextsize == 0) { if (Size() < sizeof(framesize_t)) return false; tl::expected szbuf = Read(sizeof(framesize_t)); if (!szbuf.has_value()) return tl::make_unexpected(szbuf.error()); nextsize = LoadLE32(szbuf->data()); if (nextsize == 0) return tl::make_unexpected(FrameQueueError()); } return Size() >= (nextsize & frame_size_mask); } uint16_t frame_queue::ReadPacketFlags() { static_assert(sizeof(nextsize) == 4, "framesize_t is not 4 bytes"); return static_cast(nextsize >> 16); } tl::expected frame_queue::ReadPacket() { const framesize_t packetSize = nextsize & frame_size_mask; if (nextsize == 0 || Size() < packetSize) return tl::make_unexpected(FrameQueueError()); tl::expected ret = Read(packetSize); nextsize = 0; return ret; } tl::expected frame_queue::MakeFrame(buffer_t packetbuf, uint16_t flags) { buffer_t ret; const framesize_t size = static_cast(packetbuf.size()); if (size > max_frame_size) return tl::make_unexpected("Buffer exceeds maximum frame size"); static_assert(sizeof(size) == 4, "framesize_t is not 4 bytes"); unsigned char sizeBuf[4]; WriteLE32(sizeBuf, size | (static_cast(flags) << 16)); ret.insert(ret.end(), sizeBuf, sizeBuf + 4); ret.insert(ret.end(), packetbuf.begin(), packetbuf.end()); return ret; } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/frame_queue.h ================================================ #pragma once #include #include #include #include #include #include "dvlnet/packet.h" namespace devilution { namespace net { typedef std::vector buffer_t; typedef uint32_t framesize_t; class frame_queue { public: constexpr static framesize_t frame_size_mask = 0xFFFF; constexpr static framesize_t max_frame_size = 0xFFFF; private: framesize_t current_size = 0; std::deque buffer_deque; framesize_t nextsize = 0; framesize_t Size() const; tl::expected Read(framesize_t s); public: tl::expected PacketReady(); uint16_t ReadPacketFlags(); tl::expected ReadPacket(); void Write(buffer_t buf); static tl::expected MakeFrame(buffer_t packetbuf, uint16_t flags = 0); }; } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/leaveinfo.hpp ================================================ #pragma once #include namespace devilution { namespace net { enum class leaveinfo_t : uint32_t { LEAVE_EXIT = 3, LEAVE_ENDING = 0x40000004, LEAVE_DROP = 0x40000006, }; } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/loopback.cpp ================================================ #include "dvlnet/loopback.h" #include #include "multi.h" #include "player.h" #include "utils/language.h" #include "utils/stubs.h" namespace devilution::net { int loopback::create(std::string_view /*addrstr*/) { IsLoopback = true; return plr_single; } int loopback::join(std::string_view /*addrstr*/) { ABORT(); } bool loopback::SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) { if (message_queue.empty()) return false; message_last = message_queue.front(); message_queue.pop(); *sender = plr_single; *size = message_last.size(); *data = message_last.data(); return true; } bool loopback::SNetSendMessage(uint8_t dest, void *data, size_t size) { if (dest == plr_single) { auto *rawMessage = reinterpret_cast(data); const buffer_t message(rawMessage, rawMessage + size); message_queue.push(message); } return true; } bool loopback::SNetReceiveTurns(char **data, size_t *size, uint32_t * /*status*/) { for (size_t i = 0; i < Players.size(); ++i) { size[i] = 0; data[i] = nullptr; } return true; } bool loopback::SNetSendTurn(char * /*data*/, size_t /*size*/) { return true; } void loopback::SNetGetProviderCaps(struct _SNETCAPS *caps) { caps->size = 0; // engine writes only ?!? caps->flags = 0; // unused caps->maxmessagesize = 512; // capped to 512; underflow if < 24 caps->maxqueuesize = 0; // unused caps->maxplayers = MAX_PLRS; // capped to 4 caps->bytessec = 1000000; // ? caps->latencyms = 0; // unused caps->defaultturnssec = 10; // ? caps->defaultturnsintransit = 1; // maximum acceptable number // of turns in queue? } bool loopback::SNetRegisterEventHandler(event_type /*evtype*/, SEVTHANDLER /*func*/) { // not called in real singleplayer mode // not needed in pseudo multiplayer mode (?) return true; } bool loopback::SNetUnregisterEventHandler(event_type /*evtype*/) { // not called in real singleplayer mode // not needed in pseudo multiplayer mode (?) return true; } bool loopback::SNetLeaveGame(net::leaveinfo_t /*type*/) { IsLoopback = false; return true; } bool loopback::SNetDropPlayer(int /*playerid*/, net::leaveinfo_t /*flags*/) { return true; } void loopback::setup_gameinfo(buffer_t info) { } bool loopback::SNetGetOwnerTurnsWaiting(uint32_t *turns) { *turns = 0; return true; } bool loopback::SNetGetTurnsInTransit(uint32_t *turns) { *turns = 0; return true; } std::string loopback::make_default_gamename() { return std::string(_("loopback")); } } // namespace devilution::net ================================================ FILE: Source/dvlnet/loopback.h ================================================ #pragma once #include #include #include #include "dvlnet/abstract_net.h" namespace devilution::net { class loopback : public abstract_net { private: std::queue message_queue; buffer_t message_last; uint8_t plr_single = 0; public: loopback() = default; int create(std::string_view addrstr) override; int join(std::string_view addrstr) override; bool SNetReceiveMessage(uint8_t *sender, void **data, size_t *size) override; bool SNetSendMessage(uint8_t dest, void *data, size_t size) override; bool SNetReceiveTurns(char **data, size_t *size, uint32_t *status) override; bool SNetSendTurn(char *data, size_t size) override; void SNetGetProviderCaps(struct _SNETCAPS *caps) override; bool SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) override; bool SNetUnregisterEventHandler(event_type evtype) override; bool SNetLeaveGame(net::leaveinfo_t type) override; bool SNetDropPlayer(int playerid, net::leaveinfo_t flags) override; bool SNetGetOwnerTurnsWaiting(uint32_t *turns) override; bool SNetGetTurnsInTransit(uint32_t *turns) override; void setup_gameinfo(buffer_t info) override; std::string make_default_gamename() override; }; } // namespace devilution::net ================================================ FILE: Source/dvlnet/packet.cpp ================================================ #include "dvlnet/packet.h" #include #include #ifdef PACKET_ENCRYPTION #include #else #include #include #endif #include #include "utils/algorithm/container.hpp" #include "utils/str_cat.hpp" namespace devilution::net { #ifdef PACKET_ENCRYPTION cookie_t packet_out::GenerateCookie() { cookie_t cookie; randombytes_buf(reinterpret_cast(&cookie), sizeof(cookie_t)); return cookie; } #else class cookie_generator { public: cookie_generator() { unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); generator.seed(seed); } cookie_t NewCookie() { return distribution(generator); } private: std::default_random_engine generator; std::uniform_int_distribution distribution; }; cookie_generator CookieGenerator; cookie_t packet_out::GenerateCookie() { return CookieGenerator.NewCookie(); } #endif const char *packet_type_to_string(uint8_t packetType) { switch (packetType) { case PT_MESSAGE: return "PT_MESSAGE"; case PT_TURN: return "PT_TURN"; case PT_JOIN_REQUEST: return "PT_JOIN_REQUEST"; case PT_JOIN_ACCEPT: return "PT_JOIN_ACCEPT"; case PT_CONNECT: return "PT_CONNECT"; case PT_DISCONNECT: return "PT_DISCONNECT"; case PT_INFO_REQUEST: return "PT_INFO_REQUEST"; case PT_INFO_REPLY: return "PT_INFO_REPLY"; case PT_ECHO_REQUEST: return "PT_ECHO_REQUEST"; case PT_ECHO_REPLY: return "PT_ECHO_REPLY"; default: return nullptr; } } PacketError PacketTypeError(std::uint8_t unknownPacketType) { return PacketError(StrCat("Unknown packet type ", unknownPacketType)); } PacketError PacketTypeError(std::initializer_list expectedTypes, std::uint8_t actual) { std::string message = "Expected packet of type "; const auto appendPacketType = [&](std::uint8_t t) { const char *typeStr = packet_type_to_string(t); if (typeStr != nullptr) message.append(typeStr); else StrAppend(message, t); }; constexpr char KJoinTypes[] = " or "; for (const packet_type t : expectedTypes) { appendPacketType(t); message.append(KJoinTypes); } message.resize(message.size() - (sizeof(KJoinTypes) - 1)); message.append(", got"); appendPacketType(actual); return PacketError(std::move(message)); } namespace { tl::expected CheckPacketTypeOneOf(std::initializer_list expectedTypes, std::uint8_t actualType) { if (c_none_of(expectedTypes, [actualType](uint8_t type) { return type == actualType; })) { return tl::make_unexpected(PacketTypeError(expectedTypes, actualType)); } return {}; } } // namespace const buffer_t &packet::Data() { assert(have_encrypted || have_decrypted); if (have_encrypted) return encrypted_buffer; return decrypted_buffer; } packet_type packet::Type() { assert(have_decrypted); return m_type; } plr_t packet::Source() const { assert(have_decrypted); return m_src; } plr_t packet::Destination() const { assert(have_decrypted); return m_dest; } tl::expected packet::Message() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_MESSAGE }, m_type) .transform([this]() { return &m_message; }); } tl::expected packet::Turn() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_TURN }, m_type) .transform([this]() { return m_turn; }); } tl::expected packet::Cookie() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_JOIN_REQUEST, PT_JOIN_ACCEPT }, m_type) .transform([this]() { return m_cookie; }); } tl::expected packet::NewPlayer() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_JOIN_ACCEPT, PT_CONNECT, PT_DISCONNECT }, m_type) .transform([this]() { return m_newplr; }); } tl::expected packet::Time() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_ECHO_REQUEST, PT_ECHO_REPLY }, m_type) .transform([this]() { return m_time; }); } tl::expected packet::Info() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_JOIN_REQUEST, PT_JOIN_ACCEPT, PT_CONNECT, PT_INFO_REPLY }, m_type) .transform([this]() { return &m_info; }); } tl::expected packet::LeaveInfo() { assert(have_decrypted); return CheckPacketTypeOneOf({ PT_DISCONNECT }, m_type) .transform([this]() { return m_leaveinfo; }); } tl::expected packet_in::Create(buffer_t buf) { assert(!have_encrypted && !have_decrypted); if (buf.size() < sizeof(packet_type) + 2 * sizeof(plr_t)) return tl::make_unexpected(PacketError()); decrypted_buffer = std::move(buf); have_decrypted = true; // TCP server implementation forwards the original data to clients // so although we are not decrypting anything, // we save a copy in encrypted_buffer anyway encrypted_buffer = decrypted_buffer; have_encrypted = true; return {}; } #ifdef PACKET_ENCRYPTION tl::expected packet_in::Decrypt(buffer_t buf) { assert(!have_encrypted && !have_decrypted); encrypted_buffer = std::move(buf); have_encrypted = true; if (encrypted_buffer.size() < crypto_secretbox_NONCEBYTES + crypto_secretbox_MACBYTES + sizeof(packet_type) + 2 * sizeof(plr_t)) return tl::make_unexpected(PacketError()); auto pktlen = (encrypted_buffer.size() - crypto_secretbox_NONCEBYTES - crypto_secretbox_MACBYTES); decrypted_buffer.resize(pktlen); const int status = crypto_secretbox_open_easy( decrypted_buffer.data(), encrypted_buffer.data() + crypto_secretbox_NONCEBYTES, encrypted_buffer.size() - crypto_secretbox_NONCEBYTES, encrypted_buffer.data(), key.data()); if (status != 0) { auto code = PacketError::ErrorCode::DecryptionFailed; std::string_view message = "Failed to decrypt packet data"; return tl::make_unexpected(PacketError(code, message)); } have_decrypted = true; return {}; } #endif #ifdef PACKET_ENCRYPTION tl::expected packet_out::Encrypt() { assert(have_decrypted); if (have_encrypted) return {}; auto lenCleartext = decrypted_buffer.size(); encrypted_buffer.insert(encrypted_buffer.begin(), crypto_secretbox_NONCEBYTES, 0); encrypted_buffer.insert(encrypted_buffer.end(), crypto_secretbox_MACBYTES + lenCleartext, 0); randombytes_buf(encrypted_buffer.data(), crypto_secretbox_NONCEBYTES); const int status = crypto_secretbox_easy( encrypted_buffer.data() + crypto_secretbox_NONCEBYTES, decrypted_buffer.data(), lenCleartext, encrypted_buffer.data(), key.data()); if (status != 0) { auto code = PacketError::ErrorCode::EncryptionFailed; std::string_view message = "Failed to encrypt packet data"; return tl::make_unexpected(PacketError(code, message)); } have_encrypted = true; return {}; } #endif packet_factory::packet_factory() { secure = false; } packet_factory::packet_factory(std::string pw) { secure = false; #ifdef PACKET_ENCRYPTION if (sodium_init() < 0) ABORT(); pw.resize(std::min(pw.size(), crypto_pwhash_argon2id_PASSWD_MAX)); pw.resize(std::max(pw.size(), crypto_pwhash_argon2id_PASSWD_MIN), 0); std::string salt("W9bE9dQgVaeybwr2"); salt.resize(crypto_pwhash_argon2id_SALTBYTES, 0); const int status = crypto_pwhash( key.data(), crypto_secretbox_KEYBYTES, pw.data(), pw.size(), reinterpret_cast(salt.data()), 3 * crypto_pwhash_argon2id_OPSLIMIT_MIN, 2 * crypto_pwhash_argon2id_MEMLIMIT_MIN, crypto_pwhash_ALG_ARGON2ID13); if (status != 0) ABORT(); secure = true; #endif } } // namespace devilution::net ================================================ FILE: Source/dvlnet/packet.h ================================================ #pragma once #include #include #include #include #include #include #include #ifdef PACKET_ENCRYPTION #include #endif #include "appfat.h" #include "dvlnet/abstract_net.h" #include "dvlnet/leaveinfo.hpp" #include "utils/attributes.h" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" #include "utils/str_cat.hpp" #include "utils/stubs.h" namespace devilution { namespace net { enum packet_type : uint8_t { // clang-format off PT_MESSAGE = 0x01, PT_TURN = 0x02, PT_JOIN_REQUEST = 0x11, PT_JOIN_ACCEPT = 0x12, PT_CONNECT = 0x13, PT_DISCONNECT = 0x14, PT_INFO_REQUEST = 0x21, PT_INFO_REPLY = 0x22, PT_ECHO_REQUEST = 0x31, PT_ECHO_REPLY = 0x32, // clang-format on }; // Returns NULL for an invalid packet type. const char *packet_type_to_string(uint8_t packetType); typedef uint8_t plr_t; typedef uint8_t seq_t; typedef uint32_t cookie_t; typedef uint32_t timestamp_t; #ifdef PACKET_ENCRYPTION typedef std::array key_t; #else // Stub out the key_t definition as we're not doing any encryption. using key_t = uint8_t; #endif struct turn_t { seq_t SequenceNumber; int32_t Value; }; static constexpr plr_t PLR_MASTER = 0xFE; static constexpr plr_t PLR_BROADCAST = 0xFF; class PacketError { public: enum class ErrorCode : uint8_t { None, EncryptionFailed, DecryptionFailed }; PacketError() : message_(std::string_view("Incorrect packet size")) , code_(ErrorCode::None) { } PacketError(const char message[]) : message_(std::string_view(message)) , code_(ErrorCode::None) { } PacketError(std::string &&message) : message_(std::move(message)) , code_(ErrorCode::None) { } PacketError(std::string_view message) : message_(message) , code_(ErrorCode::None) { } PacketError(ErrorCode code, const char message[]) : message_(std::string_view(message)) , code_(code) { } PacketError(ErrorCode code, std::string &&message) : message_(std::move(message)) , code_(code) { } PacketError(ErrorCode code, std::string_view message) : message_(message) , code_(code) { } PacketError(const PacketError &error) : message_(std::string(error.message_)) , code_(error.code_) { } PacketError(PacketError &&error) : message_(std::move(error.message_)) , code_(error.code_) { } std::string_view what() const { return message_; } ErrorCode code() const { return code_; } private: StringOrView message_; ErrorCode code_; }; inline PacketError IoHandlerError(std::string message) { return PacketError(std::move(message)); } PacketError PacketTypeError(std::uint8_t unknownPacketType); PacketError PacketTypeError(std::initializer_list expectedTypes, std::uint8_t actual); class packet { protected: packet_type m_type; plr_t m_src; plr_t m_dest; buffer_t m_message; turn_t m_turn; cookie_t m_cookie; plr_t m_newplr; timestamp_t m_time; buffer_t m_info; leaveinfo_t m_leaveinfo; const key_t &key; bool have_encrypted = false; bool have_decrypted = false; buffer_t encrypted_buffer; buffer_t decrypted_buffer; public: packet(const key_t &k) : key(k) {}; const buffer_t &Data(); packet_type Type(); plr_t Source() const; plr_t Destination() const; tl::expected Message(); tl::expected Turn(); tl::expected Cookie(); tl::expected NewPlayer(); tl::expected Time(); tl::expected Info(); tl::expected LeaveInfo(); }; template class packet_proc : public packet { public: using packet::packet; tl::expected process_data(); }; class packet_in : public packet_proc { public: using packet_proc::packet_proc; tl::expected Create(buffer_t buf); tl::expected process_element(buffer_t &x); template tl::expected process_element(T &x); tl::expected Decrypt(buffer_t buf); }; class packet_out : public packet_proc { public: using packet_proc::packet_proc; template void create(Args... args); tl::expected process_element(buffer_t &x); template tl::expected process_element(const T &x); static cookie_t GenerateCookie(); tl::expected Encrypt(); }; template tl::expected packet_proc

::process_data() { P &self = static_cast

(*this); { tl::expected result = self.process_element(m_type) .and_then([&]() { return self.process_element(m_src); }) .and_then([&]() { return self.process_element(m_dest); }); if (!result.has_value()) return result; } switch (m_type) { case PT_MESSAGE: return self.process_element(m_message); case PT_TURN: return self.process_element(m_turn.SequenceNumber) .and_then([&]() { return self.process_element(m_turn.Value); }); case PT_JOIN_REQUEST: return self.process_element(m_cookie) .and_then([&]() { return self.process_element(m_info); }); case PT_JOIN_ACCEPT: return self.process_element(m_cookie) .and_then([&]() { return self.process_element(m_newplr); }) .and_then([&]() { return self.process_element(m_info); }); case PT_CONNECT: return self.process_element(m_newplr) .and_then([&]() { return self.process_element(m_info); }); case PT_DISCONNECT: return self.process_element(m_newplr) .and_then([&]() { return self.process_element(m_leaveinfo); }); case PT_INFO_REPLY: return self.process_element(m_info); case PT_INFO_REQUEST: return {}; case PT_ECHO_REQUEST: case PT_ECHO_REPLY: return self.process_element(m_time); } return tl::make_unexpected(PacketTypeError(m_type)); } inline tl::expected packet_in::process_element(buffer_t &x) { x.insert(x.begin(), decrypted_buffer.begin(), decrypted_buffer.end()); decrypted_buffer.resize(0); return {}; } template tl::expected packet_in::process_element(T &x) { static_assert(std::is_integral::value || std::is_enum::value, "Unsupported T"); static_assert(sizeof(T) == 4 || sizeof(T) == 2 || sizeof(T) == 1, "Unsupported T"); if (decrypted_buffer.size() < sizeof(T)) { return tl::make_unexpected(PacketError()); } if (sizeof(T) == 4) { x = static_cast(LoadLE32(decrypted_buffer.data())); } else if (sizeof(T) == 2) { x = static_cast(LoadLE16(decrypted_buffer.data())); } else if (sizeof(T) == 1) { std::memcpy(&x, decrypted_buffer.data(), sizeof(T)); } decrypted_buffer.erase(decrypted_buffer.begin(), decrypted_buffer.begin() + sizeof(T)); return {}; } template <> inline void packet_out::create(plr_t s, plr_t d) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_INFO_REQUEST; m_src = s; m_dest = d; } template <> inline void packet_out::create(plr_t s, plr_t d, buffer_t i) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_INFO_REPLY; m_src = s; m_dest = d; m_info = std::move(i); } template <> inline void packet_out::create(plr_t s, plr_t d, buffer_t m) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_MESSAGE; m_src = s; m_dest = d; m_message = std::move(m); } template <> inline void packet_out::create(plr_t s, plr_t d, turn_t u) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_TURN; m_src = s; m_dest = d; m_turn = u; } template <> inline void packet_out::create(plr_t s, plr_t d, cookie_t c, buffer_t i) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_JOIN_REQUEST; m_src = s; m_dest = d; m_cookie = c; m_info = i; } template <> inline void packet_out::create(plr_t s, plr_t d, cookie_t c, plr_t n, buffer_t i) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_JOIN_ACCEPT; m_src = s; m_dest = d; m_cookie = c; m_newplr = n; m_info = i; } template <> inline void packet_out::create(plr_t s, plr_t d, plr_t n, buffer_t i) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_CONNECT; m_src = s; m_dest = d; m_newplr = n; m_info = i; } template <> inline void packet_out::create(plr_t s, plr_t d, plr_t n) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_CONNECT; m_src = s; m_dest = d; m_newplr = n; } template <> inline void packet_out::create(plr_t s, plr_t d, plr_t n, leaveinfo_t l) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_DISCONNECT; m_src = s; m_dest = d; m_newplr = n; m_leaveinfo = l; } template <> inline void packet_out::create(plr_t s, plr_t d, timestamp_t t) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_ECHO_REQUEST; m_src = s; m_dest = d; m_time = t; } template <> inline void packet_out::create(plr_t s, plr_t d, timestamp_t t) { if (have_encrypted || have_decrypted) ABORT(); have_decrypted = true; m_type = PT_ECHO_REPLY; m_src = s; m_dest = d; m_time = t; } inline tl::expected packet_out::process_element(buffer_t &x) { decrypted_buffer.insert(decrypted_buffer.end(), x.begin(), x.end()); return {}; } template tl::expected packet_out::process_element(const T &x) { static_assert(std::is_integral::value || std::is_enum::value, "Unsupported T"); static_assert(sizeof(T) == 4 || sizeof(T) == 2 || sizeof(T) == 1, "Unsupported T"); if (sizeof(T) == 4) { unsigned char buf[4]; if constexpr (std::is_enum::value) { WriteLE32(buf, static_cast>(x)); } else { WriteLE32(buf, x); } decrypted_buffer.insert(decrypted_buffer.end(), buf, buf + 4); } else if (sizeof(T) == 2) { unsigned char buf[2]; if constexpr (std::is_enum::value) { WriteLE16(buf, static_cast>(x)); } else { WriteLE16(buf, x); } decrypted_buffer.insert(decrypted_buffer.end(), buf, buf + 2); } else if (sizeof(T) == 1) { decrypted_buffer.push_back(static_cast(x)); } return {}; } class packet_factory { key_t key = {}; bool secure; public: static constexpr unsigned short max_packet_size = 0xFFFF; packet_factory(); packet_factory(std::string pw); tl::expected, PacketError> make_packet(buffer_t buf); template tl::expected, PacketError> make_packet(Args... args); }; inline tl::expected, PacketError> packet_factory::make_packet(buffer_t buf) { auto ret = std::make_unique(key); #ifndef PACKET_ENCRYPTION tl::expected isCreated = ret->Create(std::move(buf)); #else tl::expected isCreated = !secure ? ret->Create(std::move(buf)) : ret->Decrypt(std::move(buf)); #endif if (!isCreated.has_value()) { return tl::make_unexpected(isCreated.error()); } if (const tl::expected result = ret->process_data(); !result.has_value()) { return tl::make_unexpected(result.error()); } return ret; } template tl::expected, PacketError> packet_factory::make_packet(Args... args) { auto ret = std::make_unique(key); ret->create(args...); if (const tl::expected result = ret->process_data(); !result.has_value()) { return tl::make_unexpected(result.error()); } #ifdef PACKET_ENCRYPTION if (secure) { tl::expected isEncrypted = ret->Encrypt(); if (!isEncrypted.has_value()) { return tl::make_unexpected(isEncrypted.error()); } } #endif return ret; } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/protocol_zt.cpp ================================================ #include "dvlnet/protocol_zt.h" #include #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif #include #include #include #include #include #include "dvlnet/zerotier_native.h" #include "utils/log.hpp" namespace devilution { namespace net { namespace { bool GetMAC(const protocol_zt::endpoint &peer, uint64_t &mac) { ip6_addr_t address = {}; IP6_ADDR_PART(&address, 0, peer.addr[0], peer.addr[1], peer.addr[2], peer.addr[3]); IP6_ADDR_PART(&address, 1, peer.addr[4], peer.addr[5], peer.addr[6], peer.addr[7]); IP6_ADDR_PART(&address, 2, peer.addr[8], peer.addr[9], peer.addr[10], peer.addr[11]); IP6_ADDR_PART(&address, 3, peer.addr[12], peer.addr[13], peer.addr[14], peer.addr[15]); const u8_t *hwaddr; if (nd6_get_next_hop_addr_or_queue(netif_default, nullptr, &address, &hwaddr) != ERR_OK) return false; mac = hwaddr[0]; mac = (mac << 8) | hwaddr[1]; mac = (mac << 8) | hwaddr[2]; mac = (mac << 8) | hwaddr[3]; mac = (mac << 8) | hwaddr[4]; mac = (mac << 8) | hwaddr[5]; return true; } } // namespace protocol_zt::protocol_zt() { zerotier_network_start(); } void protocol_zt::set_nonblock(int fd) { static_assert(O_NONBLOCK == 1, "O_NONBLOCK == 1 not satisfied"); auto mode = lwip_fcntl(fd, F_GETFL, 0); mode |= O_NONBLOCK; lwip_fcntl(fd, F_SETFL, mode); } void protocol_zt::set_nodelay(int fd) { const int yes = 1; lwip_setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void *)&yes, sizeof(yes)); } void protocol_zt::set_reuseaddr(int fd) { const int yes = 1; lwip_setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void *)&yes, sizeof(yes)); } tl::expected protocol_zt::network_online() { if (!zerotier_network_ready()) return false; struct sockaddr_in6 in6 { }; in6.sin6_port = htons(default_port); in6.sin6_family = AF_INET6; in6.sin6_addr = in6addr_any; if (fd_udp == -1) { fd_udp = lwip_socket(AF_INET6, SOCK_DGRAM, 0); set_reuseaddr(fd_udp); auto ret = lwip_bind(fd_udp, (struct sockaddr *)&in6, sizeof(in6)); if (ret < 0) { std::string_view format = "Error binding to ZeroTier UDP socket: {}"; PacketError error = ProtocolError(format, strerror(errno)); return tl::make_unexpected(std::move(error)); } set_nonblock(fd_udp); } if (fd_tcp == -1) { fd_tcp = lwip_socket(AF_INET6, SOCK_STREAM, 0); set_reuseaddr(fd_tcp); auto r1 = lwip_bind(fd_tcp, (struct sockaddr *)&in6, sizeof(in6)); if (r1 < 0) { std::string_view format = "Error binding to ZeroTier TCP socket: {}"; PacketError error = ProtocolError(format, strerror(errno)); return tl::make_unexpected(std::move(error)); } auto r2 = lwip_listen(fd_tcp, 10); if (r2 < 0) { std::string_view format = "Error listening on ZeroTier TCP socket: {}"; PacketError error = ProtocolError(format, strerror(errno)); return tl::make_unexpected(std::move(error)); } set_nonblock(fd_tcp); set_nodelay(fd_tcp); } return true; } tl::expected protocol_zt::peers_ready() { return network_online() .map([&](bool isOnline) { return isOnline && zerotier_peers_ready(); }); } tl::expected protocol_zt::send(const endpoint &peer, const buffer_t &data) { tl::expected frame = frame_queue::MakeFrame(data); if (!frame.has_value()) return tl::make_unexpected(frame.error()); peer_list[peer].send_queue.push_back(*frame); return {}; } bool protocol_zt::send_oob(const endpoint &peer, const buffer_t &data) const { struct sockaddr_in6 in6 { }; in6.sin6_port = htons(default_port); in6.sin6_family = AF_INET6; std::copy(peer.addr.begin(), peer.addr.end(), in6.sin6_addr.s6_addr); lwip_sendto(fd_udp, data.data(), data.size(), 0, (const struct sockaddr *)&in6, sizeof(in6)); return true; } bool protocol_zt::send_oob_mc(const buffer_t &data) const { endpoint mc; std::copy(dvl_multicast_addr, dvl_multicast_addr + 16, mc.addr.begin()); return send_oob(mc, data); } tl::expected protocol_zt::send_queued_peer(const endpoint &peer) { peer_state &state = peer_list[peer]; if (state.fd == -1) { state.fd = lwip_socket(AF_INET6, SOCK_STREAM, 0); set_nodelay(state.fd); set_nonblock(state.fd); struct sockaddr_in6 in6 { }; in6.sin6_port = htons(default_port); in6.sin6_family = AF_INET6; std::copy(peer.addr.begin(), peer.addr.end(), in6.sin6_addr.s6_addr); lwip_connect(state.fd, (const struct sockaddr *)&in6, sizeof(in6)); } while (!state.send_queue.empty()) { auto len = state.send_queue.front().size(); auto r = lwip_send(state.fd, state.send_queue.front().data(), len, 0); if (r < 0) { // handle error return false; } if (decltype(len)(r) < len) { // partial send auto it = state.send_queue.front().begin(); state.send_queue.front().erase(it, it + r); return true; } if (decltype(len)(r) == len) { state.send_queue.pop_front(); } else { std::string_view format = "Impossible number of bytes sent: {} available, {} sent"; PacketError error = ProtocolError(format, len, decltype(len)(r)); return tl::make_unexpected(std::move(error)); } } return true; } bool protocol_zt::recv_peer(const endpoint &peer) { unsigned char buf[PKTBUF_LEN]; peer_state &state = peer_list[peer]; while (true) { auto len = lwip_recv(state.fd, buf, sizeof(buf), 0); if (len >= 0) { state.recv_queue.Write(buffer_t(buf, buf + len)); } else { return errno == EAGAIN || errno == EWOULDBLOCK; } } } bool protocol_zt::send_queued_all() { for (const auto &[endpoint, _] : peer_list) { tl::expected result = send_queued_peer(endpoint); if (!result.has_value()) { LogError("send_queued_peer: {}", result.error().what()); continue; } if (!*result) { // handle error? } } return true; } bool protocol_zt::recv_from_peers() { for (const auto &[endpoint, state] : peer_list) { if (state.fd != -1) { if (!recv_peer(endpoint)) { disconnect_queue.push_back(endpoint); } } } return true; } bool protocol_zt::recv_from_udp() { unsigned char buf[PKTBUF_LEN]; struct sockaddr_in6 in6 { }; socklen_t addrlen = sizeof(in6); auto len = lwip_recvfrom(fd_udp, buf, sizeof(buf), 0, (struct sockaddr *)&in6, &addrlen); if (len < 0) return false; buffer_t data(buf, buf + len); endpoint ep; std::copy(in6.sin6_addr.s6_addr, in6.sin6_addr.s6_addr + 16, ep.addr.begin()); oob_recv_queue.emplace_back(ep, std::move(data)); return true; } bool protocol_zt::accept_all() { struct sockaddr_in6 in6 { }; socklen_t addrlen = sizeof(in6); while (true) { auto newfd = lwip_accept(fd_tcp, (struct sockaddr *)&in6, &addrlen); if (newfd < 0) break; endpoint ep; std::copy(in6.sin6_addr.s6_addr, in6.sin6_addr.s6_addr + 16, ep.addr.begin()); peer_state &state = peer_list[ep]; if (state.fd != -1) { Log("protocol_zt::accept_all: WARNING: overwriting connection"); SDL_SetError("protocol_zt::accept_all: WARNING: overwriting connection"); lwip_close(state.fd); } set_nonblock(newfd); set_nodelay(newfd); state.fd = newfd; } return true; } bool protocol_zt::recv(endpoint &peer, buffer_t &data) { accept_all(); send_queued_all(); recv_from_peers(); recv_from_udp(); if (!oob_recv_queue.empty()) { peer = oob_recv_queue.front().first; data = oob_recv_queue.front().second; oob_recv_queue.pop_front(); return true; } for (auto &p : peer_list) { tl::expected ready = p.second.recv_queue.PacketReady(); if (!ready.has_value()) { LogError("PacketReady: {}", ready.error().what()); continue; } if (!*ready) continue; tl::expected packet = p.second.recv_queue.ReadPacket(); if (!packet.has_value()) { LogError("Failed reading packet data from peer: {}", packet.error().what()); continue; } peer = p.first; data = *packet; return true; } return false; } bool protocol_zt::get_disconnected(endpoint &peer) { if (!disconnect_queue.empty()) { peer = disconnect_queue.front(); disconnect_queue.pop_front(); return true; } return false; } void protocol_zt::disconnect(const endpoint &peer) { const auto it = peer_list.find(peer); if (it != peer_list.end()) { if (it->second.fd != -1) { if (lwip_close(it->second.fd) < 0) { Log("lwip_close: {}", strerror(errno)); SDL_SetError("lwip_close: %s", strerror(errno)); } } peer_list.erase(it); } } void protocol_zt::close_all() { if (fd_tcp != -1) { lwip_close(fd_tcp); fd_tcp = -1; } if (fd_udp != -1) { lwip_close(fd_udp); fd_udp = -1; } for (auto &[_, state] : peer_list) { if (state.fd != -1) lwip_close(state.fd); } peer_list.clear(); } protocol_zt::~protocol_zt() { close_all(); } void protocol_zt::endpoint::from_string(const std::string &str) { ip_addr_t a; if (ipaddr_aton(str.c_str(), &a) == 0) return; if (!IP_IS_V6_VAL(a)) return; const auto *r = reinterpret_cast(a.u_addr.ip6.addr); std::copy(r, r + 16, addr.begin()); } uint64_t protocol_zt::current_ms() { return 0; } bool protocol_zt::is_peer_connected(endpoint &peer) { const auto it = peer_list.find(peer); return it != peer_list.end() && it->second.fd != -1; } std::optional protocol_zt::is_peer_relayed(const endpoint &peer) const { uint64_t mac; if (!GetMAC(peer, mac)) return std::nullopt; return zerotier_is_relayed(mac); } std::optional protocol_zt::get_latency_to(const endpoint &peer) const { uint64_t mac; if (!GetMAC(peer, mac)) return std::nullopt; return zerotier_latency(mac); } std::string protocol_zt::make_default_gamename() { std::string ret; std::string allowedChars = "abcdefghkopqrstuvwxyz"; std::random_device rd; std::uniform_int_distribution dist(0, allowedChars.size() - 1); for (size_t i = 0; i < 5; ++i) { ret += allowedChars.at(dist(rd)); } return ret; } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/protocol_zt.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include "dvlnet/frame_queue.h" #include "dvlnet/packet.h" #include "utils/log.hpp" namespace devilution { namespace net { template PacketError ProtocolError(std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); return PacketError(str); } class protocol_zt { public: class endpoint { public: std::array addr = {}; explicit operator bool() const { auto empty = std::array {}; return (addr != empty); } bool operator==(const endpoint &rhs) const { return addr == rhs.addr; } bool operator!=(const endpoint &rhs) const { return !(*this == rhs); } bool operator<(const endpoint &rhs) const { return addr < rhs.addr; } buffer_t serialize() const { return buffer_t(addr.begin(), addr.end()); } tl::expected unserialize(const buffer_t &buf) { if (buf.size() != 16) { std::string_view format = "Endpoint deserialization expected 16 bytes, got {}"; PacketError error = ProtocolError(format, buf.size()); return tl::make_unexpected(std::move(error)); } std::copy(buf.begin(), buf.end(), addr.begin()); return {}; } void from_string(const std::string &str); }; struct EndpointHash { using is_avalanching = void; [[nodiscard]] uint64_t operator()(const endpoint &e) const noexcept { return ankerl::unordered_dense::hash {}( std::string_view { reinterpret_cast(e.addr.data()), e.addr.size() }); } }; protocol_zt(); ~protocol_zt(); void disconnect(const endpoint &peer); tl::expected send(const endpoint &peer, const buffer_t &data); bool send_oob(const endpoint &peer, const buffer_t &data) const; bool send_oob_mc(const buffer_t &data) const; bool recv(endpoint &peer, buffer_t &data); bool get_disconnected(endpoint &peer); tl::expected network_online(); tl::expected peers_ready(); bool is_peer_connected(endpoint &peer); std::optional is_peer_relayed(const endpoint &peer) const; std::optional get_latency_to(const endpoint &peer) const; static std::string make_default_gamename(); private: static constexpr uint32_t PKTBUF_LEN = 65536; static constexpr uint16_t default_port = 6112; struct peer_state { int fd = -1; std::deque send_queue; frame_queue recv_queue; }; std::deque> oob_recv_queue; std::deque disconnect_queue; ankerl::unordered_dense::map peer_list; int fd_tcp = -1; int fd_udp = -1; static uint64_t current_ms(); void close_all(); static void set_nonblock(int fd); static void set_nodelay(int fd); static void set_reuseaddr(int fd); tl::expected send_queued_peer(const endpoint &peer); bool recv_peer(const endpoint &peer); bool send_queued_all(); bool recv_from_peers(); bool recv_from_udp(); bool accept_all(); }; } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/tcp_client.cpp ================================================ #include "dvlnet/tcp_client.h" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include #include "options.h" #include "utils/language.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution::net { int tcp_client::create(std::string_view addrstr) { auto port = *GetOptions().Network.port; local_server = std::make_unique(ioc, std::string(addrstr), port, *pktfty); return join(local_server->LocalhostSelf()); } int tcp_client::join(std::string_view addrstr) { constexpr int MsSleep = 10; constexpr int NoSleep = 250; const char *defaultPort = "6112"; std::string_view host; std::string_view port = defaultPort; if (!addrstr.empty() && addrstr[0] == '[') { // Assume IPv6 address in square brackets, followed by port // Example: [::1]:6113 size_t pos = addrstr.find(']', 1); pos = pos != std::string::npos ? pos + 1 : addrstr.length(); host = addrstr.substr(0, pos); if (pos != addrstr.length()) { if (addrstr[pos] != ':') { SDL_SetError("Invalid hostname: expected colon after square brackets"); return -1; } if (++pos != addrstr.length()) port = addrstr.substr(pos); } } else { // Assume "hostname:port" const SplitByChar splithost(addrstr, ':'); auto it = splithost.begin(); if (it != splithost.end()) host = *it++; if (it != splithost.end()) port = *it++; // If there is more than one colon, assume it's just a plain IPv6 address if (it != splithost.end()) { host = addrstr; port = defaultPort; } } asio::error_code errorCode; const asio::ip::basic_resolver_results range = resolver.resolve(host, port, errorCode); if (errorCode) { SDL_SetError("%s", errorCode.message().c_str()); return -1; } asio::connect(sock, range, errorCode); if (errorCode) { SDL_SetError("%s", errorCode.message().c_str()); return -1; } const asio::ip::tcp::no_delay option(true); sock.set_option(option, errorCode); if (errorCode) LogError("Client error setting socket option: {}", errorCode.message()); StartReceive(); { cookie_self = packet_out::GenerateCookie(); tl::expected, PacketError> pkt = pktfty->make_packet( PLR_BROADCAST, PLR_MASTER, cookie_self, game_init_info); if (!pkt.has_value()) { const std::string_view message = pkt.error().what(); SDL_SetError("make_packet: %.*s", static_cast(message.size()), message.data()); return -1; } tl::expected sendResult = send(**pkt); if (!sendResult.has_value()) { const std::string_view message = sendResult.error().what(); SDL_SetError("send: %.*s", static_cast(message.size()), message.data()); return -1; } for (auto i = 0; i < NoSleep; ++i) { tl::expected pollResult = poll(); if (!pollResult.has_value()) { const std::string_view message = pollResult.error().what(); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } if (plr_self != PLR_BROADCAST) break; // join successful SDL_Delay(MsSleep); } } if (plr_self == PLR_BROADCAST) { const std::string_view message = _("Unable to connect"); SDL_SetError("%.*s", static_cast(message.size()), message.data()); return -1; } return plr_self; } bool tcp_client::IsGameHost() { return local_server != nullptr; } tl::expected tcp_client::poll() { while (ioc.poll_one() > 0) { if (IsGameHost()) { tl::expected serverResult = local_server->CheckIoHandlerError(); if (!serverResult.has_value()) return serverResult; } if (ioHandlerResult == std::nullopt) continue; tl::expected packetError = tl::make_unexpected(*ioHandlerResult); ioHandlerResult = std::nullopt; return packetError; } return {}; } void tcp_client::HandleReceive(const asio::error_code &error, size_t bytesRead) { if (error) { const PacketError packetError = IoHandlerError(error.message()); RaiseIoHandlerError(packetError); return; } if (bytesRead == 0) { const PacketError packetError(_("error: read 0 bytes from server")); RaiseIoHandlerError(packetError); return; } recv_buffer.resize(bytesRead); recv_queue.Write(std::move(recv_buffer)); recv_buffer.resize(frame_queue::max_frame_size); while (true) { tl::expected ready = recv_queue.PacketReady(); if (!ready.has_value()) { RaiseIoHandlerError(ready.error()); return; } if (!*ready) break; if (recv_queue.ReadPacketFlags() == TcpErrorCodeFlags) { HandleTcpErrorCode(); return; } tl::expected result = recv_queue.ReadPacket() .and_then([this](buffer_t &&pktData) { return pktfty->make_packet(pktData); }) .and_then([this](std::unique_ptr &&pkt) { return RecvLocal(*pkt); }); if (!result.has_value()) { RaiseIoHandlerError(result.error()); return; } } StartReceive(); } void tcp_client::StartReceive() { sock.async_receive( asio::buffer(recv_buffer), std::bind(&tcp_client::HandleReceive, this, std::placeholders::_1, std::placeholders::_2)); } void tcp_client::HandleSend(const asio::error_code &error, size_t bytesSent) { if (error) RaiseIoHandlerError(error.message()); } void tcp_client::HandleTcpErrorCode() { tl::expected packet = recv_queue.ReadPacket(); if (!packet.has_value()) { RaiseIoHandlerError(packet.error()); return; } buffer_t pktData = *packet; if (pktData.size() != 1) { RaiseIoHandlerError(PacketError()); return; } PacketError::ErrorCode code = static_cast(pktData[0]); if (code == PacketError::ErrorCode::DecryptionFailed) RaiseIoHandlerError(_("Server failed to decrypt your packet. Check if you typed the password correctly.")); else RaiseIoHandlerError(fmt::format("Unknown error code received from server: {:#04x}", pktData[0])); } tl::expected tcp_client::send(packet &pkt) { tl::expected frame = frame_queue::MakeFrame(pkt.Data()); if (!frame.has_value()) return tl::make_unexpected(frame.error()); std::unique_ptr framePtr = std::make_unique(*frame); const asio::mutable_buffer buf = asio::buffer(*framePtr); asio::async_write(sock, buf, [this, frame = std::move(framePtr)](const asio::error_code &error, size_t bytesSent) { HandleSend(error, bytesSent); }); return {}; } void tcp_client::DisconnectNet(plr_t plr) { if (local_server != nullptr) local_server->DisconnectNet(plr); } bool tcp_client::SNetLeaveGame(net::leaveinfo_t type) { auto ret = base::SNetLeaveGame(type); process_network_packets(); if (local_server != nullptr) local_server->Close(); sock.close(); return ret; } std::string tcp_client::make_default_gamename() { return std::string(GetOptions().Network.szBindAddress); } void tcp_client::RaiseIoHandlerError(const PacketError &error) { ioHandlerResult.emplace(error); } tcp_client::~tcp_client() = default; } // namespace devilution::net ================================================ FILE: Source/dvlnet/tcp_client.h ================================================ #pragma once #include #include // This header must be included before any 3DS code // because 3DS SDK defines a macro with the same name // as an fmt template parameter in some versions of fmt. // See https://github.com/fmtlib/fmt/issues/3632 // // 3DS uses some custom ASIO code that transitively includes // the 3DS SDK. #include #include #include #include #include #include #include "dvlnet/base.h" #include "dvlnet/frame_queue.h" #include "dvlnet/packet.h" #include "dvlnet/tcp_server.h" namespace devilution::net { class tcp_client : public base { public: int create(std::string_view addrstr) override; int join(std::string_view addrstr) override; tl::expected poll() override; tl::expected send(packet &pkt) override; void DisconnectNet(plr_t plr) override; bool SNetLeaveGame(net::leaveinfo_t type) override; ~tcp_client() override; std::string make_default_gamename() override; protected: bool IsGameHost() override; private: frame_queue recv_queue; buffer_t recv_buffer = buffer_t(frame_queue::max_frame_size); asio::io_context ioc; asio::ip::tcp::resolver resolver = asio::ip::tcp::resolver(ioc); asio::ip::tcp::socket sock = asio::ip::tcp::socket(ioc); std::unique_ptr local_server; // must be declared *after* ioc std::optional ioHandlerResult; void HandleReceive(const asio::error_code &error, size_t bytesRead); void StartReceive(); void HandleSend(const asio::error_code &error, size_t bytesSent); void HandleTcpErrorCode(); void RaiseIoHandlerError(const PacketError &error); }; } // namespace devilution::net ================================================ FILE: Source/dvlnet/tcp_server.cpp ================================================ #include "dvlnet/tcp_server.h" #include #include #include #include #include #include "dvlnet/base.h" #include "player.h" #include "utils/log.hpp" namespace devilution::net { tcp_server::tcp_server(asio::io_context &ioc, const std::string &bindaddr, unsigned short port, packet_factory &pktfty) : ioc(ioc) , pktfty(pktfty) { auto addr = asio::ip::make_address(bindaddr); auto ep = asio::ip::tcp::endpoint(addr, port); acceptor = std::make_unique(ioc, ep, true); StartAccept(); } std::string tcp_server::LocalhostSelf() { auto addr = acceptor->local_endpoint().address(); if (addr.is_unspecified()) { if (addr.is_v4()) { return asio::ip::address_v4::loopback().to_string(); } if (addr.is_v6()) { return asio::ip::address_v6::loopback().to_string(); } ABORT(); } return addr.to_string(); } tcp_server::scc tcp_server::MakeConnection() { return std::make_shared(ioc); } plr_t tcp_server::NextFree() { for (plr_t i = 0; i < Players.size(); ++i) if (!connections[i]) return i; return PLR_BROADCAST; } bool tcp_server::Empty() { for (plr_t i = 0; i < Players.size(); ++i) if (connections[i]) return false; return true; } void tcp_server::StartReceive(const scc &con) { con->socket.async_receive( asio::buffer(con->recv_buffer), std::bind(&tcp_server::HandleReceive, this, con, std::placeholders::_1, std::placeholders::_2)); } void tcp_server::HandleReceive(const scc &con, const asio::error_code &ec, size_t bytesRead) { if (ec || bytesRead == 0) { DropConnection(con); return; } con->recv_buffer.resize(bytesRead); con->recv_queue.Write(std::move(con->recv_buffer)); con->recv_buffer.resize(frame_queue::max_frame_size); while (true) { tl::expected ready = con->recv_queue.PacketReady(); if (!ready.has_value()) { Log("PacketReady: {}", ready.error().what()); DropConnection(con); return; } if (!*ready) break; tl::expected pktData = con->recv_queue.ReadPacket(); if (!pktData.has_value()) { Log("ReadPacket: {}", pktData.error().what()); DropConnection(con); return; } tl::expected, PacketError> pkt = pktfty.make_packet(*pktData); if (!pkt.has_value()) { Log("make_packet: {}", pkt.error().what()); if (pkt.error().code() == PacketError::ErrorCode::DecryptionFailed) StartSend(con, pkt.error().code()); DropConnection(con); return; } if (con->plr == PLR_BROADCAST) { tl::expected result = HandleReceiveNewPlayer(con, **pkt); if (!result.has_value()) { Log("HandleReceiveNewPlayer: {}", result.error().what()); DropConnection(con); return; } } else { con->timeout = timeout_active; tl::expected result = HandleReceivePacket(**pkt); if (!result.has_value()) { Log("Network error: {}", result.error().what()); DropConnection(con); return; } } } StartReceive(con); } tl::expected tcp_server::HandleReceiveNewPlayer(const scc &con, packet &inPkt) { auto newplr = NextFree(); if (newplr == PLR_BROADCAST) return tl::make_unexpected(ServerError()); if (Empty()) { tl::expected pktInfo = inPkt.Info(); if (!pktInfo.has_value()) return tl::make_unexpected(pktInfo.error()); game_init_info = **pktInfo; } for (plr_t player = 0; player < Players.size(); player++) { if (connections[player]) { tl::expected result = pktfty.make_packet(PLR_MASTER, PLR_BROADCAST, newplr) .and_then([&](std::unique_ptr &&pkt) { return StartSend(connections[player], *pkt); }) .and_then([&]() { return pktfty.make_packet(PLR_MASTER, PLR_BROADCAST, player); }) .and_then([&](std::unique_ptr &&pkt) { return StartSend(con, *pkt); }); if (!result.has_value()) return result; } } tl::expected result = inPkt.Cookie() .and_then([&](cookie_t &&cookie) { return pktfty.make_packet(PLR_MASTER, PLR_BROADCAST, cookie, newplr, game_init_info); }) .and_then([&](std::unique_ptr &&pkt) { return StartSend(con, *pkt); }); if (!result.has_value()) return result; con->plr = newplr; connections[newplr] = con; con->timeout = timeout_active; return {}; } tl::expected tcp_server::HandleReceivePacket(packet &pkt) { return SendPacket(pkt); } tl::expected tcp_server::SendPacket(packet &pkt) { if (pkt.Destination() == PLR_BROADCAST) { for (size_t i = 0; i < Players.size(); ++i) { if (i == pkt.Source() || !connections[i]) continue; tl::expected result = StartSend(connections[i], pkt); if (!result.has_value()) LogError("Failed to send packet {} to player {}: {}", static_cast(pkt.Type()), i, result.error().what()); } return {}; } if (pkt.Destination() >= MAX_PLRS) return tl::make_unexpected(ServerError()); if (pkt.Destination() == pkt.Source() || !connections[pkt.Destination()]) return {}; return StartSend(connections[pkt.Destination()], pkt); } tl::expected tcp_server::StartSend(const scc &con, packet &pkt) { return StartSend(con, pkt.Data(), 0); } tl::expected tcp_server::StartSend(const scc &con, PacketError::ErrorCode errorCode) { buffer_t pktData; pktData.push_back(static_cast(errorCode)); return StartSend(con, pktData, TcpErrorCodeFlags); } tl::expected tcp_server::StartSend(const scc &con, buffer_t pktData, uint16_t flags) { tl::expected frame = frame_queue::MakeFrame(pktData, flags); if (!frame.has_value()) return tl::make_unexpected(frame.error()); std::unique_ptr framePtr = std::make_unique(*frame); const asio::mutable_buffer buf = asio::buffer(*framePtr); asio::async_write(con->socket, buf, [this, con, frame = std::move(framePtr)](const asio::error_code &ec, size_t bytesSent) { HandleSend(con, ec, bytesSent); }); return {}; } void tcp_server::HandleSend(const scc &con, const asio::error_code &ec, size_t bytesSent) { if (ec) { Log("Network error: {}", ec.message()); DropConnection(con); } } void tcp_server::StartAccept() { auto nextcon = MakeConnection(); acceptor->async_accept( nextcon->socket, std::bind(&tcp_server::HandleAccept, this, nextcon, std::placeholders::_1)); } void tcp_server::HandleAccept(const scc &con, const asio::error_code &ec) { if (ec) { const PacketError packetError = IoHandlerError(ec.message()); RaiseIoHandlerError(packetError); return; } if (NextFree() == PLR_BROADCAST) { DropConnection(con); } else { asio::error_code errorCode; const asio::ip::tcp::no_delay option(true); con->socket.set_option(option, errorCode); if (errorCode) LogError("Server error setting socket option: {}", errorCode.message()); con->timeout = timeout_connect; StartReceive(con); StartTimeout(con); } StartAccept(); } void tcp_server::StartTimeout(const scc &con) { con->timer.expires_after(std::chrono::seconds(1)); con->timer.async_wait( std::bind(&tcp_server::HandleTimeout, this, con, std::placeholders::_1)); } void tcp_server::HandleTimeout(const scc &con, const asio::error_code &ec) { if (ec) { DropConnection(con); return; } if (con->timeout > 0) con->timeout -= 1; if (con->timeout <= 0) { con->timeout = 0; DropConnection(con); return; } StartTimeout(con); } void tcp_server::DropConnection(const scc &con) { const plr_t plr = con->plr; con->timer.cancel(); con->socket.close(); if (plr == PLR_BROADCAST) { return; } connections[plr] = nullptr; tl::expected, PacketError> pkt = pktfty.make_packet(PLR_MASTER, PLR_BROADCAST, plr, leaveinfo_t::LEAVE_DROP); if (pkt.has_value()) { SendPacket(**pkt); } else { LogError("make_packet: {}", pkt.error().what()); } } void tcp_server::RaiseIoHandlerError(const PacketError &error) { ioHandlerResult.emplace(error); } tl::expected tcp_server::CheckIoHandlerError() { if (ioHandlerResult == std::nullopt) return {}; tl::expected packetError = tl::make_unexpected(*ioHandlerResult); ioHandlerResult = std::nullopt; return packetError; } void tcp_server::DisconnectNet(plr_t plr) { scc &con = connections[plr]; if (con == nullptr) return; con->timer.cancel(); con->socket.close(); con = nullptr; } void tcp_server::Close() { acceptor->close(); } tcp_server::~tcp_server() = default; } // namespace devilution::net ================================================ FILE: Source/dvlnet/tcp_server.h ================================================ #pragma once #include #include #include // This header must be included before any 3DS code // because 3DS SDK defines a macro with the same name // as an fmt template parameter in some versions of fmt. // See https://github.com/fmtlib/fmt/issues/3632 // // 3DS uses some custom ASIO code that transitively includes // the 3DS SDK. #include #include #include #include #include #include #include #include "dvlnet/abstract_net.h" #include "dvlnet/frame_queue.h" #include "dvlnet/packet.h" #include "multi.h" namespace devilution::net { constexpr uint16_t TcpErrorCodeFlags = 0x8000; inline PacketError ServerError() { return PacketError("Invalid player ID"); } class tcp_server { public: tcp_server(asio::io_context &ioc, const std::string &bindaddr, unsigned short port, packet_factory &pktfty); std::string LocalhostSelf(); tl::expected CheckIoHandlerError(); void DisconnectNet(plr_t plr); void Close(); virtual ~tcp_server(); private: static constexpr int timeout_connect = 30; static constexpr int timeout_active = 60; struct client_connection { frame_queue recv_queue; buffer_t recv_buffer = buffer_t(frame_queue::max_frame_size); plr_t plr = PLR_BROADCAST; asio::ip::tcp::socket socket; asio::steady_timer timer; int timeout; client_connection(asio::io_context &ioc) : socket(ioc) , timer(ioc) { } }; typedef std::shared_ptr scc; asio::io_context &ioc; packet_factory &pktfty; std::unique_ptr acceptor; std::array connections; buffer_t game_init_info; std::optional ioHandlerResult; scc MakeConnection(); plr_t NextFree(); bool Empty(); void StartAccept(); void HandleAccept(const scc &con, const asio::error_code &ec); void StartReceive(const scc &con); void HandleReceive(const scc &con, const asio::error_code &ec, size_t bytesRead); tl::expected HandleReceiveNewPlayer(const scc &con, packet &pkt); tl::expected HandleReceivePacket(packet &pkt); tl::expected SendPacket(packet &pkt); tl::expected StartSend(const scc &con, packet &pkt); tl::expected StartSend(const scc &con, PacketError::ErrorCode errorCode); tl::expected StartSend(const scc &con, buffer_t pktData, uint16_t flags); void HandleSend(const scc &con, const asio::error_code &ec, size_t bytesSent); void StartTimeout(const scc &con); void HandleTimeout(const scc &con, const asio::error_code &ec); void DropConnection(const scc &con); void RaiseIoHandlerError(const PacketError &error); }; } // namespace devilution::net ================================================ FILE: Source/dvlnet/zerotier_lwip.cpp ================================================ #include "dvlnet/zerotier_lwip.h" #include #include #include #include #include "dvlnet/zerotier_native.h" #include "utils/log.hpp" namespace devilution { namespace net { void print_ip6_addr(void *x) { char ipstr[INET6_ADDRSTRLEN]; auto *in = static_cast(x); lwip_inet_ntop(AF_INET6, &(in->sin6_addr), ipstr, INET6_ADDRSTRLEN); Log("ZeroTier: ZTS_EVENT_ADDR_NEW_IP6, addr={}", ipstr); } void zt_ip6setup() { ip6_addr_t mcaddr; memcpy(mcaddr.addr, dvl_multicast_addr, 16); mcaddr.zone = 0; LOCK_TCPIP_CORE(); mld6_joingroup(IP6_ADDR_ANY6, &mcaddr); UNLOCK_TCPIP_CORE(); } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/zerotier_lwip.h ================================================ #pragma once namespace devilution { namespace net { void print_ip6_addr(void *x); void zt_ip6setup(); } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/zerotier_native.cpp ================================================ #include "dvlnet/zerotier_native.h" #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif #include #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include "utils/stdcompat/filesystem.hpp" #ifdef DVL_HAS_FILESYSTEM #define DVL_ZT_SYMLINK #endif #endif #ifdef DVL_ZT_SYMLINK #include #ifdef PACKET_ENCRYPTION #include #endif #include "utils/str_cat.hpp" #include "utils/utf8.hpp" #endif #include #include #include "utils/algorithm/container.hpp" #include "utils/log.hpp" #include "utils/paths.h" #include "dvlnet/zerotier_lwip.h" namespace devilution { namespace net { namespace { // static constexpr uint64_t zt_earth = 0x8056c2e21c000001; constexpr uint64_t ZtNetwork = 0xa84ac5c10a7ebb5f; std::atomic_bool zt_network_ready(false); std::atomic_bool zt_node_online(false); std::atomic_bool zt_joined(false); std::atomic_uint zt_peers_ready(0); ankerl::unordered_dense::map ztPeerEvents; #ifdef DVL_ZT_SYMLINK bool HasMultiByteChars(std::string_view path) { return c_any_of(path, IsTrailUtf8CodeUnit); } #ifdef PACKET_ENCRYPTION std::string ComputeAlternateFolderName(std::string_view path) { const size_t hashSize = crypto_generichash_BYTES; unsigned char hash[hashSize]; const int status = crypto_generichash(hash, hashSize, reinterpret_cast(path.data()), path.size(), nullptr, 0); if (status != 0) return {}; char buf[hashSize * 2]; for (size_t i = 0; i < hashSize; ++i) { BufCopy(&buf[i * 2], AsHexPad2(hash[i])); } return std::string(buf, hashSize * 2); } #else std::string ComputeAlternateFolderName(std::string_view path) { return {}; } #endif std::string ToZTCompliantPath(std::string_view configPath) { if (!HasMultiByteChars(configPath)) return std::string(configPath); char commonAppDataPath[MAX_PATH]; if (!SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_COMMON_APPDATA, NULL, 0, commonAppDataPath))) { LogVerbose("Failed to retrieve common application data path"); return std::string(configPath); } std::error_code err; std::string alternateConfigPath = StrCat(commonAppDataPath, "\\diasurgical\\devilution"); std::filesystem::create_directories(alternateConfigPath, err); if (err) { LogVerbose("Failed to create directories in ZT-compliant config path"); return std::string(configPath); } std::string alternateFolderName = ComputeAlternateFolderName(configPath); if (alternateFolderName == "") { LogVerbose("Failed to hash config path for ZT"); return std::string(configPath); } std::string symlinkPath = StrCat(alternateConfigPath, "\\", alternateFolderName); bool symlinkExists = std::filesystem::exists( std::u8string_view(reinterpret_cast(symlinkPath.data()), symlinkPath.size()), err); if (err) { LogVerbose("Failed to determine if symlink for ZT-compliant config path exists"); return std::string(configPath); } if (!symlinkExists) { std::filesystem::create_directory_symlink( std::u8string_view(reinterpret_cast(configPath.data()), configPath.size()), std::u8string_view(reinterpret_cast(symlinkPath.data()), symlinkPath.size()), err); if (err) { LogVerbose("Failed to create symlink for ZT-compliant config path"); return std::string(configPath); } } return StrCat(symlinkPath, "\\"); } #endif void Callback(void *ptr) { zts_event_msg_t *msg = reinterpret_cast(ptr); switch (msg->event_code) { case ZTS_EVENT_NODE_ONLINE: Log("ZeroTier: ZTS_EVENT_NODE_ONLINE, nodeId={:x}", (unsigned long long)msg->node->node_id); zt_node_online = true; if (!zt_joined) { zts_net_join(ZtNetwork); zt_joined = true; } break; case ZTS_EVENT_NODE_OFFLINE: Log("ZeroTier: ZTS_EVENT_NODE_OFFLINE"); zt_node_online = false; break; case ZTS_EVENT_NETWORK_READY_IP6: Log("ZeroTier: ZTS_EVENT_NETWORK_READY_IP6, networkId={:x}", (unsigned long long)msg->network->net_id); zt_ip6setup(); zt_network_ready = true; zt_peers_ready = SDL_GetTicks(); break; case ZTS_EVENT_ADDR_ADDED_IP6: print_ip6_addr(&(msg->addr->addr)); break; case ZTS_EVENT_PEER_DIRECT: case ZTS_EVENT_PEER_RELAY: ztPeerEvents[msg->peer->peer_id] = static_cast(msg->event_code); if (!zerotier_peers_ready()) zt_peers_ready = SDL_GetTicks(); break; case ZTS_EVENT_PEER_PATH_DEAD: ztPeerEvents.erase(msg->peer->peer_id); break; } } } // namespace bool zerotier_network_ready() { return zt_network_ready && zt_node_online; } bool zerotier_peers_ready() { return SDL_GetTicks() - zt_peers_ready >= 5000; } void zerotier_network_start() { std::string configPath = paths::ConfigPath(); #ifdef DVL_ZT_SYMLINK configPath = ToZTCompliantPath(configPath); #endif std::string ztpath = configPath + "zerotier"; zts_init_from_storage(ztpath.c_str()); zts_init_set_event_handler(&Callback); zts_node_start(); } bool zerotier_is_relayed(uint64_t mac) { bool isRelayed = true; if (zts_core_lock_obtain() != ZTS_ERR_OK) return isRelayed; zts_peer_info_t peerInfo; if (zts_core_query_peer_info(ZtNetwork, mac, &peerInfo) == ZTS_ERR_OK) { auto peerEvent = ztPeerEvents.find(peerInfo.peer_id); if (peerEvent != ztPeerEvents.end()) isRelayed = (peerEvent->second == ZTS_EVENT_PEER_RELAY); } zts_core_lock_release(); return isRelayed; } int zerotier_latency(uint64_t mac) { int latency = -1; if (zts_core_lock_obtain() != ZTS_ERR_OK) return latency; zts_peer_info_t peerInfo; if (zts_core_query_peer_info(ZtNetwork, mac, &peerInfo) == ZTS_ERR_OK) latency = peerInfo.latency; zts_core_lock_release(); return latency; } } // namespace net } // namespace devilution ================================================ FILE: Source/dvlnet/zerotier_native.h ================================================ #pragma once #include namespace devilution { namespace net { bool zerotier_network_ready(); bool zerotier_peers_ready(); void zerotier_network_start(); bool zerotier_is_relayed(uint64_t mac); int zerotier_latency(uint64_t mac); // NOTE: We have patched our libzt to have the corresponding multicast // MAC hardcoded, since libzt is still missing the proper handling. const unsigned char dvl_multicast_addr[16] = { 0xff, 0x0e, 0xa8, 0xa9, 0xb6, 0x11, 0x60, 0xce, 0x04, 0x12, 0xfd, 0x73, 0x37, 0x86, 0x6f, 0xb7 }; } // namespace net } // namespace devilution ================================================ FILE: Source/effects.cpp ================================================ /** * @file effects.cpp * * Implementation of functions for loading and playing sounds. */ #include "effects.h" #include #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" #include "engine/random.hpp" #include "engine/sound.h" #include "engine/sound_defs.hpp" #include "engine/sound_position.hpp" #include "game_mode.hpp" #include "options.h" #include "player.h" #include "utils/is_of.hpp" namespace devilution { int sfxdelay; SfxID sfxdnum = SfxID::None; namespace { #ifndef DISABLE_STREAMING_SOUNDS constexpr bool AllowStreaming = true; #else constexpr bool AllowStreaming = false; #endif /** Specifies the sound file and the playback state of the current sound effect. */ TSFX *sgpStreamSFX = nullptr; /** List of all sounds, except monsters and music */ std::vector sgSFX; void StreamPlay(TSFX *pSFX, int lVolume, int lPan) { assert(pSFX); assert(pSFX->bFlags & sfx_STREAM); stream_stop(); if (lVolume >= VOLUME_MIN) { if (lVolume > VOLUME_MAX) lVolume = VOLUME_MAX; if (pSFX->pSnd == nullptr) pSFX->pSnd = sound_file_load(pSFX->pszName.c_str(), AllowStreaming); if (pSFX->pSnd->DSB.IsLoaded()) pSFX->pSnd->DSB.PlayWithVolumeAndPan(lVolume, sound_get_or_set_sound_volume(1), lPan); sgpStreamSFX = pSFX; } } void StreamUpdate() { if (sgpStreamSFX != nullptr && !sgpStreamSFX->pSnd->isPlaying()) { stream_stop(); } } void PlaySfxPriv(TSFX *pSFX, bool loc, Point position) { if (MyPlayer->pLvlLoad != 0 && gbIsMultiplayer) { return; } if (!gbSndInited || !gbSoundOn || gbBufferMsgs != 0) { return; } if ((pSFX->bFlags & (sfx_STREAM | sfx_MISC)) == 0 && pSFX->pSnd != nullptr && pSFX->pSnd->isPlaying()) { return; } int lVolume = 0; int lPan = 0; if (loc && !CalculateSoundPosition(position, &lVolume, &lPan)) { return; } if ((pSFX->bFlags & sfx_STREAM) != 0) { StreamPlay(pSFX, lVolume, lPan); return; } if (pSFX->pSnd == nullptr) pSFX->pSnd = sound_file_load(pSFX->pszName.c_str()); if (pSFX->pSnd == nullptr || !pSFX->pSnd->DSB.IsLoaded()) return; const auto id = static_cast(pSFX - sgSFX.data()); const bool useCuesVolume = (id >= SfxID::AccessibilityWeapon && id <= SfxID::AccessibilityInteract); const int userVolume = useCuesVolume ? *GetOptions().Audio.audioCuesVolume : *GetOptions().Audio.soundVolume; snd_play_snd(pSFX->pSnd.get(), lVolume, lPan, userVolume); } SfxID RndSFX(SfxID psfx) { switch (psfx) { case SfxID::Warrior69: case SfxID::Sorceror69: case SfxID::Rogue69: case SfxID::Monk69: case SfxID::Swing: case SfxID::SpellAcid: case SfxID::OperateShrine: return PickRandomlyAmong({ psfx, static_cast(static_cast(psfx) + 1) }); case SfxID::Warrior14: case SfxID::Warrior15: case SfxID::Warrior16: case SfxID::Warrior2: case SfxID::Rogue14: case SfxID::Sorceror14: case SfxID::Monk14: return PickRandomlyAmong({ psfx, static_cast(static_cast(psfx) + 1), static_cast(static_cast(psfx) + 2) }); default: return psfx; } } tl::expected ParseSfxFlag(std::string_view value) { if (value == "Stream") return sfx_STREAM; if (value == "Misc") return sfx_MISC; if (value == "Ui") return sfx_UI; if (value == "Monk") return sfx_MONK; if (value == "Rogue") return sfx_ROGUE; if (value == "Warrior") return sfx_WARRIOR; if (value == "Sorcerer") return sfx_SORCERER; return tl::make_unexpected("Unknown enum value"); } void LoadEffectsData() { const std::string_view filename = "txtdata\\sound\\effects.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); sgSFX.clear(); sgSFX.reserve(dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; TSFX &item = sgSFX.emplace_back(); reader.advance(); // Skip the first column (effect ID). reader.readEnumList("flags", item.bFlags, ParseSfxFlag); reader.readString("path", item.pszName); } sgSFX.shrink_to_fit(); } void PrivSoundInit(uint8_t bLoadMask) { if (!gbSndInited) { return; } if (sgSFX.empty()) LoadEffectsData(); for (auto &sfx : sgSFX) { if (sfx.bFlags == 0 || sfx.pSnd != nullptr) { continue; } if ((sfx.bFlags & sfx_STREAM) != 0) { continue; } if ((sfx.bFlags & bLoadMask) == 0) { continue; } sfx.pSnd = sound_file_load(sfx.pszName.c_str()); } } } // namespace bool effect_is_playing(SfxID nSFX) { if (!gbSndInited) return false; TSFX *sfx = &sgSFX[static_cast(nSFX)]; if (sfx->pSnd != nullptr) return sfx->pSnd->isPlaying(); if ((sfx->bFlags & sfx_STREAM) != 0) return sfx == sgpStreamSFX; return false; } void stream_stop() { if (sgpStreamSFX != nullptr) { sgpStreamSFX->pSnd = nullptr; sgpStreamSFX = nullptr; } } void PlaySFX(SfxID psfx) { psfx = RndSFX(psfx); if (!gbSndInited) return; PlaySfxPriv(&sgSFX[static_cast(psfx)], false, { 0, 0 }); } void PlaySfxLoc(SfxID psfx, Point position, bool randomizeByCategory) { if (randomizeByCategory) { psfx = RndSFX(psfx); } if (!gbSndInited) return; if (IsAnyOf(psfx, SfxID::Walk, SfxID::ShootBow, SfxID::CastSpell, SfxID::Swing)) { TSnd *pSnd = sgSFX[static_cast(psfx)].pSnd.get(); if (pSnd != nullptr) pSnd->start_tc = 0; } PlaySfxPriv(&sgSFX[static_cast(psfx)], true, position); } void sound_stop() { if (!gbSndInited) return; ClearDuplicateSounds(); for (auto &sfx : sgSFX) { if (sfx.pSnd != nullptr && sfx.pSnd->DSB.IsLoaded()) { sfx.pSnd->DSB.Stop(); } } } void sound_update() { if (!gbSndInited) { return; } StreamUpdate(); } void effects_cleanup_sfx(bool fullUnload) { sound_stop(); if (fullUnload) { sgSFX.clear(); return; } for (auto &sfx : sgSFX) sfx.pSnd = nullptr; } void sound_init() { uint8_t mask = sfx_MISC; if (gbIsMultiplayer) { mask |= (sfx_WARRIOR | sfx_MONK); if (!gbIsSpawn) mask |= (sfx_ROGUE | sfx_SORCERER); } else { switch (MyPlayer->_pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: mask |= sfx_WARRIOR; break; case HeroClass::Rogue: case HeroClass::Bard: mask |= sfx_ROGUE; break; case HeroClass::Sorcerer: mask |= sfx_SORCERER; break; case HeroClass::Monk: mask |= sfx_MONK; break; default: if (static_cast(MyPlayer->_pClass) < GetNumPlayerClasses()) { // this is a custom class, so we need to add init sounds, since we can't determine which ones will be used by it mask |= (sfx_WARRIOR | sfx_MONK); if (!gbIsSpawn) mask |= (sfx_ROGUE | sfx_SORCERER); } else { app_fatal("effects:1"); } } } PrivSoundInit(mask); } void ui_sound_init() { PrivSoundInit(sfx_UI); } void effects_play_sound(SfxID id) { if (!gbSndInited || !gbSoundOn) { return; } TSFX &sfx = sgSFX[static_cast(id)]; if (sfx.pSnd != nullptr && !sfx.pSnd->isPlaying()) { snd_play_snd(sfx.pSnd.get(), 0, 0, *GetOptions().Audio.soundVolume); } } int GetSFXLength(SfxID nSFX) { TSFX &sfx = sgSFX[static_cast(nSFX)]; if (sfx.pSnd == nullptr) sfx.pSnd = sound_file_load(sfx.pszName.c_str(), /*stream=*/AllowStreaming && (sfx.bFlags & sfx_STREAM) != 0); return sfx.pSnd->DSB.GetLength(); } tl::expected ParseHeroSpeech(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value."); } tl::expected ParseSfxId(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value."); } } // namespace devilution ================================================ FILE: Source/effects.h ================================================ /** * @file effects.h * * Interface of functions for loading and playing sounds. */ #pragma once #include #include #include #include #include "engine/sound.h" #include "sound_effect_enums.h" namespace devilution { struct TSFX { uint8_t bFlags; std::string pszName; std::unique_ptr pSnd; }; extern int sfxdelay; extern SfxID sfxdnum; bool effect_is_playing(SfxID nSFX); void stream_stop(); void PlaySFX(SfxID psfx); void PlaySfxLoc(SfxID psfx, Point position, bool randomizeByCategory = true); void sound_stop(); void sound_update(); void effects_cleanup_sfx(bool fullUnload = true); void sound_init(); void ui_sound_init(); void effects_play_sound(SfxID); int GetSFXLength(SfxID nSFX); tl::expected ParseHeroSpeech(std::string_view value); tl::expected ParseSfxId(std::string_view value); } // namespace devilution ================================================ FILE: Source/effects_stubs.cpp ================================================ // Stubbed implementations of effects for the NOSOUND mode. #include "effects.h" #include #include #include "engine/random.hpp" namespace devilution { int sfxdelay; SfxID sfxdnum; bool effect_is_playing(SfxID nSFX) { return false; } void stream_stop() { } void PlaySFX(SfxID psfx) { switch (psfx) { case SfxID::Warrior69: case SfxID::Sorceror69: case SfxID::Rogue69: case SfxID::Monk69: case SfxID::Swing: case SfxID::SpellAcid: case SfxID::OperateShrine: case SfxID::Warrior14: case SfxID::Warrior15: case SfxID::Warrior16: case SfxID::Warrior2: case SfxID::Rogue14: case SfxID::Sorceror14: case SfxID::Monk14: AdvanceRndSeed(); break; default: break; } } void PlaySfxLoc(SfxID psfx, Point position, bool randomizeByCategory) { if (!randomizeByCategory) return; PlaySFX(psfx); } void sound_stop() { } void sound_update() { } void effects_cleanup_sfx(bool fullUnload) { } void sound_init() { } void ui_sound_init() { } void effects_play_sound(SfxID id) { } int GetSFXLength(SfxID nSFX) { return 0; } tl::expected ParseHeroSpeech(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value."); } tl::expected ParseSfxId(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value."); } } // namespace devilution ================================================ FILE: Source/encrypt.cpp ================================================ /** * @file encrypt.cpp * * Implementation of functions for compression and decompressing MPQ data. */ #include #include #include #include #include #include #include "encrypt.h" namespace devilution { namespace { struct TDataInfo { std::byte *srcData; uint32_t srcOffset; uint32_t srcSize; std::byte *destData; uint32_t destOffset; size_t destSize; bool error; }; unsigned int PkwareBufferRead(char *buf, unsigned int *size, void *param) // NOLINT(readability-non-const-parameter) { auto *pInfo = reinterpret_cast(param); uint32_t sSize; if (*size >= pInfo->srcSize - pInfo->srcOffset) { sSize = pInfo->srcSize - pInfo->srcOffset; } else { sSize = *size; } memcpy(buf, pInfo->srcData + pInfo->srcOffset, sSize); pInfo->srcOffset += sSize; return sSize; } void PkwareBufferWrite(char *buf, unsigned int *size, void *param) // NOLINT(readability-non-const-parameter) { auto *pInfo = reinterpret_cast(param); pInfo->error = pInfo->error || pInfo->destOffset + *size > pInfo->destSize; if (pInfo->error) { return; } memcpy(pInfo->destData + pInfo->destOffset, buf, *size); pInfo->destOffset += *size; } } // namespace uint32_t PkwareCompress(std::byte *srcData, uint32_t size) { const std::unique_ptr ptr = std::make_unique(CMP_BUFFER_SIZE); unsigned destSize = 2 * size; if (destSize < 2 * 4096) destSize = 2 * 4096; const std::unique_ptr destData { new std::byte[destSize] }; TDataInfo param; param.srcData = srcData; param.srcOffset = 0; param.srcSize = size; param.destData = destData.get(); param.destOffset = 0; param.destSize = destSize; param.error = false; unsigned type = 0; unsigned dsize = 4096; implode(PkwareBufferRead, PkwareBufferWrite, ptr.get(), ¶m, &type, &dsize); if (param.destOffset < size) { memcpy(srcData, destData.get(), param.destOffset); size = param.destOffset; } return size; } uint32_t PkwareDecompress(std::byte *inBuff, uint32_t recvSize, size_t maxBytes) { const std::unique_ptr ptr = std::make_unique(CMP_BUFFER_SIZE); const std::unique_ptr outBuff { new std::byte[maxBytes] }; TDataInfo info; info.srcData = inBuff; info.srcOffset = 0; info.srcSize = recvSize; info.destData = outBuff.get(); info.destOffset = 0; info.destSize = maxBytes; info.error = false; explode(PkwareBufferRead, PkwareBufferWrite, ptr.get(), &info); if (info.error) { return 0; } memcpy(inBuff, outBuff.get(), info.destOffset); return info.destOffset; } } // namespace devilution ================================================ FILE: Source/encrypt.h ================================================ /** * @file encrypt.h * * Interface of functions for compression and decompressing MPQ data. */ #pragma once #include #include namespace devilution { uint32_t PkwareCompress(std::byte *srcData, uint32_t size); uint32_t PkwareDecompress(std::byte *inBuff, uint32_t recvSize, size_t maxBytes); } // namespace devilution ================================================ FILE: Source/engine/actor_position.cpp ================================================ #include "actor_position.hpp" #include #include namespace devilution { namespace { enum class VelocityToUse : uint8_t { None, Full, NegativeFull, Half, NegativeHalf, Quarter, NegativeQuarter, }; struct RoundedWalkVelocity { int16_t quarter; int16_t half; int16_t full; int16_t getVelocity(VelocityToUse velocityToUse) const { switch (velocityToUse) { case VelocityToUse::Quarter: return quarter; case VelocityToUse::NegativeQuarter: return -quarter; case VelocityToUse::Half: return half; case VelocityToUse::NegativeHalf: return -half; case VelocityToUse::Full: return full; case VelocityToUse::NegativeFull: return -full; default: return 0; } } }; /** @brief Maps from walk animation length to monster velocity (longer/slower animation means less velocity). */ constexpr RoundedWalkVelocity WalkVelocityForFrames[24] = { // clang-format off // Quarter, Half, Full { 256, 512, 1024 }, { 128, 256, 512 }, { 85, 170, 341 }, { 64, 128, 256 }, { 51, 102, 204 }, { 42, 85, 170 }, { 36, 73, 146 }, { 32, 64, 128 }, { 28, 56, 113 }, { 26, 51, 102 }, { 23, 46, 93 }, { 21, 42, 85 }, { 19, 39, 78 }, { 18, 36, 73 }, { 17, 34, 68 }, { 16, 32, 64 }, { 15, 30, 60 }, { 14, 28, 57 }, { 13, 26, 54 }, { 12, 25, 51 }, { 12, 24, 48 }, { 11, 23, 46 }, { 11, 22, 44 }, { 10, 21, 42 } // clang-format on }; struct WalkParameter { VelocityToUse VelocityX; VelocityToUse VelocityY; DisplacementOf getVelocity(int8_t numberOfFrames) const { const RoundedWalkVelocity &walkVelocity = WalkVelocityForFrames[numberOfFrames - 1]; auto velocity = DisplacementOf { walkVelocity.getVelocity(VelocityX), walkVelocity.getVelocity(VelocityY), }; return velocity; } }; constexpr std::array WalkParameters { { // clang-format off // Direction VelocityX, VelocityY { /* South */ VelocityToUse::None, VelocityToUse::Half }, { /* SouthWest */ VelocityToUse::NegativeHalf, VelocityToUse::Quarter }, { /* West */ VelocityToUse::NegativeFull, VelocityToUse::None }, { /* NorthWest */ VelocityToUse::NegativeHalf, VelocityToUse::NegativeQuarter }, { /* North */ VelocityToUse::None, VelocityToUse::NegativeHalf }, { /* NorthEast */ VelocityToUse::Half, VelocityToUse::NegativeQuarter }, { /* East */ VelocityToUse::Full, VelocityToUse::None }, { /* SouthEast */ VelocityToUse::Half, VelocityToUse::Quarter } // clang-format on } }; } // namespace DisplacementOf ActorPosition::CalculateWalkingOffset(Direction dir, const AnimationInfo &animInfo) const { const DisplacementOf offset = CalculateWalkingOffsetShifted4(dir, animInfo); return { static_cast(offset.deltaX >> 4), static_cast(offset.deltaY >> 4) }; } DisplacementOf ActorPosition::CalculateWalkingOffsetShifted4(Direction dir, const AnimationInfo &animInfo) const { const int16_t velocityProgress = static_cast(animInfo.getAnimationProgress()) * animInfo.numberOfFrames / AnimationInfo::baseValueFraction; const WalkParameter &walkParameter = WalkParameters[static_cast(dir)]; const DisplacementOf velocity = walkParameter.getVelocity(animInfo.numberOfFrames); DisplacementOf offset = (velocity * velocityProgress); return offset; } DisplacementOf ActorPosition::CalculateWalkingOffsetShifted8(Direction dir, const AnimationInfo &animInfo) const { DisplacementOf offset = CalculateWalkingOffsetShifted4(dir, animInfo); offset.deltaX <<= 4; offset.deltaY <<= 4; return offset; } DisplacementOf ActorPosition::GetWalkingVelocityShifted4(Direction dir, const AnimationInfo &animInfo) const { const WalkParameter &walkParameter = WalkParameters[static_cast(dir)]; return walkParameter.getVelocity(animInfo.numberOfFrames); } DisplacementOf ActorPosition::GetWalkingVelocityShifted8(Direction dir, const AnimationInfo &animInfo) const { DisplacementOf velocity = GetWalkingVelocityShifted4(dir, animInfo); velocity.deltaX <<= 4; velocity.deltaY <<= 4; return velocity; } } // namespace devilution ================================================ FILE: Source/engine/actor_position.hpp ================================================ #pragma once #include #include "engine/animationinfo.h" #include "engine/point.hpp" #include "engine/world_tile.hpp" namespace devilution { struct ActorPosition { WorldTilePosition tile; /** Future tile position. Set at start of walking animation. */ WorldTilePosition future; /** Tile position of player. Set via network on player input. */ WorldTilePosition last; /** Most recent position in dPlayer. */ WorldTilePosition old; /** Used for referring to position of player when finishing moving one tile (also used to define target coordinates for spells and ranged attacks) */ WorldTilePosition temp; /** @brief Calculates the offset for the walking animation. */ DisplacementOf CalculateWalkingOffset(Direction dir, const AnimationInfo &animInfo) const; /** @brief Calculates the offset for the walking animation. */ DisplacementOf CalculateWalkingOffsetShifted4(Direction dir, const AnimationInfo &animInfo) const; /** @brief Calculates the offset for the walking animation. */ DisplacementOf CalculateWalkingOffsetShifted8(Direction dir, const AnimationInfo &animInfo) const; /** @brief Returns Pixel velocity while walking. */ DisplacementOf GetWalkingVelocityShifted4(Direction dir, const AnimationInfo &animInfo) const; /** @brief Returns Pixel velocity while walking. */ DisplacementOf GetWalkingVelocityShifted8(Direction dir, const AnimationInfo &animInfo) const; }; } // namespace devilution ================================================ FILE: Source/engine/animationinfo.cpp ================================================ /** * @file animationinfo.cpp * * Contains the core animation information and related logic */ #include "animationinfo.h" #include #include #include "appfat.h" #include "nthread.h" #include "utils/log.hpp" namespace devilution { int8_t AnimationInfo::getFrameToUseForRendering() const { // Normal logic is used, // - if no frame-skipping is required and so we have exactly one Animationframe per game tick // or // - if we load from a savegame where the new variables are not stored (we don't want to break savegame compatibility because of smoother rendering of one animation) if (relevantFramesForDistributing_ <= 0) return std::max(0, currentFrame); if (currentFrame >= relevantFramesForDistributing_) return currentFrame; int16_t ticksSinceSequenceStarted = ticksSinceSequenceStarted_; if (ticksSinceSequenceStarted_ < 0) { ticksSinceSequenceStarted = 0; Log("getFrameToUseForRendering: Invalid ticksSinceSequenceStarted_ {}", ticksSinceSequenceStarted_); } // we don't use the processed game ticks alone but also the fraction of the next game tick (if a rendering happens between game ticks). This helps to smooth the animations. const int32_t totalTicksForCurrentAnimationSequence = getProgressToNextGameTick() + ticksSinceSequenceStarted; int8_t absoluteAnimationFrame = static_cast(totalTicksForCurrentAnimationSequence * tickModifier_ / baseValueFraction / baseValueFraction); if (skippedFramesFromPreviousAnimation_ > 0) { // absoluteAnimationFrames contains also the Frames from the previous Animation, so if we want to get the current Frame we have to remove them absoluteAnimationFrame -= skippedFramesFromPreviousAnimation_; if (absoluteAnimationFrame < 0) { // We still display the remains of the previous Animation absoluteAnimationFrame = numberOfFrames + absoluteAnimationFrame; } } else if (absoluteAnimationFrame >= relevantFramesForDistributing_) { // this can happen if we are at the last frame and the next game tick is due if (absoluteAnimationFrame >= (relevantFramesForDistributing_ + 1)) { // we should never have +2 frames even if next game tick is due Log("getFrameToUseForRendering: Calculated an invalid Animation Frame (Calculated {} MaxFrame {})", absoluteAnimationFrame, relevantFramesForDistributing_); } return relevantFramesForDistributing_ - 1; } if (absoluteAnimationFrame < 0) { Log("getFrameToUseForRendering: Calculated an invalid Animation Frame (Calculated {})", absoluteAnimationFrame); return 0; } return absoluteAnimationFrame; } uint8_t AnimationInfo::getAnimationProgress() const { int16_t ticksSinceSequenceStarted = std::max(0, ticksSinceSequenceStarted_); int32_t tickModifier = tickModifier_; if (relevantFramesForDistributing_ <= 0) { if (ticksPerFrame <= 0) { Log("getAnimationProgress: Invalid ticksPerFrame {}", ticksPerFrame); return 0; } // This logic is used if animation distribution is not active (see getFrameToUseForRendering). // In this case the variables calculated with animation distribution are not initialized and we have to calculate them on the fly with the given information. ticksSinceSequenceStarted = ((currentFrame * ticksPerFrame) + tickCounterOfCurrentFrame) * baseValueFraction; tickModifier = baseValueFraction / ticksPerFrame; } const int32_t totalTicksForCurrentAnimationSequence = getProgressToNextGameTick() + ticksSinceSequenceStarted; const int32_t progressInAnimationFrames = totalTicksForCurrentAnimationSequence * tickModifier; const int32_t animationFraction = progressInAnimationFrames / numberOfFrames / baseValueFraction; assert(animationFraction <= baseValueFraction); return static_cast(animationFraction); } void AnimationInfo::setNewAnimation(OptionalClxSpriteList celSprite, int8_t numberOfFrames, int8_t ticksPerFrame, AnimationDistributionFlags flags /*= AnimationDistributionFlags::None*/, int8_t numSkippedFrames /*= 0*/, int8_t distributeFramesBeforeFrame /*= 0*/, uint8_t previewShownGameTickFragments /*= 0*/) { if ((flags & AnimationDistributionFlags::RepeatedAction) == AnimationDistributionFlags::RepeatedAction && distributeFramesBeforeFrame != 0 && this->numberOfFrames == numberOfFrames && currentFrame + 1 >= distributeFramesBeforeFrame && currentFrame != this->numberOfFrames - 1) { // We showed the same Animation (for example a melee attack) before but truncated the Animation. // So now we should add them back to the new Animation. This increases the speed of the current Animation but the game logic/ticks isn't affected. skippedFramesFromPreviousAnimation_ = this->numberOfFrames - currentFrame - 1; } else { skippedFramesFromPreviousAnimation_ = 0; } if (ticksPerFrame <= 0) { Log("setNewAnimation: Invalid ticksPerFrame {}", ticksPerFrame); ticksPerFrame = 1; } this->sprites = celSprite; this->numberOfFrames = numberOfFrames; currentFrame = numSkippedFrames; tickCounterOfCurrentFrame = 0; this->ticksPerFrame = ticksPerFrame; ticksSinceSequenceStarted_ = 0; relevantFramesForDistributing_ = 0; tickModifier_ = 0; isPetrified = false; if (numSkippedFrames != 0 || flags != AnimationDistributionFlags::None) { // Animation Frames that will be adjusted for the skipped Frames/game ticks int8_t relevantAnimationFramesForDistributing = numberOfFrames; if (distributeFramesBeforeFrame != 0) { // After an attack hits (_pAFNum or _pSFNum) it can be canceled or another attack can be queued and this means the animation is canceled. // In normal attacks frame skipping always happens before the attack actual hit. // This has the advantage that the sword or bow always points to the enemy when the hit happens (_pAFNum or _pSFNum). // Our distribution logic must also regard this behaviour, so we are not allowed to distribute the skipped animations after the actual hit (_pAnimStopDistributingAfterFrame). relevantAnimationFramesForDistributing = distributeFramesBeforeFrame - 1; } // Game ticks that will be adjusted for the skipped Frames/game ticks int32_t relevantAnimationTicksForDistribution = relevantAnimationFramesForDistributing * ticksPerFrame; // How many game ticks will the Animation be really shown (skipped Frames and game ticks removed) int32_t relevantAnimationTicksWithSkipping = relevantAnimationTicksForDistribution - (numSkippedFrames * ticksPerFrame); if ((flags & AnimationDistributionFlags::ProcessAnimationPending) == AnimationDistributionFlags::ProcessAnimationPending) { // If processAnimation will be called after setNewAnimation (in same game tick as setNewAnimation), we increment the Animation-Counter. // If no delay is specified, this will result in complete skipped frame (see processAnimation). // But if we have a delay specified, this would only result in a reduced time the first frame is shown (one skipped delay). // Because of that, we only the remove one game tick from the time the Animation is shown relevantAnimationTicksWithSkipping -= 1; // The Animation Distribution Logic needs to account how many game ticks passed since the Animation started. // Because processAnimation will increase this later (in same game tick as setNewAnimation), we correct this upfront. // This also means Rendering should never happen with ticksSinceSequenceStarted_ < 0. ticksSinceSequenceStarted_ = -baseValueFraction; } if ((flags & AnimationDistributionFlags::SkipsDelayOfLastFrame) == AnimationDistributionFlags::SkipsDelayOfLastFrame) { // The logic for player/monster/... (not processAnimation) only checks the frame not the delay. // That means if a delay is specified, the last-frame is shown less than the other frames // Example: // If we have a animation with 3 frames and with a delay of 1 (ticksPerFrame = 2). // The logic checks "if (frame == 3) { start_new_animation(); }" // This will result that frame 4 is the last shown Animation Frame. // GameTick Frame Cnt // 1 1 0 // 2 1 1 // 3 2 0 // 3 2 1 // 4 3 0 // 5 - - // in game tick 5 ProcessPlayer sees Frame = 3 and stops the animation. // But Frame 3 is only shown 1 game tick and all other Frames are shown 2 game ticks. // That's why we need to remove the Delay of the last Frame from the time (game ticks) the Animation is shown relevantAnimationTicksWithSkipping -= (ticksPerFrame - 1); } // The truncated Frames from previous Animation will also be shown, so we also have to distribute them for the given time (game ticks) relevantAnimationTicksForDistribution += (skippedFramesFromPreviousAnimation_ * ticksPerFrame); // At this point we use fixed point math for the fragment calculations relevantAnimationTicksForDistribution *= baseValueFraction; relevantAnimationTicksWithSkipping *= baseValueFraction; // The preview animation was shown some times (less than one game tick) // So we overall have a longer time the animation is shown ticksSinceSequenceStarted_ += previewShownGameTickFragments; relevantAnimationTicksWithSkipping += previewShownGameTickFragments; // if we skipped Frames we need to expand the game ticks to make one game tick for this Animation "faster" int32_t tickModifier = 0; if (relevantAnimationTicksWithSkipping != 0) tickModifier = baseValueFraction * relevantAnimationTicksForDistribution / relevantAnimationTicksWithSkipping; // tickModifier specifies the Animation fraction per game tick, so we have to remove the delay from the variable tickModifier /= ticksPerFrame; relevantFramesForDistributing_ = relevantAnimationFramesForDistributing; tickModifier_ = static_cast(tickModifier); } } void AnimationInfo::changeAnimationData(OptionalClxSpriteList celSprite, int8_t numberOfFrames, int8_t ticksPerFrame) { if (numberOfFrames != this->numberOfFrames || ticksPerFrame != this->ticksPerFrame) { // Ensure that the currentFrame is still valid and that we disable ADL because the calculated values (for example tickModifier_) could be wrong if (numberOfFrames >= 1) currentFrame = std::clamp(currentFrame, 0, numberOfFrames - 1); else currentFrame = -1; this->numberOfFrames = numberOfFrames; this->ticksPerFrame = ticksPerFrame; ticksSinceSequenceStarted_ = 0; relevantFramesForDistributing_ = 0; tickModifier_ = 0; } this->sprites = celSprite; } void AnimationInfo::processAnimation(bool reverseAnimation /*= false*/) { tickCounterOfCurrentFrame++; ticksSinceSequenceStarted_ += baseValueFraction; if (tickCounterOfCurrentFrame >= ticksPerFrame) { tickCounterOfCurrentFrame = 0; if (reverseAnimation) { --currentFrame; if (currentFrame == -1) { currentFrame = numberOfFrames - 1; ticksSinceSequenceStarted_ = 0; } } else { ++currentFrame; if (currentFrame >= numberOfFrames) { currentFrame = 0; ticksSinceSequenceStarted_ = 0; } } } } uint8_t AnimationInfo::getProgressToNextGameTick() const { if (isPetrified) return 0; return ProgressToNextGameTick; } } // namespace devilution ================================================ FILE: Source/engine/animationinfo.h ================================================ /** * @file animationinfo.h * * Contains the core animation information and related logic */ #pragma once #include #include #include "engine/clx_sprite.hpp" namespace devilution { /** * @brief Specifies what special logics are applied for a Animation */ enum AnimationDistributionFlags : uint8_t { None = 0, /** * @brief processAnimation will be called after setNewAnimation (in same game tick as NewPlrAnim) */ ProcessAnimationPending = 1 << 0, /** * @brief Delay of last Frame is ignored (for example, because only Frame and not delay is checked in game_logic) */ SkipsDelayOfLastFrame = 1 << 1, /** * @brief Repeated Animation (for example same player melee attack, that can be repeated directly after hit frame and doesn't need to show all animation frames) */ RepeatedAction = 1 << 2, }; /** * @brief Contains the core animation information and related logic */ class AnimationInfo { public: /** * @brief Animation sprite */ OptionalClxSpriteList sprites; /** * @brief How many game ticks are needed to advance one Animation Frame */ int8_t ticksPerFrame; /** * @brief Increases by one each game tick, counting how close we are to ticksPerFrame */ int8_t tickCounterOfCurrentFrame; /** * @brief Number of frames in current animation */ int8_t numberOfFrames; /** * @brief Current frame of animation */ int8_t currentFrame; /** * @brief Is the animation currently petrified and shouldn't advance with gfProgressToNextGameTick */ bool isPetrified; [[nodiscard]] ClxSprite currentSprite() const { return (*sprites)[getFrameToUseForRendering()]; } [[nodiscard]] bool isLastFrame() const { return currentFrame >= (numberOfFrames - 1); } /** * @brief Calculates the Frame to use for the Animation rendering * @return The Frame to use for rendering */ [[nodiscard]] int8_t getFrameToUseForRendering() const; /** * @brief Calculates the progress of the current animation as a fraction (see baseValueFraction) */ [[nodiscard]] uint8_t getAnimationProgress() const; /** * @brief Sets the new Animation with all relevant information for rendering * @param sprites Animation sprites * @param numberOfFrames Number of Frames in Animation * @param ticksPerFrame How many game ticks are needed to advance one Animation Frame * @param flags Specifies what special logics are applied to this Animation * @param numSkippedFrames Number of Frames that will be skipped (for example with modifier "faster attack") * @param distributeFramesBeforeFrame Distribute the numSkippedFrames only before this frame * @param previewShownGameTickFragments Defines how long (in game ticks fraction) the preview animation was shown */ void setNewAnimation(OptionalClxSpriteList sprites, int8_t numberOfFrames, int8_t ticksPerFrame, AnimationDistributionFlags flags = AnimationDistributionFlags::None, int8_t numSkippedFrames = 0, int8_t distributeFramesBeforeFrame = 0, uint8_t previewShownGameTickFragments = 0); /** * @brief Changes the Animation Data on-the-fly. This is needed if a animation is currently in progress and the player changes his gear. * @param sprites Animation sprites * @param numberOfFrames Number of Frames in Animation * @param ticksPerFrame How many game ticks are needed to advance one Animation Frame */ void changeAnimationData(OptionalClxSpriteList sprites, int8_t numberOfFrames, int8_t ticksPerFrame); /** * @brief Process the Animation for a game tick (for example advances the frame) * @param reverseAnimation Play the animation backwards (for example is used for "unseen" monster fading) */ void processAnimation(bool reverseAnimation = false); /** * @brief Fractions in AnimationInfo are stored as fixed point (baseValueFraction/128 correspondents to 1/100%). */ constexpr static uint8_t baseValueFraction = 128; private: /** * @brief returns the progress as a fraction in time to the next game tick or no progress if the animation is frozen (see baseValueFraction) */ [[nodiscard]] uint8_t getProgressToNextGameTick() const; /** * @brief Animation Frames that will be adjusted for the skipped Frames/game ticks */ int8_t relevantFramesForDistributing_; /** * @brief Animation Frames that wasn't shown from previous Animation */ int8_t skippedFramesFromPreviousAnimation_; /** * @brief Specifies how many animations-fractions (see baseValueFraction) are displayed between two game ticks. this can be more than one frame, if animations are skipped or less than one frame if the same animation is shown in multiple times (delay specified). */ uint16_t tickModifier_; /** * @brief Number of game ticks after the current animation sequence started */ int16_t ticksSinceSequenceStarted_; }; } // namespace devilution ================================================ FILE: Source/engine/assets.cpp ================================================ #include "engine/assets.hpp" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include "appfat.h" #include "game_mode.hpp" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #if defined(_WIN32) && !defined(__UWP__) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include #endif #ifndef UNPACKED_MPQS #include "mpq/mpq_sdl_rwops.hpp" #endif namespace devilution { std::vector OverridePaths; std::map> MpqArchives; bool HasHellfireMpq; namespace { #ifdef UNPACKED_MPQS char *FindUnpackedMpqFile(char *relativePath) { char *path = nullptr; for (const auto &[_, unpackedDir] : MpqArchives) { path = relativePath - unpackedDir.size(); std::memcpy(path, unpackedDir.data(), unpackedDir.size()); if (FileExists(path)) break; path = nullptr; } return path; } #else bool IsDebugLogging() { return IsLogLevel(LogCategory::Application, SDL_LOG_PRIORITY_DEBUG); } SDL_IOStream *OpenOptionalRWops(const std::string &path) { // SDL always logs an error in Debug mode. // We check the file presence in Debug mode to avoid this. if (IsDebugLogging() && !FileExists(path.c_str())) return nullptr; return SDL_IOFromFile(path.c_str(), "rb"); }; bool FindMpqFile(std::string_view filename, MpqArchive **archive, uint32_t *fileNumber) { const MpqFileHash fileHash = CalculateMpqFileHash(filename); for (auto &[_, mpqArchive] : MpqArchives) { if (mpqArchive.GetFileNumber(fileHash, *fileNumber)) { *archive = &mpqArchive; return true; } } return false; } #endif } // namespace #ifdef UNPACKED_MPQS AssetRef FindAsset(std::string_view filename) { AssetRef result; if (filename.empty() || filename.back() == '\\') return result; result.path[0] = '\0'; char pathBuf[AssetRef::PathBufSize]; char *const pathEnd = pathBuf + AssetRef::PathBufSize; char *const relativePath = &pathBuf[AssetRef::PathBufSize - filename.size() - 1]; *BufCopy(relativePath, filename) = '\0'; #if !defined(_WIN32) && !defined(__DJGPP__) std::replace(relativePath, pathEnd, '\\', '/'); #endif // Absolute path: if (relativePath[0] == '/') { if (FileExists(relativePath)) { *BufCopy(result.path, std::string_view(relativePath, filename.size())) = '\0'; } return result; } // Unpacked MPQ file: char *const unpackedMpqPath = FindUnpackedMpqFile(relativePath); if (unpackedMpqPath != nullptr) { *BufCopy(result.path, std::string_view(unpackedMpqPath, pathEnd - unpackedMpqPath)) = '\0'; return result; } // The `/assets` directory next to the devilutionx binary. const std::string &assetsPathPrefix = paths::AssetsPath(); char *assetsPath = relativePath - assetsPathPrefix.size(); std::memcpy(assetsPath, assetsPathPrefix.data(), assetsPathPrefix.size()); if (FileExists(assetsPath)) { *BufCopy(result.path, std::string_view(assetsPath, pathEnd - assetsPath)) = '\0'; } return result; } #else AssetRef FindAsset(std::string_view filename) { AssetRef result; if (filename.empty() || filename.back() == '\\') return result; std::string relativePath { filename }; #ifndef _WIN32 std::replace(relativePath.begin(), relativePath.end(), '\\', '/'); #endif if (relativePath[0] == '/') { result.directHandle = SDL_IOFromFile(relativePath.c_str(), "rb"); if (result.directHandle != nullptr) { return result; } } // Files in the `PrefPath()` directory can override MPQ contents. { for (const auto &overridePath : OverridePaths) { const std::string path = overridePath + relativePath; result.directHandle = OpenOptionalRWops(path); if (result.directHandle != nullptr) { LogVerbose("Loaded MPQ file override: {}", path); return result; } } } // Look for the file in all the MPQ archives: if (FindMpqFile(filename, &result.archive, &result.fileNumber)) { result.filename = filename; return result; } // Load from the `/assets` directory next to the devilutionx binary. result.directHandle = OpenOptionalRWops(paths::AssetsPath() + relativePath); if (result.directHandle != nullptr) return result; #if defined(__ANDROID__) || defined(__APPLE__) // Fall back to the bundled assets on supported systems. // This is handled by SDL when we pass a relative path. if (!paths::AssetsPath().empty()) { result.directHandle = SDL_IOFromFile(relativePath.c_str(), "rb"); if (result.directHandle != nullptr) return result; } #endif return result; } #endif AssetHandle OpenAsset(AssetRef &&ref, bool threadsafe) { #if UNPACKED_MPQS return AssetHandle { OpenFile(ref.path, "rb") }; #else if (ref.archive != nullptr) return AssetHandle { SDL_RWops_FromMpqFile(*ref.archive, ref.fileNumber, ref.filename, threadsafe) }; if (ref.directHandle != nullptr) { // Transfer handle ownership: auto *handle = ref.directHandle; ref.directHandle = nullptr; return AssetHandle { handle }; } return AssetHandle { nullptr }; #endif } AssetHandle OpenAsset(std::string_view filename, bool threadsafe) { AssetRef ref = FindAsset(filename); if (!ref.ok()) return AssetHandle {}; return OpenAsset(std::move(ref), threadsafe); } AssetHandle OpenAsset(std::string_view filename, size_t &fileSize, bool threadsafe) { AssetRef ref = FindAsset(filename); if (!ref.ok()) return AssetHandle {}; fileSize = ref.size(); return OpenAsset(std::move(ref), threadsafe); } SDL_IOStream *OpenAssetAsSdlRwOps(std::string_view filename, bool threadsafe) { #ifdef UNPACKED_MPQS AssetRef ref = FindAsset(filename); if (!ref.ok()) return nullptr; return SDL_IOFromFile(ref.path, "rb"); #else return OpenAsset(filename, threadsafe).release(); #endif } tl::expected LoadAsset(std::string_view path) { AssetRef ref = FindAsset(path); if (!ref.ok()) { return tl::make_unexpected(StrCat("Asset not found: ", path)); } const size_t size = ref.size(); std::unique_ptr data { new char[size] }; AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok()) { return tl::make_unexpected(StrCat("Failed to open asset: ", path, "\n", handle.error())); } if (size > 0 && !handle.read(data.get(), size)) { return tl::make_unexpected(StrCat("Read failed: ", path, "\n", handle.error())); } return AssetData { std::move(data), size }; } std::string FailedToOpenFileErrorMessage(std::string_view path, std::string_view error) { return fmt::format(fmt::runtime(_("Failed to open file:\n{:s}\n\n{:s}\n\nThe MPQ file(s) might be damaged. Please check the file integrity.")), path, error); } namespace { #ifdef UNPACKED_MPQS std::optional FindUnpackedMpqData(std::span paths, std::string_view mpqName) { std::string targetPath; for (const std::string &path : paths) { targetPath.clear(); targetPath.reserve(path.size() + mpqName.size() + 1); targetPath.append(path).append(mpqName) += DirectorySeparator; if (FileExists(targetPath)) { LogVerbose(" Found unpacked MPQ directory: {}", targetPath); return targetPath; } } return std::nullopt; } bool FindMPQ(std::span paths, std::string_view mpqName) { return FindUnpackedMpqData(paths, mpqName).has_value(); } bool LoadMPQ(std::span paths, std::string_view mpqName, int priority) { std::optional mpqPath = FindUnpackedMpqData(paths, mpqName); if (!mpqPath.has_value()) { LogVerbose("Missing: {}", mpqName); return false; } MpqArchives[priority] = *std::move(mpqPath); return true; } #else bool FindMPQ(std::span paths, std::string_view mpqName) { std::string mpqAbsPath; for (const auto &path : paths) { mpqAbsPath = StrCat(path, mpqName, ".mpq"); if (FileExists(mpqAbsPath)) { LogVerbose(" Found: {} in {}", mpqName, path); return true; } } return false; } bool LoadMPQ(std::span paths, std::string_view mpqName, int priority, std::string_view ext = ".mpq") { std::optional archive; std::string mpqAbsPath; std::int32_t error = 0; for (const auto &path : paths) { mpqAbsPath = StrCat(path, mpqName, ext); archive = MpqArchive::Open(mpqAbsPath.c_str(), error); if (archive.has_value()) { LogVerbose(" Found: {} in {}", mpqName, path); auto [it, inserted] = MpqArchives.emplace(priority, *std::move(archive)); if (!inserted) { LogError("MPQ with priority {} is already registered, skipping {}", priority, mpqName); } return true; } if (error != 0) { LogError("Error {}: {}", MpqArchive::ErrorMessage(error), mpqAbsPath); } } if (error == 0) { LogVerbose("Missing: {}", mpqName); } return false; } #endif std::vector GetMPQSearchPaths() { std::vector paths; paths.push_back(paths::BasePath()); paths.push_back(paths::PrefPath()); if (paths[0] == paths[1]) paths.pop_back(); paths.push_back(paths::ConfigPath()); if (paths[0] == paths[1] || (paths.size() == 3 && (paths[0] == paths[2] || paths[1] == paths[2]))) paths.pop_back(); #if (defined(__unix__) || defined(__APPLE__)) && !defined(__ANDROID__) && !defined(__DJGPP__) // `XDG_DATA_HOME` is usually the root path of `paths::PrefPath()`, so we only // add `XDG_DATA_DIRS`. const char *xdgDataDirs = std::getenv("XDG_DATA_DIRS"); if (xdgDataDirs != nullptr) { for (const std::string_view path : SplitByChar(xdgDataDirs, ':')) { std::string fullPath(path); if (!path.empty() && path.back() != '/') fullPath += '/'; fullPath.append("diasurgical/devilutionx/"); paths.push_back(std::move(fullPath)); } } else { paths.emplace_back("/usr/local/share/diasurgical/devilutionx/"); paths.emplace_back("/usr/share/diasurgical/devilutionx/"); } #elif defined(NXDK) paths.emplace_back("D:\\"); #elif defined(_WIN32) && !defined(__UWP__) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) char gogpath[_FSG_PATH_MAX]; fsg_get_gog_game_path(gogpath, "1412601690"); if (strlen(gogpath) > 0) { paths.emplace_back(std::string(gogpath) + "/"); paths.emplace_back(std::string(gogpath) + "/hellfire/"); } #endif if (paths.empty() || !paths.back().empty()) { paths.emplace_back(); // PWD } if (IsLogLevel(LogCategory::Application, SDL_LOG_PRIORITY_VERBOSE)) { LogVerbose("Paths:\n base: {}\n pref: {}\n config: {}\n assets: {}", paths::BasePath(), paths::PrefPath(), paths::ConfigPath(), paths::AssetsPath()); std::string message; for (std::size_t i = 0; i < paths.size(); ++i) { message.append(StrCat("\n", LeftPad(i + 1, 6, ' '), ". '", paths[i], "'")); } LogVerbose("MPQ search paths:{}", message); } return paths; } } // namespace void LoadCoreArchives() { auto paths = GetMPQSearchPaths(); #if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__3DS__) && !defined(__SWITCH__) // Load devilutionx.mpq first to get the font file for error messages #ifdef __DJGPP__ LoadMPQ(paths, "devx", DevilutionXMpqPriority); #else LoadMPQ(paths, "devilutionx", DevilutionXMpqPriority); #endif #endif LoadMPQ(paths, "fonts", FontMpqPriority); // Extra fonts HasHellfireMpq = FindMPQ(paths, "hellfire"); } void LoadLanguageArchive() { MpqArchives.erase(LangMpqPriority); const std::string_view code = GetLanguageCode(); if (code != "en") { LoadMPQ(GetMPQSearchPaths(), code, LangMpqPriority); } } void LoadGameArchives() { const std::vector paths = GetMPQSearchPaths(); bool haveDiabdat = false; bool haveSpawn = false; #ifndef UNPACKED_MPQS // DIABDAT.MPQ is uppercase on the original CD and the GOG version. haveDiabdat = LoadMPQ(paths, "DIABDAT", MainMpqPriority, ".MPQ"); #endif if (!haveDiabdat) { haveDiabdat = LoadMPQ(paths, "diabdat", MainMpqPriority); if (!haveDiabdat) { gbIsSpawn = haveSpawn = LoadMPQ(paths, "spawn", MainMpqPriority); } } if (!HeadlessMode) { if (!haveDiabdat && !haveSpawn) { LogError("{}", SDL_GetError()); InsertCDDlg(_("diabdat.mpq or spawn.mpq")); } } if (forceHellfire && !HasHellfireMpq) { #ifdef UNPACKED_MPQS InsertCDDlg("hellfire"); #else InsertCDDlg("hellfire.mpq"); #endif } #ifndef UNPACKED_MPQS // In unpacked mode, all the hellfire data is in the hellfire directory. LoadMPQ(paths, "hfbard", 8110); LoadMPQ(paths, "hfbarb", 8120); #endif } void LoadHellfireArchives() { const std::vector paths = GetMPQSearchPaths(); LoadMPQ(paths, "hellfire", 8000); #ifdef UNPACKED_MPQS const std::string &hellfireDataPath = MpqArchives.at(8000); const bool hasMonk = FileExists(hellfireDataPath + "plrgfx/monk/mha/mhaas.clx"); const bool hasMusic = FileExists(hellfireDataPath + "music/dlvlf.wav") || FileExists(hellfireDataPath + "music/dlvlf.mp3"); const bool hasVoice = FileExists(hellfireDataPath + "sfx/hellfire/cowsut1.wav") || FileExists(hellfireDataPath + "sfx/hellfire/cowsut1.mp3"); #else const bool hasMonk = LoadMPQ(paths, "hfmonk", 8100); const bool hasMusic = LoadMPQ(paths, "hfmusic", 8200); const bool hasVoice = LoadMPQ(paths, "hfvoice", 8500); #endif if (!hasMonk || !hasMusic || !hasVoice) DisplayFatalErrorAndExit(_("Some Hellfire MPQs are missing"), _("Not all Hellfire MPQs were found.\nPlease copy all the hf*.mpq files.")); } void UnloadModArchives() { OverridePaths.clear(); #ifndef UNPACKED_MPQS for (auto it = MpqArchives.begin(); it != MpqArchives.end();) { if ((it->first >= 8000 && it->first < 9000) || it->first >= 10000) { it = MpqArchives.erase(it); // erase returns the next valid iterator } else { ++it; } } #endif } void LoadModArchives(std::span modnames) { std::string targetPath; for (const std::string_view modname : modnames) { targetPath = StrCat(paths::PrefPath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR); if (FileExists(targetPath)) { OverridePaths.emplace_back(targetPath); } targetPath = StrCat(paths::BasePath(), "mods" DIRECTORY_SEPARATOR_STR, modname, DIRECTORY_SEPARATOR_STR); if (FileExists(targetPath)) { OverridePaths.emplace_back(targetPath); } } OverridePaths.emplace_back(paths::PrefPath()); int priority = 10000; auto paths = GetMPQSearchPaths(); for (const std::string_view modname : modnames) { LoadMPQ(paths, StrCat("mods" DIRECTORY_SEPARATOR_STR, modname), priority); priority++; } } } // namespace devilution ================================================ FILE: Source/engine/assets.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include #include "appfat.h" #include "game_mode.hpp" #include "headless_mode.hpp" #include "utils/file_util.h" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" #include "utils/string_or_view.hpp" #ifndef UNPACKED_MPQS #include "mpq/mpq_reader.hpp" #endif #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { #ifdef UNPACKED_MPQS struct AssetRef { static constexpr size_t PathBufSize = 4088; char path[PathBufSize]; [[nodiscard]] bool ok() const { return path[0] != '\0'; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) [[nodiscard]] const char *error() const { return "File not found"; } [[nodiscard]] size_t size() const { uintmax_t fileSize; if (!GetFileSize(path, &fileSize)) return 0; return fileSize; } }; struct AssetHandle { FILE *handle = nullptr; AssetHandle() = default; AssetHandle(FILE *handle) : handle(handle) { } AssetHandle(AssetHandle &&other) noexcept : handle(other.handle) { other.handle = nullptr; } AssetHandle &operator=(AssetHandle &&other) noexcept { handle = other.handle; other.handle = nullptr; return *this; } ~AssetHandle() { if (handle != nullptr) std::fclose(handle); } [[nodiscard]] bool ok() const { return handle != nullptr && std::ferror(handle) == 0; } bool read(void *buffer, size_t len) { return std::fread(buffer, len, 1, handle) == 1; } bool seek(long pos) { return std::fseek(handle, pos, SEEK_SET) == 0; } [[nodiscard]] const char *error() const { return std::strerror(errno); } }; #else struct AssetRef { // An MPQ file reference: MpqArchive *archive = nullptr; uint32_t fileNumber; std::string_view filename; // Alternatively, a direct SDL_IOStream handle: SDL_IOStream *directHandle = nullptr; AssetRef() = default; AssetRef(AssetRef &&other) noexcept : archive(other.archive) , fileNumber(other.fileNumber) , filename(other.filename) , directHandle(other.directHandle) { other.directHandle = nullptr; } AssetRef &operator=(AssetRef &&other) noexcept { closeDirectHandle(); archive = other.archive; fileNumber = other.fileNumber; filename = other.filename; directHandle = other.directHandle; other.directHandle = nullptr; return *this; } ~AssetRef() { closeDirectHandle(); } [[nodiscard]] bool ok() const { return directHandle != nullptr || archive != nullptr; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) [[nodiscard]] const char *error() const { return SDL_GetError(); } [[nodiscard]] size_t size() const { if (archive != nullptr) { int32_t error; return archive->GetUnpackedFileSize(fileNumber, error); } return static_cast(SDL_GetIOSize(directHandle)); } private: void closeDirectHandle() { if (directHandle != nullptr) { SDL_CloseIO(directHandle); } } }; struct AssetHandle { SDL_IOStream *handle = nullptr; AssetHandle() = default; explicit AssetHandle(SDL_IOStream *handle) : handle(handle) { } AssetHandle(AssetHandle &&other) noexcept : handle(other.handle) { other.handle = nullptr; } AssetHandle &operator=(AssetHandle &&other) noexcept { closeHandle(); handle = other.handle; other.handle = nullptr; return *this; } ~AssetHandle() { closeHandle(); } [[nodiscard]] bool ok() const { return handle != nullptr; } bool read(void *buffer, size_t len) { return SDL_ReadIO(handle, buffer, len) == len; } bool seek(long pos) { return SDL_SeekIO(handle, pos, SDL_IO_SEEK_SET) != -1; } [[nodiscard]] const char *error() const { return SDL_GetError(); } SDL_IOStream *release() && { SDL_IOStream *result = handle; handle = nullptr; return result; } private: void closeHandle() { if (handle != nullptr) { SDL_CloseIO(handle); } } }; #endif std::string FailedToOpenFileErrorMessage(std::string_view path, std::string_view error); [[noreturn]] inline void FailedToOpenFileError(std::string_view path, std::string_view error) { app_fatal(FailedToOpenFileErrorMessage(path, error)); } inline bool ValidatAssetRef(std::string_view path, const AssetRef &ref) { if (ref.ok()) return true; if (!HeadlessMode) { FailedToOpenFileError(path, ref.error()); } return false; } inline bool ValidateHandle(std::string_view path, const AssetHandle &handle) { if (handle.ok()) return true; if (!HeadlessMode) { FailedToOpenFileError(path, handle.error()); } return false; } AssetRef FindAsset(std::string_view filename); AssetHandle OpenAsset(AssetRef &&ref, bool threadsafe = false); AssetHandle OpenAsset(std::string_view filename, bool threadsafe = false); AssetHandle OpenAsset(std::string_view filename, size_t &fileSize, bool threadsafe = false); SDL_IOStream *OpenAssetAsSdlRwOps(std::string_view filename, bool threadsafe = false); struct AssetData { std::unique_ptr data; size_t size; explicit operator std::string_view() const { return std::string_view(data.get(), size); } }; tl::expected LoadAsset(std::string_view path); #ifdef UNPACKED_MPQS using MpqArchiveT = std::string; #else using MpqArchiveT = MpqArchive; #endif extern DVL_API_FOR_TEST std::map> MpqArchives; constexpr int MainMpqPriority = 1000; constexpr int DevilutionXMpqPriority = 9000; constexpr int LangMpqPriority = 9100; constexpr int FontMpqPriority = 9200; extern bool HasHellfireMpq; void LoadCoreArchives(); void LoadLanguageArchive(); void LoadGameArchives(); void LoadHellfireArchives(); void UnloadModArchives(); void LoadModArchives(std::span modnames); #ifdef BUILD_TESTING [[nodiscard]] inline bool HaveMainData() { return MpqArchives.find(MainMpqPriority) != MpqArchives.end(); } #endif [[nodiscard]] inline bool HaveExtraFonts() { return MpqArchives.find(FontMpqPriority) != MpqArchives.end(); } [[nodiscard]] inline bool HaveHellfire() { return HasHellfireMpq; } [[nodiscard]] inline bool HaveIntro() { return FindAsset("gendata\\diablo1.smk").ok(); } [[nodiscard]] inline bool HaveFullMusic() { return FindAsset("music\\dintro.wav").ok() || FindAsset("music\\dintro.mp3").ok(); } [[nodiscard]] inline bool HaveBardAssets() { return FindAsset("plrgfx\\bard\\bha\\bhaas.clx").ok(); } [[nodiscard]] inline bool HaveBarbarianAssets() { return FindAsset("plrgfx\\barbarian\\cha\\chaas.clx").ok(); } } // namespace devilution ================================================ FILE: Source/engine/backbuffer_state.cpp ================================================ #include "engine/backbuffer_state.hpp" #include #include #include "engine/dx.h" #include "utils/enum_traits.h" namespace devilution { namespace { struct RedrawState { enum { RedrawNone, RedrawViewportOnly, RedrawAll } Redraw; std::array::value> redrawComponents; }; struct BackbufferState { RedrawState redrawState; DrawnCursor cursor; }; struct BackbufferPtrAndState { void *ptr; BackbufferState state; }; std::vector States; BackbufferState &GetBackbufferState() { // `PalSurface` is null in headless mode. void *ptr = PalSurface != nullptr ? PalSurface->pixels : nullptr; for (BackbufferPtrAndState &ptrAndState : States) { if (ptrAndState.ptr == ptr) return ptrAndState.state; } States.emplace_back(); BackbufferPtrAndState &ptrAndState = States.back(); ptrAndState.ptr = ptr; ptrAndState.state.redrawState.Redraw = RedrawState::RedrawAll; return ptrAndState.state; } } // namespace bool IsRedrawEverything() { return GetBackbufferState().redrawState.Redraw == RedrawState::RedrawAll; } void RedrawViewport() { for (BackbufferPtrAndState &ptrAndState : States) { if (ptrAndState.state.redrawState.Redraw != RedrawState::RedrawAll) { ptrAndState.state.redrawState.Redraw = RedrawState::RedrawViewportOnly; } } } bool IsRedrawViewport() { return GetBackbufferState().redrawState.Redraw == RedrawState::RedrawViewportOnly; } void RedrawComplete() { GetBackbufferState().redrawState.Redraw = RedrawState::RedrawNone; } void RedrawEverything() { for (BackbufferPtrAndState &ptrAndState : States) { ptrAndState.state.redrawState.Redraw = RedrawState::RedrawAll; } } void InitBackbufferState() { States.clear(); } void RedrawComponent(PanelDrawComponent component) { for (BackbufferPtrAndState &ptrAndState : States) { ptrAndState.state.redrawState.redrawComponents[static_cast(component)] = true; } } bool IsRedrawComponent(PanelDrawComponent component) { return GetBackbufferState().redrawState.redrawComponents[static_cast(component)]; } void RedrawComponentComplete(PanelDrawComponent component) { GetBackbufferState().redrawState.redrawComponents[static_cast(component)] = false; } DrawnCursor &GetDrawnCursor() { return GetBackbufferState().cursor; } } // namespace devilution ================================================ FILE: Source/engine/backbuffer_state.hpp ================================================ #pragma once #include #include "engine/rectangle.hpp" #include "engine/surface.hpp" namespace devilution { enum class PanelDrawComponent { Health, Mana, ControlButtons, Belt, FIRST = Health, LAST = Belt }; struct DrawnCursor { Rectangle rect; uint8_t behindBuffer[8192]; }; void InitBackbufferState(); void RedrawEverything(); bool IsRedrawEverything(); void RedrawViewport(); bool IsRedrawViewport(); void RedrawComplete(); void RedrawComponent(PanelDrawComponent component); bool IsRedrawComponent(PanelDrawComponent component); void RedrawComponentComplete(PanelDrawComponent component); DrawnCursor &GetDrawnCursor(); } // namespace devilution ================================================ FILE: Source/engine/circle.hpp ================================================ #pragma once #include "engine/displacement.hpp" #include "engine/point.hpp" namespace devilution { struct Circle { Point position; int radius; constexpr bool contains(Point point) const { Displacement diff = point - position; int x = diff.deltaX; int y = diff.deltaY; return x * x + y * y < radius * radius; } }; } // namespace devilution ================================================ FILE: Source/engine/clx_sprite.hpp ================================================ #pragma once /** * @file clx_sprite.hpp * * @brief CLX format sprites. * * CLX is a format used for DevilutionX graphics at runtime. * CLX encodes pixel in the same way CL2 but encodes metadata differently. * * Unlike CL2: * * 1. CLX frame header stores frame width and height. * 2. CLX frame header does not store 32-pixel block offsets. * * CLX frame header is 6 bytes: * * Bytes | Type | Value * :-----:|:--------:|------------- * 0..2 | uint16_t | header size * 2..4 | uint16_t | width * 4..6 | uint16_t | height * * CL2 reference: https://github.com/savagesteel/d1-file-formats/blob/master/PC-Mac/CL2.md#2-file-structure */ #include #include #include #include #include #include "appfat.h" #include "utils/endian_read.hpp" #include "utils/intrusive_optional.hpp" namespace devilution { class OptionalClxSprite; /** * @brief A single CLX sprite. */ class ClxSprite { public: explicit constexpr ClxSprite(const uint8_t *data, uint32_t dataSize) : data_(data) , pixel_data_size_(dataSize - LoadLE16(data)) { assert(data != nullptr); } [[nodiscard]] constexpr uint16_t width() const { return LoadLE16(&data_[2]); } [[nodiscard]] constexpr uint16_t height() const { return LoadLE16(&data_[4]); } /** * @brief The raw pixel data (CL2 frame data). * * Format: https://github.com/savagesteel/d1-file-formats/blob/master/PC-Mac/CL2.md#42-cl2-frame-data */ [[nodiscard]] constexpr const uint8_t *pixelData() const { return &data_[LoadLE16(data_)]; } [[nodiscard]] constexpr uint32_t pixelDataSize() const { return pixel_data_size_; } constexpr bool operator==(const ClxSprite &other) const { return data_ == other.data_; } constexpr bool operator!=(const ClxSprite &other) const { return !(*this == other); } private: // For OptionalClxSprite. constexpr ClxSprite() = default; const uint8_t *data_ = nullptr; uint32_t pixel_data_size_ = 0; friend class OptionalClxSprite; }; class OwnedClxSpriteList; class OptionalClxSpriteList; class ClxSpriteListIterator; /** * @brief A list of `ClxSprite`s. */ class ClxSpriteList { public: explicit constexpr ClxSpriteList(const uint8_t *data) : data_(data) { assert(data != nullptr); } ClxSpriteList(const OwnedClxSpriteList &owned); [[nodiscard]] OwnedClxSpriteList clone() const; [[nodiscard]] constexpr uint32_t numSprites() const { return LoadLE32(data_); } [[nodiscard]] constexpr ClxSprite operator[](size_t spriteIndex) const { assert(spriteIndex < numSprites()); const uint32_t begin = spriteOffset(spriteIndex); const uint32_t end = spriteOffset(spriteIndex + 1); return ClxSprite { &data_[begin], end - begin }; } [[nodiscard]] constexpr uint32_t spriteOffset(size_t spriteIndex) const { return LoadLE32(&data_[4 + spriteIndex * 4]); } /** @brief The offset to the next sprite sheet, or file size if this is the last sprite sheet. */ [[nodiscard]] constexpr uint32_t dataSize() const { return LoadLE32(&data_[4 + numSprites() * 4]); } [[nodiscard]] constexpr const uint8_t *data() const { return data_; } [[nodiscard]] constexpr ClxSpriteListIterator begin() const; [[nodiscard]] constexpr ClxSpriteListIterator end() const; private: // For OptionalClxSpriteList. constexpr ClxSpriteList() = default; const uint8_t *data_ = nullptr; friend class OptionalClxSpriteList; }; class ClxSpriteListIterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ClxSprite; using pointer = void; using reference = value_type &; constexpr ClxSpriteListIterator(ClxSpriteList list, size_t index) : list_(list) , index_(index) { } constexpr ClxSprite operator*() { return list_[index_]; } constexpr ClxSpriteListIterator &operator++() { ++index_; return *this; } constexpr ClxSpriteListIterator operator++(int) { auto copy = *this; ++(*this); return copy; } constexpr bool operator==(const ClxSpriteListIterator &other) const { return index_ == other.index_; } constexpr bool operator!=(const ClxSpriteListIterator &other) const { return !(*this == other); } private: ClxSpriteList list_; size_t index_; }; inline constexpr ClxSpriteListIterator ClxSpriteList::begin() const { return { *this, 0 }; } inline constexpr ClxSpriteListIterator ClxSpriteList::end() const { return { *this, numSprites() }; } class OwnedClxSpriteSheet; class OptionalClxSpriteSheet; class ClxSpriteSheetIterator; /** * @brief A sprite sheet is a list of `ClxSpriteList`s. */ class ClxSpriteSheet { public: explicit constexpr ClxSpriteSheet(const uint8_t *data, uint16_t numLists) : data_(data) , num_lists_(numLists) { assert(data != nullptr); assert(num_lists_ > 0); } ClxSpriteSheet(const OwnedClxSpriteSheet &owned); [[nodiscard]] constexpr uint16_t numLists() const { return num_lists_; } [[nodiscard]] constexpr ClxSpriteList operator[](size_t sheetIndex) const { return ClxSpriteList { &data_[sheetOffset(sheetIndex)] }; } [[nodiscard]] constexpr uint32_t sheetOffset(size_t sheetIndex) const { assert(sheetIndex < num_lists_); return LoadLE32(&data_[4 * sheetIndex]); } [[nodiscard]] constexpr const uint8_t *data() const { return data_; } [[nodiscard]] constexpr ClxSpriteSheetIterator begin() const; [[nodiscard]] constexpr ClxSpriteSheetIterator end() const; [[nodiscard]] size_t dataSize() const { return static_cast(&data_[sheetOffset(num_lists_ - 1)] + (*this)[num_lists_ - 1].dataSize() - &data_[0]); } private: // For OptionalClxSpriteSheet. constexpr ClxSpriteSheet() = default; const uint8_t *data_ = nullptr; uint16_t num_lists_ = 0; friend class OptionalClxSpriteSheet; }; class ClxSpriteSheetIterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ClxSpriteList; using pointer = void; using reference = value_type &; constexpr ClxSpriteSheetIterator(ClxSpriteSheet sheet, size_t index) : sheet_(sheet) , index_(index) { } constexpr ClxSpriteList operator*() { return sheet_[index_]; } constexpr ClxSpriteSheetIterator &operator++() { ++index_; return *this; } constexpr ClxSpriteSheetIterator operator++(int) { auto copy = *this; ++(*this); return copy; } constexpr bool operator==(const ClxSpriteSheetIterator &other) const { return index_ == other.index_; } constexpr bool operator!=(const ClxSpriteSheetIterator &other) const { return !(*this == other); } private: ClxSpriteSheet sheet_; size_t index_; }; inline constexpr ClxSpriteSheetIterator ClxSpriteSheet::begin() const { return { *this, 0 }; } inline constexpr ClxSpriteSheetIterator ClxSpriteSheet::end() const { return { *this, num_lists_ }; } class OptionalOwnedClxSpriteList; class OwnedClxSpriteListOrSheet; /** * @brief Implicitly convertible to `ClxSpriteList` and owns its data. */ class OwnedClxSpriteList { public: explicit OwnedClxSpriteList(std::unique_ptr &&data) : data_(std::move(data)) { assert(data_ != nullptr); } OwnedClxSpriteList(OwnedClxSpriteList &&) noexcept = default; OwnedClxSpriteList &operator=(OwnedClxSpriteList &&) noexcept = default; [[nodiscard]] OwnedClxSpriteList clone() const { return ClxSpriteList { *this }.clone(); } [[nodiscard]] ClxSprite operator[](size_t spriteIndex) const { return ClxSpriteList { *this }[spriteIndex]; } [[nodiscard]] uint32_t numSprites() const { return ClxSpriteList { *this }.numSprites(); } [[nodiscard]] size_t dataSize() const { return ClxSpriteList { *this }.dataSize(); } private: // For OptionalOwnedClxSpriteList. OwnedClxSpriteList() = default; std::unique_ptr data_; friend class ClxSpriteList; // for implicit conversion friend class OptionalOwnedClxSpriteList; friend class OwnedClxSpriteListOrSheet; }; inline ClxSpriteList::ClxSpriteList(const OwnedClxSpriteList &owned) : data_(owned.data_.get()) { } inline OwnedClxSpriteList ClxSpriteList::clone() const { const size_t size = dataSize(); std::unique_ptr data { new uint8_t[size] }; memcpy(data.get(), data_, size); return OwnedClxSpriteList { std::move(data) }; } /** * @brief Implicitly convertible to `ClxSpriteSheet` and owns its data. */ class OwnedClxSpriteSheet { public: OwnedClxSpriteSheet(std::unique_ptr &&data, uint16_t numLists) : data_(std::move(data)) , num_lists_(numLists) { assert(data_ != nullptr); assert(numLists > 0); } OwnedClxSpriteSheet(OwnedClxSpriteSheet &&) noexcept = default; OwnedClxSpriteSheet &operator=(OwnedClxSpriteSheet &&) noexcept = default; [[nodiscard]] ClxSpriteList operator[](size_t sheetIndex) const { return ClxSpriteSheet { *this }[sheetIndex]; } [[nodiscard]] ClxSpriteSheetIterator begin() const { return ClxSpriteSheet { *this }.begin(); } [[nodiscard]] ClxSpriteSheetIterator end() const { return ClxSpriteSheet { *this }.end(); } [[nodiscard]] size_t dataSize() const { return ClxSpriteSheet { *this }.dataSize(); } private: // For OptionalOwnedClxSpriteList. OwnedClxSpriteSheet() = default; std::unique_ptr data_; uint16_t num_lists_ = 0; friend class ClxSpriteSheet; // for implicit conversion. friend class OptionalOwnedClxSpriteSheet; friend class OwnedClxSpriteListOrSheet; }; inline ClxSpriteSheet::ClxSpriteSheet(const OwnedClxSpriteSheet &owned) : data_(owned.data_.get()) , num_lists_(owned.num_lists_) { } class OwnedClxSpriteListOrSheet; class OptionalClxSpriteListOrSheet; inline uint16_t GetNumListsFromClxListOrSheetBuffer(const uint8_t *data, size_t size) { const uint32_t maybeNumFrames = LoadLE32(data); // If it is a number of frames, then the last frame offset will be equal to the size of the file. if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) return maybeNumFrames / 4; // Not a sprite sheet. return 0; } /** * @brief A CLX sprite list or a sprite sheet (list of lists). */ class ClxSpriteListOrSheet { public: constexpr ClxSpriteListOrSheet(const uint8_t *data, uint16_t numLists) : data_(data) , num_lists_(numLists) { } ClxSpriteListOrSheet(const OwnedClxSpriteListOrSheet &listOrSheet); [[nodiscard]] constexpr ClxSpriteList list() const { assert(num_lists_ == 0); return ClxSpriteList { data_ }; } [[nodiscard]] constexpr ClxSpriteSheet sheet() const { assert(num_lists_ != 0); return ClxSpriteSheet { data_, num_lists_ }; } [[nodiscard]] constexpr bool isSheet() const { return num_lists_ != 0; } [[nodiscard]] size_t dataSize() const { return isSheet() ? sheet().dataSize() : list().dataSize(); } private: // For OptionalClxSpriteListOrSheet. constexpr ClxSpriteListOrSheet() = default; const uint8_t *data_ = nullptr; uint16_t num_lists_ = 0; friend class OptionalClxSpriteListOrSheet; }; class OptionalOwnedClxSpriteListOrSheet; /** * @brief A CLX sprite list or a sprite sheet (list of lists). */ class OwnedClxSpriteListOrSheet { public: static OwnedClxSpriteListOrSheet FromBuffer(std::unique_ptr &&data, size_t size) { const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(data.get(), size); return OwnedClxSpriteListOrSheet { std::move(data), numLists }; } explicit OwnedClxSpriteListOrSheet(std::unique_ptr &&data, uint16_t numLists) : data_(std::move(data)) , num_lists_(numLists) { } explicit OwnedClxSpriteListOrSheet(OwnedClxSpriteSheet &&sheet) : data_(std::move(sheet.data_)) , num_lists_(sheet.num_lists_) { } explicit OwnedClxSpriteListOrSheet(OwnedClxSpriteList &&list) : data_(std::move(list.data_)) , num_lists_(0) { } [[nodiscard]] ClxSpriteList list() const & { assert(num_lists_ == 0); return ClxSpriteList { data_.get() }; } [[nodiscard]] OwnedClxSpriteList list() && { assert(num_lists_ == 0); return OwnedClxSpriteList { std::move(data_) }; } [[nodiscard]] ClxSpriteSheet sheet() const & { assert(num_lists_ != 0); return ClxSpriteSheet { data_.get(), num_lists_ }; } [[nodiscard]] OwnedClxSpriteSheet sheet() && { assert(num_lists_ != 0); return OwnedClxSpriteSheet { std::move(data_), num_lists_ }; } [[nodiscard]] bool isSheet() const { return num_lists_ != 0; } [[nodiscard]] uint16_t numLists() const { return num_lists_; } [[nodiscard]] size_t dataSize() const { return ClxSpriteListOrSheet { *this }.dataSize(); } private: // For OptionalOwnedClxSpriteListOrSheet. OwnedClxSpriteListOrSheet() = default; std::unique_ptr data_; uint16_t num_lists_ = 0; friend class ClxSpriteListOrSheet; friend class OptionalOwnedClxSpriteListOrSheet; }; inline ClxSpriteListOrSheet::ClxSpriteListOrSheet(const OwnedClxSpriteListOrSheet &listOrSheet) : data_(listOrSheet.data_.get()) , num_lists_(listOrSheet.num_lists_) { } /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSprite { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSprite, ClxSprite, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteList { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSpriteList, ClxSpriteList, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteSheet { DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OptionalClxSpriteSheet, ClxSpriteSheet, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalClxSpriteListOrSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalClxSpriteListOrSheet, ClxSpriteListOrSheet, data_, nullptr); }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalOwnedClxSpriteList { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteList, OwnedClxSpriteList, data_, nullptr) }; /** * @brief Equivalent to `std::optional` but smaller. */ class OptionalOwnedClxSpriteSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteSheet, OwnedClxSpriteSheet, data_, nullptr) }; class OptionalOwnedClxSpriteListOrSheet { public: DEFINE_INTRUSIVE_OPTIONAL(OptionalOwnedClxSpriteListOrSheet, OwnedClxSpriteListOrSheet, data_, nullptr); }; } // namespace devilution ================================================ FILE: Source/engine/demomode.cpp ================================================ #include "engine/demomode.h" #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/events.hpp" #include "game_mode.hpp" #include "gmenu.h" #include "headless_mode.hpp" #include "menu.h" #include "nthread.h" #include "options.h" #include "pfile.h" #include "utils/console.h" #include "utils/display.h" #include "utils/endian_stream.hpp" #include "utils/is_of.hpp" #include "utils/paths.h" #include "utils/str_cat.hpp" namespace devilution { // #define LOG_DEMOMODE_MESSAGES // #define LOG_DEMOMODE_MESSAGES_MOUSEMOTION // #define LOG_DEMOMODE_MESSAGES_RENDERING // #define LOG_DEMOMODE_MESSAGES_GAMETICK namespace { constexpr uint8_t Version = 3; enum class LoadingStatus : uint8_t { Success, FileNotFound, UnsupportedVersion, }; struct MouseMotionEventData { uint16_t x; uint16_t y; }; struct MouseButtonEventData { uint16_t x; uint16_t y; uint16_t mod; uint8_t button; }; struct MouseWheelEventData { int16_t x; int16_t y; uint16_t mod; }; struct KeyEventData { uint32_t sym; uint16_t mod; }; struct DemoMsg { enum EventType : uint8_t { GameTick = 0, Rendering = 1, // Inputs: MinEvent = 8, QuitEvent = 8, MouseMotionEvent = 9, MouseButtonDownEvent = 10, MouseButtonUpEvent = 11, MouseWheelEvent = 12, KeyDownEvent = 13, KeyUpEvent = 14, MinCustomEvent = 64, }; EventType type; uint8_t progressToNextGameTick; union { MouseMotionEventData motion; MouseButtonEventData button; MouseWheelEventData wheel; KeyEventData key; }; [[nodiscard]] bool isEvent() const { return type >= MinEvent; } }; FILE *DemoFile; int DemoFileVersion; int DemoNumber = -1; std::optional CurrentDemoMessage; bool Timedemo = false; int RecordNumber = -1; bool CreateDemoReference = false; // These options affect gameplay and are stored in the demo file. struct { uint8_t tickRate = 20; bool runInTown = false; bool theoQuest = false; bool cowQuest = false; bool autoGoldPickup = false; bool autoElixirPickup = false; bool autoOilPickup = false; bool autoPickupInTown = false; bool autoEquipWeapons = false; bool autoEquipArmor = false; bool autoEquipHelms = false; bool autoEquipShields = false; bool autoEquipJewelry = false; bool randomizeQuests = false; bool showItemLabels = false; bool autoRefillBelt = false; bool disableCripplingShrines = false; uint8_t numHealPotionPickup = 0; uint8_t numFullHealPotionPickup = 0; uint8_t numManaPotionPickup = 0; uint8_t numFullManaPotionPickup = 0; uint8_t numRejuPotionPickup = 0; uint8_t numFullRejuPotionPickup = 0; } DemoSettings; FILE *DemoRecording; uint32_t DemoModeLastTick = 0; int LogicTick = 0; uint32_t StartTime = 0; uint16_t DemoGraphicsWidth = 640; uint16_t DemoGraphicsHeight = 480; void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-length) { DemoGraphicsWidth = ReadLE16(in); DemoGraphicsHeight = ReadLE16(in); if (version > 0) { DemoSettings.runInTown = ReadByte(in) != 0; DemoSettings.theoQuest = ReadByte(in) != 0; DemoSettings.cowQuest = ReadByte(in) != 0; DemoSettings.autoGoldPickup = ReadByte(in) != 0; DemoSettings.autoElixirPickup = ReadByte(in) != 0; DemoSettings.autoOilPickup = ReadByte(in) != 0; DemoSettings.autoPickupInTown = ReadByte(in) != 0; (void)ReadByte(in); // adriaRefillsMana (removed feature, kept for backward compatibility) DemoSettings.autoEquipWeapons = ReadByte(in) != 0; DemoSettings.autoEquipArmor = ReadByte(in) != 0; DemoSettings.autoEquipHelms = ReadByte(in) != 0; DemoSettings.autoEquipShields = ReadByte(in) != 0; DemoSettings.autoEquipJewelry = ReadByte(in) != 0; DemoSettings.randomizeQuests = ReadByte(in) != 0; DemoSettings.showItemLabels = ReadByte(in) != 0; DemoSettings.autoRefillBelt = ReadByte(in) != 0; DemoSettings.disableCripplingShrines = ReadByte(in) != 0; DemoSettings.numHealPotionPickup = ReadByte(in); DemoSettings.numFullHealPotionPickup = ReadByte(in); DemoSettings.numManaPotionPickup = ReadByte(in); DemoSettings.numFullManaPotionPickup = ReadByte(in); DemoSettings.numRejuPotionPickup = ReadByte(in); DemoSettings.numFullRejuPotionPickup = ReadByte(in); } else { DemoSettings = {}; } std::string message = StrCat("⚙️\n", _("Resolution"), "=", DemoGraphicsWidth, "x", DemoGraphicsHeight); for (const auto &[key, value] : std::initializer_list> { { _("Run in Town"), DemoSettings.runInTown }, { _("Theo Quest"), DemoSettings.theoQuest }, { _("Cow Quest"), DemoSettings.cowQuest }, { _("Auto Gold Pickup"), DemoSettings.autoGoldPickup }, { _("Auto Elixir Pickup"), DemoSettings.autoGoldPickup }, { _("Auto Oil Pickup"), DemoSettings.autoOilPickup }, { _("Auto Pickup in Town"), DemoSettings.autoPickupInTown }, { _("Auto Equip Weapons"), DemoSettings.autoEquipWeapons }, { _("Auto Equip Armor"), DemoSettings.autoEquipArmor }, { _("Auto Equip Helms"), DemoSettings.autoEquipHelms }, { _("Auto Equip Shields"), DemoSettings.autoEquipShields }, { _("Auto Equip Jewelry"), DemoSettings.autoEquipJewelry }, { _("Randomize Quests"), DemoSettings.randomizeQuests }, { _("Show Item Labels"), DemoSettings.showItemLabels }, { _("Auto Refill Belt"), DemoSettings.autoRefillBelt }, { _("Disable Crippling Shrines"), DemoSettings.disableCripplingShrines } }) { StrAppend(message, "\n", key, "=", value ? "1" : "0"); } for (const auto &[key, value] : std::initializer_list> { { _("Heal Potion Pickup"), DemoSettings.numHealPotionPickup }, { _("Full Heal Potion Pickup"), DemoSettings.numFullHealPotionPickup }, { _("Mana Potion Pickup"), DemoSettings.numManaPotionPickup }, { _("Full Mana Potion Pickup"), DemoSettings.numFullManaPotionPickup }, { _("Rejuvenation Potion Pickup"), DemoSettings.numRejuPotionPickup }, { _("Full Rejuvenation Potion Pickup"), DemoSettings.numFullRejuPotionPickup } }) { StrAppend(message, "\n", key, "=", static_cast(value)); } Log("{}", message); } void WriteSettings(FILE *out) { WriteLE16(out, gnScreenWidth); WriteLE16(out, gnScreenHeight); const Options &options = GetOptions(); WriteByte(out, static_cast(*options.Gameplay.runInTown)); WriteByte(out, static_cast(*options.Gameplay.theoQuest)); WriteByte(out, static_cast(*options.Gameplay.cowQuest)); WriteByte(out, static_cast(*options.Gameplay.autoGoldPickup)); WriteByte(out, static_cast(*options.Gameplay.autoElixirPickup)); WriteByte(out, static_cast(*options.Gameplay.autoOilPickup)); WriteByte(out, static_cast(*options.Gameplay.autoPickupInTown)); WriteByte(out, 0); // adriaRefillsMana (removed feature, kept for backward compatibility) WriteByte(out, static_cast(*options.Gameplay.autoEquipWeapons)); WriteByte(out, static_cast(*options.Gameplay.autoEquipArmor)); WriteByte(out, static_cast(*options.Gameplay.autoEquipHelms)); WriteByte(out, static_cast(*options.Gameplay.autoEquipShields)); WriteByte(out, static_cast(*options.Gameplay.autoEquipJewelry)); WriteByte(out, static_cast(*options.Gameplay.randomizeQuests)); WriteByte(out, static_cast(*options.Gameplay.showItemLabels)); WriteByte(out, static_cast(*options.Gameplay.autoRefillBelt)); WriteByte(out, static_cast(*options.Gameplay.disableCripplingShrines)); WriteByte(out, *options.Gameplay.numHealPotionPickup); WriteByte(out, *options.Gameplay.numFullHealPotionPickup); WriteByte(out, *options.Gameplay.numManaPotionPickup); WriteByte(out, *options.Gameplay.numFullManaPotionPickup); WriteByte(out, *options.Gameplay.numRejuPotionPickup); WriteByte(out, *options.Gameplay.numFullRejuPotionPickup); } #if SDL_VERSION_ATLEAST(2, 0, 0) bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { const uint8_t type = dmsg.type; switch (type) { case DemoMsg::MouseMotionEvent: #ifdef USE_SDL3 event.type = SDL_EVENT_MOUSE_MOTION; event.motion.state = 0; event.motion.xrel = 0.F; event.motion.yrel = 0.F; #else event.type = SDL_MOUSEMOTION; #endif event.motion.which = 0; event.motion.x = dmsg.motion.x; event.motion.y = dmsg.motion.y; return true; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: #ifdef USE_SDL3 event.type = type == DemoMsg::MouseButtonDownEvent ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; event.button.down = type == DemoMsg::MouseButtonDownEvent; event.button.clicks = 1; #else event.type = type == DemoMsg::MouseButtonDownEvent ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.state = type == DemoMsg::MouseButtonDownEvent ? SDL_PRESSED : SDL_RELEASED; #endif event.button.which = 0; event.button.button = dmsg.button.button; event.button.x = dmsg.button.x; event.button.y = dmsg.button.y; modState = dmsg.button.mod; return true; case DemoMsg::MouseWheelEvent: #ifdef USE_SDL3 event.type = SDL_EVENT_MOUSE_WHEEL; #if SDL_VERSION_ATLEAST(3, 2, 12) event.wheel.integer_x = dmsg.wheel.x; event.wheel.integer_y = dmsg.wheel.y; #else event.wheel.x = dmsg.wheel.x; event.wheel.y = dmsg.wheel.y; #endif event.wheel.mouse_x = 0; event.wheel.mouse_y = 0; #else event.type = SDL_MOUSEWHEEL; #endif event.wheel.which = 0; event.wheel.x = dmsg.wheel.x; event.wheel.y = dmsg.wheel.y; modState = dmsg.wheel.mod; return true; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: #ifdef USE_SDL3 event.type = type == DemoMsg::KeyDownEvent ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; event.key.down = type == DemoMsg::KeyDownEvent; event.key.scancode = SDL_GetScancodeFromKey(dmsg.key.sym, nullptr); event.key.key = dmsg.key.sym; event.key.mod = dmsg.key.mod; #else event.type = type == DemoMsg::KeyDownEvent ? SDL_KEYDOWN : SDL_KEYUP; event.key.state = type == DemoMsg::KeyDownEvent ? SDL_PRESSED : SDL_RELEASED; event.key.keysym.scancode = SDL_GetScancodeFromKey(dmsg.key.sym); event.key.keysym.sym = dmsg.key.sym; event.key.keysym.mod = dmsg.key.mod; #endif return true; default: if (type >= DemoMsg::MinCustomEvent) { CustomEventToSdlEvent(event, static_cast(type - DemoMsg::MinCustomEvent)); return true; } event.type = static_cast(0); LogWarn("Unsupported demo event (type={})", type); return false; } } #else SDLKey Sdl2ToSdl1Key(uint32_t key) { if ((key & (1 << 30)) != 0) { constexpr uint32_t Keys1Start = 57; constexpr SDLKey Keys1[] { SDLK_CAPSLOCK, SDLK_F1, SDLK_F2, SDLK_F3, SDLK_F4, SDLK_F5, SDLK_F6, SDLK_F7, SDLK_F8, SDLK_F9, SDLK_F10, SDLK_F11, SDLK_F12, SDLK_PRINTSCREEN, SDLK_SCROLLLOCK, SDLK_PAUSE, SDLK_INSERT, SDLK_HOME, SDLK_PAGEUP, SDLK_DELETE, SDLK_END, SDLK_PAGEDOWN, SDLK_RIGHT, SDLK_LEFT, SDLK_DOWN, SDLK_UP, SDLK_NUMLOCKCLEAR, SDLK_KP_DIVIDE, SDLK_KP_MULTIPLY, SDLK_KP_MINUS, SDLK_KP_PLUS, SDLK_KP_ENTER, SDLK_KP_1, SDLK_KP_2, SDLK_KP_3, SDLK_KP_4, SDLK_KP_5, SDLK_KP_6, SDLK_KP_7, SDLK_KP_8, SDLK_KP_9, SDLK_KP_0, SDLK_KP_PERIOD }; constexpr uint32_t Keys2Start = 224; constexpr SDLKey Keys2[] { SDLK_LCTRL, SDLK_LSHIFT, SDLK_LALT, SDLK_LGUI, SDLK_RCTRL, SDLK_RSHIFT, SDLK_RALT, SDLK_RGUI, SDLK_MODE }; const uint32_t scancode = key & ~(1 << 30); if (scancode >= Keys1Start) { if (scancode < Keys1Start + sizeof(Keys1) / sizeof(Keys1[0])) return Keys1[scancode - Keys1Start]; if (scancode >= Keys2Start && scancode < Keys2Start + sizeof(Keys2) / sizeof(Keys2[0])) return Keys2[scancode - Keys2Start]; } LogWarn("Demo: unknown key {:d}", key); return SDLK_UNKNOWN; } if (key <= 122) { return static_cast(key); } LogWarn("Demo: unknown key {:d}", key); return SDLK_UNKNOWN; } uint8_t Sdl2ToSdl1MouseButton(uint8_t button) { switch (button) { case 4: return SDL_BUTTON_X1; case 5: return SDL_BUTTON_X2; default: return button; } } bool CreateSdlEvent(const DemoMsg &dmsg, SDL_Event &event, uint16_t &modState) { const uint8_t type = dmsg.type; switch (type) { case DemoMsg::MouseMotionEvent: event.type = SDL_MOUSEMOTION; event.motion.which = 0; event.motion.x = dmsg.motion.x; event.motion.y = dmsg.motion.y; return true; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: event.type = type == DemoMsg::MouseButtonDownEvent ? SDL_MOUSEBUTTONDOWN : SDL_MOUSEBUTTONUP; event.button.which = 0; event.button.button = Sdl2ToSdl1MouseButton(dmsg.button.button); event.button.state = type == DemoMsg::MouseButtonDownEvent ? SDL_PRESSED : SDL_RELEASED; event.button.x = dmsg.button.x; event.button.y = dmsg.button.y; modState = dmsg.button.mod; return true; case DemoMsg::MouseWheelEvent: if (dmsg.wheel.y == 0) { LogWarn("Demo: unsupported event (mouse wheel y == 0)"); return false; } event.type = SDL_MOUSEBUTTONDOWN; event.button.which = 0; event.button.button = dmsg.wheel.y > 0 ? SDL_BUTTON_WHEELUP : SDL_BUTTON_WHEELDOWN; modState = dmsg.wheel.mod; return true; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: event.type = type == DemoMsg::KeyDownEvent ? SDL_KEYDOWN : SDL_KEYUP; event.key.which = 0; event.key.state = type == DemoMsg::KeyDownEvent ? SDL_PRESSED : SDL_RELEASED; event.key.keysym.sym = Sdl2ToSdl1Key(dmsg.key.sym); event.key.keysym.mod = static_cast(dmsg.key.mod); return true; default: if (type >= DemoMsg::MinCustomEvent) { CustomEventToSdlEvent(event, static_cast(type - DemoMsg::MinCustomEvent)); return true; } event.type = static_cast(0); LogWarn("Demo: unsupported event (type={:x})", type); return false; } } #endif uint8_t MapPreV2DemoMsgEventType(uint16_t type) { switch (type) { case 0x100: return DemoMsg::QuitEvent; case 0x300: return DemoMsg::KeyDownEvent; case 0x301: return DemoMsg::KeyUpEvent; case 0x400: return DemoMsg::MouseMotionEvent; case 0x401: return DemoMsg::MouseButtonDownEvent; case 0x402: return DemoMsg::MouseButtonUpEvent; case 0x403: return DemoMsg::MouseWheelEvent; default: if (type < 0x8000) { // SDL_USEREVENT app_fatal(StrCat("Unknown event ", type)); } return DemoMsg::MinCustomEvent + (type - 0x8000); } } void LogDemoMessage(const DemoMsg &dmsg) { #ifdef LOG_DEMOMODE_MESSAGES const uint8_t progressToNextGameTick = dmsg.progressToNextGameTick; switch (dmsg.type) { case DemoMsg::GameTick: #ifdef LOG_DEMOMODE_MESSAGES_GAMETICK Log("⏲️ GameTick {:>3}", progressToNextGameTick); #endif break; case DemoMsg::Rendering: #ifdef LOG_DEMOMODE_MESSAGES_RENDERING Log("🖼️ Rendering {:>3}", progressToNextGameTick); #endif break; case DemoMsg::MouseMotionEvent: #ifdef LOG_DEMOMODE_MESSAGES_MOUSEMOTION Log("🖱️ Message {:>3} MOUSEMOTION {} {}", progressToNextGameTick, dmsg.motion.x, dmsg.motion.y); #endif break; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: Log("🖱️ Message {:>3} {} {} {} {} 0x{:x}", progressToNextGameTick, dmsg.type == DemoMsg::MouseButtonDownEvent ? "MOUSEBUTTONDOWN" : "MOUSEBUTTONUP", dmsg.button.button, dmsg.button.x, dmsg.button.y, dmsg.button.mod); break; case DemoMsg::MouseWheelEvent: Log("🖱️ Message {:>3} MOUSEWHEEL {} {} 0x{:x}", progressToNextGameTick, dmsg.wheel.x, dmsg.wheel.y, dmsg.wheel.mod); break; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: Log("🔤 Message {:>3} {} 0x{:x} 0x{:x}", progressToNextGameTick, dmsg.type == DemoMsg::KeyDownEvent ? "KEYDOWN" : "KEYUP", dmsg.key.sym, dmsg.key.mod); break; case DemoMsg::QuitEvent: Log("❎ Message {:>3} QUIT", progressToNextGameTick); break; default: Log("📨 Message {:>3} USEREVENT {}", progressToNextGameTick, static_cast(dmsg.type)); break; } #endif // LOG_DEMOMODE_MESSAGES } void CloseDemoFile() { if (DemoFile != nullptr) { std::fclose(DemoFile); DemoFile = nullptr; } } LoadingStatus OpenDemoFile(int demoNumber) { CloseDemoFile(); const std::string path = StrCat(paths::PrefPath(), "demo_", demoNumber, ".dmo"); DemoFile = OpenFile(path.c_str(), "rb"); if (DemoFile == nullptr) { return LoadingStatus::FileNotFound; } DemoFileVersion = ReadByte(DemoFile); if (DemoFileVersion > Version) { return LoadingStatus::UnsupportedVersion; } DemoNumber = demoNumber; gSaveNumber = ReadLE32(DemoFile); ReadSettings(DemoFile, DemoFileVersion); return LoadingStatus::Success; } std::optional ReadDemoMessage() { const uint8_t typeNum = DemoFileVersion >= 2 ? ReadByte(DemoFile) : ReadLE32(DemoFile); if (std::feof(DemoFile) != 0) { CloseDemoFile(); return std::nullopt; } // Events with the high bit 1 are Rendering events with the rest of the bits used // to encode `progressToNextGameTick` inline. if ((typeNum & 0b10000000) != 0) { DemoModeLastTick = SDL_GetTicks(); return DemoMsg { DemoMsg::Rendering, static_cast(typeNum & 0b01111111u), {} }; } const uint8_t progressToNextGameTick = ReadByte(DemoFile); switch (typeNum) { case DemoMsg::GameTick: case DemoMsg::Rendering: DemoModeLastTick = SDL_GetTicks(); return DemoMsg { static_cast(typeNum), progressToNextGameTick, {} }; default: { const uint8_t eventType = DemoFileVersion >= 2 ? typeNum : MapPreV2DemoMsgEventType(static_cast(ReadLE32(DemoFile))); DemoMsg result { static_cast(eventType), progressToNextGameTick, {} }; switch (eventType) { case DemoMsg::MouseMotionEvent: { result.motion.x = ReadLE16(DemoFile); result.motion.y = ReadLE16(DemoFile); } break; case DemoMsg::MouseButtonDownEvent: case DemoMsg::MouseButtonUpEvent: { result.button.button = ReadByte(DemoFile); result.button.x = ReadLE16(DemoFile); result.button.y = ReadLE16(DemoFile); result.button.mod = ReadLE16(DemoFile); } break; case DemoMsg::MouseWheelEvent: { result.wheel.x = DemoFileVersion >= 2 ? ReadLE16(DemoFile) : static_cast(ReadLE32(DemoFile)); result.wheel.y = DemoFileVersion >= 2 ? ReadLE16(DemoFile) : static_cast(ReadLE32(DemoFile)); result.wheel.mod = ReadLE16(DemoFile); } break; case DemoMsg::KeyDownEvent: case DemoMsg::KeyUpEvent: { result.key.sym = static_cast(ReadLE32(DemoFile)); result.key.mod = static_cast(ReadLE16(DemoFile)); } break; case DemoMsg::QuitEvent: // SDL_QUIT break; default: if (eventType < DemoMsg::MinCustomEvent) { app_fatal(StrCat("Unknown event ", eventType)); } break; } DemoModeLastTick = SDL_GetTicks(); return result; } break; } } void WriteDemoMsgHeader(DemoMsg::EventType type) { if (type == DemoMsg::Rendering && ProgressToNextGameTick <= 127) { WriteByte(DemoRecording, ProgressToNextGameTick | 0b10000000); return; } WriteByte(DemoRecording, type); WriteByte(DemoRecording, ProgressToNextGameTick); } } // namespace namespace demo { void InitPlayBack(int demoNumber, bool timedemo) { Timedemo = timedemo; ControlMode = ControlTypes::KeyboardAndMouse; const LoadingStatus status = OpenDemoFile(demoNumber); switch (status) { case LoadingStatus::Success: return; case LoadingStatus::FileNotFound: printInConsole("Demo file not found"); break; case LoadingStatus::UnsupportedVersion: printInConsole("Unsupported Demo version"); break; } printNewlineInConsole(); diablo_quit(1); } void InitRecording(int recordNumber, bool createDemoReference) { RecordNumber = recordNumber; CreateDemoReference = createDemoReference; } void OverrideOptions() { #ifndef USE_SDL1 GetOptions().Graphics.fitToScreen.SetValue(false); #endif #if SDL_VERSION_ATLEAST(2, 0, 0) GetOptions().Graphics.hardwareCursor.SetValue(false); #endif if (Timedemo) { GetOptions().Graphics.frameRateControl.SetValue(FrameRateControl::None); } forceResolution = Size(DemoGraphicsWidth, DemoGraphicsHeight); Options &options = GetOptions(); options.Gameplay.runInTown.SetValue(DemoSettings.runInTown); options.Gameplay.theoQuest.SetValue(DemoSettings.theoQuest); options.Gameplay.cowQuest.SetValue(DemoSettings.cowQuest); options.Gameplay.autoGoldPickup.SetValue(DemoSettings.autoGoldPickup); options.Gameplay.autoElixirPickup.SetValue(DemoSettings.autoElixirPickup); options.Gameplay.autoOilPickup.SetValue(DemoSettings.autoOilPickup); options.Gameplay.autoPickupInTown.SetValue(DemoSettings.autoPickupInTown); options.Gameplay.autoEquipWeapons.SetValue(DemoSettings.autoEquipWeapons); options.Gameplay.autoEquipArmor.SetValue(DemoSettings.autoEquipArmor); options.Gameplay.autoEquipHelms.SetValue(DemoSettings.autoEquipHelms); options.Gameplay.autoEquipShields.SetValue(DemoSettings.autoEquipShields); options.Gameplay.autoEquipJewelry.SetValue(DemoSettings.autoEquipJewelry); options.Gameplay.randomizeQuests.SetValue(DemoSettings.randomizeQuests); options.Gameplay.showItemLabels.SetValue(DemoSettings.showItemLabels); options.Gameplay.autoRefillBelt.SetValue(DemoSettings.autoRefillBelt); options.Gameplay.disableCripplingShrines.SetValue(DemoSettings.disableCripplingShrines); options.Gameplay.numHealPotionPickup.SetValue(DemoSettings.numHealPotionPickup); options.Gameplay.numFullHealPotionPickup.SetValue(DemoSettings.numFullHealPotionPickup); options.Gameplay.numManaPotionPickup.SetValue(DemoSettings.numManaPotionPickup); options.Gameplay.numFullManaPotionPickup.SetValue(DemoSettings.numFullManaPotionPickup); options.Gameplay.numRejuPotionPickup.SetValue(DemoSettings.numRejuPotionPickup); options.Gameplay.numFullRejuPotionPickup.SetValue(DemoSettings.numFullRejuPotionPickup); } bool IsRunning() { return DemoNumber != -1; } bool IsRecording() { return RecordNumber != -1; } bool GetRunGameLoop(bool &drawGame, bool &processInput) { if (CurrentDemoMessage == std::nullopt && DemoFile != nullptr) CurrentDemoMessage = ReadDemoMessage(); if (CurrentDemoMessage == std::nullopt) app_fatal("Demo queue empty"); const DemoMsg &dmsg = *CurrentDemoMessage; if (CurrentDemoMessage->isEvent()) app_fatal("Unexpected event demo message in GetRunGameLoop"); LogDemoMessage(dmsg); if (Timedemo) { // disable additional rendering to speedup replay drawGame = dmsg.type == DemoMsg::GameTick && !HeadlessMode; } else { const int currentTickCount = SDL_GetTicks(); const int ticksElapsed = currentTickCount - DemoModeLastTick; const bool tickDue = ticksElapsed >= gnTickDelay; drawGame = false; if (tickDue) { if (dmsg.type == DemoMsg::GameTick) { DemoModeLastTick = currentTickCount; } } else { int32_t fraction = ticksElapsed * AnimationInfo::baseValueFraction / gnTickDelay; fraction = std::clamp(fraction, 0, AnimationInfo::baseValueFraction); const uint8_t progressToNextGameTick = static_cast(fraction); if (dmsg.type == DemoMsg::GameTick || dmsg.progressToNextGameTick > progressToNextGameTick) { // we are ahead of the replay => add a additional rendering for smoothness if (gbRunGame && PauseMode == 0 && (gbIsMultiplayer || !gmenu_is_active()) && gbProcessPlayers) // if game is not running or paused there is no next gametick in the near future ProgressToNextGameTick = progressToNextGameTick; processInput = false; drawGame = true; return false; } } } ProgressToNextGameTick = dmsg.progressToNextGameTick; const bool isGameTick = dmsg.type == DemoMsg::GameTick; CurrentDemoMessage = std::nullopt; if (isGameTick) LogicTick++; return isGameTick; } bool FetchMessage(SDL_Event *event, uint16_t *modState) { if (CurrentEventHandler == DisableInputEventHandler) return false; SDL_Event e; if ( #ifdef USE_SDL3 SDL_PollEvent(&e) #else SDL_PollEvent(&e) != 0 #endif ) { if (e.type == #ifdef USE_SDL3 SDL_EVENT_QUIT #else SDL_QUIT #endif ) { *event = e; return true; } if (e.type == #ifdef USE_SDL3 SDL_EVENT_KEY_DOWN #else SDL_KEYDOWN #endif ) { const SDL_Keycode key = #ifdef USE_SDL3 e.key.key; #else e.key.keysym.sym; #endif if (key == SDLK_ESCAPE) { CloseDemoFile(); CurrentDemoMessage = std::nullopt; DemoNumber = -1; Timedemo = false; last_tick = SDL_GetTicks(); } else if (IsAnyOf(key, SDLK_KP_PLUS, SDLK_PLUS) && sgGameInitInfo.nTickRate < 255) { sgGameInitInfo.nTickRate++; GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; } else if (IsAnyOf(key, SDLK_KP_MINUS, SDLK_MINUS) && sgGameInitInfo.nTickRate > 1) { sgGameInitInfo.nTickRate--; GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; } } } if (CurrentDemoMessage == std::nullopt && DemoFile != nullptr) CurrentDemoMessage = ReadDemoMessage(); if (CurrentDemoMessage != std::nullopt) { const DemoMsg &dmsg = *CurrentDemoMessage; LogDemoMessage(dmsg); if (dmsg.isEvent()) { const bool hasEvent = CreateSdlEvent(dmsg, *event, *modState); ProgressToNextGameTick = dmsg.progressToNextGameTick; CurrentDemoMessage = std::nullopt; return hasEvent; } } return false; } void RecordGameLoopResult(bool runGameLoop) { WriteDemoMsgHeader(runGameLoop ? DemoMsg::GameTick : DemoMsg::Rendering); if (runGameLoop && !IsRunning()) LogicTick++; } void RecordMessage(const SDL_Event &event, uint16_t modState) { if (!gbRunGame || DemoRecording == nullptr) return; if (CurrentEventHandler == DisableInputEventHandler) return; switch (event.type) { #ifdef USE_SDL3 case SDL_EVENT_MOUSE_MOTION: #else case SDL_MOUSEMOTION: #endif WriteDemoMsgHeader(DemoMsg::MouseMotionEvent); WriteLE16(DemoRecording, event.motion.x); WriteLE16(DemoRecording, event.motion.y); break; #ifdef USE_SDL3 case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: #else case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: #endif #ifdef USE_SDL1 if (event.button.button == SDL_BUTTON_WHEELUP || event.button.button == SDL_BUTTON_WHEELDOWN) { WriteDemoMsgHeader(DemoMsg::MouseWheelEvent); WriteLE16(DemoRecording, 0); WriteLE16(DemoRecording, event.button.button == SDL_BUTTON_WHEELUP ? 1 : -1); WriteLE16(DemoRecording, modState); } else { #endif WriteDemoMsgHeader( #ifdef USE_SDL3 event.button.down #else event.type == SDL_MOUSEBUTTONDOWN #endif ? DemoMsg::MouseButtonDownEvent : DemoMsg::MouseButtonUpEvent); WriteByte(DemoRecording, event.button.button); WriteLE16(DemoRecording, event.button.x); WriteLE16(DemoRecording, event.button.y); WriteLE16(DemoRecording, modState); #ifdef USE_SDL1 } #endif break; #ifndef USE_SDL1 #ifdef USE_SDL3 case SDL_EVENT_MOUSE_WHEEL: #else case SDL_MOUSEWHEEL: #endif WriteDemoMsgHeader(DemoMsg::MouseWheelEvent); #ifdef USE_SDL3 int wheelX, wheelY; #if SDL_VERSION_ATLEAST(3, 2, 12) wheelX = event.wheel.integer_x; wheelY = event.wheel.integer_y; #else wheelX = event.wheel.x; wheelY = event.wheel.y; #endif if (wheelX < std::numeric_limits::min() || wheelX > std::numeric_limits::max() || wheelY < std::numeric_limits::min() || wheelY > std::numeric_limits::max()) { app_fatal(StrCat("Mouse wheel event integer_x/y out of int16_t range. x=", wheelX, " y=", wheelY)); } WriteLE16(DemoRecording, wheelX); WriteLE16(DemoRecording, wheelY); #else if (event.wheel.x < std::numeric_limits::min() || event.wheel.x > std::numeric_limits::max() || event.wheel.y < std::numeric_limits::min() || event.wheel.y > std::numeric_limits::max()) { app_fatal(StrCat("Mouse wheel event x/y out of int16_t range. x=", event.wheel.x, " y=", event.wheel.y)); } WriteLE16(DemoRecording, event.wheel.x); WriteLE16(DemoRecording, event.wheel.y); #endif WriteLE16(DemoRecording, modState); break; #endif #ifdef USE_SDL3 case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: WriteDemoMsgHeader(event.key.down ? DemoMsg::KeyDownEvent : DemoMsg::KeyUpEvent); WriteLE32(DemoRecording, static_cast(event.key.key)); WriteLE16(DemoRecording, static_cast(event.key.mod)); break; #else case SDL_KEYDOWN: case SDL_KEYUP: WriteDemoMsgHeader(event.type == SDL_KEYDOWN ? DemoMsg::KeyDownEvent : DemoMsg::KeyUpEvent); WriteLE32(DemoRecording, static_cast(event.key.keysym.sym)); WriteLE16(DemoRecording, static_cast(event.key.keysym.mod)); break; #endif #ifndef USE_SDL1 #ifndef USE_SDL3 case SDL_WINDOWEVENT: if (event.window.type == SDL_WINDOWEVENT_CLOSE) { WriteDemoMsgHeader(DemoMsg::QuitEvent); } break; #endif #endif #ifdef USE_SDL3 case SDL_EVENT_QUIT: #else case SDL_QUIT: #endif WriteDemoMsgHeader(DemoMsg::QuitEvent); break; default: if (IsCustomEvent(event.type)) { WriteDemoMsgHeader(static_cast( DemoMsg::MinCustomEvent + static_cast(GetCustomEvent(event)))); } break; } } void NotifyGameLoopStart() { LogicTick = 0; if (IsRunning()) { StartTime = SDL_GetTicks(); } if (IsRecording()) { const std::string path = StrCat(paths::PrefPath(), "demo_", RecordNumber, ".dmo"); DemoRecording = OpenFile(path.c_str(), "wb"); if (DemoRecording == nullptr) { RecordNumber = -1; LogError("Failed to open {} for writing", path); return; } WriteByte(DemoRecording, Version); WriteLE32(DemoRecording, gSaveNumber); WriteSettings(DemoRecording); } } void NotifyGameLoopEnd() { if (IsRecording()) { std::fclose(DemoRecording); DemoRecording = nullptr; if (CreateDemoReference) pfile_write_hero_demo(RecordNumber); RecordNumber = -1; CreateDemoReference = false; } if (IsRunning() && !HeadlessMode) { const float seconds = (SDL_GetTicks() - StartTime) / 1000.0F; Log("{} frames, {:.2f} seconds: {:.1f} fps", LogicTick, seconds, LogicTick / seconds); gbRunGameResult = false; gbRunGame = false; HeroCompareResult compareResult = pfile_compare_hero_demo(DemoNumber, false); switch (compareResult.status) { case HeroCompareResult::ReferenceNotFound: Log("Timedemo: No final comparison because reference is not present."); break; case HeroCompareResult::Same: Log("Timedemo: Same outcome as initial run. :)"); break; case HeroCompareResult::Difference: Log("Timedemo: Different outcome than initial run. ;(\n{}", compareResult.message); break; } } } uint32_t SimulateMillisecondsSinceStartup() { return LogicTick * 50; } } // namespace demo } // namespace devilution ================================================ FILE: Source/engine/demomode.h ================================================ /** * @file demomode.h * * Contains most of the the demomode specific logic */ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { namespace demo { #ifndef DISABLE_DEMOMODE void InitPlayBack(int demoNumber, bool timedemo); void InitRecording(int recordNumber, bool createDemoReference); void OverrideOptions(); bool IsRunning(); bool IsRecording(); bool GetRunGameLoop(bool &drawGame, bool &processInput); bool FetchMessage(SDL_Event *event, uint16_t *modState); void RecordGameLoopResult(bool runGameLoop); void RecordMessage(const SDL_Event &event, uint16_t modState); void NotifyGameLoopStart(); void NotifyGameLoopEnd(); uint32_t SimulateMillisecondsSinceStartup(); #else inline void OverrideOptions() { } inline bool IsRunning() { return false; } inline bool IsRecording() { return false; } inline bool GetRunGameLoop(bool &, bool &) { return false; } inline bool FetchMessage(SDL_Event *, uint16_t *) { return false; } inline void RecordGameLoopResult(bool) { } inline void RecordMessage(const SDL_Event &, uint16_t) { } inline void NotifyGameLoopStart() { } inline void NotifyGameLoopEnd() { } inline uint32_t SimulateMillisecondsSinceStartup() { return 0; } #endif } // namespace demo } // namespace devilution ================================================ FILE: Source/engine/direction.cpp ================================================ #include "engine/direction.hpp" namespace devilution { std::string_view DirectionToString(Direction direction) { switch (direction) { case Direction::South: return "South"; case Direction::SouthWest: return "SouthWest"; case Direction::West: return "West"; case Direction::NorthWest: return "NorthWest"; case Direction::North: return "North"; case Direction::NorthEast: return "NorthEast"; case Direction::East: return "East"; case Direction::SouthEast: return "SouthEast"; case Direction::NoDirection: return ""; } return "Invalid"; } } // namespace devilution ================================================ FILE: Source/engine/direction.hpp ================================================ #pragma once #include #include #include #include "utils/attributes.h" namespace devilution { enum class Direction : std::uint8_t { South, SouthWest, West, NorthWest, North, NorthEast, East, SouthEast, NoDirection }; /** Maps from direction to a left turn from the direction. */ DVL_ALWAYS_INLINE constexpr Direction Left(Direction facing) { // Direction left[8] = { Direction::SouthEast, Direction::South, Direction::SouthWest, Direction::West, Direction::NorthWest, Direction::North, Direction::NorthEast, Direction::East }; return static_cast((static_cast>(facing) + 7) % 8); } /** Maps from direction to a right turn from the direction. */ DVL_ALWAYS_INLINE constexpr Direction Right(Direction facing) { // Direction right[8] = { Direction::SouthWest, Direction::West, Direction::NorthWest, Direction::North, Direction::NorthEast, Direction::East, Direction::SouthEast, Direction::South }; return static_cast((static_cast>(facing) + 1) % 8); } /** Maps from direction to the opposite direction. */ DVL_ALWAYS_INLINE constexpr Direction Opposite(Direction facing) { // Direction opposite[8] = { Direction::North, Direction::NorthEast, Direction::East, Direction::SouthEast, Direction::South, Direction::SouthWest, Direction::West, Direction::NorthWest }; return static_cast((static_cast>(facing) + 4) % 8); } std::string_view DirectionToString(Direction direction); } // namespace devilution ================================================ FILE: Source/engine/displacement.hpp ================================================ #pragma once #include #include #ifdef BUILD_TESTING #include #endif #include "appfat.h" #include "engine/direction.hpp" #include "engine/size.hpp" #include "utils/attributes.h" namespace devilution { template struct DisplacementOf; using Displacement = DisplacementOf; template struct DisplacementOf { DeltaT deltaX; DeltaT deltaY; DisplacementOf() = default; template DVL_ALWAYS_INLINE constexpr DisplacementOf(DisplacementOf other) : deltaX(other.deltaX) , deltaY(other.deltaY) { } DVL_ALWAYS_INLINE constexpr DisplacementOf(DeltaT deltaX, DeltaT deltaY) : deltaX(deltaX) , deltaY(deltaY) { } DVL_ALWAYS_INLINE explicit constexpr DisplacementOf(DeltaT delta) : deltaX(delta) , deltaY(delta) { } template DVL_ALWAYS_INLINE explicit constexpr DisplacementOf(const SizeOf &size) : deltaX(size.width) , deltaY(size.height) { } DVL_ALWAYS_INLINE explicit constexpr DisplacementOf(Direction direction) : DisplacementOf(fromDirection(direction)) { } template DVL_ALWAYS_INLINE constexpr bool operator==(const DisplacementOf &other) const { return deltaX == other.deltaX && deltaY == other.deltaY; } template DVL_ALWAYS_INLINE constexpr bool operator!=(const DisplacementOf &other) const { return !(*this == other); } template DVL_ALWAYS_INLINE constexpr DisplacementOf &operator+=(DisplacementOf displacement) { deltaX += displacement.deltaX; deltaY += displacement.deltaY; return *this; } template DVL_ALWAYS_INLINE constexpr DisplacementOf &operator-=(DisplacementOf displacement) { deltaX -= displacement.deltaX; deltaY -= displacement.deltaY; return *this; } DVL_ALWAYS_INLINE constexpr DisplacementOf &operator*=(const int factor) { deltaX *= factor; deltaY *= factor; return *this; } DVL_ALWAYS_INLINE constexpr DisplacementOf &operator*=(const float factor) { deltaX = static_cast(deltaX * factor); deltaY = static_cast(deltaY * factor); return *this; } template DVL_ALWAYS_INLINE constexpr DisplacementOf &operator*=(const DisplacementOf factor) { deltaX = static_cast(deltaX * factor.deltaX); deltaY = static_cast(deltaY * factor.deltaY); return *this; } DVL_ALWAYS_INLINE constexpr DisplacementOf &operator/=(const int factor) { deltaX /= factor; deltaY /= factor; return *this; } DVL_ALWAYS_INLINE constexpr DisplacementOf &operator/=(const float factor) { deltaX = static_cast(deltaX / factor); deltaY = static_cast(deltaY / factor); return *this; } DVL_ALWAYS_INLINE float magnitude() const { // If [x * x <= max], then [x <= max / x] for x > 0 assert(deltaX == 0 || std::abs(deltaX) <= std::numeric_limits::max() / std::abs(deltaX)); assert(deltaY == 0 || std::abs(deltaY) <= std::numeric_limits::max() / std::abs(deltaY)); // If [x + y <= max], then [x <= max - y] assert(deltaX * deltaX <= std::numeric_limits::max() - deltaY * deltaY); // Maximum precision of float is 24 bits assert(deltaX * deltaX + deltaY * deltaY < (1 << 24)); // We do not use `std::hypot` here because it is slower and we do not need the extra precision. return sqrtf(static_cast(deltaX * deltaX + deltaY * deltaY)); } /** * @brief Returns a new Displacement object in screen coordinates. * * Transforming from world space to screen space involves a rotation of -135° and scaling to fit within a 64x32 pixel tile (since Diablo uses isometric projection) * 32 and 16 are used as the x/y scaling factors being half the relevant max dimension, the rotation matrix is [[-, +], [-, -]] as sin(-135°) = cos(-135°) = ~-0.7. * * [-32, 32] [dx] = [-32dx + 32dy] = [ 32dy - 32dx ] = [ 32(dy - dx)] * [-16, -16] [dy] = [-16dx + -16dy] = [-(16dy + 16dx)] = [-16(dy + dx)] * * @return A representation of the original displacement in screen coordinates. */ DVL_ALWAYS_INLINE constexpr DisplacementOf worldToScreen() const { static_assert(std::is_signed::value, "DeltaT must be signed for transformations involving a rotation"); return { (deltaY - deltaX) * 32, (deltaY + deltaX) * -16 }; } /** * @brief Returns a new Displacement object in world coordinates. * * This is an inverse matrix of the worldToScreen transformation. * * @return A representation of the original displacement in world coordinates. */ DVL_ALWAYS_INLINE constexpr DisplacementOf screenToWorld() const { static_assert(std::is_signed::value, "DeltaT must be signed for transformations involving a rotation"); return { (2 * deltaY + deltaX) / -64, (2 * deltaY - deltaX) / -64 }; } /** * @brief Missiles flip the axes for some reason -_- * @return negated and rounded world displacement, for use with missile movement routines. */ constexpr DisplacementOf screenToMissile() const { static_assert(std::is_signed::value, "DeltaT must be signed for transformations involving a rotation"); DeltaT xNumerator = 2 * deltaY + deltaX; DeltaT yNumerator = 2 * deltaY - deltaX; DeltaT xOffset = (xNumerator >= 0) ? 32 : -32; DeltaT yOffset = (yNumerator >= 0) ? 32 : -32; return { (xNumerator + xOffset) / 64, (yNumerator + yOffset) / 64 }; } constexpr DisplacementOf screenToLight() const { static_assert(std::is_signed::value, "DeltaT must be signed for transformations involving a rotation"); return { static_cast((2 * deltaY + deltaX) / 8), static_cast((2 * deltaY - deltaX) / 8) }; } /** * @brief Returns a 16 bit fixed point normalised displacement in isometric projection * * This will return a displacement of the form (-1.0 to 1.0, -0.5 to 0.5), to get a full tile offset you can multiply by 16 */ [[nodiscard]] Displacement worldToNormalScreen() const { static_assert(std::is_signed::value, "DeltaT must be signed for transformations involving a rotation"); // Most transformations between world and screen space take shortcuts when scaling to simplify the math. This // routine is typically used with missiles where we want a normal vector that can be multiplied with a target // velocity (given in pixels). We could normalize the vector first but then we'd need to scale it during // rotation from world to screen space. To save performing unnecessary divisions we rotate first without // correcting the scaling. This gives a vector in elevation projection aligned with screen space. DisplacementOf rotated { deltaY - deltaX, -(deltaY + deltaX) }; // then normalize this vector Displacement rotatedAndNormalized = rotated.normalized(); // and finally scale the y axis to bring it to isometric projection return { rotatedAndNormalized.deltaX, rotatedAndNormalized.deltaY / 2 }; } /** * @brief Calculates a 16 bit fixed point normalized displacement (having magnitude of ~1.0) from the current Displacement */ [[nodiscard]] Displacement normalized() const; [[nodiscard]] DVL_ALWAYS_INLINE constexpr DisplacementOf Rotate(int quadrants) const { static_assert(std::is_signed::value, "DeltaT must be signed for Rotate"); constexpr DeltaT Sines[] = { 0, 1, 0, -1 }; quadrants = (quadrants % 4 + 4) % 4; DeltaT sine = Sines[quadrants]; DeltaT cosine = Sines[(quadrants + 1) % 4]; return DisplacementOf { deltaX * cosine - deltaY * sine, deltaX * sine + deltaY * cosine }; } [[nodiscard]] constexpr DisplacementOf flipX() const { static_assert(std::is_signed::value, "DeltaT must be signed for flipX"); return { static_cast(-deltaX), deltaY }; } [[nodiscard]] constexpr DisplacementOf flipY() const { static_assert(std::is_signed::value, "DeltaT must be signed for flipY"); return { deltaX, static_cast(-deltaY) }; } [[nodiscard]] constexpr DisplacementOf flipXY() const { static_assert(std::is_signed::value, "DeltaT must be signed for flipXY"); return { static_cast(-deltaX), static_cast(-deltaY) }; } private: DVL_ALWAYS_INLINE static constexpr DisplacementOf fromDirection(Direction direction) { static_assert(std::is_signed::value, "DeltaT must be signed for conversion from Direction"); switch (direction) { case Direction::South: return { 1, 1 }; case Direction::SouthWest: return { 0, 1 }; case Direction::West: return { -1, 1 }; case Direction::NorthWest: return { -1, 0 }; case Direction::North: return { -1, -1 }; case Direction::NorthEast: return { 0, -1 }; case Direction::East: return { 1, -1 }; case Direction::SouthEast: return { 1, 0 }; case Direction::NoDirection: return { 0, 0 }; default: return { 0, 0 }; } }; }; #ifdef BUILD_TESTING /** * @brief Format displacements nicely in test failure messages * @param stream output stream, expected to have overloads for int and char* * @param offset Object to display * @return the stream, to allow chaining */ template std::ostream &operator<<(std::ostream &stream, const DisplacementOf &offset) { return stream << "(x: " << offset.deltaX << ", y: " << offset.deltaY << ")"; } #endif template DVL_ALWAYS_INLINE constexpr DisplacementOf operator+(DisplacementOf a, DisplacementOf b) { a += b; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator-(DisplacementOf a, DisplacementOf b) { a -= b; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator*(DisplacementOf a, const int factor) { a *= factor; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator*(DisplacementOf a, const float factor) { a *= factor; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator*(DisplacementOf a, const DisplacementOf factor) { a *= factor; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator/(DisplacementOf a, const int factor) { a /= factor; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator/(DisplacementOf a, const float factor) { a /= factor; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator-(DisplacementOf a) { return { -a.deltaX, -a.deltaY }; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator<<(DisplacementOf a, unsigned factor) { return { a.deltaX << factor, a.deltaY << factor }; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator>>(DisplacementOf a, unsigned factor) { return { a.deltaX >> factor, a.deltaY >> factor }; } template DVL_ALWAYS_INLINE constexpr DisplacementOf abs(DisplacementOf a) { return DisplacementOf(std::abs(a.deltaX), std::abs(a.deltaY)); } template Displacement DisplacementOf::normalized() const { const float magnitude = this->magnitude(); Displacement normalDisplacement = Displacement(*this) << 16u; normalDisplacement /= magnitude; return normalDisplacement; } } // namespace devilution ================================================ FILE: Source/engine/dx.cpp ================================================ /** * @file dx.cpp * * Implementation of functions setting up the graphics pipeline. */ #include "engine/dx.h" #include #ifdef USE_SDL3 #include #include #include #include #include #else #include #endif #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/render/primitive_render.hpp" #include "headless_mode.hpp" #include "init.hpp" #include "options.h" #include "utils/display.h" #include "utils/log.hpp" #include "utils/sdl_wrap.h" #ifndef USE_SDL1 #include "controls/touch/renderers.h" #endif #ifdef __3DS__ #include <3ds.h> #endif namespace devilution { int refreshDelay; SDL_Renderer *renderer; #ifndef USE_SDL1 SDLTextureUniquePtr texture; #endif /** Currently active palette */ SDLPaletteUniquePtr Palette; /** 24-bit renderer texture surface */ SDLSurfaceUniquePtr RendererTextureSurface; /** 8-bit surface that we render to */ SDL_Surface *PalSurface; namespace { SDLSurfaceUniquePtr PinnedPalSurface; } // namespace /** Whether we render directly to the screen surface, i.e. `PalSurface == GetOutputSurface()` */ bool RenderDirectlyToOutputSurface; namespace { bool CanRenderDirectlyToOutputSurface() { #ifdef USE_SDL1 #ifdef SDL1_FORCE_DIRECT_RENDER return true; #else auto *outputSurface = GetOutputSurface(); return ((outputSurface->flags & SDL_DOUBLEBUF) == SDL_DOUBLEBUF && outputSurface->w == gnScreenWidth && outputSurface->h == gnScreenHeight && outputSurface->format->BitsPerPixel == 8); #endif #else // !USE_SDL1 if (renderer != nullptr) return false; SDL_Surface *outputSurface = GetOutputSurface(); // Assumes double-buffering is available. return outputSurface->w == static_cast(gnScreenWidth) && outputSurface->h == static_cast(gnScreenHeight) && SDLC_SURFACE_BITSPERPIXEL(outputSurface) == 8; #endif } /** * @brief Limit FPS to avoid high CPU load, use when v-sync isn't available */ void LimitFrameRate() { if (*GetOptions().Graphics.frameRateControl != FrameRateControl::CPUSleep) return; static uint32_t frameDeadline; const uint32_t tc = SDL_GetTicks() * 1000; uint32_t v = 0; if (frameDeadline > tc) { v = tc % refreshDelay; SDL_Delay(v / 1000 + 1); // ceil } frameDeadline = tc + v + refreshDelay; } } // namespace void dx_init() { #ifndef USE_SDL1 SDL_RaiseWindow(ghMainWnd); SDL_ShowWindow(ghMainWnd); #endif Palette = SDLWrap::AllocPalette(); palette_init(); CreateBackBuffer(); } Surface GlobalBackBuffer() { return Surface(PalSurface, SDL_Rect { 0, 0, gnScreenWidth, gnScreenHeight }); } void dx_cleanup() { #ifndef USE_SDL1 if (ghMainWnd != nullptr) SDL_HideWindow(ghMainWnd); #endif PalSurface = nullptr; PinnedPalSurface = nullptr; Palette = nullptr; RendererTextureSurface = nullptr; #ifndef USE_SDL1 texture = nullptr; FreeVirtualGamepadTextures(); if (*GetOptions().Graphics.upscale) SDL_DestroyRenderer(renderer); #endif SDL_DestroyWindow(ghMainWnd); } void CreateBackBuffer() { if (CanRenderDirectlyToOutputSurface()) { Log("{}", "Will render directly to the SDL output surface"); PalSurface = GetOutputSurface(); RenderDirectlyToOutputSurface = true; } else { PinnedPalSurface = SDLWrap::CreateRGBSurfaceWithFormat( /*flags=*/0, /*width=*/gnScreenWidth, /*height=*/gnScreenHeight, /*depth=*/8, SDL_PIXELFORMAT_INDEX8); PalSurface = PinnedPalSurface.get(); } #if defined(USE_SDL3) if (!SDL_SetSurfacePalette(PalSurface, Palette.get())) ErrSdl(); #elif !defined(USE_SDL1) // In SDL2, `PalSurface` points to the global `palette`. if (SDL_SetSurfacePalette(PalSurface, Palette.get()) < 0) ErrSdl(); #else // In SDL1, `PalSurface` owns its palette and we must update it every // time the global `palette` is changed. No need to do anything here as // the global `palette` doesn't have any colors set yet. #endif } void BltFast(SDL_Rect *srcRect, SDL_Rect *dstRect) { if (RenderDirectlyToOutputSurface) return; Blit(PalSurface, srcRect, dstRect); } void Blit(SDL_Surface *src, SDL_Rect *srcRect, SDL_Rect *dstRect) { if (HeadlessMode) return; SDL_Surface *dst = GetOutputSurface(); #if defined(USE_SDL3) if (!SDL_BlitSurface(src, srcRect, dst, dstRect)) ErrSdl(); #elif !defined(USE_SDL1) if (SDL_BlitSurface(src, srcRect, dst, dstRect) < 0) ErrSdl(); #else if (!OutputRequiresScaling()) { if (SDL_BlitSurface(src, srcRect, dst, dstRect) < 0) ErrSdl(); return; } SDL_Rect scaledDstRect; if (dstRect != NULL) { scaledDstRect = *dstRect; ScaleOutputRect(&scaledDstRect); dstRect = &scaledDstRect; } // Same pixel format: We can call BlitScaled directly. if (SDLBackport_PixelFormatFormatEq(src->format, dst->format)) { if (SDL_BlitScaled(src, srcRect, dst, dstRect) < 0) ErrSdl(); return; } // If the surface has a color key, we must stretch first and can then call BlitSurface. if (SDL_HasColorKey(src)) { SDLSurfaceUniquePtr stretched = SDLWrap::CreateRGBSurface(SDL_SWSURFACE, dstRect->w, dstRect->h, src->format->BitsPerPixel, src->format->Rmask, src->format->Gmask, src->format->BitsPerPixel, src->format->Amask); SDL_SetColorKey(stretched.get(), SDL_SRCCOLORKEY, src->format->colorkey); if (src->format->palette != NULL) SDL_SetPalette(stretched.get(), SDL_LOGPAL, src->format->palette->colors, 0, src->format->palette->ncolors); SDL_Rect stretched_rect = { 0, 0, dstRect->w, dstRect->h }; if (SDL_SoftStretch(src, srcRect, stretched.get(), &stretched_rect) < 0 || SDL_BlitSurface(stretched.get(), &stretched_rect, dst, dstRect) < 0) { ErrSdl(); } return; } // A surface with a non-output pixel format but without a color key needs scaling. // We can convert the format and then call BlitScaled. SDLSurfaceUniquePtr converted = SDLWrap::ConvertSurface(src, dst->format, 0); if (SDL_BlitScaled(converted.get(), srcRect, dst, dstRect) < 0) ErrSdl(); #endif } void RenderPresent() { if (HeadlessMode) return; SDL_Surface *surface = GetOutputSurface(); if (!gbActive) { LimitFrameRate(); return; } #ifndef USE_SDL1 if (renderer != nullptr) { #ifdef USE_SDL3 if (!SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255)) ErrSdl(); if (!SDL_RenderClear(renderer)) ErrSdl(); if (!SDL_UpdateTexture(texture.get(), nullptr, surface->pixels, surface->pitch)) ErrSdl(); if (!SDL_RenderTexture(renderer, texture.get(), nullptr, nullptr)) ErrSdl(); #else if (SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255) <= -1) ErrSdl(); if (SDL_RenderClear(renderer) <= -1) ErrSdl(); if (SDL_UpdateTexture(texture.get(), nullptr, surface->pixels, surface->pitch) <= -1) ErrSdl(); if (SDL_RenderCopy(renderer, texture.get(), nullptr, nullptr) <= -1) ErrSdl(); #endif if (ControlMode == ControlTypes::VirtualGamepad) { RenderVirtualGamepad(renderer); } SDL_RenderPresent(renderer); if (*GetOptions().Graphics.frameRateControl != FrameRateControl::VerticalSync) { LimitFrameRate(); } } else { if (ControlMode == ControlTypes::VirtualGamepad) { RenderVirtualGamepad(surface); } #ifdef USE_SDL3 if (!SDL_UpdateWindowSurface(ghMainWnd)) ErrSdl(); #else if (SDL_UpdateWindowSurface(ghMainWnd) <= -1) ErrSdl(); #endif if (RenderDirectlyToOutputSurface) PalSurface = GetOutputSurface(); LimitFrameRate(); } #else if (SDL_Flip(surface) <= -1) { ErrSdl(); } if (RenderDirectlyToOutputSurface) PalSurface = GetOutputSurface(); LimitFrameRate(); #endif } } // namespace devilution ================================================ FILE: Source/engine/dx.h ================================================ /** * @file dx.h * * Interface of functions setting up the graphics pipeline. */ #pragma once #ifdef USE_SDL3 #include #include #else #include #endif #include "engine/surface.hpp" namespace devilution { /** Whether we render directly to the screen surface, i.e. `PalSurface == GetOutputSurface()` */ extern bool RenderDirectlyToOutputSurface; extern SDL_Surface *PalSurface; Surface GlobalBackBuffer(); void dx_init(); void dx_cleanup(); void CreateBackBuffer(); void BltFast(SDL_Rect *srcRect, SDL_Rect *dstRect); void Blit(SDL_Surface *src, SDL_Rect *srcRect, SDL_Rect *dstRect); void RenderPresent(); } // namespace devilution ================================================ FILE: Source/engine/events.cpp ================================================ #include "engine/events.hpp" #include #ifdef USE_SDL3 #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "controls/input.h" #include "controls/padmapper.hpp" #include "engine/demomode.h" #include "engine/render/primitive_render.hpp" #include "interfac.h" #include "movie.h" #include "options.h" #include "panels/console.hpp" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/sdl_compat.h" #ifndef USE_SDL1 #include "controls/touch/event_handlers.h" #endif #ifdef __vita__ #include "diablo.h" #include "platform/vita/touch.h" #endif #ifdef __SWITCH__ #include "platform/switch/docking.h" #include #endif namespace devilution { namespace { bool FalseAvail(const char *name, int value) { LogVerbose("Unhandled SDL event: {} {}", name, value); return true; } bool FetchMessage_Real(SDL_Event *event, uint16_t *modState) { #ifdef __SWITCH__ HandleDocking(); #endif SDL_Event e; if (!PollEvent(&e)) return false; event->type = static_cast(0); *modState = SDL_GetModState(); #ifdef __vita__ HandleTouchEvent(&e, MousePosition); #elif !defined(USE_SDL1) HandleTouchEvent(e); #endif if (e.type == SDL_EVENT_QUIT || IsCustomEvent(e.type)) { *event = e; return true; } // Erroneous events generated by RG350 kernel. if (IsAnyOf(e.type, SDL_EVENT_KEY_DOWN, SDL_EVENT_KEY_UP) && SDLC_EventKey(e) == SDLK_UNKNOWN) return true; #if !defined(USE_SDL1) && !defined(__vita__) if (!movie_playing) { // SDL generates mouse events from touch-based inputs to provide basic // touchscreeen support for apps that don't explicitly handle touch events if (IsAnyOf(e.type, SDL_EVENT_MOUSE_BUTTON_DOWN, SDL_EVENT_MOUSE_BUTTON_UP) && e.button.which == SDL_TOUCH_MOUSEID) { return true; } if (e.type == SDL_EVENT_MOUSE_MOTION && e.motion.which == SDL_TOUCH_MOUSEID) { return true; } if (e.type == SDL_EVENT_MOUSE_WHEEL && e.wheel.which == SDL_TOUCH_MOUSEID) { return true; } } #endif if (!SDLC_ConvertEventToRenderCoordinates(renderer, &e)) { LogWarn(LogCategory::Application, "SDL_ConvertEventToRenderCoordinates: {}", SDL_GetError()); SDL_ClearError(); } if (HandleControllerAddedOrRemovedEvent(e)) return true; switch (e.type) { #ifdef USE_SDL3 case SDL_EVENT_GAMEPAD_AXIS_MOTION: case SDL_EVENT_GAMEPAD_BUTTON_DOWN: case SDL_EVENT_GAMEPAD_BUTTON_UP: case SDL_EVENT_FINGER_DOWN: case SDL_EVENT_FINGER_UP: case SDL_EVENT_TEXT_EDITING: case SDL_EVENT_TEXT_INPUT: case SDL_EVENT_WINDOW_RESIZED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_MINIMIZED: case SDL_EVENT_WINDOW_MAXIMIZED: case SDL_EVENT_WINDOW_RESTORED: case SDL_EVENT_WINDOW_HDR_STATE_CHANGED: case SDL_EVENT_WINDOW_FOCUS_GAINED: case SDL_EVENT_WINDOW_FOCUS_LOST: case SDL_EVENT_MOUSE_WHEEL: case SDL_EVENT_JOYSTICK_AXIS_MOTION: case SDL_EVENT_JOYSTICK_HAT_MOTION: case SDL_EVENT_JOYSTICK_BUTTON_DOWN: case SDL_EVENT_JOYSTICK_BUTTON_UP: case SDL_EVENT_MOUSE_MOTION: case SDL_EVENT_MOUSE_BUTTON_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: *event = e; break; case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: if (e.key.key == SDLK_UNKNOWN) { return FalseAvail(e.key.down ? "SDL_EVENT_KEY_DOWN" : "SDL_EVENT_KEY_UP", e.key.key); } *event = e; break; case SDL_EVENT_AUDIO_DEVICE_ADDED: return FalseAvail("SDL_EVENT_AUDIO_DEVICE_ADDED", e.adevice.which); case SDL_EVENT_AUDIO_DEVICE_REMOVED: return FalseAvail("SDL_EVENT_AUDIO_DEVICE_REMOVED", e.adevice.which); case SDL_EVENT_KEYMAP_CHANGED: return FalseAvail("SDL_EVENT_KEYMAP_CHANGED", 0); #else #if SDL_VERSION_ATLEAST(2, 0, 0) case SDL_CONTROLLERAXISMOTION: case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERBUTTONUP: case SDL_FINGERDOWN: case SDL_FINGERUP: case SDL_TEXTEDITING: case SDL_TEXTINPUT: case SDL_WINDOWEVENT: case SDL_MOUSEWHEEL: #else case SDL_ACTIVEEVENT: #endif case SDL_JOYAXISMOTION: case SDL_JOYHATMOTION: case SDL_JOYBUTTONDOWN: case SDL_JOYBUTTONUP: case SDL_MOUSEMOTION: case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: *event = e; break; case SDL_KEYDOWN: case SDL_KEYUP: if (e.key.keysym.sym == -1) return FalseAvail(e.type == SDL_KEYDOWN ? "SDL_KEYDOWN" : "SDL_KEYUP", e.key.keysym.sym); *event = e; break; #ifndef USE_SDL1 #if SDL_VERSION_ATLEAST(2, 0, 4) case SDL_AUDIODEVICEADDED: return FalseAvail("SDL_AUDIODEVICEADDED", e.adevice.which); case SDL_AUDIODEVICEREMOVED: return FalseAvail("SDL_AUDIODEVICEREMOVED", e.adevice.which); case SDL_KEYMAPCHANGED: return FalseAvail("SDL_KEYMAPCHANGED", 0); #endif #endif #endif default: return FalseAvail("unknown", e.type); } return true; } } // namespace EventHandler CurrentEventHandler; EventHandler SetEventHandler(EventHandler eventHandler) { PadmapperReleaseAllActiveButtons(); EventHandler previousHandler = CurrentEventHandler; CurrentEventHandler = eventHandler; return previousHandler; } bool FetchMessage(SDL_Event *event, uint16_t *modState) { const bool available = demo::IsRunning() ? demo::FetchMessage(event, modState) : FetchMessage_Real(event, modState); if (available && demo::IsRecording()) demo::RecordMessage(*event, *modState); return available; } void HandleMessage(const SDL_Event &event, uint16_t modState) { assert(CurrentEventHandler != nullptr); CurrentEventHandler(event, modState); } } // namespace devilution ================================================ FILE: Source/engine/events.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "engine/point.hpp" namespace devilution { using EventHandler = void (*)(const SDL_Event &event, uint16_t modState); /** @brief The current input handler function */ extern EventHandler CurrentEventHandler; EventHandler SetEventHandler(EventHandler NewProc); bool FetchMessage(SDL_Event *event, uint16_t *modState); void HandleMessage(const SDL_Event &event, uint16_t modState); } // namespace devilution ================================================ FILE: Source/engine/lighting_defs.hpp ================================================ #pragma once #include namespace devilution { #define MAXLIGHTS 32 #define MAXVISION 4 #define NO_LIGHT -1 constexpr char LightsMax = 15; /** @brief A light table maps palette indices, so its size is the same as the palette size. */ constexpr size_t LightTableSize = 256; /** @brief Number of supported light levels */ constexpr size_t NumLightingLevels = LightsMax + 1; } // namespace devilution ================================================ FILE: Source/engine/load_cel.cpp ================================================ #include "engine/load_cel.hpp" #include #include #include #include #ifdef DEBUG_CEL_TO_CL2_SIZE #include #endif #include #include "appfat.h" #include "mpq/mpq_common.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef UNPACKED_MPQS #include "engine/load_clx.hpp" #else #include "engine/load_file.hpp" #include "utils/cel_to_clx.hpp" #endif namespace devilution { tl::expected LoadCelListOrSheetWithStatus(const char *pszName, PointerOrValue widthOrWidths) { char path[MaxMpqPathSize]; *BufCopy(path, pszName, DEVILUTIONX_CEL_EXT) = '\0'; #ifdef UNPACKED_MPQS return LoadClxListOrSheetWithStatus(path); #else size_t size; ASSIGN_OR_RETURN(std::unique_ptr data, LoadFileInMemWithStatus(path, &size)); #ifdef DEBUG_CEL_TO_CL2_SIZE std::cout << path; #endif return CelToClx(data.get(), size, widthOrWidths); #endif } OwnedClxSpriteListOrSheet LoadCelListOrSheet(const char *pszName, PointerOrValue widthOrWidths) { tl::expected result = LoadCelListOrSheetWithStatus(pszName, widthOrWidths); if (DVL_PREDICT_FALSE(!result.has_value())) app_fatal(result.error()); return std::move(result).value(); } } // namespace devilution ================================================ FILE: Source/engine/load_cel.hpp ================================================ #pragma once #include #include #include #include "engine/clx_sprite.hpp" #include "utils/pointer_value_union.hpp" #include "utils/status_macros.hpp" #ifdef UNPACKED_MPQS #define DEVILUTIONX_CEL_EXT ".clx" #else #define DEVILUTIONX_CEL_EXT ".cel" #endif namespace devilution { tl::expected LoadCelListOrSheetWithStatus(const char *pszName, PointerOrValue widthOrWidths); OwnedClxSpriteListOrSheet LoadCelListOrSheet(const char *pszName, PointerOrValue widthOrWidths); inline OwnedClxSpriteList LoadCel(const char *pszName, uint16_t width) { return LoadCelListOrSheet(pszName, PointerOrValue { width }).list(); } inline OptionalOwnedClxSpriteList LoadOptionalCel(const char *pszName, uint16_t width) { tl::expected result = LoadCelListOrSheetWithStatus(pszName, PointerOrValue { width }); if (!result.has_value()) return std::nullopt; return (*std::move(result)).list(); } inline tl::expected LoadCelWithStatus(const char *pszName, uint16_t width) { ASSIGN_OR_RETURN(OwnedClxSpriteListOrSheet result, LoadCelListOrSheetWithStatus(pszName, PointerOrValue { width })); return std::move(result).list(); } inline OwnedClxSpriteList LoadCel(const char *pszName, const uint16_t *widths) { return LoadCelListOrSheet(pszName, PointerOrValue { widths }).list(); } inline OptionalOwnedClxSpriteList LoadOptionalCel(const char *pszName, const uint16_t *widths) { tl::expected result = LoadCelListOrSheetWithStatus(pszName, PointerOrValue { widths }); if (!result.has_value()) return std::nullopt; return (*std::move(result)).list(); } inline tl::expected LoadCelWithStatus(const char *pszName, const uint16_t *widths) { ASSIGN_OR_RETURN(OwnedClxSpriteListOrSheet result, LoadCelListOrSheetWithStatus(pszName, PointerOrValue { widths })); return std::move(result).list(); } inline OwnedClxSpriteSheet LoadCelSheet(const char *pszName, uint16_t width) { return LoadCelListOrSheet(pszName, PointerOrValue { width }).sheet(); } } // namespace devilution ================================================ FILE: Source/engine/load_cl2.cpp ================================================ #include "engine/load_cl2.hpp" #include #include #include #include #include "mpq/mpq_common.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef UNPACKED_MPQS #include "engine/load_clx.hpp" #else #include "engine/load_file.hpp" #include "utils/cl2_to_clx.hpp" #endif namespace devilution { tl::expected LoadCl2ListOrSheetWithStatus(const char *pszName, PointerOrValue widthOrWidths) { char path[MaxMpqPathSize]; *BufCopy(path, pszName, DEVILUTIONX_CL2_EXT) = '\0'; #ifdef UNPACKED_MPQS return LoadClxListOrSheetWithStatus(path); #else size_t size; ASSIGN_OR_RETURN(std::unique_ptr data, LoadFileInMemWithStatus(path, &size)); return Cl2ToClx(std::move(data), size, widthOrWidths); #endif } OwnedClxSpriteListOrSheet LoadCl2ListOrSheet(const char *pszName, PointerOrValue widthOrWidths) { tl::expected result = LoadCl2ListOrSheetWithStatus(pszName, widthOrWidths); if (!result.has_value()) app_fatal(result.error()); return std::move(result).value(); } } // namespace devilution ================================================ FILE: Source/engine/load_cl2.hpp ================================================ #pragma once #include #include #include #include #include #include #include "appfat.h" #include "engine/clx_sprite.hpp" #include "engine/load_file.hpp" #include "mpq/mpq_common.hpp" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" #include "utils/pointer_value_union.hpp" #include "utils/static_vector.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef UNPACKED_MPQS #define DEVILUTIONX_CL2_EXT ".clx" #else #include "utils/cl2_to_clx.hpp" #define DEVILUTIONX_CL2_EXT ".cl2" #endif namespace devilution { tl::expected LoadCl2ListOrSheetWithStatus(const char *pszName, PointerOrValue widthOrWidths); OwnedClxSpriteListOrSheet LoadCl2ListOrSheet(const char *pszName, PointerOrValue widthOrWidths); template tl::expected LoadMultipleCl2Sheet(tl::function_ref filenames, size_t count, uint16_t width) { StaticVector, MaxCount> paths; StaticVector files; StaticVector fileSizes; const size_t sheetHeaderSize = 4 * count; size_t totalSize = sheetHeaderSize; for (size_t i = 0; i < count; ++i) { { const char *path = filenames(i); paths.emplace_back(); memcpy(paths.back().data(), path, strlen(path) + 1); } const char *path = paths.back().data(); files.emplace_back(FindAsset(path)); if (!files.back().ok()) { FailedToOpenFileError(path, files.back().error()); } const size_t size = files.back().size(); fileSizes.emplace_back(size); totalSize += size; } auto data = std::unique_ptr { new uint8_t[totalSize] }; #ifndef UNPACKED_MPQS const PointerOrValue frameWidth { width }; #endif size_t accumulatedSize = sheetHeaderSize; for (size_t i = 0; i < count; ++i) { const size_t size = fileSizes[i]; AssetHandle handle = OpenAsset(std::move(files[i])); if (!handle.ok() || !handle.read(&data[accumulatedSize], size)) { FailedToOpenFileError(paths[i].data(), handle.error()); } WriteLE32(&data[i * 4], static_cast(accumulatedSize)); accumulatedSize += size; } #ifdef UNPACKED_MPQS return OwnedClxSpriteSheet { std::move(data), static_cast(count) }; #else return Cl2ToClx(std::move(data), accumulatedSize, frameWidth).sheet(); #endif } inline tl::expected LoadCl2WithStatus(const char *pszName, uint16_t width) { ASSIGN_OR_RETURN(OwnedClxSpriteListOrSheet result, LoadCl2ListOrSheetWithStatus(pszName, PointerOrValue { width })); return std::move(result).list(); } inline OwnedClxSpriteList LoadCl2(const char *pszName, uint16_t width) { return LoadCl2ListOrSheet(pszName, PointerOrValue { width }).list(); } inline OwnedClxSpriteList LoadCl2(const char *pszName, const uint16_t *widths) { return LoadCl2ListOrSheet(pszName, PointerOrValue { widths }).list(); } inline OwnedClxSpriteSheet LoadCl2Sheet(const char *pszName, uint16_t width) { return LoadCl2ListOrSheet(pszName, PointerOrValue { width }).sheet(); } } // namespace devilution ================================================ FILE: Source/engine/load_clx.cpp ================================================ #include "engine/load_clx.hpp" #include #include #include #include "appfat.h" #include "engine/assets.hpp" #include "engine/load_file.hpp" namespace devilution { OptionalOwnedClxSpriteListOrSheet LoadOptionalClxListOrSheet(const char *path) { AssetRef ref = FindAsset(path); if (!ref.ok()) return std::nullopt; const size_t size = ref.size(); std::unique_ptr data { new uint8_t[size] }; { AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok() || !handle.read(data.get(), size)) return std::nullopt; } return OwnedClxSpriteListOrSheet::FromBuffer(std::move(data), size); } tl::expected LoadClxListOrSheetWithStatus(const char *path) { size_t size; tl::expected, std::string> data = LoadFileInMemWithStatus(path, &size); if (!data.has_value()) return tl::make_unexpected(std::move(data).error()); return OwnedClxSpriteListOrSheet::FromBuffer(std::move(data).value(), size); } OwnedClxSpriteListOrSheet LoadClxListOrSheet(const char *path) { tl::expected result = LoadClxListOrSheetWithStatus(path); if (!result.has_value()) app_fatal(result.error()); return std::move(result).value(); } } // namespace devilution ================================================ FILE: Source/engine/load_clx.hpp ================================================ #pragma once #include #include #include "clx_sprite.hpp" #include "utils/status_macros.hpp" namespace devilution { OwnedClxSpriteListOrSheet LoadClxListOrSheet(const char *path); tl::expected LoadClxListOrSheetWithStatus(const char *path); inline OwnedClxSpriteList LoadClx(const char *path) { return LoadClxListOrSheet(path).list(); } inline tl::expected LoadClxWithStatus(const char *path) { ASSIGN_OR_RETURN(OwnedClxSpriteListOrSheet result, LoadClxListOrSheetWithStatus(path)); return std::move(result).list(); } inline OwnedClxSpriteSheet LoadClxSheet(const char *path) { return LoadClxListOrSheet(path).sheet(); } OptionalOwnedClxSpriteListOrSheet LoadOptionalClxListOrSheet(const char *path); inline OptionalOwnedClxSpriteList LoadOptionalClx(const char *path) { OptionalOwnedClxSpriteListOrSheet result = LoadOptionalClxListOrSheet(path); if (!result) return std::nullopt; return std::move(*result).list(); } } // namespace devilution ================================================ FILE: Source/engine/load_file.hpp ================================================ #pragma once #include #include #include #include #include #include #include "appfat.h" #include "engine/assets.hpp" #include "headless_mode.hpp" #include "mpq/mpq_common.hpp" #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" namespace devilution { template tl::expected LoadFileInMemWithStatus(const char *path, T *data) { size_t size; AssetHandle handle = OpenAsset(path, size); if (!handle.ok()) { if (HeadlessMode) return {}; return tl::make_unexpected(FailedToOpenFileErrorMessage(path, handle.error())); } if ((size % sizeof(T)) != 0) { return tl::make_unexpected(StrCat("File size does not align with type\n", path)); } if (!handle.read(data, size)) { return tl::make_unexpected("handle.read failed"); } return {}; } template void LoadFileInMem(const char *path, T *data) { const tl::expected result = LoadFileInMemWithStatus(path, data); if (!result.has_value()) app_fatal(result.error()); } template tl::expected LoadFileInMemWithStatus(const char *path, T *data, std::size_t count) { AssetHandle handle = OpenAsset(path); if (!handle.ok()) { if (HeadlessMode) return {}; return tl::make_unexpected(FailedToOpenFileErrorMessage(path, handle.error())); } if (!handle.read(data, count * sizeof(T))) { return tl::make_unexpected("handle.read failed"); } return {}; } template void LoadFileInMem(const char *path, T *data, std::size_t count) { tl::expected result = LoadFileInMemWithStatus(path, data, count); if (!result.has_value()) app_fatal(result.error()); } template bool LoadOptionalFileInMem(const char *path, T *data, std::size_t count) { AssetHandle handle = OpenAsset(path); return handle.ok() && handle.read(data, count * sizeof(T)); } template tl::expected LoadFileInMemWithStatus(const char *path, std::array &data) { return LoadFileInMemWithStatus(path, data.data(), N); } template void LoadFileInMem(const char *path, std::array &data) { LoadFileInMem(path, data.data(), N); } template tl::expected, std::string> LoadFileInMemWithStatus(const char *path, std::size_t *numRead = nullptr) { size_t size; AssetHandle handle = OpenAsset(path, size); if (!handle.ok()) { if (HeadlessMode) return {}; return tl::make_unexpected(FailedToOpenFileErrorMessage(path, handle.error())); } if ((size % sizeof(T)) != 0) { return tl::make_unexpected(StrCat("File size does not align with type\n", path)); } if (numRead != nullptr) *numRead = size / sizeof(T); std::unique_ptr buf { new T[size / sizeof(T)] }; if (!handle.read(buf.get(), size)) { return tl::make_unexpected("handle.read failed"); } return { std::move(buf) }; } /** * @brief Load a file in to a buffer * @param path Path of file * @param numRead Number of T elements read * @return Buffer with content of file */ template std::unique_ptr LoadFileInMem(const char *path, std::size_t *numRead = nullptr) { tl::expected, std::string> result = LoadFileInMemWithStatus(path, numRead); if (!result.has_value()) app_fatal(result.error()); return std::move(result).value(); } /** * @brief Reads multiple files into a single buffer * * @tparam MaxFiles maximum number of files */ template struct MultiFileLoader { struct DefaultFilterFn { bool operator()(size_t i) const { return true; } }; /** * @param numFiles number of files to read * @param pathFn a function that returns the path for the given index * @param outOffsets a buffer index for the start of each file will be written here, then the total file size at the end. * @param filterFn a function that returns whether to load a file for the given index * @return std::unique_ptr the buffer with all the files */ template [[nodiscard]] std::unique_ptr operator()(size_t numFiles, PathFn &&pathFn, uint32_t *outOffsets, FilterFn filterFn = DefaultFilterFn {}) { StaticVector, MaxFiles> paths; StaticVector files; StaticVector sizes; size_t totalSize = 0; for (size_t i = 0, j = 0; i < numFiles; ++i) { if (!filterFn(i)) continue; { const char *path = pathFn(i); paths.emplace_back(); memcpy(paths.back().data(), path, strlen(path) + 1); } const char *path = paths.back().data(); files.emplace_back(FindAsset(path)); if (!ValidatAssetRef(path, files.back())) return nullptr; const size_t size = files.back().size(); sizes.emplace_back(static_cast(size)); outOffsets[j] = static_cast(totalSize); totalSize += size; ++j; } outOffsets[files.size()] = static_cast(totalSize); std::unique_ptr buf { new std::byte[totalSize] }; for (size_t i = 0, j = 0; i < numFiles; ++i) { if (!filterFn(i)) continue; AssetHandle handle = OpenAsset(std::move(files[j])); if (!handle.ok() || !handle.read(&buf[outOffsets[j]], sizes[j])) { FailedToOpenFileError(paths[j].data(), handle.error()); } ++j; } return buf; } }; } // namespace devilution ================================================ FILE: Source/engine/load_pcx.cpp ================================================ #include "engine/load_pcx.hpp" #include #include #include #include #include #ifdef DEBUG_PCX_TO_CL2_SIZE #include #endif #ifdef USE_SDL3 #include #else #include #endif #include "mpq/mpq_common.hpp" #include "utils/log.hpp" #include "utils/str_cat.hpp" #ifdef UNPACKED_MPQS #include "engine/load_clx.hpp" #include "engine/load_file.hpp" #else #include "engine/assets.hpp" #include "utils/pcx.hpp" #include "utils/pcx_to_clx.hpp" #endif #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { OptionalOwnedClxSpriteList LoadPcxSpriteList(const char *filename, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette, bool logError) { char path[MaxMpqPathSize]; char *pathEnd = BufCopy(path, filename, DEVILUTIONX_PCX_EXT); *pathEnd = '\0'; #ifdef UNPACKED_MPQS OptionalOwnedClxSpriteList result = LoadOptionalClx(path); if (!result) { if (logError) LogError("Missing file: {}", path); return result; } if (outPalette != nullptr) { std::memcpy(pathEnd - 3, "pal", 3); std::array palette; LoadFileInMem(path, palette); for (unsigned i = 0; i < 256; i++) { outPalette[i].r = palette[i * 3]; outPalette[i].g = palette[i * 3 + 1]; outPalette[i].b = palette[i * 3 + 2]; #ifndef USE_SDL1 outPalette[i].a = SDL_ALPHA_OPAQUE; #endif } } return result; #else size_t fileSize; AssetHandle handle = OpenAsset(path, fileSize); if (!handle.ok()) { if (logError) LogError("Missing file: {}", path); return std::nullopt; } #ifdef DEBUG_PCX_TO_CL2_SIZE std::cout << filename; #endif OptionalOwnedClxSpriteList result = PcxToClx(handle, fileSize, numFramesOrFrameHeight, transparentColor, outPalette); if (!result) return std::nullopt; return result; #endif } } // namespace devilution ================================================ FILE: Source/engine/load_pcx.hpp ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #else #include #endif #include "engine/clx_sprite.hpp" #ifdef UNPACKED_MPQS #define DEVILUTIONX_PCX_EXT ".clx" #else #define DEVILUTIONX_PCX_EXT ".pcx" #endif namespace devilution { /** * @brief Loads a PCX file as a CLX sprite list. * * @param filename * @param numFramesOrFrameHeight Pass a positive value with the number of frames, or the frame height as a negative value. * @param transparentColor * @param outPalette * @return OptionalOwnedClxSpriteList */ OptionalOwnedClxSpriteList LoadPcxSpriteList(const char *filename, int numFramesOrFrameHeight, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr, bool logError = true); /** * @brief Loads a PCX file as a CLX sprite list with a single sprite. * * @param filename * @param transparentColor * @param outPalette * @return OptionalOwnedClxSpriteList */ inline OptionalOwnedClxSpriteList LoadPcx(const char *filename, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr, bool logError = true) { return LoadPcxSpriteList(filename, 1, transparentColor, outPalette, logError); } } // namespace devilution ================================================ FILE: Source/engine/palette.cpp ================================================ /** * @file palette.cpp * * Implementation of functions for handling the engines color palette. */ #include "engine/palette.h" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include "engine/backbuffer_state.hpp" #include "engine/demomode.h" #include "engine/dx.h" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "options.h" #include "utils/display.h" #include "utils/palette_blending.hpp" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" namespace devilution { std::array logical_palette; std::array system_palette; namespace { /** Specifies whether the palette has max brightness. */ bool sgbFadedIn = true; void LoadBrightness() { int brightnessValue = *GetOptions().Graphics.brightness; brightnessValue = std::clamp(brightnessValue, 0, 100); GetOptions().Graphics.brightness.SetValue(brightnessValue - brightnessValue % 5); } /** * @brief Cycle the given range of colors in the palette * @param from First color index of the range * @param to First color index of the range */ void CycleColors(int from, int to) { std::rotate(logical_palette.begin() + from, logical_palette.begin() + from + 1, logical_palette.begin() + to + 1); std::rotate(system_palette.begin() + from, system_palette.begin() + from + 1, system_palette.begin() + to + 1); for (auto &palette : paletteTransparencyLookup) { std::rotate(std::begin(palette) + from, std::begin(palette) + from + 1, std::begin(palette) + to + 1); } std::rotate(&paletteTransparencyLookup[from][0], &paletteTransparencyLookup[from + 1][0], &paletteTransparencyLookup[to + 1][0]); #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT UpdateTransparencyLookupBlack16(from, to); #endif } /** * @brief Cycle the given range of colors in the palette in reverse direction * @param from First color index of the range * @param to Last color index of the range */ void CycleColorsReverse(int from, int to) { std::rotate(logical_palette.begin() + from, logical_palette.begin() + to, logical_palette.begin() + to + 1); std::rotate(system_palette.begin() + from, system_palette.begin() + to, system_palette.begin() + to + 1); for (auto &palette : paletteTransparencyLookup) { std::rotate(std::begin(palette) + from, std::begin(palette) + to, std::begin(palette) + to + 1); } std::rotate(&paletteTransparencyLookup[from][0], &paletteTransparencyLookup[to][0], &paletteTransparencyLookup[to + 1][0]); #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT UpdateTransparencyLookupBlack16(from, to); #endif } // When brightness==0, then a==0 (identity mapping) // When brightness==100, then a==-MaxAdjustment (maximum brightening) constexpr float CalculateToneMappingParameter(int brightness) { // Maximum adjustment factor (tweak this constant to change the effect strength) constexpr float MaxAdjustment = 2.0F; return -(brightness / 100.0f) * MaxAdjustment; } constexpr uint8_t MapTone(float a, uint8_t color) { const auto x = static_cast(color / 255.0f); // Our quadratic tone mapping: f(x) = a*x^2 + (1-a)*x. const float y = std::clamp(a * x * x + (1.0f - a) * x, 0.0f, 1.0f); return static_cast(y * 255.0f + 0.5f); } void ApplyGlobalBrightnessSingleColor(SDL_Color &dst, const SDL_Color &src) { const float a = CalculateToneMappingParameter(*GetOptions().Graphics.brightness); dst.r = MapTone(a, src.r); dst.g = MapTone(a, src.g); dst.b = MapTone(a, src.b); } } // namespace void ApplyGlobalBrightness(SDL_Color *dst, const SDL_Color *src) { // Get the brightness slider value (0 = neutral, 100 = max brightening) const int brightnessSlider = *GetOptions().Graphics.brightness; // Precompute a lookup table for speed. const float a = CalculateToneMappingParameter(brightnessSlider); uint8_t toneMap[256]; for (int i = 0; i < 256; i++) { toneMap[i] = MapTone(a, i); } // Apply the lookup table to each color channel in the palette. for (int i = 0; i < 256; i++) { dst[i].r = toneMap[src[i].r]; dst[i].g = toneMap[src[i].g]; dst[i].b = toneMap[src[i].b]; } } void ApplyFadeLevel(unsigned fadeval, SDL_Color *dst, const SDL_Color *src) { for (int i = 0; i < 256; i++) { dst[i].r = (fadeval * src[i].r) / 256; dst[i].g = (fadeval * src[i].g) / 256; dst[i].b = (fadeval * src[i].b) / 256; } } // Applies a tone mapping curve based on the brightness slider value. // The brightness value is in the range [0, 100] where 0 is neutral (no change) // and 100 produces maximum brightening. void UpdateSystemPalette(std::span src) { ApplyGlobalBrightness(system_palette.data(), src.data()); SystemPaletteUpdated(); RedrawEverything(); } void SystemPaletteUpdated(int first, int ncolor) { if (HeadlessMode) return; assert(Palette); if (!SDLC_SetSurfaceAndPaletteColors(PalSurface, Palette.get(), system_palette.data() + first, first, ncolor)) { ErrSdl(); } } void palette_init() { LoadBrightness(); } void LoadPalette(const char *path) { assert(path != nullptr); if (HeadlessMode) return; LogVerbose("Loading palette from {}", path); std::array palData; LoadFileInMem(path, palData); for (unsigned i = 0; i < palData.size(); i++) { logical_palette[i] = palData[i].toSDL(); } } void LoadPaletteAndInitBlending(const char *path) { assert(path != nullptr); if (HeadlessMode) return; LoadPalette(path); if (leveltype == DTYPE_CAVES || leveltype == DTYPE_CRYPT) { GenerateBlendedLookupTable(logical_palette.data(), /*skipFrom=*/1, /*skipTo=*/31); } else if (leveltype == DTYPE_NEST) { GenerateBlendedLookupTable(logical_palette.data(), /*skipFrom=*/1, /*skipTo=*/15); } else { GenerateBlendedLookupTable(logical_palette.data()); } } void LoadRndLvlPal(dungeon_type l) { if (HeadlessMode) return; if (l == DTYPE_TOWN) { LoadPaletteAndInitBlending("levels\\towndata\\town.pal"); return; } if (l == DTYPE_CRYPT) { LoadPaletteAndInitBlending("nlevels\\l5data\\l5base.pal"); return; } int rv = RandomIntBetween(1, 4); char szFileName[27]; if (l == DTYPE_NEST) { if (!*GetOptions().Graphics.alternateNestArt) { rv++; } *BufCopy(szFileName, R"(nlevels\l6data\l6base)", rv, ".pal") = '\0'; } else { char nbuf[3]; const char *end = BufCopy(nbuf, static_cast(l)); const std::string_view n = std::string_view(nbuf, end - nbuf); *BufCopy(szFileName, "levels\\l", n, "data\\l", n, "_", rv, ".pal") = '\0'; } LoadPaletteAndInitBlending(szFileName); } void IncreaseBrightness() { const int brightnessValue = *GetOptions().Graphics.brightness; if (brightnessValue < 100) { const int newBrightness = std::min(brightnessValue + 5, 100); GetOptions().Graphics.brightness.SetValue(newBrightness); UpdateSystemPalette(logical_palette); } } void DecreaseBrightness() { const int brightnessValue = *GetOptions().Graphics.brightness; if (brightnessValue > 0) { const int newBrightness = std::max(brightnessValue - 5, 0); GetOptions().Graphics.brightness.SetValue(newBrightness); UpdateSystemPalette(logical_palette); } } int UpdateBrightness(int brightness) { if (brightness >= 0) { GetOptions().Graphics.brightness.SetValue(brightness); UpdateSystemPalette(logical_palette); } return *GetOptions().Graphics.brightness; } void BlackPalette() { for (SDL_Color &c : system_palette) { c.r = c.g = c.b = 0; } SystemPaletteUpdated(); } void PaletteFadeIn(int fr, const std::array &srcPalette) { if (HeadlessMode) return; if (demo::IsRunning()) fr = 0; std::array palette; #ifndef USE_SDL1 for (SDL_Color &color : palette) { color.a = SDL_ALPHA_OPAQUE; } #endif ApplyGlobalBrightness(palette.data(), srcPalette.data()); if (fr > 0) { const uint32_t tc = SDL_GetTicks(); fr *= 3; uint32_t prevFadeValue = 255; for (uint32_t i = 0; i < 256; i = fr * (SDL_GetTicks() - tc) / 50) { if (i == prevFadeValue) { SDL_Delay(1); continue; } ApplyFadeLevel(i, system_palette.data(), palette.data()); SystemPaletteUpdated(); // We can skip hardware cursor update for fade level 0 (everything is black). if (i != 0 && IsHardwareCursor()) { ReinitializeHardwareCursor(); } prevFadeValue = i; BltFast(nullptr, nullptr); RenderPresent(); } } system_palette = palette; SystemPaletteUpdated(); RedrawEverything(); if (IsHardwareCursor()) ReinitializeHardwareCursor(); if (fr <= 0) { BltFast(nullptr, nullptr); RenderPresent(); } sgbFadedIn = true; } void PaletteFadeOut(int fr, const std::array &srcPalette) { if (!sgbFadedIn || HeadlessMode) return; if (demo::IsRunning()) fr = 0; if (fr > 0) { SDL_Color palette[256]; ApplyGlobalBrightness(palette, srcPalette.data()); const uint32_t tc = SDL_GetTicks(); fr *= 3; uint32_t prevFadeValue = 0; for (uint32_t i = 0; i < 256; i = fr * (SDL_GetTicks() - tc) / 50) { if (i == prevFadeValue) { SDL_Delay(1); continue; } ApplyFadeLevel(256 - i, system_palette.data(), palette); SystemPaletteUpdated(); prevFadeValue = i; BltFast(nullptr, nullptr); RenderPresent(); } } BlackPalette(); if (IsHardwareCursor()) ReinitializeHardwareCursor(); if (fr <= 0) { BltFast(nullptr, nullptr); RenderPresent(); } sgbFadedIn = false; } void palette_update_caves() { CycleColors(1, 31); SystemPaletteUpdated(1, 31); } /** * @brief Cycle the lava every other frame, and glow every frame * Lava has 15 colors and the glow 16, so the full animation has 240 frames before it loops */ void palette_update_crypt() { static bool delayLava = false; if (!delayLava) { CycleColorsReverse(1, 15); delayLava = false; } CycleColorsReverse(16, 31); SystemPaletteUpdated(1, 31); delayLava = !delayLava; } /** * @brief Cycle the pond waves and bubles colors every 3rd frame * Bubles have 8 colors and waves 7, so the full animation has 56 frames before it loops */ void palette_update_hive() { static uint8_t delay = 0; if (delay != 2) { delay++; return; } CycleColorsReverse(1, 8); CycleColorsReverse(9, 15); SystemPaletteUpdated(1, 15); delay = 0; } void SetLogicalPaletteColor(unsigned i, const SDL_Color &color) { logical_palette[i] = color; ApplyGlobalBrightnessSingleColor(system_palette[i], logical_palette[i]); SystemPaletteUpdated(i, 1); UpdateBlendedLookupTableSingleColor(logical_palette.data(), i); } } // namespace devilution ================================================ FILE: Source/engine/palette.h ================================================ /** * @file palette.h * * Interface of functions for handling the engines color palette. */ #pragma once #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "levels/gendung_defs.hpp" namespace devilution { // Diablo uses a 256 color palette // Entry 0-127 (0x00-0x7F) are level specific // Entry 128-255 (0x80-0xFF) are global // standard palette for all levels // 8 or 16 shades per color // example (dark blue): PAL16_BLUE+14, PAL8_BLUE+7 // example (light red): PAL16_RED+2, PAL8_RED // example (orange): PAL16_ORANGE+8, PAL8_ORANGE+4 #define PAL8_BLUE 128 #define PAL8_RED 136 #define PAL8_YELLOW 144 #define PAL8_ORANGE 152 #define PAL16_BEIGE 160 #define PAL16_BLUE 176 #define PAL16_YELLOW 192 #define PAL16_ORANGE 208 #define PAL16_RED 224 #define PAL16_GRAY 240 /** * @brief An RGB color without an alpha component. */ struct Color { uint8_t rgb[3]; [[nodiscard]] SDL_Color toSDL() const { SDL_Color sdlColor; sdlColor.r = rgb[0]; sdlColor.g = rgb[1]; sdlColor.b = rgb[2]; #ifndef USE_SDL1 sdlColor.a = SDL_ALPHA_OPAQUE; #endif return sdlColor; } }; /** * @brief The palette before global brightness / fade effects. * * However, color cycling / swapping is applied to this palette. */ extern std::array logical_palette; /** * @brief This palette is the actual palette used for rendering. * * It is usually `logical_palette` with the global brightness setting * and fade-in/out applied. */ extern std::array system_palette; void palette_init(); /** * @brief Loads `logical_palette` from path. */ void LoadPalette(const char *path); /** * @brief Loads `logical_palette` from path, and generates the blending lookup table */ void LoadPaletteAndInitBlending(const char *path); /** * @brief Sets a single `logical_palette` color and updates the corresponding `system_color`. */ void SetLogicalPaletteColor(unsigned colorIndex, const SDL_Color &color); void LoadRndLvlPal(dungeon_type l); void IncreaseBrightness(); /** * @brief Updates the system palette by copying from `src` and applying the global brightness setting. * * `src` which is usually `logical_palette`. */ void UpdateSystemPalette(std::span src); /** * @brief Fade screen from black * @param fr Steps per 50ms */ void PaletteFadeIn(int fr, const std::array &srcPalette = logical_palette); /** * @brief Fade screen to black * @param fr Steps per 50ms */ void PaletteFadeOut(int fr, const std::array &srcPalette = logical_palette); /** * @brief Applies global brightness setting to `src` and writes the result to `dst`. */ void ApplyGlobalBrightness(SDL_Color *dst, const SDL_Color *src); /** * @brief Applies a fade-to-black effect to `src` and writes the result to `dst`. * * @param fadeval 0 - completely black, 256 - no effect. */ void ApplyFadeLevel(unsigned fadeval, SDL_Color *dst, const SDL_Color *src); /** * @brief Call this when `system_palette` is updated directly. * * You do not need to call this when updating the system palette via `UpdateSystemPalette`, `PaletteFadeIn/Out`, or `BlackPalette`. */ void SystemPaletteUpdated(int first = 0, int ncolor = 256); void DecreaseBrightness(); int UpdateBrightness(int sliderValue); /** * @brief Sets `system_palette` to all-black and calls `SystemPaletteUpdated`. */ void BlackPalette(); void palette_update_caves(); void palette_update_crypt(); void palette_update_hive(); } // namespace devilution ================================================ FILE: Source/engine/path.cpp ================================================ /** * @file path.cpp * * Implementation of the path finding algorithms. */ #include "engine/path.h" #include #include #include #include #include #include #include #include #include "appfat.h" #include "crawl.hpp" #include "engine/displacement.hpp" #include "engine/point.hpp" #include "utils/algorithm/container.hpp" #include "utils/static_vector.hpp" namespace devilution { // The frame times for axis-aligned and diagonal steps are actually the same. // // However, we set the diagonal step cost a bit higher to avoid // excessive diagonal movement. For example, the frame times for these // two paths are the same: ↑↑ and ↗↖. However, ↑↑ looks more natural. const int PathAxisAlignedStepCost = 100; const int PathDiagonalStepCost = 101; namespace { constexpr size_t MaxPathNodes = 1024; using NodeIndexType = uint16_t; using CoordType = uint8_t; using CostType = uint16_t; using PointT = PointOf; struct FrontierNode { PointT position; // Current best guess of the cost of the path to destination // if it goes through this node. CostType f; }; struct ExploredNode { // Preceding node (needed to reconstruct the path at the end). PointT prev; // The current lowest cost from start to this node (0 for the start node). CostType g; }; // A simple map with a fixed number of buckets and static storage. class ExploredNodes { static const size_t NumBuckets = 64; static const size_t BucketCapacity = 3 * MaxPathNodes / NumBuckets; using Entry = std::pair; using Bucket = StaticVector; public: using value_type = Entry; using iterator = value_type *; using const_iterator = const value_type *; [[nodiscard]] const_iterator find(const PointT &point) const { const Bucket &b = bucket(point); const auto *const it = c_find_if(b, [r = repr(point)](const Entry &e) { return e.first == r; }); if (it == b.end()) return nullptr; return it; } [[nodiscard]] iterator find(const PointT &point) { Bucket &b = bucket(point); auto *it = c_find_if(b, [r = repr(point)](const Entry &e) { return e.first == r; }); if (it == b.end()) return nullptr; return it; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) [[nodiscard]] const_iterator end() const { return nullptr; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) [[nodiscard]] iterator end() { return nullptr; } void emplace(const PointT &point, const ExploredNode &exploredNode) { bucket(point).emplace_back(repr(point), exploredNode); } [[nodiscard]] bool canInsert(const PointT &point) const { return bucket(point).size() < BucketCapacity; } private: [[nodiscard]] const Bucket &bucket(const PointT &point) const { return buckets_[bucketIndex(point)]; } [[nodiscard]] Bucket &bucket(const PointT &point) { return buckets_[bucketIndex(point)]; } [[nodiscard]] static size_t bucketIndex(const PointT &point) { return ((point.x & 0b111) << 3) | (point.y & 0b111); } [[nodiscard]] static uint16_t repr(const PointT &point) { return (point.x << 8) | point.y; } std::array buckets_; }; bool IsDiagonalStep(const Point &a, const Point &b) { return a.x != b.x && a.y != b.y; } /** * @brief Returns the distance between 2 adjacent nodes. */ CostType GetDistance(PointT startPosition, PointT destinationPosition) { return IsDiagonalStep(startPosition, destinationPosition) ? PathDiagonalStepCost : PathAxisAlignedStepCost; } /** * @brief heuristic, estimated cost from startPosition to destinationPosition. */ CostType GetHeuristicCost(PointT startPosition, PointT destinationPosition) { // This function needs to be admissible, i.e. it should never over-estimate // the distance. // // This calculation assumes we can take diagonal steps until we reach // the same row or column and then take the remaining axis-aligned steps. const int dx = std::abs(static_cast(startPosition.x) - static_cast(destinationPosition.x)); const int dy = std::abs(static_cast(startPosition.y) - static_cast(destinationPosition.y)); const int diagSteps = std::min(dx, dy); // After we've taken `diagSteps`, the remaining steps in one coordinate // will be zero, and in the other coordinate it will be reduced by `diagSteps`. // We then still need to take the remaining steps: // max(dx, dy) - diagSteps = max(dx, dy) - min(dx, dy) = abs(dx - dy) const int axisAlignedSteps = std::abs(dx - dy); return diagSteps * PathDiagonalStepCost + axisAlignedSteps * PathAxisAlignedStepCost; } int ReconstructPath(const ExploredNodes &explored, PointT dest, int8_t *path, size_t maxPathLength) { size_t len = 0; PointT cur = dest; while (true) { const auto *const it = explored.find(cur); if (it == explored.end()) app_fatal("Failed to reconstruct path"); if (it->second.g == 0) break; // reached start if (len == maxPathLength) { // Path too long. len = 0; break; } path[len++] = GetPathDirection(it->second.prev, cur); cur = it->second.prev; } std::reverse(path, path + len); std::fill(path + len, path + maxPathLength, -1); return static_cast(len); } } // namespace int8_t GetPathDirection(Point startPosition, Point destinationPosition) { constexpr int8_t PathDirections[9] = { 5, 1, 6, 2, 0, 3, 8, 4, 7 }; return PathDirections[3 * (destinationPosition.y - startPosition.y) + 4 + destinationPosition.x - startPosition.x]; } int FindPath(tl::function_ref canStep, tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t *path, size_t maxPathLength) { const PointT start { startPosition }; const PointT dest { destinationPosition }; const CostType initialHeuristicCost = GetHeuristicCost(start, dest); if (initialHeuristicCost > PathDiagonalStepCost * maxPathLength) { // Heuristic cost never underestimates the true cost, so we can give up early. return 0; } StaticVector frontier; ExploredNodes explored; { frontier.emplace_back(FrontierNode { .position = start, .f = initialHeuristicCost }); explored.emplace(start, ExploredNode { .prev = {}, .g = 0 }); } const auto frontierComparator = [&explored, &dest](const FrontierNode &a, const FrontierNode &b) { // We use heap functions from which form a max-heap. // We reverse the comparison sign here to get a min-heap. if (a.f != b.f) return a.f > b.f; // For nodes with the same f-score, prefer the ones with lower // heuristic cost (likely to be closer to the goal). const CostType hA = GetHeuristicCost(a.position, dest); const CostType hB = GetHeuristicCost(b.position, dest); if (hA != hB) return hA > hB; // Prefer diagonal steps first. const ExploredNode &aInfo = explored.find(a.position)->second; const ExploredNode &bInfo = explored.find(b.position)->second; const bool isDiagonalA = IsDiagonalStep(aInfo.prev, a.position); const bool isDiagonalB = IsDiagonalStep(bInfo.prev, b.position); if (isDiagonalA != isDiagonalB) return isDiagonalB; // Finally, disambiguate by coordinate: if (a.position.x != b.position.x) return a.position.x > b.position.x; return a.position.y > b.position.y; }; while (!frontier.empty()) { const FrontierNode cur = frontier.front(); // argmin(node.f) for node in openSet if (cur.position == destinationPosition) { return ReconstructPath(explored, cur.position, path, maxPathLength); } std::pop_heap(frontier.begin(), frontier.end(), frontierComparator); frontier.pop_back(); const CostType curG = explored.find(cur.position)->second.g; // Discard invalid nodes. // If this node is already at the maximum number of steps, we can skip processing it. // We don't keep track of the maximum number of steps, so we approximate it. if (curG >= PathDiagonalStepCost * maxPathLength) continue; // When we discover a better path to a node, we push the node to the heap // with the new `f` value even if the node is already in the heap. if (curG + GetHeuristicCost(cur.position, dest) > cur.f) continue; for (const DisplacementOf d : PathDirs) { // We're using `uint8_t` for coordinates. Avoid underflow: if ((cur.position.x == 0 && d.deltaX < 0) || (cur.position.y == 0 && d.deltaY < 0)) continue; const PointT neighborPos = cur.position + d; const bool ok = posOk(neighborPos); if (ok) { if (!canStep(cur.position, neighborPos)) continue; } else { // We allow targeting a non-walkable node if it is the destination. if (neighborPos != dest) continue; } const CostType g = curG + GetDistance(cur.position, neighborPos); if (curG >= PathDiagonalStepCost * maxPathLength) continue; bool improved = false; if (auto *it = explored.find(neighborPos); it == explored.end()) { if (explored.canInsert(neighborPos)) { explored.emplace(neighborPos, ExploredNode { .prev = cur.position, .g = g }); improved = true; } } else if (it->second.g > g) { it->second.prev = cur.position; it->second.g = g; improved = true; } if (improved) { const CostType f = g + GetHeuristicCost(neighborPos, dest); if (frontier.size() < MaxPathNodes) { // We always push the node to the heap, even if the same position already exists in it. // When popping from the heap, we discard invalid nodes by checking that `g + h <= f`. frontier.emplace_back(FrontierNode { .position = neighborPos, .f = f }); std::push_heap(frontier.begin(), frontier.end(), frontierComparator); } } } } return 0; // no path } std::optional FindClosestValidPosition(tl::function_ref posOk, Point startingPosition, unsigned int minimumRadius, unsigned int maximumRadius) { return Crawl(minimumRadius, maximumRadius, [&](Displacement displacement) -> std::optional { Point candidatePosition = startingPosition + displacement; if (posOk(candidatePosition)) return candidatePosition; return {}; }); } #ifdef BUILD_TESTING int TestPathGetHeuristicCost(Point startPosition, Point destinationPosition) { return GetHeuristicCost(startPosition, destinationPosition); } #endif } // namespace devilution ================================================ FILE: Source/engine/path.h ================================================ /** * @file path.h * * Interface of the path finding algorithms. */ #pragma once #include #include #include #include #include "engine/displacement.hpp" #include "engine/point.hpp" namespace devilution { constexpr size_t MaxPathLengthMonsters = 25; constexpr size_t MaxPathLengthPlayer = 100; // Cost for an axis-aligned step (up/down/left/right). Visible for testing. extern const int PathAxisAlignedStepCost; // Cost for a diagonal step. Visible for testing. extern const int PathDiagonalStepCost; /** * @brief Find the shortest path from `startPosition` to `destinationPosition`. * * @param canStep specifies whether a step between two adjacent points is allowed. * @param posOk specifies whether a position can be stepped on. * @param startPosition * @param destinationPosition * @param path Resulting path represented as the step directions, which are indices in `PathDirs`. Must have room for `maxPathLength` steps. * @param maxPathLength The maximum allowed length of the resulting path. * @return The length of the resulting path, or 0 if there is no valid path. */ int FindPath(tl::function_ref canStep, tl::function_ref posOk, Point startPosition, Point destinationPosition, int8_t *path, size_t maxPathLength); /** For iterating over the 8 possible movement directions */ const Displacement PathDirs[8] = { // clang-format off { -1, -1 }, //Direction::North { -1, 1 }, //Direction::West { 1, -1 }, //Direction::East { 1, 1 }, //Direction::South { -1, 0 }, //Direction::NorthWest { 0, -1 }, //Direction::NorthEast { 1, 0 }, //Direction::SouthEast { 0, 1 }, //Direction::SouthWest // clang-format on }; /** * Returns a number representing the direction from a starting tile to a neighbouring tile. * * Used in the pathfinding code, each step direction is assigned a number like this: * dx * -1 0 1 * +----- * -1|5 1 6 * dy 0|2 0 3 * 1|8 4 7 */ [[nodiscard]] int8_t GetPathDirection(Point startPosition, Point destinationPosition); /** * @brief Searches for the closest position that passes the check in expanding "rings". * * The search space is roughly equivalent to a square of tiles where the walking distance is equal to the radius except * the corners are "rounded" (inset) by one tile. For example the following is a search space of radius 4: * _XXXXXXX_ * XX_____XX * X_______X * < snip > * X_______X * XX_____XX * _XXXXXXX_ * * @param posOk Used to check if a position is valid * @param startingPosition dungeon tile location to start the search from * @param minimumRadius A value from 0 to 50, allows skipping nearby tiles (e.g. specify radius 1 to skip checking the starting tile) * @param maximumRadius The maximum distance to check, defaults to 18 for vanilla compatibility but supports values up to 50 * @return either the closest valid point or an empty optional */ std::optional FindClosestValidPosition(tl::function_ref posOk, Point startingPosition, unsigned int minimumRadius = 0, unsigned int maximumRadius = 18); } // namespace devilution ================================================ FILE: Source/engine/point.hpp ================================================ #pragma once #include #include #include #ifdef BUILD_TESTING #include #endif #include "engine/direction.hpp" #include "engine/displacement.hpp" #include "utils/attributes.h" namespace devilution { template struct PointOf; using Point = PointOf; template constexpr DisplacementOf operator-(PointOf a, PointOf b); template struct PointOf { CoordT x; CoordT y; PointOf() = default; template DVL_ALWAYS_INLINE constexpr PointOf(PointOf other) : x(other.x) , y(other.y) { } DVL_ALWAYS_INLINE constexpr PointOf(CoordT x, CoordT y) : x(x) , y(y) { } template DVL_ALWAYS_INLINE explicit constexpr PointOf(DisplacementOf other) : x(other.deltaX) , y(other.deltaY) { } template DVL_ALWAYS_INLINE constexpr bool operator==(const PointOf &other) const { return x == other.x && y == other.y; } template DVL_ALWAYS_INLINE constexpr bool operator!=(const PointOf &other) const { return !(*this == other); } template DVL_ALWAYS_INLINE constexpr PointOf &operator+=(const DisplacementOf &displacement) { x += displacement.deltaX; y += displacement.deltaY; return *this; } DVL_ALWAYS_INLINE constexpr PointOf &operator+=(Direction direction) { return (*this) += DisplacementOf::type>(direction); } template DVL_ALWAYS_INLINE constexpr PointOf &operator-=(const DisplacementOf &displacement) { x -= displacement.deltaX; y -= displacement.deltaY; return *this; } DVL_ALWAYS_INLINE constexpr PointOf &operator*=(const float factor) { x = static_cast(x * factor); y = static_cast(y * factor); return *this; } DVL_ALWAYS_INLINE constexpr PointOf &operator*=(const int factor) { x *= factor; y *= factor; return *this; } DVL_ALWAYS_INLINE constexpr PointOf &operator/=(const int factor) { x /= factor; y /= factor; return *this; } DVL_ALWAYS_INLINE constexpr PointOf operator-() const { static_assert(std::is_signed::value, "CoordT must be signed"); return { -x, -y }; } /** * @brief Fast approximate distance between two points, using only integer arithmetic, with less than ~5% error * @param other Pointer to which we want the distance * @return Magnitude of vector this -> other */ template constexpr int ApproxDistance(PointOf other) const { const Displacement offset = abs(Point(*this) - Point(other)); const auto [min, max] = std::minmax(offset.deltaX, offset.deltaY); int approx = max * 1007 + min * 441; if (max < (min * 16)) approx -= max * 40; return (approx + 512) / 1024; } /** * @brief Calculates the exact distance between two points (as accurate as the closest integer representation) * * In practice it is likely that ApproxDistance gives the same result, especially for nearby points. * @param other Point to which we want the distance * @return Exact magnitude of vector this -> other */ template int ExactDistance(PointOf other) const { const Displacement vector = Point(*this) - Point(other); // No need to call abs() as we square the values anyway // Casting multiplication operands to a wide type to address overflow warnings return static_cast(std::sqrt(static_cast(vector.deltaX) * vector.deltaX + static_cast(vector.deltaY) * vector.deltaY)); } template DVL_ALWAYS_INLINE constexpr int ManhattanDistance(PointOf other) const { return std::abs(static_cast(x) - static_cast(other.x)) + std::abs(static_cast(y) - static_cast(other.y)); } template DVL_ALWAYS_INLINE constexpr int WalkingDistance(PointOf other) const { return std::max( std::abs(static_cast(x) - static_cast(other.x)), std::abs(static_cast(y) - static_cast(other.y))); } /** * @brief Converts a coordinate in megatiles to the northmost of the 4 corresponding world tiles */ DVL_ALWAYS_INLINE constexpr PointOf megaToWorld() const { return { static_cast(16 + 2 * x), static_cast(16 + 2 * y) }; } /** * @brief Converts a coordinate in world tiles back to the corresponding megatile */ DVL_ALWAYS_INLINE constexpr PointOf worldToMega() const { return { static_cast((x - 16) / 2), static_cast((y - 16) / 2) }; } }; #ifdef BUILD_TESTING /** * @brief Format points nicely in test failure messages * @param stream output stream, expected to have overloads for int and char* * @param point Object to display * @return the stream, to allow chaining */ template std::ostream &operator<<(std::ostream &stream, const PointOf &point) { return stream << "(x: " << point.x << ", y: " << point.y << ")"; } #endif template DVL_ALWAYS_INLINE constexpr PointOf operator+(PointOf a, DisplacementOf displacement) { a += displacement; return a; } template DVL_ALWAYS_INLINE constexpr PointOf operator+(PointOf a, Direction direction) { a += direction; return a; } template DVL_ALWAYS_INLINE constexpr DisplacementOf operator-(PointOf a, PointOf b) { static_assert(std::is_signed::value == std::is_signed::value, "points must have the same signedness"); return { static_cast(a.x - b.x), static_cast(a.y - b.y) }; } template DVL_ALWAYS_INLINE constexpr PointOf operator-(PointOf a, DisplacementOf displacement) { a -= displacement; return a; } template DVL_ALWAYS_INLINE constexpr PointOf operator*(PointOf a, const float factor) { a *= factor; return a; } template DVL_ALWAYS_INLINE constexpr PointOf operator*(PointOf a, const int factor) { a *= factor; return a; } template DVL_ALWAYS_INLINE constexpr PointOf abs(PointOf a) { return { std::abs(a.x), std::abs(a.y) }; } /** * @brief Calculate the best fit direction between two points * @param start Tile coordinate * @param destination Tile coordinate * @return A value from the direction enum */ inline Direction GetDirection(Point start, Point destination) { Direction md; int mx = destination.x - start.x; int my = destination.y - start.y; if (mx >= 0) { if (my >= 0) { if (5 * mx <= (my * 2)) // mx/my <= 0.4, approximation of tan(22.5) return Direction::SouthWest; md = Direction::South; } else { my = -my; if (5 * mx <= (my * 2)) return Direction::NorthEast; md = Direction::East; } if (5 * my <= (mx * 2)) // my/mx <= 0.4 md = Direction::SouthEast; } else { mx = -mx; if (my >= 0) { if (5 * mx <= (my * 2)) return Direction::SouthWest; md = Direction::West; } else { my = -my; if (5 * mx <= (my * 2)) return Direction::NorthEast; md = Direction::North; } if (5 * my <= (mx * 2)) md = Direction::NorthWest; } return md; } } // namespace devilution ================================================ FILE: Source/engine/points_in_rectangle_range.hpp ================================================ #pragma once #include #include "point.hpp" #include "rectangle.hpp" namespace devilution { template class PointsInRectangleIteratorBase { public: using iterator_category = std::random_access_iterator_tag; using difference_type = CoordT; using value_type = PointOf; using pointer = void; using reference = value_type; protected: constexpr PointsInRectangleIteratorBase(PointOf origin, int majorDimension, int majorIndex, int minorIndex) : origin(origin) , majorDimension(majorDimension) , majorIndex(majorIndex) , minorIndex(minorIndex) { } explicit constexpr PointsInRectangleIteratorBase(PointOf origin, int majorDimension, int index = 0) : PointsInRectangleIteratorBase(origin, majorDimension, index / majorDimension, index % majorDimension) { } DVL_ALWAYS_INLINE void Increment() { ++minorIndex; if (minorIndex >= majorDimension) { ++majorIndex; minorIndex -= majorDimension; } } void Decrement() { if (minorIndex <= 0) { --majorIndex; minorIndex += majorDimension; } --minorIndex; } void Offset(difference_type delta) { majorIndex += (minorIndex + delta) / majorDimension; minorIndex = (minorIndex + delta) % majorDimension; if (minorIndex < 0) { minorIndex += majorDimension; --majorIndex; } } PointOf origin; int majorDimension; int majorIndex; int minorIndex; }; template class PointsInRectangle { public: using const_iterator = class PointsInRectangleIterator : public PointsInRectangleIteratorBase { public: using iterator_category = typename PointsInRectangleIteratorBase::iterator_category; using difference_type = typename PointsInRectangleIteratorBase::difference_type; using value_type = typename PointsInRectangleIteratorBase::value_type; using pointer = typename PointsInRectangleIteratorBase::pointer; using reference = typename PointsInRectangleIteratorBase::reference; constexpr PointsInRectangleIterator(RectangleOf region, int index = 0) : PointsInRectangleIteratorBase(region.position, region.size.width, index) { } DVL_ALWAYS_INLINE value_type operator*() const { // Row-major iteration e.g. {0, 0}, {1, 0}, {2, 0}, {0, 1}, {1, 1}, ... return this->origin + Displacement { this->minorIndex, this->majorIndex }; } // Equality comparable concepts DVL_ALWAYS_INLINE bool operator==(const PointsInRectangleIterator &rhs) const { return this->majorIndex == rhs.majorIndex && this->minorIndex == rhs.minorIndex; } DVL_ALWAYS_INLINE bool operator!=(const PointsInRectangleIterator &rhs) const { return !(*this == rhs); } // Partially ordered concepts bool operator>=(const PointsInRectangleIterator &rhs) const { return this->majorIndex > rhs.majorIndex || (this->majorIndex == rhs.majorIndex && this->minorIndex >= rhs.minorIndex); } bool operator<(const PointsInRectangleIterator &rhs) const { return !(*this >= rhs); } bool operator<=(const PointsInRectangleIterator &rhs) const { return this->majorIndex < rhs.majorIndex || (this->majorIndex == rhs.majorIndex && this->minorIndex <= rhs.minorIndex); } bool operator>(const PointsInRectangleIterator &rhs) const { return !(*this <= rhs); } difference_type operator-(const PointsInRectangleIterator &rhs) const { return (this->majorIndex - rhs.majorIndex) * this->majorDimension + (this->minorIndex - rhs.minorIndex); } // Forward concepts DVL_ALWAYS_INLINE PointsInRectangleIterator &operator++() { this->Increment(); return *this; } PointsInRectangleIterator operator++(int) { auto copy = *this; ++(*this); return copy; } // Bidirectional concepts PointsInRectangleIterator &operator--() { this->Decrement(); return *this; } PointsInRectangleIterator operator--(int) { auto copy = *this; --(*this); return copy; } // Random access concepts PointsInRectangleIterator operator+(difference_type delta) const { auto copy = *this; return copy += delta; } PointsInRectangleIterator &operator+=(difference_type delta) { this->Offset(delta); return *this; } friend PointsInRectangleIterator operator+(difference_type delta, const PointsInRectangleIterator &it) { return it + delta; } PointsInRectangleIterator &operator-=(difference_type delta) { return *this += -delta; } PointsInRectangleIterator operator-(difference_type delta) const { auto copy = *this; return copy -= delta; } value_type operator[](difference_type offset) const { return **(this + offset); } }; constexpr PointsInRectangle(RectangleOf region) : region(region) { } [[nodiscard]] const_iterator cbegin() const { return region; } [[nodiscard]] const_iterator begin() const { return cbegin(); } [[nodiscard]] const_iterator cend() const { return { region, region.size.width * region.size.height }; } [[nodiscard]] const_iterator end() const { return cend(); } [[nodiscard]] auto crbegin() const { // explicit type needed for older GCC versions return std::reverse_iterator(cend()); } [[nodiscard]] auto rbegin() const { return crbegin(); } [[nodiscard]] auto crend() const { // explicit type needed for older GCC versions return std::reverse_iterator(cbegin()); } [[nodiscard]] auto rend() const { return crend(); } protected: RectangleOf region; }; template class PointsInRectangleColMajor { public: using const_iterator = class PointsInRectangleIteratorColMajor : public PointsInRectangleIteratorBase { public: using iterator_category = typename PointsInRectangleIteratorBase::iterator_category; using difference_type = typename PointsInRectangleIteratorBase::difference_type; using value_type = typename PointsInRectangleIteratorBase::value_type; using pointer = typename PointsInRectangleIteratorBase::pointer; using reference = typename PointsInRectangleIteratorBase::reference; constexpr PointsInRectangleIteratorColMajor(RectangleOf region, int index = 0) : PointsInRectangleIteratorBase(region.position, region.size.height, index) { } DVL_ALWAYS_INLINE value_type operator*() const { // Col-major iteration e.g. {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, ... return this->origin + Displacement { this->majorIndex, this->minorIndex }; } // Equality comparable concepts DVL_ALWAYS_INLINE bool operator==(const PointsInRectangleIteratorColMajor &rhs) const { return this->majorIndex == rhs.majorIndex && this->minorIndex == rhs.minorIndex; } DVL_ALWAYS_INLINE bool operator!=(const PointsInRectangleIteratorColMajor &rhs) const { return !(*this == rhs); } // Partially ordered concepts bool operator>=(const PointsInRectangleIteratorColMajor &rhs) const { return this->majorIndex > rhs.majorIndex || (this->majorIndex == rhs.majorIndex && this->minorIndex >= rhs.minorIndex); } bool operator<(const PointsInRectangleIteratorColMajor &rhs) const { return !(*this >= rhs); } bool operator<=(const PointsInRectangleIteratorColMajor &rhs) const { return this->majorIndex < rhs.majorIndex || (this->majorIndex == rhs.majorIndex && this->minorIndex <= rhs.minorIndex); } bool operator>(const PointsInRectangleIteratorColMajor &rhs) const { return !(*this <= rhs); } difference_type operator-(const PointsInRectangleIteratorColMajor &rhs) const { return (this->majorIndex - rhs.majorIndex) * this->majorDimension + (this->minorIndex - rhs.minorIndex); } // Forward concepts DVL_ALWAYS_INLINE PointsInRectangleIteratorColMajor &operator++() { this->Increment(); return *this; } PointsInRectangleIteratorColMajor operator++(int) { auto copy = *this; ++(*this); return copy; } // Bidirectional concepts PointsInRectangleIteratorColMajor &operator--() { this->Decrement(); return *this; } PointsInRectangleIteratorColMajor operator--(int) { auto copy = *this; --(*this); return copy; } // Random access concepts PointsInRectangleIteratorColMajor operator+(difference_type delta) const { auto copy = *this; return copy += delta; } PointsInRectangleIteratorColMajor &operator+=(difference_type delta) { this->Offset(delta); return *this; } friend PointsInRectangleIteratorColMajor operator+(difference_type delta, const PointsInRectangleIteratorColMajor &it) { return it + delta; } PointsInRectangleIteratorColMajor &operator-=(difference_type delta) { return *this += -delta; } PointsInRectangleIteratorColMajor operator-(difference_type delta) const { auto copy = *this; return copy -= delta; } value_type operator[](difference_type offset) const { return **(this + offset); } }; // gcc6 needs a defined constructor? constexpr PointsInRectangleColMajor(RectangleOf region) : region(region) { } [[nodiscard]] const_iterator cbegin() const { return region; } [[nodiscard]] const_iterator begin() const { return cbegin(); } [[nodiscard]] const_iterator cend() const { return { region, region.size.width * region.size.height }; } [[nodiscard]] const_iterator end() const { return cend(); } [[nodiscard]] auto crbegin() const { // explicit type needed for older GCC versions return std::reverse_iterator(cend()); } [[nodiscard]] auto rbegin() const { return crbegin(); } [[nodiscard]] auto crend() const { // explicit type needed for older GCC versions return std::reverse_iterator(cbegin()); } [[nodiscard]] auto rend() const { return crend(); } protected: RectangleOf region; }; } // namespace devilution ================================================ FILE: Source/engine/random.cpp ================================================ #include "engine/random.hpp" #include #include #include #include #include #include #include namespace devilution { /** Current game seed */ uint32_t sglGameSeed; /** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ std::linear_congruential_engine diabloGenerator; /** Xoshiro pseudo-random number generator to provide less predictable seeds */ xoshiro128plusplus seedGenerator; uint32_t xoshiro128plusplus::next() { const uint32_t result = std::rotl(s[0] + s[3], 7) + s[0]; const uint32_t t = s[1] << 9; s[2] ^= s[0]; s[3] ^= s[1]; s[1] ^= s[2]; s[0] ^= s[3]; s[2] ^= t; s[3] = std::rotl(s[3], 11); return result; } uint64_t xoshiro128plusplus::timeSeed() { auto now = std::chrono::system_clock::now(); auto nano = std::chrono::nanoseconds(now.time_since_epoch()); return static_cast(nano.count()); } void xoshiro128plusplus::copy(state &dst, const state &src) { memcpy(dst, src, sizeof(dst)); } xoshiro128plusplus ReserveSeedSequence() { xoshiro128plusplus reserved = seedGenerator; seedGenerator.jump(); return reserved; } uint32_t GenerateSeed() { return seedGenerator.next(); } void SetRndSeed(uint32_t seed) { diabloGenerator.seed(seed); sglGameSeed = seed; } uint32_t GetLCGEngineState() { return sglGameSeed; } void DiscardRandomValues(unsigned count) { while (count != 0) { GenerateRandomNumber(); count--; } } uint32_t GenerateRandomNumber() { sglGameSeed = diabloGenerator(); return sglGameSeed; } int32_t AdvanceRndSeed() { const int32_t seed = static_cast(GenerateRandomNumber()); // since abs(INT_MIN) is undefined behavior, handle this value specially return seed == std::numeric_limits::min() ? std::numeric_limits::min() : std::abs(seed); } int32_t GenerateRnd(int32_t v) { if (v <= 0) return 0; if (v <= 0x7FFF) // use the high bits to correct for LCG bias return (AdvanceRndSeed() >> 16) % v; return AdvanceRndSeed() % v; } bool FlipCoin(unsigned frequency) { // Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned. return GenerateRnd(static_cast(frequency)) == 0; } } // namespace devilution ================================================ FILE: Source/engine/random.hpp ================================================ /** * @file random.hpp * * Contains convenience functions for random number generation * * This includes specific engine/distribution functions for logic that needs to be compatible with the base game. */ #pragma once #include #include #include #include namespace devilution { class DiabloGenerator { private: /** Borland C/C++ psuedo-random number generator needed for vanilla compatibility */ std::linear_congruential_engine lcg; public: /** * @brief Set the state of the RandomNumberEngine used by the base game to the specific seed * @param seed New engine state */ DiabloGenerator(uint32_t seed) { lcg.seed(seed); } /** * @brief Advance the global RandomNumberEngine state by the specified number of rounds * * Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. * @param count How many values to discard */ void discardRandomValues(unsigned count) { lcg.discard(count); } /** * @brief Generates a random non-negative integer (most of the time) using the vanilla RNG * * This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try * discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. * * This function is only used when the base game wants to store the seed used to generate an item or level, however * as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more * appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. * * @return A random number in the range [0,2^31) or -2^31 */ int32_t advanceRndSeed() { const int32_t seed = static_cast(lcg()); // since abs(INT_MIN) is undefined behavior, handle this value specially return seed == std::numeric_limits::min() ? std::numeric_limits::min() : std::abs(seed); } /** * @brief Generates a random integer less than the given limit using the vanilla RNG * * If v is not a positive number this function returns 0 without calling the RNG. * * Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value * less than 32768 for limits in that range. * * This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() * * @see AdvanceRndSeed() * @param v The upper limit for the return value * @return A random number in the range [0, v) or rarely a negative value in (-v, -1] */ int32_t generateRnd(int32_t v) { if (v <= 0) return 0; if (v <= 0x7FFF) // use the high bits to correct for LCG bias return (advanceRndSeed() >> 16) % v; return advanceRndSeed() % v; } /** * @brief Generates a random boolean value using the vanilla RNG * * This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 * represents a 50/50 chance. * * @param frequency odds of returning a true value * @return A random boolean value */ bool flipCoin(unsigned frequency) { // Casting here because GenerateRnd takes a signed argument when it should take and yield unsigned. return generateRnd(static_cast(frequency)) == 0; } /** * @brief Picks one of the elements in the list randomly. * * @param values The values to pick from * @return A random value from the 'values' list. */ template const T pickRandomlyAmong(const std::initializer_list &values) { const auto index { std::max(generateRnd(static_cast(values.size())), 0) }; return *(values.begin() + index); } /** * @brief Generates a random non-negative integer * * Effectively the same as GenerateRnd but will never return a negative value * @param v upper limit for the return value * @return a value between 0 and v-1 inclusive, i.e. the range [0, v) */ inline int32_t randomIntLessThan(int32_t v) { return std::max(generateRnd(v), 0); } /** * @brief Randomly chooses a value somewhere within the given range * @param min lower limit, minimum possible value * @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range * @param halfOpen whether to use the limits as a half-open range or not * @return a randomly selected integer */ inline int32_t randomIntBetween(int32_t min, int32_t max, bool halfOpen = false) { return randomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; } }; // Based on fmix32 implementation from MurmurHash3 created by Austin Appleby in 2008 // https://github.com/aappleby/smhasher/blob/61a0530f28277f2e850bfc39600ce61d02b518de/src/MurmurHash3.cpp#L68 // and adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna // // See also: // Guy L. Steele, Doug Lea, and Christine H. Flood. 2014. // Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472. // https://doi.org/10.1145/2714064.2660195 class SplitMix32 { uint32_t state; public: SplitMix32(uint32_t state) : state(state) { } uint32_t next() { uint32_t z = (state += 0x9e3779b9); z = (z ^ (z >> 16)) * 0x85ebca6b; z = (z ^ (z >> 13)) * 0xc2b2ae35; return z ^ (z >> 16); } void generate(uint32_t *begin, const uint32_t *end) { while (begin != end) { *begin = next(); ++begin; } } }; // Adapted from https://prng.di.unimi.it/splitmix64.c written in 2015 by Sebastiano Vigna // // See also: // Guy L. Steele, Doug Lea, and Christine H. Flood. 2014. // Fast splittable pseudorandom number generators. SIGPLAN Not. 49, 10 (October 2014), 453–472. // https://doi.org/10.1145/2714064.2660195 class SplitMix64 { uint64_t state; public: SplitMix64(uint64_t state) : state(state) { } uint64_t next() { uint64_t z = (state += 0x9e3779b97f4a7c15); z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9; z = (z ^ (z >> 27)) * 0x94d049bb133111eb; return z ^ (z >> 31); } void generate(uint64_t *begin, const uint64_t *end) { while (begin != end) { *begin = next(); ++begin; } } }; /** Adapted from https://prng.di.unimi.it/xoshiro128plusplus.c written in 2019 by David Blackman and Sebastiano Vigna */ class xoshiro128plusplus { public: typedef uint32_t state[4]; xoshiro128plusplus() { seed(); } xoshiro128plusplus(const state &s) { copy(this->s, s); } xoshiro128plusplus(uint64_t initialSeed) { seed(initialSeed); } xoshiro128plusplus(uint32_t initialSeed) { seed(initialSeed); } uint32_t next(); /* This is the jump function for the generator. It is equivalent to 2^64 calls to next(); it can be used to generate 2^64 non-overlapping subsequences for parallel computations. */ void jump() { static constexpr uint32_t JUMP[] = { 0x8764000b, 0xf542d2d3, 0x6fa035c3, 0x77f2db5b }; uint32_t s0 = 0; uint32_t s1 = 0; uint32_t s2 = 0; uint32_t s3 = 0; for (const uint32_t entry : JUMP) for (int b = 0; b < 32; b++) { if (entry & UINT32_C(1) << b) { s0 ^= s[0]; s1 ^= s[1]; s2 ^= s[2]; s3 ^= s[3]; } next(); } s[0] = s0; s[1] = s1; s[2] = s2; s[3] = s3; } void save(state &s) const { copy(s, this->s); } private: state s; void seed(uint64_t value) { uint64_t seeds[2]; SplitMix64 seedSequence { value }; seedSequence.generate(seeds, seeds + 2); s[0] = static_cast(seeds[0] >> 32); s[1] = static_cast(seeds[0]); s[2] = static_cast(seeds[1] >> 32); s[3] = static_cast(seeds[1]); } void seed(uint32_t value) { SplitMix32 seedSequence { value }; seedSequence.generate(s, s + 4); } void seed() { seed(timeSeed()); #if !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) static std::random_device rd; std::uniform_int_distribution dist; for (uint32_t &cell : s) cell ^= dist(rd); #endif } static uint64_t timeSeed(); static void copy(state &dst, const state &src); }; /** * @brief Returns a copy of the global seed generator and fast-forwards the global seed generator to avoid collisions */ xoshiro128plusplus ReserveSeedSequence(); /** * @brief Advances the global seed generator state and returns the new value */ uint32_t GenerateSeed(); /** * @brief Set the state of the RandomNumberEngine used by the base game to the specific seed * @param seed New engine state */ void SetRndSeed(uint32_t seed); /** * @brief Returns the current state of the RandomNumberEngine used by the base game * * This is only exposed to allow for debugging vanilla code and testing. Using this engine for new code is discouraged * due to the poor randomness and bugs in the implementation that need to be retained for compatibility. * * @return The current engine state */ uint32_t GetLCGEngineState(); /** * @brief Advance the global RandomNumberEngine state by the specified number of rounds * * Only used to maintain vanilla compatibility until logic requiring reproducible random number generation is isolated. * @param count How many values to discard */ void DiscardRandomValues(unsigned count); /** * @brief Advances the global RandomNumberEngine state and returns the new value */ uint32_t GenerateRandomNumber(); /** * @brief Generates a random non-negative integer (most of the time) using the vanilla RNG * * This advances the engine state then interprets the new engine state as a signed value and calls std::abs to try * discard the high bit of the result. This usually returns a positive number but may very rarely return -2^31. * * This function is only used when the base game wants to store the seed used to generate an item or level, however * as the returned value is transformed about 50% of values do not reflect the actual engine state. It would be more * appropriate to use GetLCGEngineState() in these cases but that may break compatibility with the base game. * * @return A random number in the range [0,2^31) or -2^31 */ [[nodiscard]] int32_t AdvanceRndSeed(); /** * @brief Generates a random integer less than the given limit using the vanilla RNG * * If v is not a positive number this function returns 0 without calling the RNG. * * Limits between 32768 and 65534 should be avoided as a bug in vanilla means this function always returns a value * less than 32768 for limits in that range. * * This can very rarely return a negative value in the range (-v, -1] due to the bug in AdvanceRndSeed() * * @see AdvanceRndSeed() * @param v The upper limit for the return value * @return A random number in the range [0, v) or rarely a negative value in (-v, -1] */ int32_t GenerateRnd(int32_t v); /** * @brief Generates a random boolean value using the vanilla RNG * * This function returns true 1 in `frequency` of the time, otherwise false. For example the default frequency of 2 * represents a 50/50 chance. * * @param frequency odds of returning a true value * @return A random boolean value */ bool FlipCoin(unsigned frequency = 2); /** * @brief Picks one of the elements in the list randomly. * * @param values The values to pick from * @return A random value from the 'values' list. */ template const T PickRandomlyAmong(const std::initializer_list &values) { const auto index { std::max(GenerateRnd(static_cast(values.size())), 0) }; return *(values.begin() + index); } /** * @brief Generates a random non-negative integer * * Effectively the same as GenerateRnd but will never return a negative value * @param v upper limit for the return value * @return a value between 0 and v-1 inclusive, i.e. the range [0, v) */ inline int32_t RandomIntLessThan(int32_t v) { return std::max(GenerateRnd(v), 0); } /** * @brief Randomly chooses a value somewhere within the given range * @param min lower limit, minimum possible value * @param max upper limit, either the maximum possible value for a closed range (the default behaviour) or one greater than the maximum value for a half-open range * @param halfOpen whether to use the limits as a half-open range or not * @return a randomly selected integer */ inline int32_t RandomIntBetween(int32_t min, int32_t max, bool halfOpen = false) { return RandomIntLessThan(max - min + (halfOpen ? 0 : 1)) + min; } } // namespace devilution ================================================ FILE: Source/engine/rectangle.hpp ================================================ #pragma once #include "engine/point.hpp" #include "engine/size.hpp" #include "utils/attributes.h" namespace devilution { template struct RectangleOf { PointOf position; SizeOf size; RectangleOf() = default; DVL_ALWAYS_INLINE constexpr RectangleOf(PointOf position, SizeOf size) : position(position) , size(size) { } /** * @brief Constructs a rectangle centered on the given point and including all tiles within the given radius. * * The resulting rectangle will be square with an odd size equal to 2*radius + 1. * * @param center center point of the target rectangle * @param radius a non-negative value indicating how many tiles to include around the center */ DVL_ALWAYS_INLINE explicit constexpr RectangleOf(PointOf center, SizeT radius) : position(center - DisplacementOf { radius }) , size(static_cast(2 * radius + 1)) { } /** * @brief Whether this rectangle contains the given point. * Works correctly even if the point uses a different underlying numeric type */ template DVL_ALWAYS_INLINE constexpr bool contains(PointOf point) const { return contains(point.x, point.y); } template constexpr bool contains(T x, T y) const { return x >= this->position.x && x < (this->position.x + this->size.width) && y >= this->position.y && y < (this->position.y + this->size.height); } /** * @brief Computes the center of this rectangle in integer coordinates. Values are truncated towards zero. */ constexpr PointOf Center() const { return position + DisplacementOf(size / 2); } /** * @brief Returns a rectangle with all sides shrunk according to the given displacement * * Effectively moves the left/right edges in by deltaX, and the top/bottom edges in by deltaY */ constexpr RectangleOf inset(DisplacementOf factor) const { return { position + factor, SizeOf(size.width - factor.deltaX * 2, size.height - factor.deltaY * 2) }; } }; using Rectangle = RectangleOf; } // namespace devilution ================================================ FILE: Source/engine/render/automap_render.cpp ================================================ /** * @file automap_render.cpp * * Line drawing routines for the automap. */ #include "engine/render/automap_render.hpp" #include #include "automap.h" #include "engine/render/primitive_render.hpp" namespace devilution { namespace { enum class DirectionX : int8_t { EAST = 1, WEST = -1, }; enum class DirectionY : int8_t { SOUTH = 1, NORTH = -1, }; template void DrawMapLine(const Surface &out, Point from, int height, std::uint8_t colorIndex) { while (height-- > 0) { SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); from.x += static_cast(DirX); SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); from.x += static_cast(DirX); from.y += static_cast(DirY); } SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); } template void DrawMapLineSteep(const Surface &out, Point from, int width, std::uint8_t colorIndex) { while (width-- > 0) { SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); from.y += static_cast(DirY); SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); from.y += static_cast(DirY); from.x += static_cast(DirX); } SetMapPixel(out, { from.x, from.y + 1 }, 0); SetMapPixel(out, from, colorIndex); } } // namespace void DrawMapLineNS(const Surface &out, Point from, int height, std::uint8_t colorIndex) { if (from.x < 0 || from.x >= out.w() || from.y >= out.h() || height <= 0 || from.y + height <= 0) return; if (from.y < 0) { height += from.y; from.y = 0; } if (from.y + height > out.h()) height = out.h() - from.y; for (int i = 0; i < height; ++i) { SetMapPixel(out, { from.x, from.y + i }, colorIndex); } } void DrawMapLineWE(const Surface &out, Point from, int width, std::uint8_t colorIndex) { if (from.y < 0 || from.y >= out.h() || from.x >= out.w() || width <= 0 || from.x + width <= 0) return; if (from.x < 0) { width += from.x; from.x = 0; } if (from.x + width > out.w()) width = out.w() - from.x; for (int i = 0; i < width; ++i) { SetMapPixel(out, { from.x + i, from.y }, colorIndex); } } void DrawMapLineNE(const Surface &out, Point from, int height, std::uint8_t colorIndex) { DrawMapLine(out, from, height, colorIndex); } void DrawMapLineSE(const Surface &out, Point from, int height, std::uint8_t colorIndex) { DrawMapLine(out, from, height, colorIndex); } void DrawMapLineNW(const Surface &out, Point from, int height, std::uint8_t colorIndex) { DrawMapLine(out, from, height, colorIndex); } void DrawMapLineSW(const Surface &out, Point from, int height, std::uint8_t colorIndex) { DrawMapLine(out, from, height, colorIndex); } void DrawMapLineSteepNE(const Surface &out, Point from, int width, std::uint8_t colorIndex) { DrawMapLineSteep(out, from, width, colorIndex); } void DrawMapLineSteepSE(const Surface &out, Point from, int width, std::uint8_t colorIndex) { DrawMapLineSteep(out, from, width, colorIndex); } void DrawMapLineSteepNW(const Surface &out, Point from, int width, std::uint8_t colorIndex) { DrawMapLineSteep(out, from, width, colorIndex); } void DrawMapLineSteepSW(const Surface &out, Point from, int width, std::uint8_t colorIndex) { DrawMapLineSteep(out, from, width, colorIndex); } /** * @brief Draws a line from first point to second point, unrestricted to the standard automap angles. Doesn't include shadow. */ void DrawMapFreeLine(const Surface &out, Point from, Point to, uint8_t colorIndex) { const int dx = std::abs(to.x - from.x); const int dy = std::abs(to.y - from.y); const int sx = from.x < to.x ? 1 : -1; const int sy = from.y < to.y ? 1 : -1; int err = dx - dy; while (true) { SetMapPixel(out, from, colorIndex); if (from.x == to.x && from.y == to.y) { break; } const int e2 = 2 * err; if (e2 > -dy) { err -= dy; from.x += sx; } if (e2 < dx) { err += dx; from.y += sy; } } } void SetMapPixel(const Surface &out, Point position, uint8_t color) { if (GetAutomapType() == AutomapType::Minimap && !MinimapRect.contains(position)) return; if (GetAutomapType() == AutomapType::Transparent) { SetHalfTransparentPixel(out, position, color); } else { out.SetPixel(position, color); } } } // namespace devilution ================================================ FILE: Source/engine/render/automap_render.hpp ================================================ /** * @file automap_render.hpp * Defines 2 sets of rendering primitives for drawing automap lines. * * 1. DrawMapLine* - used for rendering most map lines - 2 pixels horizontally for each pixel vertically. * 2. DrawMapLineSteep* - currently only used for rendering the player arrow - 2 pixels vertically for each pixel horizontally. * * These functions draw a single extra pixel at the end of the line -- they always draw an odd number of pixels. * These functions clip to the output buffer -- they are safe to call with out-of-bounds coordinates. */ #pragma once #include #include "engine/point.hpp" #include "engine/surface.hpp" namespace devilution { void DrawMapLineNS(const Surface &out, Point from, int height, std::uint8_t colorIndex); void DrawMapLineWE(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards north east at an `atan(1/2)` angle. * * Draws 2 horizontal pixels for each vertical step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x + 2 * height + 1, from.y - height }`. */ void DrawMapLineNE(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards south east at an `atan(1/2)` angle. * * Draws 2 horizontal pixels for each vertical step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x + 2 * height + 1, from.y + height }`. */ void DrawMapLineSE(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards north west at an `atan(1/2)` angle. * * Draws 2 horizontal pixels for each vertical step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x - 2 * height + 1, from.y - height }`. */ void DrawMapLineNW(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards south west at an `atan(1/2)` angle. * * Draws 2 horizontal pixels for each vertical step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x - 2 * height + 1, from.y + height }`. */ void DrawMapLineSW(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards north east at an `atan(1/2)` angle. * * Draws 2 vertical pixels for each horizontal step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x + width + 1, from.y - 2 * width }`. */ void DrawMapLineSteepNE(const Surface &out, Point from, int width, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards south east at an `atan(2)` angle. * * Draws 2 vertical pixels for each horizontal step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x + width + 1, from.y + 2 * width }`. */ void DrawMapLineSteepSE(const Surface &out, Point from, int width, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards north west at an `atan(1/2)` angle. * * Draws 2 vertical pixels for each horizontal step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x - (width + 1), from.y - 2 * width }`. */ void DrawMapLineSteepNW(const Surface &out, Point from, int width, std::uint8_t colorIndex); /** * @brief Draw a line in the target buffer from the given point towards south west at an `atan(1/2)` angle. * * Draws 2 vertical pixels for each horizontal step, then an additional one where it draws 1 pixel. * * The end point is at `{ from.x - (width + 1), from.y + 2 * width }`. */ void DrawMapLineSteepSW(const Surface &out, Point from, int width, std::uint8_t colorIndex); void DrawMapFreeLine(const Surface &out, Point from, Point to, uint8_t colorIndex); /** * @brief Draw an automap pixel. * * Draw either an opaque pixel or a transparent pixel, depending on the automap mode. */ void SetMapPixel(const Surface &out, Point position, uint8_t color); } // namespace devilution ================================================ FILE: Source/engine/render/blit_impl.hpp ================================================ #pragma once #include #include #include #include #include "engine/render/light_render.hpp" #include "utils/attributes.h" #include "utils/palette_blending.hpp" namespace devilution { #if __cpp_lib_execution >= 201902L #define DEVILUTIONX_BLIT_EXECUTION_POLICY std::execution::unseq, #else #define DEVILUTIONX_BLIT_EXECUTION_POLICY #endif DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillDirect(uint8_t *dst, unsigned length, uint8_t color) { DVL_ASSUME(length != 0); std::memset(dst, color, length); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsDirect(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length) { DVL_ASSUME(length != 0); std::memcpy(dst, src, length); } struct BlitDirect { DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsDirect(dst, src, length); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillDirect(dst, length, color); } }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithMap(uint8_t *dst, unsigned length, uint8_t color, const uint8_t *DVL_RESTRICT colorMap) { DVL_ASSUME(length != 0); std::memset(dst, colorMap[color], length); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsWithMap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const uint8_t *DVL_RESTRICT colorMap) { DVL_ASSUME(length != 0); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, dst, [colorMap](uint8_t srcColor) { return colorMap[srcColor]; }); } struct BlitWithMap { const uint8_t *DVL_RESTRICT colorMap; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsWithMap(dst, src, length, colorMap); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillWithMap(dst, length, color, colorMap); } }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap &lightmap) { DVL_ASSUME(length != 0); const uint8_t *light = lightmap.getLightingAt(dst); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY light, light + length, dst, [color, &lightmap](uint8_t lightLevel) { return lightmap.adjustColor(color, lightLevel); }); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsWithLightmap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const Lightmap &lightmap) { DVL_ASSUME(length != 0); const uint8_t *light = lightmap.getLightingAt(dst); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, light, dst, [&lightmap](uint8_t srcColor, uint8_t lightLevel) { return lightmap.adjustColor(srcColor, lightLevel); }); } struct BlitWithLightmap { const Lightmap &lightmap; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsWithLightmap(dst, src, length, lightmap); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillWithLightmap(dst, length, color, lightmap); } }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillBlended(uint8_t *dst, unsigned length, uint8_t color) { DVL_ASSUME(length != 0); std::for_each(DEVILUTIONX_BLIT_EXECUTION_POLICY dst, dst + length, [tbl = paletteTransparencyLookup[color]](uint8_t &dstColor) { dstColor = tbl[dstColor]; }); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsBlended(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length) { DVL_ASSUME(length != 0); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, dst, dst, [pal = paletteTransparencyLookup](uint8_t srcColor, uint8_t dstColor) { return pal[srcColor][dstColor]; }); } struct BlitBlended { DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsBlended(dst, src, length); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillBlended(dst, length, color); } }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsBlendedWithMap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const uint8_t *DVL_RESTRICT colorMap) { DVL_ASSUME(length != 0); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, dst, dst, [colorMap, pal = paletteTransparencyLookup](uint8_t srcColor, uint8_t dstColor) { return pal[dstColor][colorMap[srcColor]]; }); } struct BlitBlendedWithMap { const uint8_t *DVL_RESTRICT colorMap; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsBlendedWithMap(dst, src, length, colorMap); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillBlended(dst, length, colorMap[color]); } }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitFillBlendedWithLightmap(uint8_t *dst, unsigned length, uint8_t color, const Lightmap &lightmap) { DVL_ASSUME(length != 0); const uint8_t *light = lightmap.getLightingAt(dst); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY light, light + length, dst, dst, [color, &lightmap, pal = paletteTransparencyLookup](uint8_t lightLevel, uint8_t dstColor) { uint8_t srcColor = lightmap.adjustColor(color, lightLevel); return pal[srcColor][dstColor]; }); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void BlitPixelsBlendedWithLightmap(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, unsigned length, const Lightmap &lightmap) { DVL_ASSUME(length != 0); const uint8_t *light = lightmap.getLightingAt(dst); if (length < 1024) { uint8_t litSrc[1024]; std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY src, src + length, light, litSrc, [&lightmap](uint8_t srcColor, uint8_t lightLevel) { return lightmap.adjustColor(srcColor, lightLevel); }); std::transform(DEVILUTIONX_BLIT_EXECUTION_POLICY litSrc, litSrc + length, dst, dst, [pal = paletteTransparencyLookup](uint8_t srcColor, uint8_t dstColor) { return pal[dstColor][srcColor]; }); return; } for (size_t i = 0; i < length; i++) { uint8_t srcColor = src[i]; uint8_t dstColor = dst[i]; uint8_t lightLevel = light[i]; uint8_t litColor = lightmap.adjustColor(srcColor, lightLevel); dst[i] = paletteTransparencyLookup[dstColor][litColor]; } } struct BlitBlendedWithLightmap { const Lightmap &lightmap; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src) const { BlitPixelsBlendedWithLightmap(dst, src, length, lightmap); } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void operator()(unsigned length, uint8_t color, uint8_t *DVL_RESTRICT dst) const { BlitFillBlendedWithLightmap(dst, length, color, lightmap); } }; } // namespace devilution ================================================ FILE: Source/engine/render/clx_render.cpp ================================================ /** * @file clx_render.cpp * * CL2 rendering. */ #include "clx_render.hpp" #include #include #include "engine/point.hpp" #include "engine/render/blit_impl.hpp" #include "engine/surface.hpp" #include "utils/attributes.h" #include "utils/clx_decode.hpp" #include "utils/static_vector.hpp" #ifdef DEBUG_CLX #include #include "utils/str_cat.hpp" #endif namespace devilution { namespace { /** * CL2 is similar to CEL, with the following differences: * * 1. Transparent runs can cross line boundaries. * 2. Control bytes are different, and the [0x80, 0xBE] control byte range * indicates a fill-N command. */ struct BlitCommandInfo { const uint8_t *srcEnd; unsigned length; }; BlitCommandInfo ClxBlitInfo(const uint8_t *src) { const uint8_t control = *src; if (!IsClxOpaque(control)) return { src + 1, control }; if (IsClxOpaqueFill(control)) { const uint8_t width = GetClxOpaqueFillWidth(control); return { src + 2, width }; } const uint8_t width = GetClxOpaquePixelsWidth(control); return { src + 1 + width, width }; } struct ClipX { int_fast16_t left; int_fast16_t right; int_fast16_t width; }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT ClipX CalculateClipX(int_fast16_t x, std::size_t w, const Surface &out) { ClipX clip; clip.left = static_cast(x < 0 ? -x : 0); clip.right = static_cast(static_cast(x + w) > out.w() ? x + w - out.w() : 0); clip.width = static_cast(w - clip.left - clip.right); return clip; } // Source data for rendering backwards: first line of input -> last line of output. struct RenderSrc { const uint8_t *begin; const uint8_t *end; uint_fast16_t width; }; DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT const uint8_t *SkipRestOfLineWithOverrun( const uint8_t *src, int_fast16_t srcWidth, SkipSize &skipSize) { int_fast16_t remainingWidth = srcWidth - skipSize.xOffset; while (remainingWidth > 0) { const auto [srcEnd, length] = ClxBlitInfo(src); src = srcEnd; remainingWidth -= length; } skipSize = GetSkipSize(remainingWidth, srcWidth); return src; } // Returns the horizontal overrun. DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT int_fast16_t SkipLinesForRenderBackwardsWithOverrun( Point &position, RenderSrc &src, int_fast16_t dstHeight) { SkipSize skipSize { 0, 0 }; while (position.y >= dstHeight && src.begin != src.end) { src.begin = SkipRestOfLineWithOverrun( src.begin, static_cast(src.width), skipSize); position.y -= static_cast(skipSize.wholeLines); } return skipSize.xOffset; } template void DoRenderBackwardsClipY( const Surface &out, Point position, RenderSrc src, BlitFn &&blitFn) { // Skip the bottom clipped lines. int_fast16_t xOffset = SkipLinesForRenderBackwardsWithOverrun(position, src, out.h()); if (src.begin >= src.end) return; auto *dst = &out[position]; const auto *dstBegin = out.begin(); const int dstPitch = out.pitch(); while (src.begin != src.end && dst >= dstBegin) { auto remainingWidth = static_cast(src.width) - xOffset; dst += xOffset; while (remainingWidth > 0) { uint8_t v = *src.begin++; if (IsClxOpaque(v)) { if (IsClxOpaqueFill(v)) { v = GetClxOpaqueFillWidth(v); const uint8_t color = *src.begin++; blitFn(v, color, dst); } else { v = GetClxOpaquePixelsWidth(v); blitFn(v, dst, src.begin); src.begin += v; } } dst += v; remainingWidth -= v; } const SkipSize skipSize = GetSkipSize(remainingWidth, static_cast(src.width)); xOffset = skipSize.xOffset; dst -= skipSize.wholeLines * dstPitch + src.width - remainingWidth; } } template void DoRenderBackwardsClipXY( const Surface &out, Point position, RenderSrc src, ClipX clipX, BlitFn &&blitFn) { // Skip the bottom clipped lines. int_fast16_t xOffset = SkipLinesForRenderBackwardsWithOverrun(position, src, out.h()); if (src.begin >= src.end) return; position.x += static_cast(clipX.left); auto *dst = &out[position]; const auto *dstBegin = out.begin(); const int dstPitch = out.pitch(); while (src.begin != src.end && dst >= dstBegin) { // Skip initial src if clipping on the left. // Handles overshoot, i.e. when the RLE segment goes into the unclipped area. int_fast16_t remainingWidth = clipX.width; int_fast16_t remainingLeftClip = clipX.left - xOffset; if (remainingLeftClip < 0) { dst += std::min(remainingWidth, -remainingLeftClip); remainingWidth += remainingLeftClip; } while (remainingLeftClip > 0) { auto [srcEnd, length] = ClxBlitInfo(src.begin); if (static_cast(length) > remainingLeftClip) { const uint8_t control = *src.begin; const auto overshoot = static_cast(length - remainingLeftClip); length = std::min(remainingWidth, overshoot); if (IsClxOpaque(control)) { if (IsClxOpaqueFill(control)) { blitFn(length, src.begin[1], dst); } else { blitFn(length, dst, src.begin + 1 + remainingLeftClip); } } dst += length; remainingWidth -= overshoot; src.begin = srcEnd; break; } src.begin = srcEnd; remainingLeftClip -= length; } while (remainingWidth > 0) { auto [srcEnd, length] = ClxBlitInfo(src.begin); const uint8_t control = *src.begin; const unsigned unclippedLength = length; length = std::min(remainingWidth, length); if (IsClxOpaque(control)) { if (IsClxOpaqueFill(control)) { blitFn(length, src.begin[1], dst); } else { blitFn(length, dst, src.begin + 1); } } src.begin = srcEnd; dst += length; remainingWidth -= unclippedLength; // result can be negative } // `remainingWidth` can be negative, in which case it is the amount of pixels // that the source has overran the line. remainingWidth += clipX.right; SkipSize skipSize; if (remainingWidth > 0) { skipSize.xOffset = static_cast(src.width) - remainingWidth; src.begin = SkipRestOfLineWithOverrun( src.begin, static_cast(src.width), skipSize); } else { skipSize = GetSkipSize(remainingWidth, static_cast(src.width)); } xOffset = skipSize.xOffset; dst -= dstPitch * skipSize.wholeLines + clipX.width; } } template void DoRenderBackwards( const Surface &out, Point position, const uint8_t *src, size_t srcSize, unsigned srcWidth, unsigned srcHeight, BlitFn &&blitFn) { if (position.y < 0 || position.y + 1 >= static_cast(out.h() + srcHeight)) return; const ClipX clipX = CalculateClipX(position.x, srcWidth, out); if (clipX.width <= 0) return; const RenderSrc srcForBackwards { src, src + srcSize, static_cast(srcWidth) }; if (static_cast(clipX.width) == srcWidth) { DoRenderBackwardsClipY( out, position, srcForBackwards, std::forward(blitFn)); } else { DoRenderBackwardsClipXY( out, position, srcForBackwards, clipX, std::forward(blitFn)); } } constexpr size_t MaxOutlinePixels = 4096; constexpr size_t MaxOutlineSpriteWidth = 253; using OutlinePixels = StaticVector, MaxOutlinePixels>; using OutlineRowSolidRuns = StaticVector, MaxOutlineSpriteWidth / 2 + 1>; struct OutlinePixelsCacheEntry { OutlinePixels outlinePixels; const void *spriteData = nullptr; bool skipColorIndexZero; }; OutlinePixelsCacheEntry OutlinePixelsCache; void PopulateOutlinePixelsForRow( const OutlineRowSolidRuns &runs, const bool *DVL_RESTRICT below, bool *DVL_RESTRICT cur, bool *DVL_RESTRICT above, uint8_t y, OutlinePixels &result) { DVL_ASSUME(!runs.empty()); for (const auto &[begin, end] : runs) { if (!cur[static_cast(begin - 1)]) { result.emplace_back(static_cast(begin - 1), y); cur[static_cast(begin - 1)] = true; } if (!cur[end]) { result.emplace_back(end, y); cur[end] = true; } for (uint8_t x = begin; x < end; ++x) { if (!below[x]) { result.emplace_back(x, static_cast(y + 1)); } if (!above[x]) { result.emplace_back(x, static_cast(y - 1)); above[x] = true; } } } } void AppendOutlineRowSolidRuns(uint8_t x, uint8_t w, OutlineRowSolidRuns &solidRuns) { if (solidRuns.empty() || solidRuns.back().second != x) { solidRuns.emplace_back(x, x + w); } else { solidRuns.back().second = static_cast(x + w); } } template void GetOutline(ClxSprite sprite, OutlinePixels &result) // NOLINT(readability-function-cognitive-complexity) { const unsigned width = sprite.width(); assert(width < MaxOutlineSpriteWidth); int x = 1; auto y = static_cast(sprite.height()); bool rows[3][MaxOutlineSpriteWidth + 2] = { {}, {}, {} }; bool *rowAbove = rows[0]; bool *row = rows[1]; bool *rowBelow = rows[2]; OutlineRowSolidRuns solidRuns[2]; OutlineRowSolidRuns *solidRunAbove = &solidRuns[0]; OutlineRowSolidRuns *solidRun = &solidRuns[1]; const uint8_t *src = sprite.pixelData(); const uint8_t *const end = src + sprite.pixelDataSize(); while (src < end) { while (x <= static_cast(width)) { const auto v = static_cast(*src++); uint8_t w; if (IsClxOpaque(v)) { if constexpr (SkipColorIndexZero) { if (IsClxOpaqueFill(v)) { w = GetClxOpaqueFillWidth(v); const auto color = static_cast(*src++); if (color != 0) { AppendOutlineRowSolidRuns(x, w, *solidRunAbove); } } else { w = GetClxOpaquePixelsWidth(v); bool prevZero = solidRunAbove->empty() || solidRunAbove->back().second != x; for (unsigned i = 0; i < w; ++i) { const auto color = static_cast(src[i]); if (color == 0) { if (!prevZero) ++solidRunAbove->back().second; prevZero = true; } else { if (prevZero) solidRunAbove->emplace_back(x + i, x + i); ++solidRunAbove->back().second; prevZero = false; } } src += w; } } else { if (IsClxOpaqueFill(v)) { w = GetClxOpaqueFillWidth(v); ++src; } else { w = GetClxOpaquePixelsWidth(v); src += w; } AppendOutlineRowSolidRuns(x, w, *solidRunAbove); } } else { w = v; } x += w; } for (const auto &[xBegin, xEnd] : *solidRunAbove) { std::fill(rowAbove + xBegin, rowAbove + xEnd, true); } if (!solidRun->empty()) { PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, static_cast(y + 1), result); } // (0, 1, 2) => (2, 0, 1) std::swap(row, rowBelow); std::swap(row, rowAbove); std::fill_n(rowAbove, width, false); std::swap(solidRunAbove, solidRun); solidRunAbove->clear(); if (x > static_cast(width + 1)) { // Transparent overrun. const unsigned numWholeTransparentLines = (x - 1) / width; if (numWholeTransparentLines > 1) { if (!solidRun->empty()) { PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, y, result); } solidRun->clear(); std::fill_n(row, width, false); } if (numWholeTransparentLines > 2) std::fill_n(rowBelow, width, false); y -= static_cast(numWholeTransparentLines); x = static_cast((x - 1) % width) + 1; } else { --y; x = 1; } } std::fill_n(rowAbove, width, false); if (!solidRun->empty()) { PopulateOutlinePixelsForRow(*solidRun, rowBelow, row, rowAbove, static_cast(y + 1), result); } } template void UpdateOutlinePixelsCache(ClxSprite sprite) { if (OutlinePixelsCache.spriteData == sprite.pixelData() && OutlinePixelsCache.skipColorIndexZero == SkipColorIndexZero) { return; } OutlinePixelsCache.skipColorIndexZero = SkipColorIndexZero; OutlinePixelsCache.spriteData = sprite.pixelData(); OutlinePixelsCache.outlinePixels.clear(); GetOutline(sprite, OutlinePixelsCache.outlinePixels); } template void RenderClxOutline(const Surface &out, Point position, ClxSprite sprite, uint8_t color) { UpdateOutlinePixelsCache(sprite); --position.x; position.y -= sprite.height(); if (position.x >= 0 && position.x + sprite.width() + 2 < out.w() && position.y >= 0 && position.y + sprite.height() + 2 < out.h()) { for (const auto &[x, y] : OutlinePixelsCache.outlinePixels) { *out.at(position.x + x, position.y + y) = color; } } else { for (const auto &[x, y] : OutlinePixelsCache.outlinePixels) { out.SetPixel(Point(position.x + x, position.y + y), color); } } } void ClxApplyTrans(ClxSprite sprite, const uint8_t *trn) { // A bit of a hack but this is the only place in the code where we need mutable sprites. auto *dst = const_cast(sprite.pixelData()); uint16_t remaining = sprite.pixelDataSize(); while (remaining != 0) { uint8_t val = *dst++; --remaining; if (!IsClxOpaque(val)) continue; if (IsClxOpaqueFill(val)) { --remaining; *dst = trn[*dst]; dst++; } else { val = GetClxOpaquePixelsWidth(val); remaining -= val; while (val-- > 0) { *dst = trn[*dst]; dst++; } } } } } // namespace void ClxApplyTrans(ClxSpriteList list, const uint8_t *trn) { for (const ClxSprite sprite : list) { ClxApplyTrans(sprite, trn); } } void ClxApplyTrans(ClxSpriteSheet sheet, const uint8_t *trn) { for (const ClxSpriteList list : sheet) { ClxApplyTrans(list, trn); } } bool IsPointWithinClx(Point position, ClxSprite clx) { const uint8_t *src = clx.pixelData(); const uint8_t *end = src + clx.pixelDataSize(); const uint16_t width = clx.width(); int xCur = 0; int yCur = clx.height() - 1; while (src < end) { if (yCur != position.y) { SkipSize skipSize {}; skipSize.xOffset = xCur; src = SkipRestOfLineWithOverrun(src, width, skipSize); yCur -= skipSize.wholeLines; xCur = skipSize.xOffset; if (yCur < position.y) return false; continue; } while (xCur < width) { uint8_t val = *src++; if (!IsClxOpaque(val)) { // ignore transparent xCur += val; if (xCur > position.x) return false; continue; } if (IsClxOpaqueFill(val)) { val = GetClxOpaqueFillWidth(val); const uint8_t color = *src++; if (xCur <= position.x && position.x < xCur + val) return color != 0; // ignore shadows xCur += val; } else { val = GetClxOpaquePixelsWidth(val); for (uint8_t pixel = 0; pixel < val; pixel++) { const uint8_t color = *src++; if (xCur == position.x) return color != 0; // ignore shadows xCur++; } } } return false; } return false; } std::pair ClxMeasureSolidHorizontalBounds(ClxSprite clx) { const uint8_t *src = clx.pixelData(); const uint8_t *end = src + clx.pixelDataSize(); const uint16_t width = clx.width(); int xBegin = width; int xEnd = 0; int xCur = 0; while (src < end) { while (xCur < width) { auto val = *src++; if (!IsClxOpaque(val)) { xCur += val; continue; } if (IsClxOpaqueFill(val)) { val = GetClxOpaqueFillWidth(val); ++src; } else { val = GetClxOpaquePixelsWidth(val); src += val; } xBegin = std::min(xBegin, xCur); xCur += val; xEnd = std::max(xEnd, xCur); } while (xCur >= width) xCur -= width; if (xBegin == 0 && xEnd == width) break; } return { xBegin, xEnd }; } #ifdef DEBUG_CLX std::string ClxDescribe(ClxSprite clx) { std::string out = StrCat( "CLX sprite: ", clx.width(), "x", clx.height(), " pixelDataSize=", clx.pixelDataSize(), "b\n\n" "command | width | bytes | color(s)\n" "--------|------:|------:|---------\n"); const uint8_t *src = clx.pixelData(); const uint8_t *end = src + clx.pixelDataSize(); while (src < end) { const uint8_t control = *src++; if (IsClxOpaque(control)) { if (IsClxOpaqueFill(control)) { const uint8_t length = GetClxOpaqueFillWidth(control); out.append(fmt::format("Fill | {:>5} | {:>5} | {}\n", length, 2, src[1])); ++src; } else { const uint8_t length = GetClxOpaquePixelsWidth(control); out.append(fmt::format("Pixels | {:>5} | {:>5} | {}\n", length, length + 1, fmt::join(src + 1, src + 1 + length, " "))); src += length; } } else { out.append(fmt::format("Transp. | {:>5} | {:>5} |\n", control, 1)); } } return out; } #endif // DEBUG_CLX void ClxDraw(const Surface &out, Point position, ClxSprite clx) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitDirect {}); } void ClxDrawTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitWithMap { trn }); } void ClxDrawWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitWithLightmap { lightmap }); } void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlended {}); } void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithMap { trn }); } void ClxDrawBlendedWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap) { DoRenderBackwards(out, position, clx.pixelData(), clx.pixelDataSize(), clx.width(), clx.height(), BlitBlendedWithLightmap { lightmap }); } void ClxDrawOutline(const Surface &out, uint8_t col, Point position, ClxSprite clx) { RenderClxOutline(out, position, clx, col); } void ClxDrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, ClxSprite clx) { RenderClxOutline(out, position, clx, col); } void ClearClxDrawCache() { OutlinePixelsCache.spriteData = nullptr; } } // namespace devilution ================================================ FILE: Source/engine/render/clx_render.hpp ================================================ /** * @file clx_render.hpp * * CL2 rendering. */ #pragma once #include #include #ifdef DEBUG_CLX #include #endif #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/render/light_render.hpp" #include "engine/surface.hpp" namespace devilution { /** * @brief Apply the color swaps to a CLX sprite list; */ void ClxApplyTrans(ClxSpriteList list, const uint8_t *trn); void ClxApplyTrans(ClxSpriteSheet sheet, const uint8_t *trn); /** * @brief Blit CL2 sprite, to the back buffer at the given coordianates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame * @param frame CL2 frame number */ void ClxDraw(const Surface &out, Point position, ClxSprite clx); /** * @brief Same as ClxDraw but position.y is the top of the sprite instead of the bottom. */ inline void RenderClxSprite(const Surface &out, ClxSprite clx, Point position) { ClxDraw(out, { position.x, position.y + static_cast(clx.height()) - 1 }, clx); } /** * @brief Blit a solid colder shape one pixel larger than the given sprite shape, to the given buffer at the given coordinates * @param col Color index from current palette * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame */ void ClxDrawOutline(const Surface &out, uint8_t col, Point position, ClxSprite clx); /** * @brief Same as `ClxDrawOutline` but considers colors with index 0 (usually shadows) to be transparent. * * @param col Color index from current palette * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame */ void ClxDrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position, ClxSprite clx); /** * @brief Blit CL2 sprite, and apply given TRN to the given buffer at the given coordinates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame * @param trn TRN to use */ void ClxDrawTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn); /** * @brief Same as ClxDrawTRN but position.y is the top of the sprite instead of the bottom. */ inline void RenderClxSpriteWithTRN(const Surface &out, ClxSprite clx, Point position, const uint8_t *trn) { ClxDrawTRN(out, { position.x, position.y + static_cast(clx.height()) - 1 }, clx, trn); } void ClxDrawBlendedTRN(const Surface &out, Point position, ClxSprite clx, const uint8_t *trn); /** * @brief Blit CLX sprite with 50% transparency to the given buffer at the given coordinates. * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame */ void ClxDrawBlended(const Surface &out, Point position, ClxSprite clx); /** * @brief Blit CL2 sprite, and apply lighting, to the given buffer at the given coordinates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame * @param lightmap Per-pixel light buffer */ void ClxDrawWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap); /** * @brief Blit CL2 sprite, and apply lighting and transparency blending, to the given buffer at the given coordinates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame * @param lightmap Per-pixel light buffer */ void ClxDrawBlendedWithLightmap(const Surface &out, Point position, ClxSprite clx, const Lightmap &lightmap); /** * Returns if cursor is within the CLX sprite (ignores shadow) */ bool IsPointWithinClx(Point position, ClxSprite clx); /** * Returns a pair of X coordinates containing the start (inclusive) and end (exclusive) * of fully transparent columns in the sprite. */ std::pair ClxMeasureSolidHorizontalBounds(ClxSprite clx); /** * @brief Clears the CLX draw cache. * * Must be called whenever CLX sprites are freed. */ void ClearClxDrawCache(); #ifdef DEBUG_CLX std::string ClxDescribe(ClxSprite clx); #endif } // namespace devilution ================================================ FILE: Source/engine/render/dun_render.cpp ================================================ /** * @file dun_render.cpp * * Implementation of functionality for rendering the level tiles. */ // Debugging variables // #define DEBUG_STR // #define DEBUG_RENDER_COLOR // #define DEBUG_RENDER_OFFSET_X 5 // #define DEBUG_RENDER_OFFSET_Y 5 #include "engine/render/dun_render.hpp" #include #include #include "appfat.h" #include "engine/point.hpp" #include "engine/render/blit_impl.hpp" #include "levels/dun_tile.hpp" #include "options.h" #include "utils/attributes.h" #ifdef DEBUG_STR #include "engine/render/text_render.hpp" #endif #if defined(DEBUG_STR) || defined(DUN_RENDER_STATS) #include "utils/str_cat.hpp" #endif namespace devilution { namespace { /** Width of a tile rendering primitive. */ constexpr int_fast16_t Width = DunFrameWidth; /** Height of a tile rendering primitive (except triangles). */ constexpr int_fast16_t Height = DunFrameHeight; /** Height of the lower triangle of a triangular or a trapezoid tile. */ constexpr int_fast16_t LowerHeight = DunFrameHeight / 2; /** Height of the upper triangle of a triangular tile. */ constexpr int_fast16_t TriangleUpperHeight = DunFrameHeight / 2 - 1; /** Height of the upper rectangle of a trapezoid tile. */ constexpr int_fast16_t TrapezoidUpperHeight = DunFrameHeight / 2; constexpr int_fast16_t TriangleHeight = DunFrameTriangleHeight; /** For triangles, for each pixel drawn vertically, this many pixels are drawn horizontally. */ constexpr int_fast16_t XStep = 2; #ifdef DEBUG_STR std::pair GetTileDebugStr(TileType tile) { // clang-format off switch (tile) { case TileType::Square: return {"S", UiFlags::AlignCenter | UiFlags::VerticalCenter}; case TileType::TransparentSquare: return {"T", UiFlags::AlignCenter | UiFlags::VerticalCenter}; case TileType::LeftTriangle: return {"<", UiFlags::AlignRight | UiFlags::VerticalCenter}; case TileType::RightTriangle: return {">", UiFlags::VerticalCenter}; case TileType::LeftTrapezoid: return {"\\", UiFlags::AlignCenter}; case TileType::RightTrapezoid: return {"/", UiFlags::AlignCenter}; default: return {"", {}}; } // clang-format on } #endif #ifdef DEBUG_RENDER_COLOR int DBGCOLOR = 0; int GetTileDebugColor(TileType tile) { // clang-format off switch (tile) { case TileType::Square: return PAL16_YELLOW + 5; case TileType::TransparentSquare: return PAL16_ORANGE + 5; case TileType::LeftTriangle: return PAL16_GRAY + 5; case TileType::RightTriangle: return PAL16_BEIGE; case TileType::LeftTrapezoid: return PAL16_RED + 5; case TileType::RightTrapezoid: return PAL16_BLUE + 5; default: return 0; } // clang-format on } #endif // DEBUG_RENDER_COLOR // How many pixels to increment the transparent (Left) or opaque (Right) // prefix width after each line (drawing bottom-to-top). template constexpr int8_t PrefixIncrement = 0; template <> constexpr int8_t PrefixIncrement = 2; template <> constexpr int8_t PrefixIncrement = -2; // Initial value for the prefix. template int8_t InitialPrefix = PrefixIncrement >= 0 ? -32 : 64; // The initial value for the prefix at y-th line (counting from the bottom). template DVL_ALWAYS_INLINE int8_t InitPrefix(int8_t y) { return InitialPrefix + PrefixIncrement * y; } enum class LightType : uint8_t { FullyDark, PartiallyLit, FullyLit, PerPixel, }; template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap); template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { BlitFillDirect(dst, n, 0); } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { #ifndef DEBUG_RENDER_COLOR BlitPixelsDirect(dst, src, n); #else BlitFillDirect(dst, n, DBGCOLOR); #endif } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { #ifndef DEBUG_RENDER_COLOR BlitPixelsWithMap(dst, src, n, tbl); #else BlitFillDirect(dst, n, tbl[DBGCOLOR]); #endif } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { #ifndef DEBUG_RENDER_COLOR BlitPixelsWithLightmap(dst, src, n, *lightmap); #else BlitFillWithLightmap(dst, n, DBGCOLOR, *lightmap); #endif } #ifndef DEBUG_RENDER_COLOR template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap); template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { BlitFillBlended(dst, n, 0); } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { BlitPixelsBlended(dst, src, n); } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { BlitPixelsBlendedWithMap(dst, src, n, tbl); } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { BlitPixelsBlendedWithLightmap(dst, src, n, *lightmap); } #else // DEBUG_RENDER_COLOR template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparent(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { for (size_t i = 0; i < n; i++) { dst[i] = paletteTransparencyLookup[dst[i]][tbl[DBGCOLOR + 4]]; } } #endif template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentOrOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { if constexpr (Transparent) { RenderLineTransparent(dst, src, width, tbl, lightmap); } else { RenderLineOpaque(dst, src, width, tbl, lightmap); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLineTransparentAndOpaque(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t prefixWidth, uint_fast8_t width, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { if constexpr (OpaqueFirst) { RenderLineOpaque(dst, src, prefixWidth, tbl, &lightmap); RenderLineTransparent(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl, &lightmap); } else { RenderLineTransparent(dst, src, prefixWidth, tbl, &lightmap); RenderLineOpaque(dst + prefixWidth, src + prefixWidth, width - prefixWidth, tbl, &lightmap); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLine(uint8_t *DVL_RESTRICT dst, const uint8_t *DVL_RESTRICT src, uint_fast8_t n, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, int8_t prefix) { if constexpr (Mask == MaskType::Solid || Mask == MaskType::Transparent) { RenderLineTransparentOrOpaque(dst, src, n, tbl, &lightmap); } else if (prefix >= static_cast(n)) { // We std::clamp the prefix to (0, n] and avoid calling `RenderLineTransparent/Opaque` with width=0. if constexpr (Mask == MaskType::Right) { RenderLineOpaque(dst, src, n, tbl, &lightmap); } else { RenderLineTransparent(dst, src, n, tbl, &lightmap); } } else if (prefix <= 0) { if constexpr (Mask == MaskType::Left) { RenderLineOpaque(dst, src, n, tbl, &lightmap); } else { RenderLineTransparent(dst, src, n, tbl, &lightmap); } } else { RenderLineTransparentAndOpaque(dst, src, prefix, n, tbl, lightmap); } } struct Clip { int_fast16_t top; int_fast16_t bottom; int_fast16_t left; int_fast16_t right; int_fast16_t width; int_fast16_t height; }; DVL_ALWAYS_INLINE Clip CalculateClip(int_fast16_t x, int_fast16_t y, int_fast16_t w, int_fast16_t h, const Surface &out) { Clip clip; clip.top = y + 1 < h ? h - (y + 1) : 0; clip.bottom = y + 1 > out.h() ? (y + 1) - out.h() : 0; clip.left = x < 0 ? -x : 0; clip.right = x + w > out.w() ? x + w - out.w() : 0; clip.width = w - clip.left - clip.right; clip.height = h - clip.top - clip.bottom; return clip; } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { for (auto i = 0; i < Height; ++i, dst -= dstPitch) { RenderLineTransparentOrOpaque(dst, src, Width, tbl, &lightmap); src += Width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { src += clip.bottom * Height + clip.left; for (auto i = 0; i < clip.height; ++i, dst -= dstPitch) { RenderLineTransparentOrOpaque(dst, src, clip.width, tbl, &lightmap); src += Width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (clip.width == Width && clip.height == Height) { RenderSquareFull(dst, dstPitch, src, tbl, lightmap); } else { RenderSquareClipped(dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, unsigned height) { int8_t prefix = InitialPrefix; DVL_ASSUME(height >= 16); DVL_ASSUME(height <= 32); for (unsigned i = 0; i < height; ++i, dst -= dstPitch + Width) { uint_fast8_t drawWidth = Width; while (drawWidth > 0) { auto v = static_cast(*src++); if (v > 0) { RenderLine(dst, src, v, tbl, lightmap, prefix - (Width - drawWidth)); src += v; } else { v = -v; } dst += v; drawWidth -= v; } prefix += PrefixIncrement; } } template // NOLINTNEXTLINE(readability-function-cognitive-complexity): Actually complex and has to be fast. DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquareClipped(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const auto skipRestOfTheLine = [&src](int_fast16_t remainingWidth) { while (remainingWidth > 0) { const auto v = static_cast(*src++); if (v > 0) { src += v; remainingWidth -= v; } else { remainingWidth -= -v; } } assert(remainingWidth == 0); }; // Skip the bottom clipped lines. for (auto i = 0; i < clip.bottom; ++i) { skipRestOfTheLine(Width); } int8_t prefix = InitPrefix(clip.bottom); for (auto i = 0; i < clip.height; ++i, dst -= dstPitch + clip.width) { auto drawWidth = clip.width; // Skip initial src if clipping on the left. // Handles overshoot, i.e. when the RLE segment goes into the unclipped area. auto remainingLeftClip = clip.left; while (remainingLeftClip > 0) { auto v = static_cast(*src++); if (v > 0) { if (v > remainingLeftClip) { const auto overshoot = v - remainingLeftClip; RenderLine(dst, src + remainingLeftClip, overshoot, tbl, lightmap, prefix - (Width - remainingLeftClip)); dst += overshoot; drawWidth -= overshoot; } src += v; } else { v = -v; if (v > remainingLeftClip) { const auto overshoot = v - remainingLeftClip; dst += overshoot; drawWidth -= overshoot; } } remainingLeftClip -= v; } // Draw the non-clipped segment while (drawWidth > 0) { auto v = static_cast(*src++); if (v > 0) { if (v > drawWidth) { RenderLine(dst, src, drawWidth, tbl, lightmap, prefix - (Width - drawWidth)); src += v; dst += drawWidth; drawWidth -= v; break; } RenderLine(dst, src, v, tbl, lightmap, prefix - (Width - drawWidth)); src += v; } else { v = -v; if (v > drawWidth) { dst += drawWidth; drawWidth -= v; break; } } dst += v; drawWidth -= v; } // Skip the rest of src line if clipping on the right assert(drawWidth <= 0); skipRestOfTheLine(clip.right + drawWidth); prefix += PrefixIncrement; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTransparentSquare(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (clip.width == Width && clip.bottom == 0 && clip.top == 0) { RenderTransparentSquareFull(dst, dstPitch, src, tbl, lightmap, clip.height); } else { RenderTransparentSquareClipped(dst, dstPitch, src, tbl, lightmap, clip); } } /** Vertical clip for the lower and upper triangles of a diamond tile (L/RTRIANGLE).*/ struct DiamondClipY { int_fast16_t lowerBottom; int_fast16_t lowerTop; int_fast16_t upperBottom; int_fast16_t upperTop; }; template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT DiamondClipY CalculateDiamondClipY(const Clip &clip) { DiamondClipY result; if (clip.bottom > LowerHeight) { result.lowerBottom = LowerHeight; result.upperBottom = clip.bottom - LowerHeight; result.lowerTop = result.upperTop = 0; } else if (clip.top > UpperHeight) { result.upperTop = UpperHeight; result.lowerTop = clip.top - UpperHeight; result.upperBottom = result.lowerBottom = 0; } else { result.upperTop = clip.top; result.lowerBottom = clip.bottom; result.lowerTop = result.upperBottom = 0; } return result; } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT std::size_t CalculateTriangleSourceSkipLowerBottom(int_fast16_t numLines) { return XStep * numLines * (numLines + 1) / 2; } DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT std::size_t CalculateTriangleSourceSkipUpperBottom(int_fast16_t numLines) { return 2 * TriangleUpperHeight * numLines - numLines * (numLines - 1); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { RenderLineTransparentOrOpaque(dst - 0 * dstLineOffset, src + 0, 2, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 1 * dstLineOffset, src + 2, 4, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 2 * dstLineOffset, src + 6, 6, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 3 * dstLineOffset, src + 12, 8, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 4 * dstLineOffset, src + 20, 10, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 5 * dstLineOffset, src + 30, 12, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 6 * dstLineOffset, src + 42, 14, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 7 * dstLineOffset, src + 56, 16, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 8 * dstLineOffset, src + 72, 18, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 9 * dstLineOffset, src + 90, 20, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 10 * dstLineOffset, src + 110, 22, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 11 * dstLineOffset, src + 132, 24, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 12 * dstLineOffset, src + 156, 26, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 13 * dstLineOffset, src + 182, 28, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 14 * dstLineOffset, src + 210, 30, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 15 * dstLineOffset, src + 240, 32, tbl, lightmap); src += 272; dst -= 16 * dstLineOffset; } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleLower(uint8_t *DVL_RESTRICT &dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT &src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { unsigned width = XStep; for (unsigned i = 0; i < LowerHeight; ++i) { BlitFillDirect(dst, width, 0); dst -= dstLineOffset; width += XStep; } src += 272; } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { RenderLineTransparentOrOpaque(dst - 0 * dstLineOffset, src + 0, 30, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 1 * dstLineOffset, src + 30, 28, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 2 * dstLineOffset, src + 58, 26, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 3 * dstLineOffset, src + 84, 24, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 4 * dstLineOffset, src + 108, 22, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 5 * dstLineOffset, src + 130, 20, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 6 * dstLineOffset, src + 150, 18, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 7 * dstLineOffset, src + 168, 16, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 8 * dstLineOffset, src + 184, 14, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 9 * dstLineOffset, src + 198, 12, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 10 * dstLineOffset, src + 210, 10, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 11 * dstLineOffset, src + 220, 8, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 12 * dstLineOffset, src + 228, 6, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 13 * dstLineOffset, src + 234, 4, tbl, lightmap); RenderLineTransparentOrOpaque(dst - 14 * dstLineOffset, src + 238, 2, tbl, lightmap); } template <> DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTriangleUpper(uint8_t *DVL_RESTRICT dst, ptrdiff_t dstLineOffset, [[maybe_unused]] const uint8_t *DVL_RESTRICT src, [[maybe_unused]] const uint8_t *DVL_RESTRICT tbl, [[maybe_unused]] const Lightmap *lightmap) { unsigned width = Width - XStep; for (unsigned i = 0; i < TriangleUpperHeight; ++i) { BlitFillDirect(dst, width, 0); dst -= dstLineOffset; width -= XStep; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { dst += XStep * (LowerHeight - 1); RenderTriangleLower(dst, dstPitch + XStep, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); dst += XStep * (LowerHeight - clipY.lowerBottom - 1); const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch + XStep) { const auto width = XStep * i; RenderLineTransparentOrOpaque(dst, src, width, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); dst += XStep * (LowerHeight - clipY.lowerBottom - 1) - clipLeft; const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch + XStep) { const auto width = XStep * i; const auto startX = Width - XStep * i; const auto skip = startX < clipLeft ? clipLeft - startX : 0; if (width > skip) RenderLineTransparentOrOpaque(dst + skip, src + skip, width - skip, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); dst += XStep * (LowerHeight - clipY.lowerBottom - 1); const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch + XStep) { const auto width = XStep * i; if (width > clipRight) RenderLineTransparentOrOpaque(dst, src, width - clipRight, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { RenderLeftTriangleLower(dst, dstPitch, src, tbl, lightmap); dst += 2 * XStep; RenderTriangleUpper(dst, dstPitch - XStep, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderLeftTriangleLowerClipVertical(clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); dst += 2 * XStep + XStep * clipY.upperBottom; const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch - XStep) { const auto width = Width - XStep * i; RenderLineTransparentOrOpaque(dst, src, width, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); const int_fast16_t clipLeft = clip.left; RenderLeftTriangleLowerClipLeftAndVertical(clipLeft, clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); dst += 2 * XStep + XStep * clipY.upperBottom; const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch - XStep) { const auto width = Width - XStep * i; const auto startX = XStep * i; const auto skip = startX < clipLeft ? clipLeft - startX : 0; RenderLineTransparentOrOpaque(dst + skip, src + skip, width > skip ? width - skip : 0, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); const int_fast16_t clipRight = clip.right; RenderLeftTriangleLowerClipRightAndVertical(clipRight, clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); dst += 2 * XStep + XStep * clipY.upperBottom; const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch - XStep) { const auto width = Width - XStep * i; if (width <= clipRight) break; RenderLineTransparentOrOpaque(dst, src, width - clipRight, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { if (clip.width == Width) { if (clip.height == TriangleHeight) { RenderLeftTriangleFull(dst, dstPitch, src, tbl, lightmap); } else { RenderLeftTriangleClipVertical(dst, dstPitch, src, tbl, lightmap, clip); } } else if (clip.right == 0) { RenderLeftTriangleClipLeftAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } else { RenderLeftTriangleClipRightAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLower(uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { RenderTriangleLower(dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipVertical(const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch) { const auto width = XStep * i; RenderLineTransparentOrOpaque(dst, src, width, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipLeftAndVertical(int_fast16_t clipLeft, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch) { const auto width = XStep * i; if (width > clipLeft) RenderLineTransparentOrOpaque(dst, src + clipLeft, width - clipLeft, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleLowerClipRightAndVertical(int_fast16_t clipRight, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT &dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT &src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { src += CalculateTriangleSourceSkipLowerBottom(clipY.lowerBottom); const auto lowerMax = LowerHeight - clipY.lowerTop; for (auto i = 1 + clipY.lowerBottom; i <= lowerMax; ++i, dst -= dstPitch) { const auto width = XStep * i; const auto skip = Width - width < clipRight ? clipRight - (Width - width) : 0; if (width > skip) RenderLineTransparentOrOpaque(dst, src, width - skip, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap) { RenderRightTriangleLower(dst, dstPitch, src, tbl, lightmap); RenderTriangleUpper(dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderRightTriangleLowerClipVertical(clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { const auto width = Width - XStep * i; RenderLineTransparentOrOpaque(dst, src, width, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); const int_fast16_t clipLeft = clip.left; RenderRightTriangleLowerClipLeftAndVertical(clipLeft, clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { const auto width = Width - XStep * i; if (width <= clipLeft) break; RenderLineTransparentOrOpaque(dst, src + clipLeft, width - clipLeft, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangleClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); const int_fast16_t clipRight = clip.right; RenderRightTriangleLowerClipRightAndVertical(clipRight, clipY, dst, dstPitch, src, tbl, lightmap); src += CalculateTriangleSourceSkipUpperBottom(clipY.upperBottom); const auto upperMax = TriangleUpperHeight - clipY.upperTop; for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { const auto width = Width - XStep * i; const auto skip = Width - width < clipRight ? clipRight - (Width - width) : 0; RenderLineTransparentOrOpaque(dst, src, width > skip ? width - skip : 0, tbl, lightmap); src += width; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTriangle(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap *lightmap, Clip clip) { if (clip.width == Width) { if (clip.height == TriangleHeight) { RenderRightTriangleFull(dst, dstPitch, src, tbl, lightmap); } else { RenderRightTriangleClipVertical(dst, dstPitch, src, tbl, lightmap, clip); } } else if (clip.right == 0) { RenderRightTriangleClipLeftAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } else { RenderRightTriangleClipRightAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalf(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { if constexpr (Mask == MaskType::Left || Mask == MaskType::Right) { // The first line is always fully opaque. // We handle it specially to avoid calling the blitter with width=0. const uint8_t *srcEnd = src + Width * TrapezoidUpperHeight; RenderLineOpaque(dst, src, Width, tbl, &lightmap); src += Width; dst -= dstPitch; uint8_t prefixWidth = (PrefixIncrement < 0 ? 32 : 0) + PrefixIncrement; do { RenderLineTransparentAndOpaque(dst, src, prefixWidth, Width, tbl, lightmap); prefixWidth += PrefixIncrement; src += Width; dst -= dstPitch; } while (src != srcEnd); } else { // Mask == MaskType::Solid || Mask == MaskType::Transparent const uint8_t *srcEnd = src + Width * TrapezoidUpperHeight; do { RenderLineTransparentOrOpaque(dst, src, Width, tbl, &lightmap); src += Width; dst -= dstPitch; } while (src != srcEnd); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { const auto upperMax = TrapezoidUpperHeight - clipY.upperTop; int8_t prefix = InitPrefix(clip.bottom); for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { RenderLine(dst, src, Width, tbl, lightmap, prefix); src += Width; prefix += PrefixIncrement; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipLeftAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { const auto upperMax = TrapezoidUpperHeight - clipY.upperTop; int8_t prefix = InitPrefix(clip.bottom); for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { RenderLine(dst, src, clip.width, tbl, lightmap, prefix - clip.left); src += Width; prefix += PrefixIncrement; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTrapezoidUpperHalfClipRightAndVertical(const Clip &clip, const DiamondClipY &clipY, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { const auto upperMax = TrapezoidUpperHeight - clipY.upperTop; int8_t prefix = InitPrefix(clip.bottom); for (auto i = 1 + clipY.upperBottom; i <= upperMax; ++i, dst -= dstPitch) { RenderLine(dst, src, clip.width, tbl, lightmap, prefix); src += Width; prefix += PrefixIncrement; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { RenderLeftTriangleLower(dst, dstPitch, src, tbl, &lightmap); dst += XStep; RenderTrapezoidUpperHalf(dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderLeftTriangleLowerClipVertical(clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width; dst += XStep; RenderTrapezoidUpperHalfClipVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderLeftTriangleLowerClipLeftAndVertical(clip.left, clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width + clip.left; dst += XStep + clip.left; RenderTrapezoidUpperHalfClipLeftAndVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderLeftTriangleLowerClipRightAndVertical(clip.right, clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width; dst += XStep; RenderTrapezoidUpperHalfClipRightAndVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (clip.width == Width) { if (clip.height == Height) { RenderLeftTrapezoidFull(dst, dstPitch, src, tbl, lightmap); } else { RenderLeftTrapezoidClipVertical(dst, dstPitch, src, tbl, lightmap, clip); } } else if (clip.right == 0) { RenderLeftTrapezoidClipLeftAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } else { RenderLeftTrapezoidClipRightAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidFull(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap) { RenderRightTriangleLower(dst, dstPitch, src, tbl, &lightmap); RenderTrapezoidUpperHalf(dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderRightTriangleLowerClipVertical(clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width; RenderTrapezoidUpperHalfClipVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipLeftAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderRightTriangleLowerClipLeftAndVertical(clip.left, clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width + clip.left; RenderTrapezoidUpperHalfClipLeftAndVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidClipRightAndVertical(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { const DiamondClipY clipY = CalculateDiamondClipY(clip); RenderRightTriangleLowerClipRightAndVertical(clip.right, clipY, dst, dstPitch, src, tbl, &lightmap); src += clipY.upperBottom * Width; RenderTrapezoidUpperHalfClipRightAndVertical(clip, clipY, dst, dstPitch, src, tbl, lightmap); } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoid(uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (clip.width == Width) { if (clip.height == Height) { RenderRightTrapezoidFull(dst, dstPitch, src, tbl, lightmap); } else { RenderRightTrapezoidClipVertical(dst, dstPitch, src, tbl, lightmap, clip); } } else if (clip.right == 0) { RenderRightTrapezoidClipLeftAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } else { RenderRightTrapezoidClipRightAndVertical(dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileType(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { switch (tile) { case TileType::Square: RenderSquare(dst, dstPitch, src, tbl, lightmap, clip); break; case TileType::TransparentSquare: RenderTransparentSquare(dst, dstPitch, src, tbl, lightmap, clip); break; case TileType::LeftTriangle: RenderLeftTriangle(dst, dstPitch, src, tbl, &lightmap, clip); break; case TileType::RightTriangle: RenderRightTriangle(dst, dstPitch, src, tbl, &lightmap, clip); break; case TileType::LeftTrapezoid: RenderLeftTrapezoid(dst, dstPitch, src, tbl, lightmap, clip); break; case TileType::RightTrapezoid: RenderRightTrapezoid(dst, dstPitch, src, tbl, lightmap, clip); break; } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { switch (tile) { case TileType::TransparentSquare: RenderTransparentSquare(dst, dstPitch, src, tbl, lightmap, clip); break; case TileType::LeftTrapezoid: RenderLeftTrapezoid(dst, dstPitch, src, tbl, lightmap, clip); break; default: app_fatal("Given mask can only be applied to TransparentSquare or LeftTrapezoid tiles"); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquare(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { switch (tile) { case TileType::TransparentSquare: RenderTransparentSquare(dst, dstPitch, src, tbl, lightmap, clip); break; case TileType::RightTrapezoid: RenderRightTrapezoid(dst, dstPitch, src, tbl, lightmap, clip); break; default: app_fatal("Given mask can only be applied to TransparentSquare or LeftTrapezoid tiles"); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderLeftTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (*GetOptions().Graphics.perPixelLighting) { RenderLeftTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyDarkLightTable(tbl)) { RenderLeftTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyLitLightTable(tbl)) { RenderLeftTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else { RenderLeftTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderRightTrapezoidOrTransparentSquareDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (*GetOptions().Graphics.perPixelLighting) { RenderRightTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyDarkLightTable(tbl)) { RenderRightTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyLitLightTable(tbl)) { RenderRightTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } else { RenderRightTrapezoidOrTransparentSquare(tile, dst, dstPitch, src, tbl, lightmap, clip); } } template DVL_ALWAYS_INLINE DVL_ATTRIBUTE_HOT void RenderTileDispatch(TileType tile, uint8_t *DVL_RESTRICT dst, uint16_t dstPitch, const uint8_t *DVL_RESTRICT src, const uint8_t *DVL_RESTRICT tbl, const Lightmap &lightmap, Clip clip) { if (*GetOptions().Graphics.perPixelLighting) { RenderTileType(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyDarkLightTable(tbl)) { RenderTileType(tile, dst, dstPitch, src, tbl, lightmap, clip); } else if (lightmap.isFullyLitLightTable(tbl)) { RenderTileType(tile, dst, dstPitch, src, tbl, lightmap, clip); } else { RenderTileType(tile, dst, dstPitch, src, tbl, lightmap, clip); } } } // namespace #ifdef DUN_RENDER_STATS ankerl::unordered_dense::map DunRenderStats; std::string_view TileTypeToString(TileType tileType) { // clang-format off switch (tileType) { case TileType::Square: return "Square"; case TileType::TransparentSquare: return "TransparentSquare"; case TileType::LeftTriangle: return "LeftTriangle"; case TileType::RightTriangle: return "RightTriangle"; case TileType::LeftTrapezoid: return "LeftTrapezoid"; case TileType::RightTrapezoid: return "RightTrapezoid"; default: return "???"; } // clang-format on } std::string_view MaskTypeToString(MaskType maskType) { // clang-format off switch (maskType) { case MaskType::Solid: return "Solid"; case MaskType::Transparent: return "Transparent"; case MaskType::Right: return "Right"; case MaskType::Left: return "Left"; case MaskType::RightFoliage: return "RightFoliage"; case MaskType::LeftFoliage: return "LeftFoliage"; default: return "???"; } // clang-format on } #endif DVL_ATTRIBUTE_HOT void RenderTileFrame(const Surface &out, const Lightmap &lightmap, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height, MaskType maskType, const uint8_t *tbl) { #ifdef DEBUG_RENDER_OFFSET_X position.x += DEBUG_RENDER_OFFSET_X; #endif #ifdef DEBUG_RENDER_OFFSET_Y position.y += DEBUG_RENDER_OFFSET_Y; #endif const Clip clip = CalculateClip(position.x, position.y, DunFrameWidth, height, out); if (clip.width <= 0 || clip.height <= 0) return; uint8_t *dst = out.at(static_cast(position.x + clip.left), static_cast(position.y - clip.bottom)); const uint16_t dstPitch = out.pitch(); #ifdef DUN_RENDER_STATS ++DunRenderStats[DunRenderType { tile, maskType }]; #endif switch (maskType) { case MaskType::Solid: RenderTileDispatch(tile, dst, dstPitch, src, tbl, lightmap, clip); break; case MaskType::Transparent: RenderTileDispatch(tile, dst, dstPitch, src, tbl, lightmap, clip); break; case MaskType::Left: RenderLeftTrapezoidOrTransparentSquareDispatch(tile, dst, dstPitch, src, tbl, lightmap, clip); break; case MaskType::Right: RenderRightTrapezoidOrTransparentSquareDispatch(tile, dst, dstPitch, src, tbl, lightmap, clip); break; } #ifdef DEBUG_STR const auto [debugStr, flags] = GetTileDebugStr(tile); DrawString(out, debugStr, Rectangle { Point { position.x + 2, position.y - 29 }, Size { 28, 28 } }, { .flags = flags }); #endif } void world_draw_black_tile(const Surface &out, int sx, int sy) { #ifdef DEBUG_RENDER_OFFSET_X sx += DEBUG_RENDER_OFFSET_X; #endif #ifdef DEBUG_RENDER_OFFSET_Y sy += DEBUG_RENDER_OFFSET_Y; #endif const Clip clipLeft = CalculateClip(sx, sy, Width, TriangleHeight, out); if (clipLeft.height <= 0) return; Clip clipRight; clipRight.top = clipLeft.top; clipRight.bottom = clipLeft.bottom; clipRight.left = (sx + Width) < 0 ? -(sx + Width) : 0; clipRight.right = sx + Width + Width > out.w() ? sx + Width + Width - out.w() : 0; clipRight.width = Width - clipRight.left - clipRight.right; clipRight.height = clipLeft.height; const uint16_t dstPitch = out.pitch(); if (clipLeft.width > 0) { uint8_t *dst = out.at(static_cast(sx + clipLeft.left), static_cast(sy - clipLeft.bottom)); RenderLeftTriangle(dst, dstPitch, nullptr, nullptr, nullptr, clipLeft); } if (clipRight.width > 0) { uint8_t *dst = out.at(static_cast(sx + Width + clipRight.left), static_cast(sy - clipRight.bottom)); RenderRightTriangle(dst, dstPitch, nullptr, nullptr, nullptr, clipRight); } } } // namespace devilution ================================================ FILE: Source/engine/render/dun_render.hpp ================================================ /** * @file dun_render.hpp * * Interface of functionality for rendering the level tiles. */ #pragma once #include #include #include "engine/point.hpp" #include "engine/render/light_render.hpp" #include "engine/surface.hpp" #include "levels/dun_tile.hpp" #include "utils/attributes.h" #include "utils/endian_swap.hpp" // #define DUN_RENDER_STATS #ifdef DUN_RENDER_STATS #include #endif namespace devilution { /** * @brief Specifies the mask to use for rendering. */ enum class MaskType : uint8_t { /** @brief The entire tile is opaque. */ Solid, /** @brief The entire tile is blended with transparency. */ Transparent, /** * @brief Upper-right triangle is blended with transparency. * * Can only be used with `TileType::LeftTrapezoid` and * `TileType::TransparentSquare`. * * The lower 16 rows are opaque. * The upper 16 rows are arranged like this (🮆 = opaque, 🮐 = blended): * * 🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮐🮐 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 */ Right, /** * @brief Upper-left triangle is blended with transparency. * * Can only be used with `TileType::RightTrapezoid` and * `TileType::TransparentSquare`. * * The lower 16 rows are opaque. * The upper 16 rows are arranged like this (🮆 = opaque, 🮐 = blended): * * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮐🮐🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 * 🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆🮆 */ Left, }; #ifdef DUN_RENDER_STATS struct DunRenderType { TileType tileType; MaskType maskType; bool operator==(const DunRenderType &other) const { return tileType == other.tileType && maskType == other.maskType; } }; struct DunRenderTypeHash { size_t operator()(DunRenderType t) const noexcept { return std::hash {}((1 < static_cast(t.tileType)) | static_cast(t.maskType)); } }; extern ankerl::unordered_dense::map DunRenderStats; std::string_view TileTypeToString(TileType tileType); std::string_view MaskTypeToString(MaskType maskType); #endif /** * @brief Low-level tile rendering function. */ void RenderTileFrame(const Surface &out, const Lightmap &lightmap, const Point &position, TileType tile, const uint8_t *src, int_fast16_t height, MaskType maskType, const uint8_t *tbl); /** * @brief Returns the raw data for the given dungeon frame. */ DVL_ALWAYS_INLINE const uint8_t *GetDunFrame(const std::byte *dungeonCelData, uint32_t frame) { const auto *frameTable = reinterpret_cast(dungeonCelData); return reinterpret_cast(&dungeonCelData[Swap32LE(frameTable[frame])]); } /** * @brief Returns the raw data for the given dungeon frame's foliage. */ DVL_ALWAYS_INLINE const uint8_t *GetDunFrameFoliage(const std::byte *dungeonCelData, uint32_t frame) { return GetDunFrame(dungeonCelData, frame) + ReencodedTriangleFrameSize; } /** * @brief Blit current world CEL to the given buffer * @param out Target buffer * @param lightmap Per-pixel light buffer * @param position Target buffer coordinates * @param dungeonCelData Dungeon CEL data. * @param levelCelBlock The MIN block of the level CEL file. * @param maskType The mask to use, * @param tbl LightTable or TRN for a tile. */ DVL_ALWAYS_INLINE void RenderTile(const Surface &out, const Lightmap &lightmap, const Point &position, const std::byte *dungeonCelData, LevelCelBlock levelCelBlock, MaskType maskType, const uint8_t *tbl) { const TileType tileType = levelCelBlock.type(); RenderTileFrame(out, lightmap, position, tileType, GetDunFrame(dungeonCelData, levelCelBlock.frame()), (tileType == TileType::LeftTriangle || tileType == TileType::RightTriangle) ? DunFrameTriangleHeight : DunFrameHeight, maskType, tbl); } /** * @brief Renders a floor foliage tile. */ DVL_ALWAYS_INLINE void RenderTileFoliage(const Surface &out, const Lightmap &lightmap, const Point &position, const std::byte *dungeonCelData, LevelCelBlock levelCelBlock, const uint8_t *tbl) { RenderTileFrame(out, lightmap, Point { position.x, position.y - 16 }, TileType::TransparentSquare, GetDunFrameFoliage(dungeonCelData, levelCelBlock.frame()), /*height=*/16, MaskType::Solid, tbl); } /** * @brief Render a black 64x31 tile ◆ * @param out Target buffer * @param sx Target buffer coordinate (left corner of the tile) * @param sy Target buffer coordinate (bottom corner of the tile) */ void world_draw_black_tile(const Surface &out, int sx, int sy); } // namespace devilution ================================================ FILE: Source/engine/render/light_render.cpp ================================================ #include "engine/render/light_render.hpp" #include #include #include #include #include #include #include #include "engine/displacement.hpp" #include "engine/lighting_defs.hpp" #include "engine/point.hpp" #include "levels/dun_tile.hpp" #include "levels/gendung_defs.hpp" namespace devilution { namespace { std::vector LightmapBuffer; void RenderFullTile(Point position, uint8_t lightLevel, uint8_t *lightmap, uint16_t pitch) { uint8_t *top = lightmap + (position.y + 1) * pitch + position.x - TILE_WIDTH / 2; uint8_t *bottom = top + (TILE_HEIGHT - 2) * pitch; for (int y = 0, w = 4; y < TILE_HEIGHT / 2 - 1; y++, w += 4) { const int x = (TILE_WIDTH - w) / 2; memset(top + x, lightLevel, w); memset(bottom + x, lightLevel, w); top += pitch; bottom -= pitch; } memset(top, lightLevel, TILE_WIDTH); } int DecrementTowardZero(int num) { return num > 0 ? num - 1 : num + 1; } // Half-space method for drawing triangles // Points must be provided using counter-clockwise rotation // https://web.archive.org/web/20050408192410/http://sw-shader.sourceforge.net/rasterizer.html void RenderTriangle(Point p1, Point p2, Point p3, uint8_t lightLevel, uint8_t *lightmap, uint16_t pitch, uint16_t scanLines) { // Deltas (points are already 28.4 fixed-point) const int dx12 = p1.x - p2.x; const int dx23 = p2.x - p3.x; const int dx31 = p3.x - p1.x; const int dy12 = p1.y - p2.y; const int dy23 = p2.y - p3.y; const int dy31 = p3.y - p1.y; // 24.8 fixed-point deltas const int fdx12 = dx12 << 4; const int fdx23 = dx23 << 4; const int fdx31 = dx31 << 4; const int fdy12 = dy12 << 4; const int fdy23 = dy23 << 4; const int fdy31 = dy31 << 4; // Bounding rectangle const int minx = std::max((std::min({ p1.x, p2.x, p3.x }) + 0xF) >> 4, 0); const int maxx = std::min((std::max({ p1.x, p2.x, p3.x }) + 0xF) >> 4, pitch); const int xlen = maxx - minx; if (xlen <= 0) return; const int miny = std::max((std::min({ p1.y, p2.y, p3.y }) + 0xF) >> 4, 0); const int maxy = std::min((std::max({ p1.y, p2.y, p3.y }) + 0xF) >> 4, scanLines); if (maxy <= miny) return; uint8_t *dst = lightmap + static_cast(miny * pitch); // Half-edge constants constexpr auto CalcHalfEdge = [](const Point &p, int dx, int dy) { return (dy * p.x) - (dx * p.y) + // Correct for fill convention (dy < 0 || (dy == 0 && dx > 0) ? 1 : 0); }; const int c1 = CalcHalfEdge(p1, dx12, dy12); const int c2 = CalcHalfEdge(p2, dx23, dy23); const int c3 = CalcHalfEdge(p3, dx31, dy31); constexpr auto CalcCy = [](int minx, int miny, int dx, int dy) { return (dx * (miny << 4)) - (dy * (minx << 4)); }; int cy1 = c1 + CalcCy(minx, miny, dx12, dy12); int cy2 = c2 + CalcCy(minx, miny, dx23, dy23); int cy3 = c3 + CalcCy(minx, miny, dx31, dy31); for (int y = miny; y < maxy; y++) { const int cxe1 = cy1 - (fdy12 * xlen); const int cxe2 = cy2 - (fdy23 * xlen); const int cxe3 = cy3 - (fdy31 * xlen); constexpr auto CalcStartX = [](int xlen, int cx, int cxe, int fdy) -> int { if (cx > 0) return 0; if (cxe <= 0) return xlen; return (cx + DecrementTowardZero(fdy)) / fdy; }; const int startx = minx + std::max({ CalcStartX(xlen, cy1, cxe1, fdy12), CalcStartX(xlen, cy2, cxe2, fdy23), CalcStartX(xlen, cy3, cxe3, fdy31), }); constexpr auto CalcEndX = [](int xlen, int cx, int cxe, int fdy) -> int { if (cxe > 0) return xlen; if (cx <= 0) return 0; return (cx + DecrementTowardZero(fdy)) / fdy; }; const int endx = minx + std::min({ CalcEndX(xlen, cy1, cxe1, fdy12), CalcEndX(xlen, cy2, cxe2, fdy23), CalcEndX(xlen, cy3, cxe3, fdy31), }); if (startx < endx) memset(&dst[startx], lightLevel, endx - startx); cy1 += fdx12; cy2 += fdx23; cy3 += fdx31; dst += pitch; } } uint8_t GetLightLevel(const uint8_t tileLights[MAXDUNX][MAXDUNY], Point tile) { const int x = std::clamp(tile.x, 0, MAXDUNX - 1); const int y = std::clamp(tile.y, 0, MAXDUNY - 1); return tileLights[x][y]; } uint8_t Interpolate(int q1, int q2, int lightLevel) { // Result will be 28.4 fixed-point const int numerator = (lightLevel - q1) << 4; const int result = (numerator + 0x8) / (q2 - q1); assert(result >= 0); return static_cast(result); } void RenderCell(uint8_t quad[4], Point position, uint8_t lightLevel, uint8_t *lightmap, uint16_t pitch, uint16_t scanLines) { const Point center0 = position; const Point center1 = position + Displacement { TILE_WIDTH / 2, TILE_HEIGHT / 2 }; const Point center2 = position + Displacement { 0, TILE_HEIGHT }; const Point center3 = position + Displacement { -TILE_WIDTH / 2, TILE_HEIGHT / 2 }; // 28.4 fixed-point coordinates const Point fpCenter0 = center0 * (1 << 4); const Point fpCenter1 = center1 * (1 << 4); const Point fpCenter2 = center2 * (1 << 4); const Point fpCenter3 = center3 * (1 << 4); // Marching squares // https://en.wikipedia.org/wiki/Marching_squares uint8_t shape = 0; shape |= quad[0] <= lightLevel ? 8 : 0; shape |= quad[1] <= lightLevel ? 4 : 0; shape |= quad[2] <= lightLevel ? 2 : 0; shape |= quad[3] <= lightLevel ? 1 : 0; switch (shape) { // The whole cell is darker than lightLevel case 0: break; // Fill in the bottom-left corner of the cell // In isometric view, only the west tile of the quad is lit case 1: { const uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel); const uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel); const Point p1 = fpCenter3 + (center2 - center3) * bottomFactor; const Point p2 = fpCenter3; const Point p3 = fpCenter3 + (center0 - center3) * leftFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the bottom-right corner of the cell // In isometric view, only the south tile of the quad is lit case 2: { const uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel); const uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel); const Point p1 = fpCenter2 + (center1 - center2) * rightFactor; const Point p2 = fpCenter2; const Point p3 = fpCenter2 + (center3 - center2) * bottomFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the bottom half of the cell // In isometric view, the south and west tiles of the quad are lit case 3: { const uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel); const uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel); const Point p1 = fpCenter2 + (center1 - center2) * rightFactor; const Point p2 = fpCenter2; const Point p3 = fpCenter3; const Point p4 = fpCenter3 + (center1 - center2) * leftFactor; RenderTriangle(p1, p4, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the top-right corner of the cell // In isometric view, only the east tile of the quad is lit case 4: { const uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel); const uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel); const Point p1 = fpCenter1 + (center0 - center1) * topFactor; const Point p2 = fpCenter1; const Point p3 = fpCenter1 + (center2 - center1) * rightFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the top-right and bottom-left corners of the cell // Use the average of all values in the quad to determine whether to fill in the center // In isometric view, the east and west tiles of the quad are lit case 5: { const uint8_t cell = (quad[0] + quad[1] + quad[2] + quad[3] + 2) / 4; const uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel); const uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel); const uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel); const uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel); const Point p1 = fpCenter1 + (center0 - center1) * topFactor; const Point p2 = fpCenter1; const Point p3 = fpCenter1 + (center2 - center1) * rightFactor; const Point p4 = fpCenter3 + (center2 - center3) * bottomFactor; const Point p5 = fpCenter3; const Point p6 = fpCenter3 + (center0 - center3) * leftFactor; if (cell <= lightLevel) { const uint8_t midFactor0 = Interpolate(quad[0], cell, lightLevel); const uint8_t midFactor2 = Interpolate(quad[2], cell, lightLevel); const Point p7 = fpCenter0 + (center2 - center0) / 2 * midFactor0; const Point p8 = fpCenter2 + (center0 - center2) / 2 * midFactor2; RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p7, p8, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p8, p3, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p5, p8, p7, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p5, p7, p6, lightLevel, lightmap, pitch, scanLines); } else { const uint8_t midFactor1 = Interpolate(quad[1], cell, lightLevel); const uint8_t midFactor3 = Interpolate(quad[3], cell, lightLevel); const Point p7 = fpCenter1 + (center3 - center1) / 2 * midFactor1; const Point p8 = fpCenter3 + (center1 - center3) / 2 * midFactor3; RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p7, p3, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p5, p8, p6, lightLevel, lightmap, pitch, scanLines); } } break; // Fill in the right half of the cell // In isometric view, the south and east tiles of the quad are lit case 6: { const uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel); const uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel); const Point p1 = fpCenter1 + (center0 - center1) * topFactor; const Point p2 = fpCenter1; const Point p3 = fpCenter2; const Point p4 = fpCenter2 + (center3 - center2) * bottomFactor; RenderTriangle(p1, p4, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines); } break; // Fill in everything except the top-left corner of the cell // In isometric view, the south, east, and west tiles of the quad are lit case 7: { const uint8_t topFactor = Interpolate(quad[1], quad[0], lightLevel); const uint8_t leftFactor = Interpolate(quad[3], quad[0], lightLevel); const Point p1 = fpCenter1 + (center0 - center1) * topFactor; const Point p2 = fpCenter1; const Point p3 = fpCenter2; const Point p4 = fpCenter3; const Point p5 = fpCenter3 + (center0 - center3) * leftFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p5, p3, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p3, p5, p4, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the top-left corner of the cell // In isometric view, only the north tile of the quad is lit case 8: { const uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel); const uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter0 + (center1 - center0) * topFactor; const Point p3 = fpCenter0 + (center3 - center0) * leftFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the left half of the cell // In isometric view, the north and west tiles of the quad are lit case 9: { const uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel); const uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter0 + (center1 - center0) * topFactor; const Point p3 = fpCenter3 + (center2 - center3) * bottomFactor; const Point p4 = fpCenter3; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the top-left and bottom-right corners of the cell // Use the average of all values in the quad to determine whether to fill in the center // In isometric view, the north and south tiles of the quad are lit case 10: { const uint8_t cell = (quad[0] + quad[1] + quad[2] + quad[3] + 2) / 4; const uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel); const uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel); const uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel); const uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter0 + (center1 - center0) * topFactor; const Point p3 = fpCenter2 + (center1 - center2) * rightFactor; const Point p4 = fpCenter2; const Point p5 = fpCenter2 + (center3 - center2) * bottomFactor; const Point p6 = fpCenter0 + (center3 - center0) * leftFactor; if (cell <= lightLevel) { const uint8_t midFactor1 = Interpolate(quad[1], cell, lightLevel); const uint8_t midFactor3 = Interpolate(quad[3], cell, lightLevel); const Point p7 = fpCenter1 + (center3 - center1) / 2 * midFactor1; const Point p8 = fpCenter3 + (center1 - center3) / 2 * midFactor3; RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p6, p8, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p8, p7, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p3, p7, p4, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p4, p7, p8, lightLevel, lightmap, pitch, scanLines); } else { const uint8_t midFactor0 = Interpolate(quad[0], cell, lightLevel); const uint8_t midFactor2 = Interpolate(quad[2], cell, lightLevel); const Point p7 = fpCenter0 + (center2 - center0) / 2 * midFactor0; const Point p8 = fpCenter2 + (center0 - center2) / 2 * midFactor2; RenderTriangle(p1, p7, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p6, p7, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p3, p8, p4, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p4, p8, p5, lightLevel, lightmap, pitch, scanLines); } } break; // Fill in everything except the top-right corner of the cell // In isometric view, the north, south, and west tiles of the quad are lit case 11: { const uint8_t topFactor = Interpolate(quad[0], quad[1], lightLevel); const uint8_t rightFactor = Interpolate(quad[2], quad[1], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter0 + (center1 - center0) * topFactor; const Point p3 = fpCenter2 + (center1 - center2) * rightFactor; const Point p4 = fpCenter2; const Point p5 = fpCenter3; RenderTriangle(p1, p5, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p5, p3, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p3, p5, p4, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the top half of the cell // In isometric view, the north and east tiles of the quad are lit case 12: { const uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel); const uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter1; const Point p3 = fpCenter1 + (center2 - center1) * rightFactor; const Point p4 = fpCenter0 + (center3 - center0) * leftFactor; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines); } break; // Fill in everything except the bottom-right corner of the cell // In isometric view, the north, east, and west tiles of the quad are lit case 13: { const uint8_t rightFactor = Interpolate(quad[1], quad[2], lightLevel); const uint8_t bottomFactor = Interpolate(quad[3], quad[2], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter1; const Point p3 = fpCenter1 + (center2 - center1) * rightFactor; const Point p4 = fpCenter3 + (center2 - center3) * bottomFactor; const Point p5 = fpCenter3; RenderTriangle(p1, p3, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p4, p3, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p1, p5, p4, lightLevel, lightmap, pitch, scanLines); } break; // Fill in everything except the bottom-left corner of the cell // In isometric view, the north, south, and east tiles of the quad are lit case 14: { const uint8_t bottomFactor = Interpolate(quad[2], quad[3], lightLevel); const uint8_t leftFactor = Interpolate(quad[0], quad[3], lightLevel); const Point p1 = fpCenter0; const Point p2 = fpCenter1; const Point p3 = fpCenter2; const Point p4 = fpCenter2 + (center3 - center2) * bottomFactor; const Point p5 = fpCenter0 + (center3 - center0) * leftFactor; RenderTriangle(p1, p5, p2, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p5, p4, lightLevel, lightmap, pitch, scanLines); RenderTriangle(p2, p4, p3, lightLevel, lightmap, pitch, scanLines); } break; // Fill in the whole cell // All four tiles in the quad are lit case 15: { if (center3.x < 0 || center1.x >= pitch || center0.y < 0 || center2.y >= scanLines) { RenderTriangle(fpCenter0, fpCenter2, fpCenter1, lightLevel, lightmap, pitch, scanLines); RenderTriangle(fpCenter0, fpCenter3, fpCenter2, lightLevel, lightmap, pitch, scanLines); } else { // Optimized rendering path if full tile is visible RenderFullTile(center0, lightLevel, lightmap, pitch); } } break; } } void BuildLightmap(Point tilePosition, Point targetBufferPosition, uint16_t viewportWidth, uint16_t viewportHeight, int rows, int columns, const uint8_t tileLights[MAXDUNX][MAXDUNY], uint_fast8_t microTileLen) { // Since light may need to bleed up to the top of wall tiles, // expand the buffer space to include the full base diamond of the tallest tile graphics const uint16_t bufferHeight = viewportHeight + TILE_HEIGHT * (microTileLen / 2 + 1); rows += microTileLen + 2; const size_t totalPixels = static_cast(viewportWidth) * bufferHeight; LightmapBuffer.resize(totalPixels); // Since rendering occurs in cells between quads, // expand the rendering space to include tiles outside the viewport tilePosition += Displacement(Direction::NorthWest) * 2; targetBufferPosition -= Displacement { TILE_WIDTH, TILE_HEIGHT }; rows += 3; columns++; uint8_t *lightmap = LightmapBuffer.data(); memset(lightmap, LightsMax, totalPixels); for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) { const Point center0 = targetBufferPosition + Displacement { TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; const Point tile0 = tilePosition; const Point tile1 = tilePosition + Displacement { 1, 0 }; const Point tile2 = tilePosition + Displacement { 1, 1 }; const Point tile3 = tilePosition + Displacement { 0, 1 }; uint8_t quad[] = { GetLightLevel(tileLights, tile0), GetLightLevel(tileLights, tile1), GetLightLevel(tileLights, tile2), GetLightLevel(tileLights, tile3) }; const uint8_t maxLight = std::max({ quad[0], quad[1], quad[2], quad[3] }); const uint8_t minLight = std::min({ quad[0], quad[1], quad[2], quad[3] }); for (uint8_t i = 0; i < LightsMax; i++) { const uint8_t lightLevel = LightsMax - i - 1; if (lightLevel > maxLight) continue; if (lightLevel < minLight) break; RenderCell(quad, center0, lightLevel, lightmap, viewportWidth, bufferHeight); } } // Return to start of row tilePosition += Displacement(Direction::West) * columns; targetBufferPosition.x -= columns * TILE_WIDTH; // Jump to next row targetBufferPosition.y += TILE_HEIGHT / 2; if ((i & 1) != 0) { tilePosition.x++; columns--; targetBufferPosition.x += TILE_WIDTH / 2; } else { tilePosition.y++; columns++; targetBufferPosition.x -= TILE_WIDTH / 2; } } } } // namespace Lightmap::Lightmap(const uint8_t *outBuffer, uint16_t outPitch, std::span lightmapBuffer, uint16_t lightmapPitch, std::span, NumLightingLevels> lightTables, const uint8_t *fullyLitLightTable, const uint8_t *fullyDarkLightTable) : outBuffer(outBuffer) , outPitch(outPitch) , lightmapBuffer(lightmapBuffer) , lightmapPitch(lightmapPitch) , lightTables(lightTables) , fullyLitLightTable_(fullyLitLightTable) , fullyDarkLightTable_(fullyDarkLightTable) { } Lightmap Lightmap::build(bool perPixelLighting, Point tilePosition, Point targetBufferPosition, int viewportWidth, int viewportHeight, int rows, int columns, const uint8_t *outBuffer, uint16_t outPitch, std::span, NumLightingLevels> lightTables, const uint8_t *fullyLitLightTable, const uint8_t *fullyDarkLightTable, const uint8_t tileLights[MAXDUNX][MAXDUNY], uint_fast8_t microTileLen) { if (perPixelLighting) { BuildLightmap(tilePosition, targetBufferPosition, viewportWidth, viewportHeight, rows, columns, tileLights, microTileLen); } return Lightmap(outBuffer, outPitch, LightmapBuffer, viewportWidth, lightTables, fullyLitLightTable, fullyDarkLightTable); } Lightmap Lightmap::bleedUp(bool perPixelLighting, const Lightmap &source, Point targetBufferPosition, std::span lightmapBuffer) { assert(lightmapBuffer.size() >= TILE_WIDTH * TILE_HEIGHT); if (!perPixelLighting) return source; const int sourceHeight = static_cast(source.lightmapBuffer.size() / source.lightmapPitch); const int clipLeft = std::max(0, -targetBufferPosition.x); const int clipTop = std::max(0, -(targetBufferPosition.y - TILE_HEIGHT + 1)); const int clipRight = std::max(0, targetBufferPosition.x + TILE_WIDTH - source.lightmapPitch); const int clipBottom = std::max(0, targetBufferPosition.y - sourceHeight + 1); // Nothing we can do if the tile is completely outside the bounds of the lightmap if (clipLeft + clipRight >= TILE_WIDTH) return source; if (clipTop + clipBottom >= TILE_HEIGHT) return source; const uint16_t lightmapPitch = std::max(0, TILE_WIDTH - clipLeft - clipRight); const uint16_t lightmapHeight = TILE_HEIGHT - clipTop - clipBottom; // Find the left edge of the last row in the tile const int outOffset = std::max(0, (targetBufferPosition.y - clipBottom) * source.outPitch + targetBufferPosition.x + clipLeft); const uint8_t *outLoc = source.outBuffer + outOffset; const uint8_t *outBuffer = outLoc - (lightmapHeight - 1) * source.outPitch; // Start copying bytes from the bottom row of the tile const uint8_t *src = source.getLightingAt(outLoc); uint8_t *dst = lightmapBuffer.data() + (lightmapHeight - 1) * lightmapPitch; int rowCount = clipBottom; while (src >= source.lightmapBuffer.data() && dst >= lightmapBuffer.data()) { const int bleed = std::max(0, (rowCount - TILE_HEIGHT / 2) * 2); const int lightOffset = std::max(bleed, clipLeft) - clipLeft; const int lightLength = std::max(0, TILE_WIDTH - clipLeft - std::max(bleed, clipRight) - lightOffset); // Bleed pixels up by copying data from the row below this one if (rowCount > clipBottom && lightLength < lightmapPitch) memcpy(dst, dst + lightmapPitch, lightmapPitch); // Copy data from the source lightmap between the top edge of the base diamond assert(dst + lightOffset + lightLength <= lightmapBuffer.data() + TILE_WIDTH * TILE_HEIGHT); assert(src + lightOffset + lightLength <= source.lightmapBuffer.data() + source.lightmapBuffer.size()); memcpy(dst + lightOffset, src + lightOffset, lightLength); src -= source.lightmapPitch; dst -= lightmapPitch; rowCount++; } return Lightmap(outBuffer, source.outPitch, lightmapBuffer, lightmapPitch, source.lightTables, source.fullyLitLightTable_, source.fullyDarkLightTable_); } } // namespace devilution ================================================ FILE: Source/engine/render/light_render.hpp ================================================ #pragma once #include #include #include #include #include "engine/lighting_defs.hpp" #include "engine/point.hpp" #include "levels/gendung_defs.hpp" namespace devilution { class Lightmap { public: explicit Lightmap(const uint8_t *outBuffer, std::span lightmapBuffer, uint16_t pitch, std::span, NumLightingLevels> lightTables, const uint8_t *fullyLitLightTable, const uint8_t *fullyDarkLightTable) : Lightmap(outBuffer, pitch, lightmapBuffer, pitch, lightTables, fullyLitLightTable, fullyDarkLightTable) { } explicit Lightmap(const uint8_t *outBuffer, uint16_t outPitch, std::span lightmapBuffer, uint16_t lightmapPitch, std::span, NumLightingLevels> lightTables, const uint8_t *fullyLitLightTable, const uint8_t *fullyDarkLightTable); [[nodiscard]] uint8_t adjustColor(uint8_t color, uint8_t lightLevel) const { return lightTables[lightLevel][color]; } const uint8_t *getLightingAt(const uint8_t *outLoc) const { const ptrdiff_t outDist = outLoc - outBuffer; const ptrdiff_t rowOffset = outDist % outPitch; if (outDist < 0) { // In order to support "bleed up" for wall tiles, // reuse the first row whenever outLoc is out of bounds const int modOffset = rowOffset < 0 ? outPitch : 0; return lightmapBuffer.data() + rowOffset + modOffset; } const ptrdiff_t row = outDist / outPitch; return lightmapBuffer.data() + row * lightmapPitch + rowOffset; } [[nodiscard]] bool isFullyLitLightTable(const uint8_t *lightTable) const { return lightTable == fullyLitLightTable_; } [[nodiscard]] bool isFullyDarkLightTable(const uint8_t *lightTable) const { return lightTable == fullyDarkLightTable_; } static Lightmap build(bool perPixelLighting, Point tilePosition, Point targetBufferPosition, int viewportWidth, int viewportHeight, int rows, int columns, const uint8_t *outBuffer, uint16_t outPitch, std::span, NumLightingLevels> lightTables, const uint8_t *fullyLitLightTable, const uint8_t *fullyDarkLightTable, const uint8_t tileLights[MAXDUNX][MAXDUNY], uint_fast8_t microTileLen); static Lightmap bleedUp(bool perPixelLighting, const Lightmap &source, Point targetBufferPosition, std::span lightmapBuffer); private: const uint8_t *outBuffer; const uint16_t outPitch; std::span lightmapBuffer; const uint16_t lightmapPitch; std::span, NumLightingLevels> lightTables; const uint8_t *fullyLitLightTable_; const uint8_t *fullyDarkLightTable_; }; } // namespace devilution ================================================ FILE: Source/engine/render/primitive_render.cpp ================================================ #include "engine/render/primitive_render.hpp" #include #include #include #include "engine/point.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" #include "utils/palette_blending.hpp" namespace devilution { namespace { void DrawHalfTransparentUnalignedBlendedRectTo(const Surface &out, unsigned sx, unsigned sy, unsigned width, unsigned height, uint8_t color) { uint8_t *pix = out.at(static_cast(sx), static_cast(sy)); const uint8_t *const lookupTable = paletteTransparencyLookup[color]; const unsigned skipX = out.pitch() - width; for (unsigned y = 0; y < height; ++y) { for (unsigned x = 0; x < width; ++x, ++pix) { *pix = lookupTable[*pix]; } pix += skipX; } } #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT // Expects everything to be 4-byte aligned. void DrawHalfTransparentAligned32BlendedRectTo(const Surface &out, unsigned sx, unsigned sy, unsigned width, unsigned height) { assert(out.pitch() % 4 == 0); auto *pix = reinterpret_cast(out.at(static_cast(sx), static_cast(sy))); assert(reinterpret_cast(pix) % 4 == 0); const uint16_t *lookupTable = paletteTransparencyLookupBlack16; const unsigned skipX = (out.pitch() - width) / 4; width /= 4; while (height-- > 0) { for (unsigned i = 0; i < width; ++i, ++pix) { const uint32_t v = *pix; *pix = lookupTable[v & 0xFFFF] | (lookupTable[(v >> 16) & 0xFFFF] << 16); } pix += skipX; } } void DrawHalfTransparentBlendedRectTo(const Surface &out, unsigned sx, unsigned sy, unsigned width, unsigned height) { // All SDL surfaces are 4-byte aligned and divisible by 4. // However, our coordinates and widths may not be. // First, draw the leading unaligned part. if (sx % 4 != 0) { const unsigned w = 4 - sx % 4; DrawHalfTransparentUnalignedBlendedRectTo(out, sx, sy, w, height, 0); sx += w; width -= w; } if (static_cast(sx + width) == out.w()) { // The pitch is 4-byte aligned, so we can simply extend the width to the pitch. width = out.pitch() - sx; } else if (width % 4 != 0) { // Draw the trailing unaligned part. const unsigned w = width % 4; DrawHalfTransparentUnalignedBlendedRectTo(out, sx + (width / 4) * 4, sy, w, height, 0); width -= w; } // Now everything is divisible by 4. Draw the aligned part. DrawHalfTransparentAligned32BlendedRectTo(out, sx, sy, width, height); } #else #define DrawHalfTransparentBlendedRectTo DrawHalfTransparentUnalignedBlendedRectTo #endif } // namespace void FillRect(const Surface &out, int x, int y, int width, int height, uint8_t colorIndex) { for (int j = 0; j < height; j++) { DrawHorizontalLine(out, { x, y + j }, width, colorIndex); } } void DrawHorizontalLine(const Surface &out, Point from, int width, std::uint8_t colorIndex) { if (from.y < 0 || from.y >= out.h() || from.x >= out.w() || width <= 0 || from.x + width <= 0) return; if (from.x < 0) { width += from.x; from.x = 0; } if (from.x + width > out.w()) width = out.w() - from.x; return UnsafeDrawHorizontalLine(out, from, width, colorIndex); } void UnsafeDrawHorizontalLine(const Surface &out, Point from, int width, std::uint8_t colorIndex) { std::memset(&out[from], colorIndex, width); } void DrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t colorIndex) { if (from.x < 0 || from.x >= out.w() || from.y >= out.h() || height <= 0 || from.y + height <= 0) return; if (from.y < 0) { height += from.y; from.y = 0; } if (from.y + height > out.h()) height = (from.y + height) - out.h(); return UnsafeDrawVerticalLine(out, from, height, colorIndex); } void UnsafeDrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t colorIndex) { auto *dst = &out[from]; const auto pitch = out.pitch(); while (height-- > 0) { *dst = colorIndex; dst += pitch; } } void DrawHalfTransparentHorizontalLine(const Surface &out, Point from, int width, uint8_t colorIndex) { // completely off-bounds? if (from.y < 0 || from.y >= out.h() || width <= 0 || from.x >= out.w() || from.x + width <= 0) return; const int x0 = std::max(0, from.x); const int x1 = std::min(out.w(), from.x + width); for (int x = x0; x < x1; ++x) { SetHalfTransparentPixel(out, { x, from.y }, colorIndex); } } // Draw a half-transparent vertical line of `height` pixels starting at `from`. void DrawHalfTransparentVerticalLine(const Surface &out, Point from, int height, uint8_t colorIndex) { // completely off-bounds? if (from.x < 0 || from.x >= out.w() || height <= 0 || from.y >= out.h() || from.y + height <= 0) return; const int y0 = std::max(0, from.y); const int y1 = std::min(out.h(), from.y + height); for (int y = y0; y < y1; ++y) { SetHalfTransparentPixel(out, { from.x, y }, colorIndex); } } void DrawHalfTransparentRectTo(const Surface &out, int sx, int sy, int width, int height) { if (sx + width < 0) return; if (sy + height < 0) return; if (sx >= out.w()) return; if (sy >= out.h()) return; if (sx < 0) { width += sx; sx = 0; } else if (sx + width >= out.w()) { width = out.w() - sx; } if (sy < 0) { height += sy; sy = 0; } else if (sy + height >= out.h()) { height = out.h() - sy; } DrawHalfTransparentBlendedRectTo(out, sx, sy, width, height); } void DrawHalfTransparentRectTo(const Surface &out, int sx, int sy, int width, int height, uint8_t color) { if (sx + width < 0) return; if (sy + height < 0) return; if (sx >= out.w()) return; if (sy >= out.h()) return; if (sx < 0) { width += sx; sx = 0; } else if (sx + width >= out.w()) { width = out.w() - sx; } if (sy < 0) { height += sy; sy = 0; } else if (sy + height >= out.h()) { height = out.h() - sy; } DrawHalfTransparentUnalignedBlendedRectTo(out, sx, sy, width, height, color); } void SetHalfTransparentPixel(const Surface &out, Point position, uint8_t color) { if (out.InBounds(position)) { uint8_t *pix = out.at(position.x, position.y); const auto &lookupTable = paletteTransparencyLookup[color]; *pix = lookupTable[*pix]; } } void UnsafeDrawBorder2px(const Surface &out, Rectangle rect, uint8_t color) { const size_t width = rect.size.width; const size_t height = rect.size.height; uint8_t *buf = &out[rect.position]; std::memset(buf, color, width); buf += out.pitch(); std::memset(buf, color, width); buf += out.pitch(); for (size_t i = 4; i < height; ++i) { buf[0] = buf[1] = color; buf[width - 2] = buf[width - 1] = color; buf += out.pitch(); } std::memset(buf, color, width); buf += out.pitch(); std::memset(buf, color, width); } } // namespace devilution ================================================ FILE: Source/engine/render/primitive_render.hpp ================================================ #pragma once #include #include #include "engine/point.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" namespace devilution { /** * @brief Fill a rectangle with the given color. */ void FillRect(const Surface &out, int x, int y, int width, int height, uint8_t colorIndex); /** * @brief Draw a horizontal line segment in the target buffer (left to right) * @param out Target buffer * @param from Start of the line segment * @param width * @param colorIndex Color index from current palette */ void DrawHorizontalLine(const Surface &out, Point from, int width, std::uint8_t colorIndex); /** Same as DrawHorizontalLine but without bounds clipping. */ void UnsafeDrawHorizontalLine(const Surface &out, Point from, int width, std::uint8_t colorIndex); /** * @brief Draw a vertical line segment in the target buffer (top to bottom) * @param out Target buffer * @param from Start of the line segment * @param height * @param colorIndex Color index from current palette */ void DrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t colorIndex); /** Same as DrawVerticalLine but without bounds clipping. */ void UnsafeDrawVerticalLine(const Surface &out, Point from, int height, std::uint8_t colorIndex); void DrawHalfTransparentHorizontalLine(const Surface &out, Point from, int width, uint8_t colorIndex); void DrawHalfTransparentVerticalLine(const Surface &out, Point from, int width, uint8_t colorIndex); /** * Draws a half-transparent rectangle by palette blending with black. * * @brief Render a transparent black rectangle * @param out Target buffer * @param sx Screen coordinate * @param sy Screen coordinate * @param width Rectangle width * @param height Rectangle height */ void DrawHalfTransparentRectTo(const Surface &out, int sx, int sy, int width, int height); void DrawHalfTransparentRectTo(const Surface &out, int sx, int sy, int width, int height, uint8_t color); /** * Draws a half-transparent pixel * * @brief Render a transparent pixel * @param out Target buffer * @param position Screen coordinates * @param col Pixel color */ void SetHalfTransparentPixel(const Surface &out, Point position, uint8_t color); /** * Draws a 2px inset border. * * @param out Target buffer * @param rect The rectangle that border pixels are rendered inside of. * @param color Border color. */ void UnsafeDrawBorder2px(const Surface &out, Rectangle rect, uint8_t color); } // namespace devilution ================================================ FILE: Source/engine/render/scrollrt.cpp ================================================ /** * @file scrollrt.cpp * * Implementation of functionality for rendering the dungeons, monsters and calling other render routines. */ #include "engine/render/scrollrt.h" #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "dead.h" #include "diablo_msg.hpp" #include "doom.h" #include "engine/backbuffer_state.hpp" #include "engine/displacement.hpp" #include "engine/dx.h" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/dun_render.hpp" #include "engine/render/light_render.hpp" #include "engine/render/text_render.hpp" #include "engine/trn.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "gmenu.h" #include "headless_mode.hpp" #include "help.h" #include "hwcursor.hpp" #include "init.hpp" #include "inv.h" #include "levels/dun_tile.hpp" #include "levels/gendung.h" #include "levels/tile_properties.hpp" #include "lighting.h" #include "lua/lua_event.hpp" #include "minitext.h" #include "missiles.h" #include "nthread.h" #include "options.h" #include "panels/charpanel.hpp" #include "panels/console.hpp" #include "panels/partypanel.hpp" #include "panels/spell_list.hpp" #include "plrmsg.h" #include "qol/chatlog.h" #include "qol/floatingnumbers.h" #include "qol/itemlabels.h" #include "qol/monhealthbar.h" #include "qol/stash.h" #include "qol/xpbar.h" #include "stores.h" #include "towners.h" #include "utils/attributes.h" #include "utils/display.h" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" #ifndef USE_SDL1 #include "controls/touch/renderers.h" #endif #ifdef _DEBUG #include "debug.h" #endif #ifdef DUN_RENDER_STATS #include "utils/format_int.hpp" #endif namespace devilution { enum OutlineColors : uint8_t { OutlineColorsPlayer1 = (PAL16_ORANGE + 7), OutlineColorsPlayer2 = (PAL16_YELLOW + 7), OutlineColorsPlayer3 = (PAL16_RED + 7), OutlineColorsPlayer4 = (PAL16_BLUE + 7), OutlineColorsObject = (PAL16_YELLOW + 2), OutlineColorsTowner = (PAL16_BEIGE + 6), OutlineColorsMonster = (PAL16_RED + 9), }; bool AutoMapShowItems; // DevilutionX extension. extern void DrawControllerModifierHints(const Surface &out); bool frameflag; namespace { constexpr auto RightFrameDisplacement = Displacement { DunFrameWidth, 0 }; [[nodiscard]] DVL_ALWAYS_INLINE bool IsFloor(Point tilePosition) { return !TileHasAny(tilePosition, TileProperties::Solid | TileProperties::BlockMissile); } [[nodiscard]] DVL_ALWAYS_INLINE bool IsWall(Point tilePosition) { return !IsFloor(tilePosition) || dSpecial[tilePosition.x][tilePosition.y] != 0; } /** * @brief Contains all Missile at rendering position */ ankerl::unordered_dense::map> MissilesAtRenderingTile; /** * @brief Could the missile (at the next game tick) collide? This method is a simplified version of CheckMissileCol (for example without random). */ bool CouldMissileCollide(Point tile, bool checkPlayerAndMonster) { if (!InDungeonBounds(tile)) return true; if (checkPlayerAndMonster) { if (dMonster[tile.x][tile.y] > 0) return true; if (dPlayer[tile.x][tile.y] > 0) return true; } return IsMissileBlockedByTile(tile); } void UpdateMissilePositionForRendering(Missile &m, int progress) { DisplacementOf velocity = m.position.velocity; velocity *= progress; velocity /= AnimationInfo::baseValueFraction; const Displacement pixelsTravelled = (m.position.traveled + Displacement { static_cast(velocity.deltaX), static_cast(velocity.deltaY) }) >> 16; const Displacement tileOffset = pixelsTravelled.screenToMissile(); // calculate the future missile position m.position.tileForRendering = m.position.start + tileOffset; m.position.offsetForRendering = pixelsTravelled + tileOffset.worldToScreen(); } void UpdateMissileRendererData(Missile &m) { m.position.tileForRendering = m.position.tile; m.position.offsetForRendering = m.position.offset; const MissileMovementDistribution missileMovement = GetMissileData(m._mitype).movementDistribution; // don't calculate missile position if they don't move if (missileMovement == MissileMovementDistribution::Disabled || m.position.velocity == Displacement {}) return; int progress = ProgressToNextGameTick; UpdateMissilePositionForRendering(m, progress); // In some cases this calculated position is invalid. // For example a missile shouldn't move inside a wall. // In this case the game logic don't advance the missile position and removes the missile or shows an explosion animation at the old position. // For the animation distribution logic this means we are not allowed to move to a tile where the missile could collide, because this could be a invalid position. // If we are still at the current tile, this tile was already checked and is a valid tile if (m.position.tileForRendering == m.position.tile) return; // If no collision can happen at the new tile we can advance if (!CouldMissileCollide(m.position.tileForRendering, missileMovement == MissileMovementDistribution::Blockable)) return; // The new tile could be invalid, so don't advance to it. // We search the last offset that is in the old (valid) tile. // Implementation note: If someone knows the correct math to calculate this without the loop, I would really appreciate it. while (m.position.tile != m.position.tileForRendering) { progress -= 1; if (progress <= 0) { m.position.tileForRendering = m.position.tile; m.position.offsetForRendering = m.position.offset; return; } UpdateMissilePositionForRendering(m, progress); } } void UpdateMissilesRendererData() { MissilesAtRenderingTile.clear(); for (auto &m : Missiles) { UpdateMissileRendererData(m); MissilesAtRenderingTile[m.position.tileForRendering].push_back(&m); } } uint32_t lastFpsUpdateInMs; Rectangle PrevCursorRect; void BlitCursor(uint8_t *dst, uint32_t dstPitch, uint8_t *src, uint32_t srcPitch, uint32_t srcWidth, uint32_t srcHeight) { for (std::uint32_t i = 0; i < srcHeight; ++i, src += srcPitch, dst += dstPitch) { memcpy(dst, src, srcWidth); } } /** * @brief Remove the cursor from the buffer */ void UndrawCursor(const Surface &out) { DrawnCursor &cursor = GetDrawnCursor(); BlitCursor(&out[cursor.rect.position], out.pitch(), cursor.behindBuffer, cursor.rect.size.width, cursor.rect.size.width, cursor.rect.size.height); PrevCursorRect = cursor.rect; } bool ShouldShowCursor() { if (ControlMode == ControlTypes::KeyboardAndMouse) return true; if (pcurs == CURSOR_TELEPORT) return true; if (invflag) return true; if (CharFlag && MyPlayer->_pStatPts > 0) return true; return false; } /** * @brief Blit CL2 sprite, and apply lighting, to the given buffer at the given coordinates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame */ inline void ClxDrawLight(const Surface &out, Point position, ClxSprite clx, int lightTableIndex) { if (lightTableIndex != 0) { ClxDrawTRN(out, position, clx, LightTables[lightTableIndex].data()); } else { ClxDraw(out, position, clx); } } /** * @brief Blit CL2 sprite, and apply lighting and transparency blending, to the given buffer at the given coordinates * @param out Output buffer * @param position Target buffer coordinate * @param clx CLX frame */ inline void ClxDrawLightBlended(const Surface &out, Point position, ClxSprite clx, int lightTableIndex) { if (lightTableIndex != 0) { ClxDrawBlendedTRN(out, position, clx, LightTables[lightTableIndex].data()); } else { ClxDrawBlended(out, position, clx); } } /** * @brief Save the content behind the cursor to a temporary buffer, then draw the cursor. */ void DrawCursor(const Surface &out) { DrawnCursor &cursor = GetDrawnCursor(); if (IsHardwareCursor()) { SetHardwareCursorVisible(ShouldShowCursor()); cursor.rect.size = { 0, 0 }; return; } if (pcurs <= CURSOR_NONE || !ShouldShowCursor()) { cursor.rect.size = { 0, 0 }; return; } const Size cursSize = GetInvItemSize(pcurs); if (cursSize.width == 0 || cursSize.height == 0) { cursor.rect.size = { 0, 0 }; return; } constexpr auto Clip = [](int &pos, int &length, int posEnd) { if (pos + length <= 0 || pos >= posEnd) { pos = 0; length = 0; } else if (pos < 0) { length += pos; pos = 0; } else if (pos + length > posEnd) { length = posEnd - pos; } }; // Copy the buffer before the item cursor and its 1px outline are drawn to a temporary buffer. const int outlineWidth = !MyPlayer->HoldItem.isEmpty() ? 1 : 0; const Displacement offset = !MyPlayer->HoldItem.isEmpty() ? Displacement { cursSize / 2 } : Displacement { 0 }; const Point cursPosition = MousePosition - offset; Rectangle &rect = cursor.rect; rect.position.x = cursPosition.x - outlineWidth; rect.size.width = cursSize.width + 2 * outlineWidth; Clip(rect.position.x, rect.size.width, out.w()); rect.position.y = cursPosition.y - outlineWidth; rect.size.height = cursSize.height + 2 * outlineWidth; Clip(rect.position.y, rect.size.height, out.h()); if (rect.size.width == 0 || rect.size.height == 0) return; BlitCursor(cursor.behindBuffer, rect.size.width, &out[rect.position], out.pitch(), rect.size.width, rect.size.height); DrawSoftwareCursor(out, cursPosition + Displacement { 0, cursSize.height - 1 }, pcurs); } /** * @brief Render a missile sprite * @param out Output buffer * @param missile Pointer to Missile struct * @param targetBufferPosition Output buffer coordinate * @param pre Is the sprite in the background */ void DrawMissilePrivate(const Surface &out, const Missile &missile, Point targetBufferPosition, bool pre, int lightTableIndex) { if (missile._miPreFlag != pre || !missile._miDrawFlag) return; const Point missileRenderPosition { targetBufferPosition + missile.position.offsetForRendering - Displacement { missile._miAnimWidth2, 0 } }; const ClxSprite sprite = (*missile._miAnimData)[missile._miAnimFrame - 1]; if (missile._miUniqTrans != 0) { ClxDrawTRN(out, missileRenderPosition, sprite, Monsters[missile._misource].uniqueMonsterTRN.get()); } else if (missile._miLightFlag) { ClxDrawLight(out, missileRenderPosition, sprite, lightTableIndex); } else { ClxDraw(out, missileRenderPosition, sprite); } } /** * @brief Render a missile sprites for a given tile * @param out Output buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates * @param pre Is the sprite in the background */ void DrawMissile(const Surface &out, WorldTilePosition tilePosition, Point targetBufferPosition, bool pre, int lightTableIndex) { const auto it = MissilesAtRenderingTile.find(tilePosition); if (it == MissilesAtRenderingTile.end()) return; for (Missile *missile : it->second) { DrawMissilePrivate(out, *missile, targetBufferPosition, pre, lightTableIndex); } } /** * @brief Render a monster sprite * @param out Output buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates * @param monster Monster reference */ void DrawMonster(const Surface &out, Point tilePosition, Point targetBufferPosition, const Monster &monster, int lightTableIndex) { if (!monster.animInfo.sprites) { Log("Draw Monster \"{}\": NULL Cel Buffer", monster.name()); return; } const ClxSprite sprite = monster.animInfo.currentSprite(); if (!IsTileLit(tilePosition)) { ClxDrawTRN(out, targetBufferPosition, sprite, GetInfravisionTRN()); return; } uint8_t *trn = nullptr; if (monster.isUnique()) trn = monster.uniqueMonsterTRN.get(); if (monster.mode == MonsterMode::Petrified) trn = GetStoneTRN(); if (MyPlayer->_pInfraFlag && lightTableIndex > 8) trn = GetInfravisionTRN(); if (trn != nullptr) ClxDrawTRN(out, targetBufferPosition, sprite, trn); else ClxDrawLight(out, targetBufferPosition, sprite, lightTableIndex); } /** * @brief Helper for rendering a specific player icon (Mana Shield or Reflect) */ void DrawPlayerIconHelper(const Surface &out, MissileGraphicID missileGraphicId, Point position, const Player &player, bool infraVision, int lightTableIndex) { const bool lighting = &player != MyPlayer; if (player.isWalking()) position += GetOffsetForWalking(player.AnimInfo, player._pdir); position.x -= GetMissileSpriteData(missileGraphicId).animWidth2; const ClxSprite sprite = (*GetMissileSpriteData(missileGraphicId).sprites).list()[0]; if (!lighting) { ClxDraw(out, position, sprite); return; } if (infraVision) { ClxDrawTRN(out, position, sprite, GetInfravisionTRN()); return; } ClxDrawLight(out, position, sprite, lightTableIndex); } /** * @brief Helper for rendering player icons (Mana Shield and Reflect) * @param out Output buffer * @param player Player reference * @param position Output buffer coordinates * @param infraVision Should infravision be applied */ void DrawPlayerIcons(const Surface &out, const Player &player, Point position, bool infraVision, int lightTableIndex) { if (player.pManaShield) DrawPlayerIconHelper(out, MissileGraphicID::ManaShield, position, player, infraVision, lightTableIndex); if (player.wReflections > 0) DrawPlayerIconHelper(out, MissileGraphicID::Reflect, position + Displacement { 0, 16 }, player, infraVision, lightTableIndex); } uint8_t GetPlayerOutlineColor(int id) { static constexpr uint8_t PlayerOutlineColors[] = { OutlineColorsPlayer1, OutlineColorsPlayer2, OutlineColorsPlayer3, OutlineColorsPlayer4, }; if (id < 0 || id >= static_cast(SDL_arraysize(PlayerOutlineColors))) return OutlineColorsPlayer1; return PlayerOutlineColors[id]; } /** * @brief Render a player sprite * @param out Output buffer * @param player Player reference * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates */ void DrawPlayer(const Surface &out, const Player &player, Point tilePosition, Point targetBufferPosition, int lightTableIndex) { if (!IsTileLit(tilePosition) && !MyPlayer->_pInfraFlag && !MyPlayer->isOnArenaLevel() && leveltype != DTYPE_TOWN) { return; } const ClxSprite sprite = player.currentSprite(); const Point spriteBufferPosition = targetBufferPosition + player.getRenderingOffset(sprite); if (&player == PlayerUnderCursor) ClxDrawOutlineSkipColorZero(out, GetPlayerOutlineColor(player.getId()), spriteBufferPosition, sprite); if (&player == MyPlayer && IsNoneOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { ClxDraw(out, spriteBufferPosition, sprite); DrawPlayerIcons(out, player, targetBufferPosition, /*infraVision=*/false, lightTableIndex); return; } if (!IsTileLit(tilePosition) || ((MyPlayer->_pInfraFlag || MyPlayer->isOnArenaLevel()) && lightTableIndex > 8)) { ClxDrawTRN(out, spriteBufferPosition, sprite, GetInfravisionTRN()); DrawPlayerIcons(out, player, targetBufferPosition, /*infraVision=*/true, lightTableIndex); return; } lightTableIndex = std::max(lightTableIndex - 5, 0); ClxDrawLight(out, spriteBufferPosition, sprite, lightTableIndex); DrawPlayerIcons(out, player, targetBufferPosition, /*infraVision=*/false, lightTableIndex); } /** * @brief Render a player sprite * @param out Output buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates */ void DrawDeadPlayer(const Surface &out, Point tilePosition, Point targetBufferPosition, int lightTableIndex) { dFlags[tilePosition.x][tilePosition.y] &= ~DungeonFlag::DeadPlayer; for (const Player &player : Players) { if (player.plractive && player.hasNoLife() && player.isOnActiveLevel() && player.position.tile == tilePosition) { dFlags[tilePosition.x][tilePosition.y] |= DungeonFlag::DeadPlayer; const Point playerRenderPosition { targetBufferPosition }; DrawPlayer(out, player, tilePosition, playerRenderPosition, lightTableIndex); } } } /** * @brief Render an object sprite * @param out Output buffer * @param objectToDraw Dungeone object to draw * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates * @param pre Is the sprite in the background */ void DrawObject(const Surface &out, const Object &objectToDraw, Point tilePosition, Point targetBufferPosition, int lightTableIndex) { const ClxSprite sprite = objectToDraw.currentSprite(); const Point screenPosition = targetBufferPosition + objectToDraw.getRenderingOffset(sprite, tilePosition); if (&objectToDraw == ObjectUnderCursor) { ClxDrawOutlineSkipColorZero(out, OutlineColorsObject, screenPosition, sprite); } if (objectToDraw.applyLighting) { ClxDrawLight(out, screenPosition, sprite, lightTableIndex); } else { ClxDraw(out, screenPosition, sprite); } } static void DrawDungeon(const Surface & /*out*/, const Lightmap & /*lightmap*/, Point /*tilePosition*/, Point /*targetBufferPosition*/); /** * @brief Render a cell * @param out Target buffer * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Target buffer coordinates */ void DrawCell(const Surface &out, const Lightmap lightmap, Point tilePosition, Point targetBufferPosition, int lightTableIndex) { const uint16_t levelPieceId = dPiece[tilePosition.x][tilePosition.y]; const MICROS *pMap = &DPieceMicros[levelPieceId]; const uint8_t *tbl = LightTables[lightTableIndex].data(); const uint8_t *foliageTbl = tbl; #ifdef _DEBUG int walkpathIdx = -1; Point originalTargetBufferPosition; if (DebugPath) { walkpathIdx = MyPlayer->GetPositionPathIndex(tilePosition); if (walkpathIdx != -1) { originalTargetBufferPosition = targetBufferPosition; tbl = GetPauseTRN(); } } #endif bool transparency = TileHasAny(tilePosition, TileProperties::Transparent) && TransList[dTransVal[tilePosition.x][tilePosition.y]]; #ifdef _DEBUG if ((SDL_GetModState() & SDL_KMOD_ALT) != 0) { transparency = false; } #endif const auto getFirstTileMaskLeft = [=](TileType tile) -> MaskType { if (transparency) { switch (tile) { case TileType::LeftTrapezoid: case TileType::TransparentSquare: return TileHasAny(tilePosition, TileProperties::TransparentLeft) ? MaskType::Left : MaskType::Solid; case TileType::LeftTriangle: return MaskType::Solid; default: return MaskType::Transparent; } } return MaskType::Solid; }; const auto getFirstTileMaskRight = [=](TileType tile) -> MaskType { if (transparency) { switch (tile) { case TileType::RightTrapezoid: case TileType::TransparentSquare: return TileHasAny(tilePosition, TileProperties::TransparentRight) ? MaskType::Right : MaskType::Solid; case TileType::RightTriangle: return MaskType::Solid; default: return MaskType::Transparent; } } return MaskType::Solid; }; // Create a special lightmap buffer to bleed light up walls uint8_t lightmapBuffer[TILE_WIDTH * TILE_HEIGHT]; const Lightmap bleedLightmap = Lightmap::bleedUp(*GetOptions().Graphics.perPixelLighting, lightmap, targetBufferPosition, lightmapBuffer); // If the first micro tile is a floor tile, it may be followed // by foliage which should be rendered now. const bool isFloor = IsFloor(tilePosition); if (const LevelCelBlock levelCelBlock { pMap->mt[0] }; levelCelBlock.hasValue()) { const TileType tileType = levelCelBlock.type(); if (!isFloor || tileType == TileType::TransparentSquare) { if (isFloor && tileType == TileType::TransparentSquare) { RenderTileFoliage(out, bleedLightmap, targetBufferPosition, pDungeonCels.get(), levelCelBlock, foliageTbl); } else { RenderTile(out, bleedLightmap, targetBufferPosition, pDungeonCels.get(), levelCelBlock, getFirstTileMaskLeft(tileType), tbl); } } } if (const LevelCelBlock levelCelBlock { pMap->mt[1] }; levelCelBlock.hasValue()) { const TileType tileType = levelCelBlock.type(); if (!isFloor || tileType == TileType::TransparentSquare) { if (isFloor && tileType == TileType::TransparentSquare) { RenderTileFoliage(out, bleedLightmap, targetBufferPosition + RightFrameDisplacement, pDungeonCels.get(), levelCelBlock, foliageTbl); } else { RenderTile(out, bleedLightmap, targetBufferPosition + RightFrameDisplacement, pDungeonCels.get(), levelCelBlock, getFirstTileMaskRight(tileType), tbl); } } } targetBufferPosition.y -= TILE_HEIGHT; for (uint_fast8_t i = 2, n = MicroTileLen; i < n; i += 2) { { const LevelCelBlock levelCelBlock { pMap->mt[i] }; if (levelCelBlock.hasValue()) { RenderTile(out, bleedLightmap, targetBufferPosition, pDungeonCels.get(), levelCelBlock, transparency ? MaskType::Transparent : MaskType::Solid, foliageTbl); } } { const LevelCelBlock levelCelBlock { pMap->mt[i + 1] }; if (levelCelBlock.hasValue()) { RenderTile(out, bleedLightmap, targetBufferPosition + RightFrameDisplacement, pDungeonCels.get(), levelCelBlock, transparency ? MaskType::Transparent : MaskType::Solid, foliageTbl); } } targetBufferPosition.y -= TILE_HEIGHT; } #ifdef _DEBUG if (DebugPath && walkpathIdx != -1) { DrawString(out, StrCat(walkpathIdx), Rectangle(originalTargetBufferPosition + Displacement { 0, -TILE_HEIGHT }, Size { TILE_WIDTH, TILE_HEIGHT }), TextRenderOptions { .flags = UiFlags::AlignCenter | UiFlags::VerticalCenter | (IsTileSolid(tilePosition) ? UiFlags::ColorYellow : UiFlags::ColorWhite) }); } #endif } /** * @brief Render a floor tile. * @param out Target buffer * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Target buffer coordinate */ void DrawFloorTile(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition) { const int lightTableIndex = dLight[tilePosition.x][tilePosition.y]; const uint8_t *tbl = LightTables[lightTableIndex].data(); #ifdef _DEBUG if (DebugPath && MyPlayer->GetPositionPathIndex(tilePosition) != -1) tbl = GetPauseTRN(); #endif const uint16_t levelPieceId = dPiece[tilePosition.x][tilePosition.y]; { const LevelCelBlock levelCelBlock { DPieceMicros[levelPieceId].mt[0] }; if (levelCelBlock.hasValue()) { RenderTileFrame(out, lightmap, targetBufferPosition, TileType::LeftTriangle, GetDunFrame(pDungeonCels.get(), levelCelBlock.frame()), DunFrameTriangleHeight, MaskType::Solid, tbl); } } { const LevelCelBlock levelCelBlock { DPieceMicros[levelPieceId].mt[1] }; if (levelCelBlock.hasValue()) { RenderTileFrame(out, lightmap, targetBufferPosition + RightFrameDisplacement, TileType::RightTriangle, GetDunFrame(pDungeonCels.get(), levelCelBlock.frame()), DunFrameTriangleHeight, MaskType::Solid, tbl); } } } /** * @brief Draw item for a given tile * @param out Output buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates * @param pre Is the sprite in the background */ void DrawItem(const Surface &out, int8_t itemIndex, Point targetBufferPosition, int lightTableIndex) { const Item &item = Items[itemIndex]; const ClxSprite sprite = item.AnimInfo.currentSprite(); const Point position = targetBufferPosition + item.getRenderingOffset(sprite); if (!IsPlayerInStore() && (itemIndex == pcursitem || AutoMapShowItems)) { ClxDrawOutlineSkipColorZero(out, GetOutlineColor(item, false), position, sprite); } ClxDrawLight(out, position, sprite, lightTableIndex); if (item.AnimInfo.isLastFrame() || item._iCurs == ICURS_MAGIC_ROCK) AddItemToLabelQueue(itemIndex, position); } /** * @brief Check if and how a monster should be rendered * @param out Output buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Output buffer coordinates */ void DrawMonsterHelper(const Surface &out, Point tilePosition, Point targetBufferPosition, int lightTableIndex) { int mi = dMonster[tilePosition.x][tilePosition.y]; mi = std::abs(mi) - 1; if (leveltype == DTYPE_TOWN) { auto &towner = Towners[mi]; const Point position = targetBufferPosition + towner.getRenderingOffset(); const ClxSprite sprite = towner.currentSprite(); if (mi == pcursmonst) { ClxDrawOutlineSkipColorZero(out, OutlineColorsTowner, position, sprite); } ClxDraw(out, position, sprite); return; } if (!IsTileLit(tilePosition) && !(MyPlayer->_pInfraFlag && IsFloor(tilePosition))) { return; } if (static_cast(mi) >= MaxMonsters) { Log("Draw Monster: tried to draw illegal monster {}", mi); return; } const auto &monster = Monsters[mi]; if ((monster.flags & MFLAG_HIDDEN) != 0) { return; } const ClxSprite sprite = monster.animInfo.currentSprite(); const Displacement offset = monster.getRenderingOffset(sprite); const Point monsterRenderPosition = targetBufferPosition + offset; if (mi == pcursmonst) { ClxDrawOutlineSkipColorZero(out, OutlineColorsMonster, monsterRenderPosition, sprite); } DrawMonster(out, tilePosition, monsterRenderPosition, monster, lightTableIndex); } /** * @brief Render object sprites * @param out Target buffer * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Target buffer coordinates */ void DrawDungeon(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition) { assert(InDungeonBounds(tilePosition)); const int lightTableIndex = dLight[tilePosition.x][tilePosition.y]; DrawCell(out, lightmap, tilePosition, targetBufferPosition, lightTableIndex); const int8_t bDead = dCorpse[tilePosition.x][tilePosition.y]; const int8_t bMap = dTransVal[tilePosition.x][tilePosition.y]; #ifdef _DEBUG if (DebugVision && IsTileLit(tilePosition)) { ClxDraw(out, targetBufferPosition, (*pSquareCel)[0]); } #endif if (MissilePreFlag) { DrawMissile(out, tilePosition, targetBufferPosition, true, lightTableIndex); } if (lightTableIndex < LightsMax && bDead != 0) { const Corpse &corpse = Corpses[(bDead & 0x1F) - 1]; const Point position { targetBufferPosition.x - CalculateSpriteTileCenterX(corpse.width), targetBufferPosition.y }; const ClxSprite sprite = corpse.spritesForDirection(static_cast((bDead >> 5) & 7))[corpse.frame]; if (corpse.translationPaletteIndex != 0) { const uint8_t *trn = Monsters[corpse.translationPaletteIndex - 1].uniqueMonsterTRN.get(); ClxDrawTRN(out, position, sprite, trn); } else { ClxDrawLight(out, position, sprite, lightTableIndex); } } const int8_t bItem = dItem[tilePosition.x][tilePosition.y]; const Object *object = lightTableIndex < LightsMax ? FindObjectAtPosition(tilePosition) : nullptr; if (object != nullptr && object->_oPreFlag) { DrawObject(out, *object, tilePosition, targetBufferPosition, lightTableIndex); } if (bItem > 0 && !Items[bItem - 1]._iPostDraw) { DrawItem(out, static_cast(bItem - 1), targetBufferPosition, lightTableIndex); } if (TileContainsDeadPlayer(tilePosition)) { DrawDeadPlayer(out, tilePosition, targetBufferPosition, lightTableIndex); } Player *player = PlayerAtPosition(tilePosition); if (player != nullptr) { const uint8_t pid = player->getId(); assert(pid < MAX_PLRS); int playerId = static_cast(pid) + 1; // If sprite is moving southwards or east, we want to draw it offset from the tile it's moving to, so we need negative ID // This respests the order that tiles are drawn. By using the negative id, we ensure that the sprite is drawn with priority if (player->_pmode == PM_WALK_SOUTHWARDS || (player->_pmode == PM_WALK_SIDEWAYS && player->_pdir == Direction::East)) playerId = -playerId; if (dPlayer[tilePosition.x][tilePosition.y] == playerId) { auto tempTilePosition = tilePosition; auto tempTargetBufferPosition = targetBufferPosition; // Offset the sprite to the tile it's moving from if (player->_pmode == PM_WALK_SOUTHWARDS) { switch (player->_pdir) { case Direction::SouthWest: tempTargetBufferPosition += { TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; break; case Direction::South: tempTargetBufferPosition += { 0, -TILE_HEIGHT }; break; case Direction::SouthEast: tempTargetBufferPosition += { -TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; break; default: DVL_UNREACHABLE(); } tempTilePosition += Opposite(player->_pdir); } else if (player->_pmode == PM_WALK_SIDEWAYS && player->_pdir == Direction::East) { tempTargetBufferPosition += { -TILE_WIDTH, 0 }; tempTilePosition += Opposite(player->_pdir); } DrawPlayer(out, *player, tempTilePosition, tempTargetBufferPosition, lightTableIndex); } } Monster *monster = FindMonsterAtPosition(tilePosition); if (monster != nullptr) { auto mid = monster->getId(); assert(mid < MaxMonsters); int monsterId = static_cast(mid) + 1; // If sprite is moving southwards or east, we want to draw it offset from the tile it's moving to, so we need negative ID // This respests the order that tiles are drawn. By using the negative id, we ensure that the sprite is drawn with priority if (monster->mode == MonsterMode::MoveSouthwards || (monster->mode == MonsterMode::MoveSideways && monster->direction == Direction::East)) monsterId = -monsterId; if (dMonster[tilePosition.x][tilePosition.y] == monsterId) { auto tempTilePosition = tilePosition; auto tempTargetBufferPosition = targetBufferPosition; // Offset the sprite to the tile it's moving from if (monster->mode == MonsterMode::MoveSouthwards) { switch (monster->direction) { case Direction::SouthWest: tempTargetBufferPosition += { TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; break; case Direction::South: tempTargetBufferPosition += { 0, -TILE_HEIGHT }; break; case Direction::SouthEast: tempTargetBufferPosition += { -TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; break; default: DVL_UNREACHABLE(); } tempTilePosition += Opposite(monster->direction); } else if (monster->mode == MonsterMode::MoveSideways && monster->direction == Direction::East) { tempTargetBufferPosition += { -TILE_WIDTH, 0 }; tempTilePosition += Opposite(monster->direction); } DrawMonsterHelper(out, tempTilePosition, tempTargetBufferPosition, lightTableIndex); } } DrawMissile(out, tilePosition, targetBufferPosition, false, lightTableIndex); if (object != nullptr && !object->_oPreFlag) { DrawObject(out, *object, tilePosition, targetBufferPosition, lightTableIndex); } if (bItem > 0 && Items[bItem - 1]._iPostDraw) { DrawItem(out, static_cast(bItem - 1), targetBufferPosition, lightTableIndex); } if (leveltype != DTYPE_TOWN) { const bool perPixelLighting = *GetOptions().Graphics.perPixelLighting; const int8_t bArch = dSpecial[tilePosition.x][tilePosition.y] - 1; if (bArch >= 0) { bool transparency = TransList[bMap]; #ifdef _DEBUG // Turn transparency off here for debugging transparency = transparency && (SDL_GetModState() & SDL_KMOD_ALT) == 0; #endif if (perPixelLighting) { // Create a special lightmap buffer to bleed light up walls uint8_t lightmapBuffer[TILE_WIDTH * TILE_HEIGHT]; const Lightmap bleedLightmap = Lightmap::bleedUp(*GetOptions().Graphics.perPixelLighting, lightmap, targetBufferPosition, lightmapBuffer); if (transparency) ClxDrawBlendedWithLightmap(out, targetBufferPosition, (*pSpecialCels)[bArch], bleedLightmap); else ClxDrawWithLightmap(out, targetBufferPosition, (*pSpecialCels)[bArch], bleedLightmap); } else if (transparency) { ClxDrawLightBlended(out, targetBufferPosition, (*pSpecialCels)[bArch], lightTableIndex); } else { ClxDrawLight(out, targetBufferPosition, (*pSpecialCels)[bArch], lightTableIndex); } } } else { // Tree leaves should always cover player when entering or leaving the tile, // So delay the rendering until after the next row is being drawn. // This could probably have been better solved by sprites in screen space. if (tilePosition.x > 0 && tilePosition.y > 0 && targetBufferPosition.y > TILE_HEIGHT) { const int8_t bArch = dSpecial[tilePosition.x - 1][tilePosition.y - 1] - 1; if (bArch >= 0) ClxDraw(out, targetBufferPosition + Displacement { 0, -TILE_HEIGHT }, (*pSpecialCels)[bArch]); } } } /** * @brief Render a row of tiles * @param out Buffer to render to * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Target buffer coordinates * @param rows Number of rows * @param columns Tile in a row */ void DrawFloor(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns) { for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) { if (!InDungeonBounds(tilePosition)) continue; if (IsFloor(tilePosition)) { DrawFloorTile(out, lightmap, tilePosition, targetBufferPosition); } } // Return to start of row tilePosition += Displacement(Direction::West) * columns; targetBufferPosition.x -= columns * TILE_WIDTH; // Jump to next row targetBufferPosition.y += TILE_HEIGHT / 2; if ((i & 1) != 0) { tilePosition.x++; columns--; targetBufferPosition.x += TILE_WIDTH / 2; } else { tilePosition.y++; columns++; targetBufferPosition.x -= TILE_WIDTH / 2; } } } /** * @brief Renders the floor tiles * @param out Output buffer * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Buffer coordinates * @param rows Number of rows * @param columns Tile in a row */ void DrawTileContent(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns) { // Keep evaluating until MicroTiles can't affect screen rows += MicroTileLen; #ifdef _DEBUG DebugCoordsMap.reserve(rows * columns); #endif for (int i = 0; i < rows; i++) { bool skip = false; for (int j = 0; j < columns; j++) { if (InDungeonBounds(tilePosition)) { bool skipNext = false; #ifdef _DEBUG DebugCoordsMap[tilePosition.x + tilePosition.y * MAXDUNX] = targetBufferPosition; #endif if (tilePosition.x + 1 < MAXDUNX && tilePosition.y - 1 >= 0 && targetBufferPosition.x + TILE_WIDTH <= gnScreenWidth) { // Render objects behind walls first to prevent sprites, that are moving // between tiles, from poking through the walls as they exceed the tile bounds. // A proper fix for this would probably be to layout the scene and render by // sprite screen position rather than tile position. if (IsWall(tilePosition) && (IsWall(tilePosition + Displacement { 1, 0 }) || (tilePosition.x > 0 && IsWall(tilePosition + Displacement { -1, 0 })))) { // Part of a wall aligned on the x-axis if (IsTileNotSolid(tilePosition + Displacement { 1, -1 }) && IsTileNotSolid(tilePosition + Displacement { 0, -1 })) { // Has walkable area behind it DrawDungeon(out, lightmap, tilePosition + Direction::East, { targetBufferPosition.x + TILE_WIDTH, targetBufferPosition.y }); skipNext = true; } } } if (!skip) { DrawDungeon(out, lightmap, tilePosition, targetBufferPosition); } skip = skipNext; } tilePosition += Direction::East; targetBufferPosition.x += TILE_WIDTH; } // Return to start of row tilePosition += Displacement(Direction::West) * columns; targetBufferPosition.x -= columns * TILE_WIDTH; // Jump to next row targetBufferPosition.y += TILE_HEIGHT / 2; if ((i & 1) != 0) { tilePosition.x++; columns--; targetBufferPosition.x += TILE_WIDTH / 2; } else { tilePosition.y++; columns++; targetBufferPosition.x -= TILE_WIDTH / 2; } } } void DrawDirtTile(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition) { // This should be the *top-left* of the 2×2 dirt pattern in the actual dungeon. // You might need to tweak these to where your dirt patch actually lives. constexpr Point base { 0, 0 }; // Decide which of the 4 tiles of the 2×2 block to use, // based on where this OOB tile is in the world grid. const int ox = (tilePosition.x & 1); // 0 or 1 const int oy = (tilePosition.y & 1); // 0 or 1 Point sample { base.x + ox, base.y + oy, }; // Safety: clamp in case tilePosition is wildly outside and base+offset ever escapes sample.x = std::clamp(sample.x, 0, MAXDUNX - 1); sample.y = std::clamp(sample.y, 0, MAXDUNY - 1); if (!InDungeonBounds(sample) || dPiece[sample.x][sample.y] == 0) { // Failsafe: if our sample somehow isn't valid, fall back to black world_draw_black_tile(out, targetBufferPosition.x, targetBufferPosition.y); return; } const int lightTableIndex = dLight[sample.x][sample.y]; // Let the normal dungeon tile renderer compose the full tile DrawCell(out, lightmap, sample, targetBufferPosition, lightTableIndex); } /** * @brief Render a row of tiles * @param out Buffer to render to * @param lightmap Per-pixel light buffer * @param tilePosition dPiece coordinates * @param targetBufferPosition Target buffer coordinates * @param rows Number of rows * @param columns Tile in a row */ void DrawOOB(const Surface &out, const Lightmap &lightmap, Point tilePosition, Point targetBufferPosition, int rows, int columns) { for (int i = 0; i < rows + 5; i++) { // 5 extra rows needed to make sure everything gets rendered at the bottom half of the screen for (int j = 0; j < columns; j++, tilePosition += Direction::East, targetBufferPosition.x += TILE_WIDTH) { if (!InDungeonBounds(tilePosition)) { if (leveltype == DTYPE_TOWN) { world_draw_black_tile(out, targetBufferPosition.x, targetBufferPosition.y); } else { DrawDirtTile(out, lightmap, tilePosition, targetBufferPosition); } } } // Return to start of row tilePosition += Displacement(Direction::West) * columns; targetBufferPosition.x -= columns * TILE_WIDTH; // Jump to next row targetBufferPosition.y += TILE_HEIGHT / 2; if ((i & 1) != 0) { tilePosition.x++; columns--; targetBufferPosition.x += TILE_WIDTH / 2; } else { tilePosition.y++; columns++; targetBufferPosition.x -= TILE_WIDTH / 2; } } } /** * @brief Scale up the top left part of the buffer 2x. */ void Zoom(const Surface &out) { int viewportWidth = out.w(); int viewportOffsetX = 0; if (CanPanelsCoverView()) { if (IsLeftPanelOpen()) { viewportWidth -= SidePanelSize.width; viewportOffsetX = SidePanelSize.width; } else if (IsRightPanelOpen()) { viewportWidth -= SidePanelSize.width; } } // We round to even for the source width and height. // If the width / height was odd, we copy just one extra pixel / row later on. const int srcWidth = (viewportWidth + 1) / 2; const int doubleableWidth = viewportWidth / 2; const int srcHeight = (out.h() + 1) / 2; const int doubleableHeight = out.h() / 2; uint8_t *src = out.at(srcWidth - 1, srcHeight - 1); uint8_t *dst = out.at(viewportOffsetX + viewportWidth - 1, out.h() - 1); const bool oddViewportWidth = (viewportWidth % 2) == 1; for (int hgt = 0; hgt < doubleableHeight; hgt++) { // Double the pixels in the line. for (int i = 0; i < doubleableWidth; i++) { *dst-- = *src; *dst-- = *src; --src; } // Copy a single extra pixel if the output width is odd. if (oddViewportWidth) { *dst-- = *src; --src; } // Skip the rest of the source line. src -= (out.pitch() - srcWidth); // Double the line. memcpy(dst - out.pitch() + 1, dst + 1, viewportWidth); // Skip the rest of the destination line. dst -= 2 * out.pitch() - viewportWidth; } if ((out.h() % 2) == 1) { memcpy(dst - out.pitch() + 1, dst + 1, viewportWidth); } } Displacement tileOffset; Displacement tileShift; int tileColumns; int tileRows; void CalcFirstTilePosition(Point &position, Displacement &offset) { // Adjust by player offset and tile grid alignment const Player &myPlayer = *MyPlayer; offset = tileOffset; if (myPlayer.isWalking()) offset += GetOffsetForWalking(myPlayer.AnimInfo, myPlayer._pdir, true); position += tileShift; // Skip rendering parts covered by the panels if (CanPanelsCoverView() && (IsLeftPanelOpen() || IsRightPanelOpen())) { const int multiplier = (*GetOptions().Graphics.zoom) ? 1 : 2; position += Displacement(Direction::East) * multiplier; offset.deltaX += -TILE_WIDTH * multiplier / 2 / 2; if (IsLeftPanelOpen() && !*GetOptions().Graphics.zoom) { offset.deltaX += SidePanelSize.width; // SidePanelSize.width accounted for in Zoom() } } // Draw areas moving in and out of the screen if (myPlayer.isWalking()) { switch (myPlayer._pdir) { case Direction::North: case Direction::NorthEast: offset.deltaY -= TILE_HEIGHT; position += Direction::North; break; case Direction::SouthWest: case Direction::West: offset.deltaX -= TILE_WIDTH; position += Direction::West; break; case Direction::NorthWest: offset.deltaX -= TILE_WIDTH / 2; offset.deltaY -= TILE_HEIGHT / 2; position += Direction::NorthWest; default: break; } } } /** * @brief Configure render and process screen rows * @param fullOut Buffer to render to * @param position First tile of view in dPiece coordinate * @param offset Amount to offset the rendering in screen space */ void DrawGame(const Surface &fullOut, Point position, Displacement offset) { // Limit rendering to the view area const Surface &out = !*GetOptions().Graphics.zoom ? fullOut.subregionY(0, gnViewportHeight) : fullOut.subregionY(0, (gnViewportHeight + 1) / 2); int columns = tileColumns; int rows = tileRows; // Skip rendering parts covered by the panels if (CanPanelsCoverView() && (IsLeftPanelOpen() || IsRightPanelOpen())) { columns -= (*GetOptions().Graphics.zoom) ? 2 : 4; } UpdateMissilesRendererData(); // Draw areas moving in and out of the screen if (MyPlayer->isWalking()) { switch (MyPlayer->_pdir) { case Direction::NoDirection: break; case Direction::North: case Direction::South: rows += 2; break; case Direction::NorthEast: columns++; rows += 2; break; case Direction::East: case Direction::West: columns++; break; case Direction::SouthEast: case Direction::SouthWest: case Direction::NorthWest: columns++; rows++; break; } } #ifdef DUN_RENDER_STATS DunRenderStats.clear(); #endif Lightmap lightmap = Lightmap::build(*GetOptions().Graphics.perPixelLighting, position, Point {} + offset, gnScreenWidth, gnViewportHeight, rows, columns, out.at(0, 0), out.pitch(), LightTables, FullyLitLightTable, FullyDarkLightTable, dLight, MicroTileLen); DrawFloor(out, lightmap, position, Point {} + offset, rows, columns); DrawTileContent(out, lightmap, position, Point {} + offset, rows, columns); DrawOOB(out, lightmap, position, Point {} + offset, rows, columns); if (*GetOptions().Graphics.zoom) { Zoom(fullOut.subregionY(0, gnViewportHeight)); } #ifdef DUN_RENDER_STATS std::vector> sortedStats(DunRenderStats.begin(), DunRenderStats.end()); c_sort(sortedStats, [](const std::pair &a, const std::pair &b) { return a.first.maskType == b.first.maskType ? static_cast(a.first.tileType) < static_cast(b.first.tileType) : static_cast(a.first.maskType) < static_cast(b.first.maskType); }); Point pos { 100, 20 }; for (size_t i = 0; i < sortedStats.size(); ++i) { const auto &stat = sortedStats[i]; DrawString(out, StrCat(i, "."), Rectangle(pos, Size { 20, 16 }), { .flags = UiFlags::AlignRight }); DrawString(out, MaskTypeToString(stat.first.maskType), { pos.x + 24, pos.y }); DrawString(out, TileTypeToString(stat.first.tileType), { pos.x + 184, pos.y }); DrawString(out, FormatInteger(stat.second), Rectangle({ pos.x + 354, pos.y }, Size(40, 16)), { .flags = UiFlags::AlignRight }); pos.y += 16; } #endif } /** * @brief Start rendering of screen, town variation * @param out Buffer to render to * @param startPosition Center of view in dPiece coordinates */ void DrawView(const Surface &out, Point startPosition) { #ifdef _DEBUG DebugCoordsMap.clear(); #endif Displacement offset = {}; CalcFirstTilePosition(startPosition, offset); DrawGame(out, startPosition, offset); if (AutomapActive) { DrawAutomap(out.subregionY(0, gnViewportHeight)); } #ifdef _DEBUG bool debugGridTextNeeded = IsDebugGridTextNeeded(); if (debugGridTextNeeded || DebugGrid) { // force redrawing or debug stuff stays on panel on 640x480 resolution RedrawEverything(); std::string debugGridText; bool megaTiles = IsDebugGridInMegatiles(); for (auto [dunCoordVal, pixelCoords] : DebugCoordsMap) { Point dunCoords = { dunCoordVal % MAXDUNX, dunCoordVal / MAXDUNX }; if (megaTiles && (dunCoords.x % 2 == 1 || dunCoords.y % 2 == 1)) continue; if (megaTiles) pixelCoords += Displacement { 0, TILE_HEIGHT / 2 }; if (*GetOptions().Graphics.zoom) pixelCoords *= 2; if (debugGridTextNeeded && GetDebugGridText(dunCoords, debugGridText)) { Size tileSize = { TILE_WIDTH, TILE_HEIGHT }; if (*GetOptions().Graphics.zoom) tileSize *= 2; DrawString(out, debugGridText, { pixelCoords - Displacement { 0, tileSize.height }, tileSize }, { .flags = UiFlags::ColorRed | UiFlags::AlignCenter | UiFlags::VerticalCenter }); } if (DebugGrid) { int halfTileWidth = TILE_WIDTH / 2; int halfTileHeight = TILE_HEIGHT / 2; if (*GetOptions().Graphics.zoom) { halfTileWidth *= 2; halfTileHeight *= 2; } const Point center { pixelCoords.x + halfTileWidth, pixelCoords.y - halfTileHeight }; if (megaTiles) { halfTileWidth *= 2; halfTileHeight *= 2; } const uint8_t col = PAL16_BEIGE; for (const auto &[originX, dx] : { std::pair(center.x - halfTileWidth, 1), std::pair(center.x + halfTileWidth, -1) }) { // We only need to draw half of the grid cell boundaries (one triangle). // The other triangle will be drawn when drawing the adjacent grid cells. const int dy = 1; Point from { originX, center.y }; int height = halfTileHeight; if (out.InBounds(from) && out.InBounds(from + Displacement { 2 * dx * height, dy * height })) { uint8_t *dst = out.at(from.x, from.y); const int pitch = out.pitch(); while (height-- > 0) { *dst = col; dst += dx; *dst = col; dst += dx; dst += static_cast(dy * pitch); } } else { while (height-- > 0) { out.SetPixel(from, col); from.x += dx; out.SetPixel(from, col); from.x += dx; from.y += dy; } } } } } } #endif DrawItemNameLabels(out); DrawMonsterHealthBar(out); DrawFloatingNumbers(out, startPosition, offset); if (IsPlayerInStore() && !qtextflag) DrawSText(out); if (invflag) { DrawInv(out); } else if (SpellbookFlag) { DrawSpellBook(out); } DrawDurIcon(out); if (CharFlag) { DrawChr(out); } else if (QuestLogIsOpen) { DrawQuestLog(out); } else if (IsStashOpen) { DrawStash(out); } DrawLevelButton(out); if (ShowUniqueItemInfoBox) { DrawUniqueInfo(out); } if (qtextflag) { DrawQText(out); } if (SpellSelectFlag) { DrawSpellList(out); } if (DropGoldFlag) { DrawGoldSplit(out); } DrawGoldWithdraw(out); if (HelpFlag) { DrawHelp(out); } if (ChatLogFlag) { DrawChatLog(out); } if (MyPlayerIsDead) { RedBack(out); DrawDeathText(out); } else if (PauseMode != 0) { gmenu_draw_pause(out); } if (IsDiabloMsgAvailable()) { DrawDiabloMsg(out.subregionY(0, out.h() - GetMainPanel().size.height)); } DrawControllerModifierHints(out); DrawPlrMsg(out); gmenu_draw(out); doom_draw(out); DrawInfoBox(out); UpdateLifeManaPercent(); // Update life/mana totals before rendering any portion of the flask. DrawLifeFlaskUpper(out); DrawManaFlaskUpper(out); } /** * @brief Display the current average FPS over 1 sec */ void DrawFPS(const Surface &out) { static int framesSinceLastUpdate = 0; static std::string_view formatted {}; if (!frameflag || !gbActive) { return; } framesSinceLastUpdate++; const uint32_t runtimeInMs = SDL_GetTicks(); const uint32_t msSinceLastUpdate = runtimeInMs - lastFpsUpdateInMs; if (msSinceLastUpdate >= 1000) { lastFpsUpdateInMs = runtimeInMs; constexpr int FpsPow10 = 10; const uint32_t fps = 1000 * FpsPow10 * framesSinceLastUpdate / msSinceLastUpdate; framesSinceLastUpdate = 0; static char buf[15] {}; const char *end = fps >= 100 * FpsPow10 ? BufCopy(buf, fps / FpsPow10, " FPS") : BufCopy(buf, fps / FpsPow10, ".", fps % FpsPow10, " FPS"); formatted = { buf, static_cast(end - buf) }; }; DrawString(out, formatted, Point { 8, 8 }, { .flags = UiFlags::ColorRed }); } /** * @brief Update part of the screen from the back buffer */ void DoBlitScreen(Rectangle area) { #ifdef DEBUG_DO_BLIT_SCREEN const Surface &out = GlobalBackBuffer(); const uint8_t debugColor = PAL8_RED; DrawHorizontalLine(out, area.position, area.size.width, debugColor); DrawHorizontalLine(out, area.position + Displacement { 0, area.size.height - 1 }, area.size.width, debugColor); DrawVerticalLine(out, area.position, area.size.height, debugColor); DrawVerticalLine(out, area.position + Displacement { area.size.width - 1, 0 }, area.size.height, debugColor); #endif SDL_Rect srcRect = MakeSdlRect(area); SDL_Rect dstRect = MakeSdlRect(area); BltFast(&srcRect, &dstRect); } /** * @brief Check render pipeline and update individual screen parts * @param out Output surface. * @param dwHgt Section of screen to update from top to bottom * @param drawDesc Render info box * @param drawHp Render health bar * @param drawMana Render mana bar * @param drawSbar Render belt * @param drawBtn Render panel buttons */ void DrawMain(int dwHgt, bool drawDesc, bool drawHp, bool drawMana, bool drawSbar, bool drawBtn) { if (!gbActive || RenderDirectlyToOutputSurface) { return; } assert(dwHgt >= 0 && dwHgt <= gnScreenHeight); if (dwHgt > 0) { DoBlitScreen({ { 0, 0 }, { gnScreenWidth, dwHgt } }); } if (dwHgt < gnScreenHeight) { const Point mainPanelPosition = GetMainPanel().position; if (drawSbar) { DoBlitScreen({ mainPanelPosition + Displacement { 204, 5 }, { 232, 28 } }); } if (drawDesc) { if (ChatFlag) { // When chat input is displayed, the belt is hidden and the chat moves up. DoBlitScreen({ mainPanelPosition + Displacement { 171, 6 }, { 298, 116 } }); } else { DoBlitScreen({ mainPanelPosition + Displacement { InfoBoxRect.position.x, InfoBoxRect.position.y }, { InfoBoxRect.size } }); } } if (drawMana) { DoBlitScreen({ mainPanelPosition + Displacement { 460, 0 }, { 88, 72 } }); DoBlitScreen({ mainPanelPosition + Displacement { 564, 64 }, { 56, 56 } }); } if (drawHp) { DoBlitScreen({ mainPanelPosition + Displacement { 96, 0 }, { 88, 72 } }); } if (drawBtn) { DoBlitScreen({ mainPanelPosition + Displacement { 8, 7 }, { 74, 114 } }); DoBlitScreen({ mainPanelPosition + Displacement { 559, 7 }, { 74, 48 } }); if (gbIsMultiplayer) { DoBlitScreen({ mainPanelPosition + Displacement { 86, 91 }, { 34, 32 } }); DoBlitScreen({ mainPanelPosition + Displacement { 526, 91 }, { 34, 32 } }); } } if (PrevCursorRect.size.width != 0 && PrevCursorRect.size.height != 0) { DoBlitScreen(PrevCursorRect); } const Rectangle &cursorRect = GetDrawnCursor().rect; if (cursorRect.size.width != 0 && cursorRect.size.height != 0) { DoBlitScreen(cursorRect); } } } void OptionShowFPSChanged() { if (*GetOptions().Graphics.showFPS) EnableFrameCount(); else frameflag = false; } const auto OptionChangeHandlerShowFPS = (GetOptions().Graphics.showFPS.SetValueChangedCallback(OptionShowFPSChanged), true); } // namespace Displacement GetOffsetForWalking(const AnimationInfo &animationInfo, const Direction dir, bool cameraMode /*= false*/) { // clang-format off // South, SouthWest, West, NorthWest, North, NorthEast, East, SouthEast, constexpr Displacement MovingOffset[8] = { { 0, 32 }, { -32, 16 }, { -64, 0 }, { -32, -16 }, { 0, -32 }, { 32, -16 }, { 64, 0 }, { 32, 16 } }; // clang-format on const uint8_t animationProgress = animationInfo.getAnimationProgress(); Displacement offset = MovingOffset[static_cast(dir)]; offset *= animationProgress; offset /= AnimationInfo::baseValueFraction; if (cameraMode) { offset = -offset; } return offset; } void ClearCursor() // CODE_FIX: this was supposed to be in cursor.cpp { PrevCursorRect = {}; } void ShiftGrid(Point *offset, int horizontal, int vertical) { offset->x += vertical + horizontal; offset->y += vertical - horizontal; } int RowsCoveredByPanel() { const auto &mainPanelSize = GetMainPanel().size; if (GetScreenWidth() <= mainPanelSize.width) { return 0; } int rows = mainPanelSize.height / TILE_HEIGHT; if (*GetOptions().Graphics.zoom) { rows /= 2; } return rows; } void CalcTileOffset(int *offsetX, int *offsetY) { const uint16_t screenWidth = GetScreenWidth(); const uint16_t viewportHeight = GetViewportHeight(); int x; int y; if (!*GetOptions().Graphics.zoom) { x = screenWidth % TILE_WIDTH; y = viewportHeight % TILE_HEIGHT; } else { x = (screenWidth / 2) % TILE_WIDTH; y = (viewportHeight / 2) % TILE_HEIGHT; } if (x != 0) x = (TILE_WIDTH - x) / 2; if (y != 0) y = (TILE_HEIGHT - y) / 2; *offsetX = x; *offsetY = y; } void TilesInView(int *rcolumns, int *rrows) { const uint16_t screenWidth = GetScreenWidth(); const uint16_t viewportHeight = GetViewportHeight(); int columns = screenWidth / TILE_WIDTH; if ((screenWidth % TILE_WIDTH) != 0) { columns++; } int rows = viewportHeight / TILE_HEIGHT; if ((viewportHeight % TILE_HEIGHT) != 0) { rows++; } if (*GetOptions().Graphics.zoom) { // Half the number of tiles, rounded up if ((columns & 1) != 0) { columns++; } columns /= 2; if ((rows & 1) != 0) { rows++; } rows /= 2; } *rcolumns = columns; *rrows = rows; } void CalcViewportGeometry() { const int zoomFactor = *GetOptions().Graphics.zoom ? 2 : 1; const int screenWidth = GetScreenWidth() / zoomFactor; const int screenHeight = GetScreenHeight() / zoomFactor; const int panelHeight = GetMainPanel().size.height / zoomFactor; const int pixelsToPanel = screenHeight - panelHeight; Point playerPosition { screenWidth / 2, pixelsToPanel / 2 }; if (*GetOptions().Graphics.zoom) playerPosition.y += TILE_HEIGHT / 4; const int tilesToTop = (playerPosition.y + TILE_HEIGHT - 1) / TILE_HEIGHT; const int tilesToLeft = (playerPosition.x + TILE_WIDTH - 1) / TILE_WIDTH; // Location of the center of the tile from which to start rendering, relative to the viewport origin Point startPosition = playerPosition - Displacement { tilesToLeft * TILE_WIDTH, tilesToTop * TILE_HEIGHT }; // Position of the tile from which to start rendering in tile space, // relative to the tile the player character occupies tileShift = { 0, 0 }; tileShift += Displacement(Direction::North) * tilesToTop; tileShift += Displacement(Direction::West) * tilesToLeft; // The rendering loop expects to start on a row with fewer columns if (tilesToLeft * TILE_WIDTH >= playerPosition.x) { startPosition += Displacement { TILE_WIDTH / 2, -TILE_HEIGHT / 2 }; tileShift += Displacement(Direction::NorthEast); } else if (tilesToTop * TILE_HEIGHT < playerPosition.y) { // There is one row above the current row that needs to be rendered, // but we skip to the row above it because it has too many columns startPosition += Displacement { 0, -TILE_HEIGHT }; tileShift += Displacement(Direction::North); } // Location of the bottom-left corner of the bounding box around the // tile from which to start rendering, relative to the viewport origin tileOffset = { startPosition.x - TILE_WIDTH / 2, startPosition.y + TILE_HEIGHT / 2 - 1 }; // Compute the number of rows to be rendered as well as // the number of columns to be rendered in the first row const int viewportHeight = GetViewportHeight() / zoomFactor; const Point renderStart = startPosition - Displacement { TILE_WIDTH / 2, TILE_HEIGHT / 2 }; tileRows = (viewportHeight - renderStart.y + TILE_HEIGHT / 2 - 1) / (TILE_HEIGHT / 2); tileColumns = (screenWidth - renderStart.x + TILE_WIDTH - 1) / TILE_WIDTH; } Point GetScreenPosition(Point tile) { Point firstTile = ViewPosition; Displacement offset = {}; CalcFirstTilePosition(firstTile, offset); const Displacement delta = firstTile - tile; Point position {}; position += delta.worldToScreen(); position += offset; return position; } extern SDL_Surface *PalSurface; void ClearScreenBuffer() { if (HeadlessMode) return; assert(PalSurface != nullptr); SDL_FillSurfaceRect(PalSurface, nullptr, 0); } #ifdef _DEBUG void ScrollView() { if (!MyPlayer->HoldItem.isEmpty()) return; if (MousePosition.x < 20) { if (dmaxPosition.y - 1 <= ViewPosition.y || dminPosition.x >= ViewPosition.x) { if (dmaxPosition.y - 1 > ViewPosition.y) { ViewPosition.y++; } if (dminPosition.x < ViewPosition.x) { ViewPosition.x--; } } else { ViewPosition.y++; ViewPosition.x--; } } if (MousePosition.x > gnScreenWidth - 20) { if (dmaxPosition.x - 1 <= ViewPosition.x || dminPosition.y >= ViewPosition.y) { if (dmaxPosition.x - 1 > ViewPosition.x) { ViewPosition.x++; } if (dminPosition.y < ViewPosition.y) { ViewPosition.y--; } } else { ViewPosition.y--; ViewPosition.x++; } } if (MousePosition.y < 20) { if (dminPosition.y >= ViewPosition.y || dminPosition.x >= ViewPosition.x) { if (dminPosition.y < ViewPosition.y) { ViewPosition.y--; } if (dminPosition.x < ViewPosition.x) { ViewPosition.x--; } } else { ViewPosition.x--; ViewPosition.y--; } } if (MousePosition.y > gnScreenHeight - 20) { if (dmaxPosition.y - 1 <= ViewPosition.y || dmaxPosition.x - 1 <= ViewPosition.x) { if (dmaxPosition.y - 1 > ViewPosition.y) { ViewPosition.y++; } if (dmaxPosition.x - 1 > ViewPosition.x) { ViewPosition.x++; } } else { ViewPosition.x++; ViewPosition.y++; } } } #endif void EnableFrameCount() { frameflag = true; lastFpsUpdateInMs = SDL_GetTicks(); } void scrollrt_draw_game_screen() { if (HeadlessMode) return; int hgt = 0; if (IsRedrawEverything()) { RedrawComplete(); hgt = gnScreenHeight; } const Surface &out = GlobalBackBuffer(); UndrawCursor(out); DrawCursor(out); DrawMain(hgt, false, false, false, false, false); RenderPresent(); } void DrawAndBlit() { if (!gbRunGame || HeadlessMode) { return; } int hgt = 0; bool drawHealth = IsRedrawComponent(PanelDrawComponent::Health); bool drawMana = IsRedrawComponent(PanelDrawComponent::Mana); bool drawControlButtons = IsRedrawComponent(PanelDrawComponent::ControlButtons); bool drawBelt = IsRedrawComponent(PanelDrawComponent::Belt); const bool drawChatInput = ChatFlag; bool drawInfoBox = false; bool drawCtrlPan = false; const Rectangle &mainPanel = GetMainPanel(); if (gnScreenWidth > mainPanel.size.width || IsRedrawEverything()) { drawHealth = true; drawMana = true; drawControlButtons = true; drawBelt = true; drawInfoBox = false; drawCtrlPan = true; hgt = gnScreenHeight; } else if (IsRedrawViewport()) { drawInfoBox = true; drawCtrlPan = false; hgt = gnViewportHeight; } const Surface &out = GlobalBackBuffer(); UndrawCursor(out); nthread_UpdateProgressToNextGameTick(); DrawView(out, ViewPosition); if (drawCtrlPan) { DrawMainPanel(out); } if (drawHealth) { DrawLifeFlaskLower(out, !drawCtrlPan); } if (drawMana) { DrawManaFlaskLower(out, !drawCtrlPan); DrawSpell(out); } if (drawControlButtons) { DrawMainPanelButtons(out); } if (drawBelt) { DrawInvBelt(out); } if (drawChatInput) { DrawChatBox(out); } DrawXPBar(out); if (*GetOptions().Gameplay.showHealthValues) DrawFlaskValues(out, { mainPanel.position.x + 134, mainPanel.position.y + 28 }, MyPlayer->_pHitPoints >> 6, MyPlayer->_pMaxHP >> 6); if (*GetOptions().Gameplay.showManaValues) DrawFlaskValues(out, { mainPanel.position.x + mainPanel.size.width - 138, mainPanel.position.y + 28 }, (HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) || MyPlayer->hasNoMana()) ? 0 : MyPlayer->_pMana >> 6, HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) ? 0 : MyPlayer->_pMaxMana >> 6); if (*GetOptions().Gameplay.floatingInfoBox) DrawFloatingInfoBox(out); if (*GetOptions().Gameplay.showMultiplayerPartyInfo && PartySidePanelOpen) DrawPartyMemberInfoPanel(out); DrawCursor(out); DrawFPS(out); lua::GameDrawComplete(); DrawMain(hgt, drawInfoBox, drawHealth, drawMana, drawBelt, drawControlButtons); #ifdef _DEBUG DrawConsole(out); #endif RedrawComplete(); for (const PanelDrawComponent component : enum_values()) { if (IsRedrawComponent(component)) { RedrawComponentComplete(component); } } RenderPresent(); } } // namespace devilution ================================================ FILE: Source/engine/render/scrollrt.h ================================================ /** * @file scrollrt.h * * Interface of functionality for rendering the dungeons, monsters and calling other render routines. */ #pragma once #include "engine/animationinfo.h" #include "engine/direction.hpp" #include "engine/displacement.hpp" #include "engine/point.hpp" #include "engine/surface.hpp" namespace devilution { extern bool AutoMapShowItems; extern bool frameflag; /** * @brief Returns the offset for the walking animation * @param animationInfo the current active walking animation * @param dir walking direction * @param cameraMode Adjusts the offset relative to the camera */ Displacement GetOffsetForWalking(const AnimationInfo &animationInfo, Direction dir, bool cameraMode = false); /** * @brief Clear cursor state */ void ClearCursor(); /** * @brief Shifting the view area along the logical grid * Note: this won't allow you to shift between even and odd rows * @param horizontal Shift the screen left or right * @param vertical Shift the screen up or down */ void ShiftGrid(Point *offset, int horizontal, int vertical); /** * @brief Gets the number of rows covered by the main panel */ int RowsCoveredByPanel(); /** * @brief Calculate the offset needed for centering tiles in view area * @param offsetX Offset in pixels * @param offsetY Offset in pixels */ void CalcTileOffset(int *offsetX, int *offsetY); /** * @brief Calculate the needed diamond tile to cover the view area * @param columns Tiles needed per row * @param rows Both even and odd rows */ void TilesInView(int *columns, int *rows); void CalcViewportGeometry(); /** * @brief Calculate the screen position of a given tile * @param tile Position of a dungeon tile */ Point GetScreenPosition(Point tile); /** * @brief Render the whole screen black */ void ClearScreenBuffer(); #ifdef _DEBUG /** * @brief Scroll the screen when mouse is close to the edge */ void ScrollView(); #endif /** * @brief Initialize the FPS meter */ void EnableFrameCount(); /** * @brief Redraw screen */ void scrollrt_draw_game_screen(); /** * @brief Render the game */ void DrawAndBlit(); } // namespace devilution ================================================ FILE: Source/engine/render/text_render.cpp ================================================ /** * @file text_render.cpp * * Text rendering. */ #include "text_render.hpp" #include #include #include #include #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_clx.hpp" #include "engine/load_file.hpp" #include "engine/load_pcx.hpp" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/surface.hpp" #include "engine/ticks.hpp" #include "game_mode.hpp" #include "utils/algorithm/container.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" namespace devilution { OptionalOwnedClxSpriteList pSPentSpn2Cels; namespace { constexpr char32_t ZWSP = U'\u200B'; // Zero-width space struct OwnedFontStack { OptionalOwnedClxSpriteList baseFont; OptionalOwnedClxSpriteList overrideFont; }; struct FontStack { OptionalClxSpriteList baseFont; OptionalClxSpriteList overrideFont; FontStack() = default; explicit FontStack(const OwnedFontStack &owned) { if (owned.baseFont.has_value()) baseFont.emplace(*owned.baseFont); if (owned.overrideFont.has_value()) overrideFont.emplace(*owned.overrideFont); } [[nodiscard]] bool has_value() const // NOLINT(readability-identifier-naming) { return baseFont.has_value() || overrideFont.has_value(); } [[nodiscard]] ClxSprite glyph(size_t i) const { if (overrideFont.has_value()) { ClxSprite overrideGlyph = (*overrideFont)[i]; if (overrideGlyph.width() != 0) return overrideGlyph; } return (*baseFont)[i]; } }; ankerl::unordered_dense::map Fonts; std::array FontSizes = { 12, 24, 30, 42, 46, 22 }; constexpr std::array LineHeights = { 12, 26, 38, 42, 50, 22 }; constexpr int SmallFontTallLineHeight = 16; std::array BaseLineOffset = { -3, -2, -3, -6, -7, 3 }; std::array ColorTranslations = { "fonts\\goldui.trn", "fonts\\grayui.trn", "fonts\\golduis.trn", "fonts\\grayuis.trn", nullptr, // ColorDialogWhite nullptr, // ColorDialogRed "fonts\\yellow.trn", nullptr, "fonts\\black.trn", "fonts\\white.trn", "fonts\\whitegold.trn", "fonts\\red.trn", "fonts\\blue.trn", "fonts\\orange.trn", "fonts\\buttonface.trn", "fonts\\buttonpushed.trn", "fonts\\gamedialogwhite.trn", "fonts\\gamedialogyellow.trn", "fonts\\gamedialogred.trn", }; std::array>, 19> ColorTranslationsData; text_color GetColorFromFlags(UiFlags flags) { if (HasAnyOf(flags, UiFlags::ColorWhite)) return ColorWhite; if (HasAnyOf(flags, UiFlags::ColorBlue)) return ColorBlue; if (HasAnyOf(flags, UiFlags::ColorOrange)) return ColorOrange; if (HasAnyOf(flags, UiFlags::ColorRed)) return ColorRed; if (HasAnyOf(flags, UiFlags::ColorBlack)) return ColorBlack; if (HasAnyOf(flags, UiFlags::ColorGold)) return ColorGold; if (HasAnyOf(flags, UiFlags::ColorUiGold)) return ColorUiGold; if (HasAnyOf(flags, UiFlags::ColorUiSilver)) return ColorUiSilver; if (HasAnyOf(flags, UiFlags::ColorUiGoldDark)) return ColorUiGoldDark; if (HasAnyOf(flags, UiFlags::ColorUiSilverDark)) return ColorUiSilverDark; if (HasAnyOf(flags, UiFlags::ColorDialogWhite)) return gbRunGame ? ColorInGameDialogWhite : ColorDialogWhite; if (HasAnyOf(flags, UiFlags::ColorDialogYellow)) return ColorInGameDialogYellow; if (HasAnyOf(flags, UiFlags::ColorDialogRed)) return ColorInGameDialogRed; if (HasAnyOf(flags, UiFlags::ColorYellow)) return ColorYellow; if (HasAnyOf(flags, UiFlags::ColorButtonface)) return ColorButtonface; if (HasAnyOf(flags, UiFlags::ColorButtonpushed)) return ColorButtonpushed; return ColorWhitegold; } uint16_t GetUnicodeRow(char32_t codePoint) { return static_cast(codePoint) >> 8; } bool IsCJK(uint16_t row) { return row >= 0x30 && row <= 0x9f; } bool IsHangul(uint16_t row) { return row >= 0xac && row <= 0xd7; } bool IsSmallFontTallRow(uint16_t row) { return IsCJK(row) || IsHangul(row); } void GetFontPath(GameFontTables size, uint16_t row, std::string_view ext, char *out) { *BufCopy(out, "fonts\\", FontSizes[size], "-", AsHexPad2(row), ext) = '\0'; } void GetFontPath(std::string_view language_code, GameFontTables size, uint16_t row, std::string_view ext, char *out) { *BufCopy(out, "fonts\\", language_code, "\\", FontSizes[size], "-", AsHexPad2(row), ext) = '\0'; } uint32_t GetFontId(GameFontTables size, uint16_t row) { return (size << 16) | row; } FontStack LoadFont(GameFontTables size, text_color color, uint16_t row) { if (ColorTranslations[color] != nullptr && !ColorTranslationsData[color]) { ColorTranslationsData[color].emplace(); LoadFileInMem(ColorTranslations[color], *ColorTranslationsData[color]); } const uint32_t fontId = GetFontId(size, row); auto hotFont = Fonts.find(fontId); if (hotFont != Fonts.end()) { return FontStack(hotFont->second); } OwnedFontStack &font = Fonts[fontId]; char path[32]; // Load language-specific glyphs: const std::string_view languageCode = GetLanguageCode(); const std::string_view lang = languageCode.substr(0, 2); if (lang == "zh" || lang == "ja" || lang == "ko" || (lang == "tr" && row == 0)) { GetFontPath(languageCode, size, row, ".clx", &path[0]); font.overrideFont = LoadOptionalClx(path); } // Load the base glyphs: GetFontPath(size, row, ".clx", &path[0]); font.baseFont = LoadOptionalClx(path); #ifndef UNPACKED_MPQS if (!font.baseFont.has_value()) { // Could be an old devilutionx.mpq or fonts.mpq with PCX instead of CLX. // // We'll show an error elsewhere (in `CheckArchivesUpToDate`) and we need to load // the font files to display it. char pcxPath[32]; GetFontPath(size, row, "", &pcxPath[0]); font.baseFont = LoadPcxSpriteList(pcxPath, /*numFramesOrFrameHeight=*/256, /*transparentColor=*/1); } #endif if (!font.baseFont.has_value()) { LogError("Error loading font: {}", path); } return FontStack(font); } class CurrentFont { public: FontStack fontStack; [[nodiscard]] ClxSprite glyph(size_t i) const { return fontStack.glyph(i); } bool load(GameFontTables size, text_color color, char32_t next) { const uint32_t unicodeRow = GetUnicodeRow(next); if (unicodeRow == currentUnicodeRow_ && hasAttemptedLoad_) { return true; } fontStack = LoadFont(size, color, unicodeRow); hasAttemptedLoad_ = true; currentUnicodeRow_ = unicodeRow; return fontStack.has_value(); } void clear() { hasAttemptedLoad_ = false; } private: bool hasAttemptedLoad_ = false; uint32_t currentUnicodeRow_ = 0; }; void DrawFont(const Surface &out, Point position, ClxSprite glyph, text_color color, bool outline) { if (outline) { ClxDrawOutlineSkipColorZero(out, 0, { position.x, position.y + glyph.height() - 1 }, glyph); } if (ColorTranslationsData[color]) { RenderClxSpriteWithTRN(out, glyph, position, ColorTranslationsData[color]->data()); } else { RenderClxSprite(out, glyph, position); } } bool IsFullWidthPunct(char32_t c) { return IsAnyOf(c, U',', U'、', U'。', U'?', U'!'); } bool IsBreakAllowed(char32_t codepoint, char32_t nextCodepoint) { return IsFullWidthPunct(codepoint) && !IsFullWidthPunct(nextCodepoint); } std::size_t CountNewlines(std::string_view fmt, const DrawStringFormatArg *args, std::size_t argsLen) { std::size_t result = c_count(fmt, '\n'); for (std::size_t i = 0; i < argsLen; ++i) { if (std::holds_alternative(args[i].value())) result += c_count(args[i].GetFormatted(), '\n'); } return result; } class FmtArgParser { public: FmtArgParser(std::string_view fmt, DrawStringFormatArg *args, size_t len, size_t offset = 0) : fmt_(fmt) , args_(args) , len_(len) , next_(offset) { } std::optional operator()(std::string_view &rest) { std::optional result; if (rest[0] != '{') return result; const std::size_t closingBracePos = rest.find('}', 1); if (closingBracePos == std::string_view::npos) { LogError("Unclosed format argument: {}", fmt_); return result; } std::size_t fmtLen; bool positional; if (closingBracePos == 2 && rest[1] >= '0' && rest[1] <= '9') { result = rest[1] - '0'; fmtLen = 3; positional = true; } else { result = next_++; fmtLen = closingBracePos + 1; positional = false; } if (!result) { LogError("Unsupported format argument: {}", rest); } else if (*result >= len_) { LogError("Not enough format arguments, {} given for: {}", len_, fmt_); result = std::nullopt; } else { if (!args_[*result].HasFormatted()) { const auto fmtStr = positional ? "{}" : std::string_view(rest.data(), fmtLen); args_[*result].SetFormatted(fmt::format(fmt::runtime(fmtStr), std::get(args_[*result].value()))); } rest.remove_prefix(fmtLen); } return result; } size_t offset() const { return next_; } private: std::string_view fmt_; DrawStringFormatArg *args_; std::size_t len_; std::size_t next_; }; bool ContainsSmallFontTallCodepoints(std::string_view text) { while (!text.empty()) { const char32_t next = ConsumeFirstUtf8CodePoint(&text); if (next == Utf8DecodeError) break; if (next == ZWSP) continue; if (IsSmallFontTallRow(GetUnicodeRow(next))) return true; } return false; } int GetLineHeight(std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, GameFontTables fontIndex) { constexpr std::array LineHeights = { 12, 26, 38, 42, 50, 22 }; if (fontIndex == GameFont12 && IsSmallFontTall()) { FmtArgParser fmtArgParser { fmt, args, argsLen }; std::string_view rest = fmt; while (!rest.empty()) { const std::optional fmtArgPos = fmtArgParser(rest); if (fmtArgPos) { if (ContainsSmallFontTallCodepoints(args[*fmtArgPos].GetFormatted())) { return SmallFontTallLineHeight; } continue; } const char32_t cp = ConsumeFirstUtf8CodePoint(&rest); if (cp == Utf8DecodeError) break; if (cp == ZWSP) continue; if (IsSmallFontTallRow(GetUnicodeRow(cp))) return SmallFontTallLineHeight; } } return LineHeights[fontIndex]; } Surface ClipSurface(const Surface &out, Rectangle rect) { if (rect.size.height == 0) { return out.subregion(0, 0, std::min(rect.position.x + rect.size.width, out.w()), out.h()); } return out.subregion(0, 0, std::min(rect.position.x + rect.size.width, out.w()), std::min(rect.position.y + rect.size.height, out.h())); } int AdjustSpacingToFitHorizontally(int &lineWidth, int maxSpacing, int charactersInLine, int availableWidth) { if (lineWidth <= availableWidth || charactersInLine < 2) return maxSpacing; const int overhang = lineWidth - availableWidth; const int spacingRedux = (overhang + charactersInLine - 2) / (charactersInLine - 1); lineWidth -= spacingRedux * (charactersInLine - 1); return maxSpacing - spacingRedux; } void MaybeWrap(Point &characterPosition, int characterWidth, int rightMargin, int initialX, int lineHeight) { if (characterPosition.x + characterWidth > rightMargin) { characterPosition.x = initialX; characterPosition.y += lineHeight; } } int GetLineStartX(UiFlags flags, const Rectangle &rect, int lineWidth) { if (HasAnyOf(flags, UiFlags::AlignCenter)) { return std::max(rect.position.x, rect.position.x + (rect.size.width - lineWidth) / 2); } if (HasAnyOf(flags, UiFlags::AlignRight)) return rect.position.x + rect.size.width - lineWidth; return rect.position.x; } void DrawLine( const Surface &out, std::string_view text, Point characterPosition, Rectangle rect, UiFlags flags, int curSpacing, GameFontTables size, text_color color, bool outline, const TextRenderOptions &opts, size_t lineStartPos, int totalWidth) { CurrentFont currentFont; std::string_view lineCopy = text; size_t currentPos = 0; size_t cpLen; const auto maybeDrawCursor = [&]() { const auto byteIndex = static_cast(lineStartPos + currentPos); Point position = characterPosition; if (opts.cursorPosition == byteIndex) { if (GetAnimationFrame(2, 500) != 0 || opts.cursorStatic) { FontStack baseFont = LoadFont(size, color, 0); if (baseFont.has_value()) { DrawFont(out, position, baseFont.glyph('|'), color, outline); } } if (opts.renderedCursorPositionOut != nullptr) { *opts.renderedCursorPositionOut = position; } } }; // Start from the beginning of the line characterPosition.x = GetLineStartX(flags, rect, totalWidth); while (!lineCopy.empty()) { char32_t c = DecodeFirstUtf8CodePoint(lineCopy, &cpLen); if (c == Utf8DecodeError) break; if (c == ZWSP) { currentPos += cpLen; lineCopy.remove_prefix(cpLen); continue; } if (!currentFont.load(size, color, c)) { c = U'?'; if (!currentFont.load(size, color, c)) { app_fatal("Missing fonts"); } } const uint8_t frame = c & 0xFF; const ClxSprite glyph = currentFont.glyph(frame); const int charWidth = glyph.width(); const auto byteIndex = static_cast(lineStartPos + currentPos); // Draw highlight if (byteIndex >= opts.highlightRange.begin && byteIndex < opts.highlightRange.end) { const bool lastInRange = static_cast(byteIndex + cpLen) == opts.highlightRange.end; FillRect(out, characterPosition.x, characterPosition.y, glyph.width() + (lastInRange ? 0 : curSpacing), glyph.height(), opts.highlightColor); } DrawFont(out, characterPosition, glyph, color, outline); maybeDrawCursor(); // Move to the next position characterPosition.x += charWidth + curSpacing; currentPos += cpLen; lineCopy.remove_prefix(cpLen); } assert(currentPos == text.size()); maybeDrawCursor(); } uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point &characterPosition, int lineWidth, int charactersInLine, int rightMargin, int bottomMargin, GameFontTables size, text_color color, bool outline, TextRenderOptions &opts) { CurrentFont currentFont; int curSpacing = opts.spacing; if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) { curSpacing = AdjustSpacingToFitHorizontally(lineWidth, opts.spacing, charactersInLine, rect.size.width); if (curSpacing != opts.spacing && HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) { const int adjustedLineWidth = GetLineWidth(text, size, curSpacing, &charactersInLine); characterPosition.x = GetLineStartX(opts.flags, rect, adjustedLineWidth); } } char32_t next; std::string_view remaining = text; size_t cpLen; // Track line boundaries size_t lineStartPos = 0; size_t lineEndPos = 0; const auto drawLine = [&]() { std::string_view lineText = text.substr(lineStartPos, lineEndPos - lineStartPos); if (!lineText.empty()) { DrawLine( out, lineText, characterPosition, rect, opts.flags, curSpacing, size, color, outline, opts, lineStartPos, lineWidth); } }; for (; !remaining.empty() && remaining[0] != '\0' && (next = DecodeFirstUtf8CodePoint(remaining, &cpLen)) != Utf8DecodeError; remaining.remove_prefix(cpLen)) { if (next == ZWSP) continue; if (!currentFont.load(size, color, next)) { next = U'?'; if (!currentFont.load(size, color, next)) { app_fatal("Missing fonts"); } } const uint8_t frame = next & 0xFF; const uint16_t width = currentFont.glyph(frame).width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { lineEndPos = text.size() - remaining.size(); drawLine(); const int nextLineY = characterPosition.y + opts.lineHeight; if (nextLineY >= bottomMargin) break; characterPosition.y = nextLineY; if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) { int nextLineWidth = GetLineWidth(remaining.substr(cpLen), size, opts.spacing, &charactersInLine); curSpacing = AdjustSpacingToFitHorizontally(nextLineWidth, opts.spacing, charactersInLine, rect.size.width); } if (HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) { lineWidth = width; if (remaining.size() > cpLen) lineWidth += curSpacing + GetLineWidth(remaining.substr(cpLen), size, curSpacing); } characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth); // Start a new line lineStartPos = next == U'\n' ? (text.size() - remaining.size() + cpLen) : (text.size() - remaining.size()); lineEndPos = lineStartPos; if (next == U'\n') continue; } // Update end position as we add characters lineEndPos = text.size() - remaining.size() + cpLen; // Update position for the next character characterPosition.x += width + curSpacing; } // Draw any remaining characters in the last line if (lineStartPos < lineEndPos) { drawLine(); } return static_cast(remaining.data() - text.data()); } } // namespace void LoadSmallSelectionSpinner() { pSPentSpn2Cels = LoadCel("data\\pentspn2", 12); } void UnloadFonts() { Fonts.clear(); } int GetLineWidth(std::string_view text, GameFontTables size, int spacing, int *charactersInLine) { int lineWidth = 0; CurrentFont currentFont; uint32_t codepoints = 0; char32_t next; while (!text.empty()) { next = ConsumeFirstUtf8CodePoint(&text); if (next == Utf8DecodeError) break; if (next == ZWSP) continue; if (next == U'\n') break; if (!currentFont.load(size, text_color::ColorDialogWhite, next)) { next = U'?'; if (!currentFont.load(size, text_color::ColorDialogWhite, next)) { app_fatal("Missing fonts"); } } const uint8_t frame = next & 0xFF; lineWidth += currentFont.glyph(frame).width() + spacing; ++codepoints; } if (charactersInLine != nullptr) *charactersInLine = codepoints; return lineWidth != 0 ? (lineWidth - spacing) : 0; } bool IsConsumed(std::string_view s) { return s.empty() || s[0] == '\0'; }; int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine, std::optional firstArgOffset) { int lineWidth = 0; CurrentFont currentFont; uint32_t codepoints = 0; char32_t prev = U'\0'; char32_t next; std::string_view remaining = fmt; FmtArgParser fmtArgParser { fmt, args, argsLen, argsOffset }; size_t cpLen; // The current formatted argument value being processed. std::string_view curFormatted; // The string that we're currently processing: either `remaining` or `curFormatted`. std::string_view *str; if (firstArgOffset.has_value()) { curFormatted = args[argsOffset - 1].GetFormatted().substr(*firstArgOffset); } for (; !(IsConsumed(curFormatted) && IsConsumed(remaining)); str->remove_prefix(cpLen), prev = next) { const bool isProcessingFormatArgValue = !IsConsumed(curFormatted); str = isProcessingFormatArgValue ? &curFormatted : &remaining; next = DecodeFirstUtf8CodePoint(*str, &cpLen); if (next == Utf8DecodeError) break; // {{ and }} escapes in fmt. if (!isProcessingFormatArgValue && (prev == U'{' || prev == U'}') && prev == next) continue; // ZWSP are line-breaking opportunities that can otherwise be skipped for rendering as they have 0-width. if (next == ZWSP) continue; if (next == U'\n') break; if (!isProcessingFormatArgValue) { const std::optional fmtArgPos = fmtArgParser(*str); if (fmtArgPos.has_value()) { // `fmtArgParser` has already consumed `*str`. Ensure the loop doesn't consume any more. cpLen = 0; // The loop assigns `prev = next`. // We reset it to U'\0' to ensure that {{ and }} escapes are not processed accross // the boundary of the format string and a formatted value. next = U'\0'; currentFont.clear(); const DrawStringFormatArg &arg = args[*fmtArgPos]; curFormatted = arg.GetFormatted(); continue; } } if (!currentFont.load(size, text_color::ColorDialogWhite, next)) { next = U'?'; if (!currentFont.load(size, text_color::ColorDialogWhite, next)) { app_fatal("Missing fonts"); } } const uint8_t frame = next & 0xFF; lineWidth += currentFont.glyph(frame).width() + spacing; ++codepoints; } if (charactersInLine != nullptr) *charactersInLine = codepoints; return lineWidth != 0 ? (lineWidth - spacing) : 0; } int GetLineHeight(std::string_view text, GameFontTables fontIndex) { if (fontIndex == GameFont12 && IsSmallFontTall() && ContainsSmallFontTallCodepoints(text)) { return SmallFontTallLineHeight; } return LineHeights[fontIndex]; } std::string WordWrapString(std::string_view text, unsigned width, GameFontTables size, int spacing) { std::string output; if (text.empty() || text[0] == '\0') return output; output.reserve(text.size()); const char *begin = text.data(); const char *processedEnd = text.data(); std::string_view::size_type lastBreakablePos = std::string_view::npos; std::size_t lastBreakableLen = 0; unsigned lineWidth = 0; CurrentFont currentFont; char32_t codepoint = U'\0'; // the current codepoint char32_t nextCodepoint; // the next codepoint std::size_t nextCodepointLen; std::string_view remaining = text; nextCodepoint = DecodeFirstUtf8CodePoint(remaining, &nextCodepointLen); do { codepoint = nextCodepoint; const std::size_t codepointLen = nextCodepointLen; if (codepoint == Utf8DecodeError) break; remaining.remove_prefix(codepointLen); nextCodepoint = !remaining.empty() ? DecodeFirstUtf8CodePoint(remaining, &nextCodepointLen) : U'\0'; if (codepoint == U'\n') { // Existing line break, scan next line lastBreakablePos = std::string_view::npos; lineWidth = 0; output.append(processedEnd, remaining.data()); processedEnd = remaining.data(); continue; } if (codepoint != ZWSP) { const uint8_t frame = codepoint & 0xFF; if (!currentFont.load(size, text_color::ColorDialogWhite, codepoint)) { codepoint = U'?'; if (!currentFont.load(size, text_color::ColorDialogWhite, codepoint)) { app_fatal("Missing fonts"); } } lineWidth += currentFont.glyph(frame).width() + spacing; } if (IsBreakableWhitespace(codepoint)) { lastBreakablePos = remaining.data() - begin - codepointLen; lastBreakableLen = codepointLen; continue; } if (lineWidth - spacing <= width) { if (IsBreakAllowed(codepoint, nextCodepoint)) { lastBreakablePos = remaining.data() - begin; lastBreakableLen = 0; } continue; // String is still within the limit, continue to the next symbol } if (lastBreakablePos == std::string_view::npos) { // Single word longer than width lastBreakablePos = remaining.data() - begin - codepointLen; lastBreakableLen = 0; } // Break line and continue to next line const char *end = &text[lastBreakablePos]; output.append(processedEnd, end); output += '\n'; // Restart from the beginning of the new line. remaining = text.substr(lastBreakablePos + lastBreakableLen); processedEnd = remaining.data(); lastBreakablePos = std::string_view::npos; lineWidth = 0; nextCodepoint = !remaining.empty() ? DecodeFirstUtf8CodePoint(remaining, &nextCodepointLen) : U'\0'; } while (!remaining.empty() && remaining[0] != '\0'); output.append(processedEnd, remaining.data()); return output; } /** * @todo replace Rectangle with cropped Surface */ uint32_t DrawString(const Surface &out, std::string_view text, const Rectangle &rect, TextRenderOptions opts) { const GameFontTables size = GetFontSizeFromUiFlags(opts.flags); const text_color color = GetColorFromFlags(opts.flags); int charactersInLine = 0; int lineWidth = 0; if (HasAnyOf(opts.flags, (UiFlags::AlignCenter | UiFlags::AlignRight | UiFlags::KerningFitSpacing))) lineWidth = GetLineWidth(text, size, opts.spacing, &charactersInLine); Point characterPosition { GetLineStartX(opts.flags, rect, lineWidth), rect.position.y }; const int initialX = characterPosition.x; const int rightMargin = rect.position.x + rect.size.width; const int bottomMargin = rect.size.height != 0 ? std::min(rect.position.y + rect.size.height + BaseLineOffset[size], out.h()) : out.h(); if (opts.lineHeight == -1) opts.lineHeight = GetLineHeight(text, size); if (HasAnyOf(opts.flags, UiFlags::VerticalCenter)) { const int textHeight = static_cast((c_count(text, '\n') + 1) * opts.lineHeight); characterPosition.y += std::max(0, (rect.size.height - textHeight) / 2); } characterPosition.y += BaseLineOffset[size]; const bool outlined = HasAnyOf(opts.flags, UiFlags::Outlined); const Surface clippedOut = ClipSurface(out, rect); // Only draw the PentaCursor if the cursor is not at the end. if (HasAnyOf(opts.flags, UiFlags::PentaCursor) && static_cast(opts.cursorPosition) == text.size()) { opts.cursorPosition = -1; } const uint32_t bytesDrawn = DoDrawString(clippedOut, text, rect, characterPosition, lineWidth, charactersInLine, rightMargin, bottomMargin, size, color, outlined, opts); if (HasAnyOf(opts.flags, UiFlags::PentaCursor)) { const ClxSprite sprite = (*pSPentSpn2Cels)[PentSpn2Spin()]; MaybeWrap(characterPosition, sprite.width(), rightMargin, initialX, opts.lineHeight); ClxDraw(clippedOut, characterPosition + Displacement { 0, opts.lineHeight - BaseLineOffset[size] }, sprite); } return bytesDrawn; } void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, const Rectangle &rect, TextRenderOptions opts) { const GameFontTables size = GetFontSizeFromUiFlags(opts.flags); const text_color color = GetColorFromFlags(opts.flags); int charactersInLine = 0; int lineWidth = 0; if (HasAnyOf(opts.flags, (UiFlags::AlignCenter | UiFlags::AlignRight | UiFlags::KerningFitSpacing))) lineWidth = GetLineWidth(fmt, args, argsLen, 0, size, opts.spacing, &charactersInLine); Point characterPosition { GetLineStartX(opts.flags, rect, lineWidth), rect.position.y }; const int initialX = characterPosition.x; const int rightMargin = rect.position.x + rect.size.width; const int bottomMargin = rect.size.height != 0 ? std::min(rect.position.y + rect.size.height + BaseLineOffset[size], out.h()) : out.h(); if (opts.lineHeight == -1) opts.lineHeight = GetLineHeight(fmt, args, argsLen, size); if (HasAnyOf(opts.flags, UiFlags::VerticalCenter)) { const int textHeight = static_cast((CountNewlines(fmt, args, argsLen) + 1) * opts.lineHeight); characterPosition.y += std::max(0, (rect.size.height - textHeight) / 2); } characterPosition.y += BaseLineOffset[size]; const bool outlined = HasAnyOf(opts.flags, UiFlags::Outlined); const Surface clippedOut = ClipSurface(out, rect); CurrentFont currentFont; const int originalSpacing = opts.spacing; if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) { opts.spacing = AdjustSpacingToFitHorizontally(lineWidth, originalSpacing, charactersInLine, rect.size.width); if (opts.spacing != originalSpacing && HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) { const int adjustedLineWidth = GetLineWidth(fmt, args, argsLen, 0, size, opts.spacing, &charactersInLine); characterPosition.x = GetLineStartX(opts.flags, rect, adjustedLineWidth); } } char32_t prev = U'\0'; char32_t next; std::string_view remaining = fmt; FmtArgParser fmtArgParser { fmt, args, argsLen }; size_t cpLen; // The current formatted argument value being processed. std::string_view curFormatted; text_color curFormattedColor; // The string that we're currently processing: either `remaining` or `curFormatted`. std::string_view *str; for (; !(IsConsumed(curFormatted) && IsConsumed(remaining)); str->remove_prefix(cpLen), prev = next) { const bool isProcessingFormatArgValue = !IsConsumed(curFormatted); str = isProcessingFormatArgValue ? &curFormatted : &remaining; next = DecodeFirstUtf8CodePoint(*str, &cpLen); if (next == Utf8DecodeError) break; // {{ and }} escapes in fmt. if (!isProcessingFormatArgValue && (prev == U'{' || prev == U'}') && prev == next) continue; // ZWSP are line-breaking opportunities that can otherwise be skipped for rendering as they have 0-width. if (next == ZWSP) continue; if (!isProcessingFormatArgValue) { const std::optional fmtArgPos = fmtArgParser(*str); if (fmtArgPos.has_value()) { // `fmtArgParser` has already consumed `*str`. Ensure the loop doesn't consume any more. cpLen = 0; // The loop assigns `prev = next`. // We reset it to U'\0' to ensure that {{ and }} escapes are not processed accross // the boundary of the format string and a formatted value. next = U'\0'; currentFont.clear(); const DrawStringFormatArg &arg = args[*fmtArgPos]; curFormatted = arg.GetFormatted(); curFormattedColor = GetColorFromFlags(arg.GetFlags()); continue; } } const text_color curColor = isProcessingFormatArgValue ? curFormattedColor : color; if (!currentFont.load(size, curColor, next)) { next = U'?'; if (!currentFont.load(size, curColor, next)) { app_fatal("Missing fonts"); } } const uint8_t frame = next & 0xFF; const uint16_t width = currentFont.glyph(frame).width(); if (next == U'\n' || characterPosition.x + width > rightMargin) { const int nextLineY = characterPosition.y + opts.lineHeight; if (nextLineY >= bottomMargin) break; characterPosition.y = nextLineY; if (HasAnyOf(opts.flags, UiFlags::KerningFitSpacing)) { int nextLineWidth = isProcessingFormatArgValue ? GetLineWidth(remaining, args, argsLen, fmtArgParser.offset(), size, originalSpacing, &charactersInLine, /*firstArgOffset=*/args[fmtArgParser.offset() - 1].GetFormatted().size() - (curFormatted.size() - cpLen)) : GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, originalSpacing, &charactersInLine); opts.spacing = AdjustSpacingToFitHorizontally(nextLineWidth, originalSpacing, charactersInLine, rect.size.width); } if (HasAnyOf(opts.flags, UiFlags::AlignCenter | UiFlags::AlignRight)) { lineWidth = width; if (str->size() > cpLen) { lineWidth += opts.spacing + (isProcessingFormatArgValue ? GetLineWidth(remaining, args, argsLen, fmtArgParser.offset(), size, opts.spacing, &charactersInLine, /*firstArgOffset=*/args[fmtArgParser.offset() - 1].GetFormatted().size() - (curFormatted.size() - cpLen)) : GetLineWidth(remaining.substr(cpLen), args, argsLen, fmtArgParser.offset(), size, opts.spacing, &charactersInLine)); } } characterPosition.x = GetLineStartX(opts.flags, rect, lineWidth); if (next == U'\n') continue; } DrawFont(clippedOut, characterPosition, currentFont.glyph(frame), curColor, outlined); characterPosition.x += width + opts.spacing; } if (HasAnyOf(opts.flags, UiFlags::PentaCursor)) { const ClxSprite sprite = (*pSPentSpn2Cels)[PentSpn2Spin()]; MaybeWrap(characterPosition, sprite.width(), rightMargin, initialX, opts.lineHeight); ClxDraw(clippedOut, characterPosition + Displacement { 0, opts.lineHeight - BaseLineOffset[size] }, sprite); } } uint8_t PentSpn2Spin() { return GetAnimationFrame(8, 50); } bool IsBreakableWhitespace(char32_t c) { return IsAnyOf(c, U' ', U' ', ZWSP); } } // namespace devilution ================================================ FILE: Source/engine/render/text_render.hpp ================================================ /** * @file text_render.hpp * * Text rendering. */ #pragma once #include #include #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "engine/clx_sprite.hpp" #include "engine/palette.h" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/surface.hpp" #include "utils/enum_traits.h" namespace devilution { enum GameFontTables : uint8_t { GameFont12, GameFont24, GameFont30, GameFont42, GameFont46, FontSizeDialog, }; enum text_color : uint8_t { ColorUiGold, ColorUiSilver, ColorUiGoldDark, ColorUiSilverDark, ColorDialogWhite, // Dialog white in main menu ColorDialogRed, ColorYellow, ColorGold, ColorBlack, ColorWhite, ColorWhitegold, ColorRed, ColorBlue, ColorOrange, ColorButtonface, ColorButtonpushed, ColorInGameDialogWhite, // Dialog white in-game ColorInGameDialogYellow, // Dialog yellow in-game ColorInGameDialogRed, // Dialog red in-game }; constexpr GameFontTables GetFontSizeFromUiFlags(UiFlags flags) { if (HasAnyOf(flags, UiFlags::FontSize24)) return GameFont24; if (HasAnyOf(flags, UiFlags::FontSize30)) return GameFont30; if (HasAnyOf(flags, UiFlags::FontSize42)) return GameFont42; if (HasAnyOf(flags, UiFlags::FontSize46)) return GameFont46; if (HasAnyOf(flags, UiFlags::FontSizeDialog)) return FontSizeDialog; return GameFont12; } /** * @brief A format argument for `DrawStringWithColors`. */ class DrawStringFormatArg { public: using Value = std::variant; DrawStringFormatArg(std::string_view value, UiFlags flags) : value_(value) , flags_(flags) { } DrawStringFormatArg(int value, UiFlags flags) : value_(value) , flags_(flags) { } std::string_view GetFormatted() const { if (std::holds_alternative(value_)) return std::get(value_); return formatted_; } void SetFormatted(std::string &&value) { formatted_ = std::move(value); } bool HasFormatted() const { return std::holds_alternative(value_) || !formatted_.empty(); } const Value &value() const { return value_; } UiFlags GetFlags() const { return flags_; } private: Value value_; UiFlags flags_; std::string formatted_; }; /** @brief Text rendering options. */ struct TextRenderOptions { /** @brief A combination of UiFlags to describe font size, color, alignment, etc. See ui_items.h for available options */ UiFlags flags = UiFlags::None; /** * @brief Additional space to add between characters. * * This value may be adjusted if the flag UiFlags::KerningFitSpacing is set. */ int spacing = 1; /** @brief Allows overriding the default line height, useful for multi-line strings. */ int lineHeight = -1; /** @brief If non-negative, draws a blinking cursor after the given byte index.*/ int cursorPosition = -1; /** @brief Highlight text background in this range. */ struct { int begin; int end; } highlightRange = { 0, 0 }; uint8_t highlightColor = PAL8_RED + 6; /** @brief If a cursor is rendered, the surface coordinates are saved here. */ std::optional *renderedCursorPositionOut = nullptr; bool cursorStatic = false; }; /** * @brief Small text selection cursor. * * Also used in the stores and the quest log. */ extern OptionalOwnedClxSpriteList pSPentSpn2Cels; void LoadSmallSelectionSpinner(); /** * @brief Calculate pixel width of first line of text, respecting kerning * @param text Text to check, will read until first eol or terminator * @param size Font size to use * @param spacing Extra spacing to add per character * @param charactersInLine Receives characters read until newline or terminator * @return Line width in pixels */ int GetLineWidth(std::string_view text, GameFontTables size = GameFont12, int spacing = 1, int *charactersInLine = nullptr); /** * @brief Calculate pixel width of first line of text, respecting kerning * @param fmt An fmt::format string. * @param args Format arguments. * @param argsLen Number of format arguments. * @param argsOffset Index of the first unprocessed format argument. * @param size Font size to use * @param spacing Extra spacing to add per character * @param charactersInLine Receives characters read until newline or terminator * @param firstArgOffset If given, starts counting at `args[argsOffset - 1].GetFormatted().substr(*firstArgOffset)`. * @return Line width in pixels */ int GetLineWidth(std::string_view fmt, DrawStringFormatArg *args, size_t argsLen, size_t argsOffset, GameFontTables size, int spacing, int *charactersInLine = nullptr, std::optional firstArgOffset = std::nullopt); int GetLineHeight(std::string_view text, GameFontTables fontIndex); /** * @brief Builds a multi-line version of the given text so it'll fit within the given width. * * This function will not break words, if the given width is smaller than the width of the longest word in the given * font then it will likely overflow the output region. * * @param text Source text * @param width Width in pixels of the output region * @param size Font size to use for the width calculation * @param spacing Any adjustment to apply between each character * @return A copy of the source text with newlines inserted where appropriate */ [[nodiscard]] std::string WordWrapString(std::string_view text, unsigned width, GameFontTables size = GameFont12, int spacing = 1); /** * @brief Draws a line of text within a clipping rectangle (positioned relative to the origin of the output buffer). * * Specifying a small width (0 to less than two characters wide) should be avoided as this causes issues when laying * out the text. To wrap based on available space use the overload taking a Point. If the rect passed through has 0 * height then the clipping area is extended to the bottom edge of the output buffer. If the clipping rectangle * dimensions extend past the edge of the output buffer text wrapping will be calculated using those dimensions (as if * the text was being rendered off screen). The text will not actually be drawn beyond the bounds of the output * buffer, this is purely to allow for clipping without wrapping. * * @param out The screen buffer to draw on. * @param text String to be drawn. * @param rect Clipping region relative to the output buffer describing where to draw the text and when to wrap long lines. * @param opts Rendering options. * @return The number of bytes rendered, including characters "drawn" outside the buffer. */ uint32_t DrawString(const Surface &out, std::string_view text, const Rectangle &rect, TextRenderOptions opts = {}); /** * @brief Draws a line of text at the given position relative to the origin of the output buffer. * * This method is provided as a convenience to pass through to DrawString(..., Rectangle, ...) when no explicit * clipping/wrapping is requested. Note that this will still wrap the rendered string if it would end up being drawn * beyond the right edge of the output buffer and clip it if it would extend beyond the bottom edge of the buffer. * * @param out The screen buffer to draw on. * @param text String to be drawn. * @param position Location of the top left corner of the string relative to the top left corner of the output buffer. * @param opts Rendering options. */ inline void DrawString(const Surface &out, std::string_view text, const Point &position, TextRenderOptions opts = {}) { DrawString(out, text, { position, { out.w() - position.x, 0 } }, opts); } /** * @brief Draws a line of text with different colors for certain parts of the text. * * DrawStringWithColors(out, "Press {} to start", {{"Ⓧ", UiFlags::ColorBlue}}, {.flags = UiFlags::ColorWhite}) * * @param out Output buffer to draw the text on. * @param fmt An fmt::format string. * @param args Format arguments. * @param argsLen Number of format arguments. * @param rect Clipping region relative to the output buffer describing where to draw the text and when to wrap long lines. * @param opts Rendering options. */ void DrawStringWithColors(const Surface &out, std::string_view fmt, DrawStringFormatArg *args, std::size_t argsLen, const Rectangle &rect, TextRenderOptions opts = {}); inline void DrawStringWithColors(const Surface &out, std::string_view fmt, std::vector args, const Rectangle &rect, TextRenderOptions opts = {}) { return DrawStringWithColors(out, fmt, args.data(), args.size(), rect, opts); } uint8_t PentSpn2Spin(); void UnloadFonts(); /** @brief Whether this character can be substituted by a newline when word-wrapping. */ bool IsBreakableWhitespace(char32_t c); } // namespace devilution ================================================ FILE: Source/engine/size.hpp ================================================ #pragma once #include "utils/attributes.h" #ifdef BUILD_TESTING #include #endif namespace devilution { template struct SizeOf { SizeT width; SizeT height; SizeOf() = default; DVL_ALWAYS_INLINE constexpr SizeOf(SizeT width, SizeT height) : width(width) , height(height) { } DVL_ALWAYS_INLINE explicit constexpr SizeOf(SizeT size) : width(size) , height(size) { } DVL_ALWAYS_INLINE bool operator==(const SizeOf &other) const { return width == other.width && height == other.height; } DVL_ALWAYS_INLINE bool operator!=(const SizeOf &other) const { return !(*this == other); } DVL_ALWAYS_INLINE constexpr SizeOf &operator+=(SizeT factor) { width += factor; height += factor; return *this; } DVL_ALWAYS_INLINE constexpr SizeOf &operator-=(SizeT factor) { return *this += -factor; } DVL_ALWAYS_INLINE constexpr SizeOf &operator*=(SizeT factor) { width *= factor; height *= factor; return *this; } DVL_ALWAYS_INLINE constexpr SizeOf &operator*=(float factor) { width = static_cast(width * factor); height = static_cast(height * factor); return *this; } DVL_ALWAYS_INLINE constexpr SizeOf &operator/=(SizeT factor) { width /= factor; height /= factor; return *this; } DVL_ALWAYS_INLINE constexpr friend SizeOf operator+(SizeOf a, SizeT factor) { a += factor; return a; } DVL_ALWAYS_INLINE constexpr friend SizeOf operator-(SizeOf a, SizeT factor) { a -= factor; return a; } DVL_ALWAYS_INLINE constexpr friend SizeOf operator*(SizeOf a, SizeT factor) { a *= factor; return a; } DVL_ALWAYS_INLINE constexpr friend SizeOf operator/(SizeOf a, SizeT factor) { a /= factor; return a; } #ifdef BUILD_TESTING /** * @brief Format sizes nicely in test failure messages * @param stream output stream, expected to have overloads for int and char* * @param size Object to display * @return the stream, to allow chaining */ friend std::ostream &operator<<(std::ostream &stream, const SizeOf &size) { return stream << "(width: " << size.width << ", height: " << size.height << ")"; } #endif }; using Size = SizeOf; } // namespace devilution ================================================ FILE: Source/engine/sound.cpp ================================================ /** * @file sound.cpp * * Implementation of functions setting up the audio pipeline. */ #include "engine/sound.h" #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #include #endif #include #include "appfat.h" #include "engine/assets.hpp" #include "game_mode.hpp" #include "options.h" #include "utils/log.hpp" #include "utils/math.h" #include "utils/sdl_mutex.h" #include "utils/status_macros.hpp" #include "utils/stdcompat/shared_ptr_array.hpp" #include "utils/str_cat.hpp" #include "utils/stubs.h" namespace devilution { bool gbSndInited; #ifdef USE_SDL3 SDL_AudioDeviceID CurrentAudioDeviceId; #endif /** The active background music track id. */ _music_id sgnMusicTrack = NUM_MUSIC; bool gbMusicOn = true; /** Specifies whether sound effects are enabled. */ bool gbSoundOn = true; namespace { SoundSample music; std::string GetMp3Path(const char *path) { std::string mp3Path = path; const std::string::size_type dot = mp3Path.find_last_of('.'); mp3Path.replace(dot + 1, mp3Path.size() - (dot + 1), "mp3"); return mp3Path; } tl::expected LoadAudioFile(const char *path, bool stream, SoundSample &result) { bool isMp3 = true; std::string foundPath = GetMp3Path(path); AssetRef ref = FindAsset(foundPath.c_str()); if (!ref.ok()) { ref = FindAsset(path); foundPath = path; isMp3 = false; } if (!ref.ok()) { return tl::make_unexpected(StrCat("Audio file not found\n", path, "\n", SDL_GetError(), "\n" __FILE__ ":", __LINE__)); } #ifdef STREAM_ALL_AUDIO_MIN_FILE_SIZE #if STREAM_ALL_AUDIO_MIN_FILE_SIZE == 0 stream = true; #else size_t size; if (!stream) { size = ref.size(); stream = size >= STREAM_ALL_AUDIO_MIN_FILE_SIZE; } #endif #endif if (stream) { if (result.SetChunkStream(foundPath, isMp3, /*logErrors=*/true) != 0) { return tl::make_unexpected(StrCat("Failed to load audio file\n", foundPath, "\n", SDL_GetError(), "\n" __FILE__ ":", __LINE__)); } } else { #if !defined(STREAM_ALL_AUDIO_MIN_FILE_SIZE) || STREAM_ALL_AUDIO_MIN_FILE_SIZE == 0 const size_t size = ref.size(); #endif AssetHandle handle = OpenAsset(std::move(ref)); if (!handle.ok()) { return tl::make_unexpected(StrCat("Failed to load audio file\n", foundPath, "\n", SDL_GetError(), "\n" __FILE__ ":", __LINE__)); } auto waveFile = MakeArraySharedPtr(size); if (!handle.read(waveFile.get(), size)) { return tl::make_unexpected(StrCat("Failed to read file\n", foundPath, ": ", SDL_GetError(), __FILE__ ":", __LINE__)); } const int error = result.SetChunk(waveFile, size, isMp3); if (error != 0) { return tl::make_unexpected(SDL_GetError()); } } return {}; } std::list> duplicateSounds; std::optional duplicateSoundsMutex; SoundSample *DuplicateSound(const SoundSample &sound) { #ifdef USE_SDL3 return nullptr; #else auto duplicate = std::make_unique(); if (duplicate->DuplicateFrom(sound) != 0) return nullptr; auto *result = duplicate.get(); decltype(duplicateSounds.begin()) it; { const std::lock_guard lock(*duplicateSoundsMutex); duplicateSounds.push_back(std::move(duplicate)); it = duplicateSounds.end(); --it; } result->SetFinishCallback([it]([[maybe_unused]] Aulib::Stream &stream) { const std::lock_guard lock(*duplicateSoundsMutex); duplicateSounds.erase(it); }); return result; #endif } /** Maps from track ID to track name in spawn. */ const char *const SpawnMusicTracks[NUM_MUSIC] = { "music\\stowne.wav", "music\\slvla.wav", "music\\slvla.wav", "music\\slvla.wav", "music\\slvla.wav", "music\\dlvlf.wav", "music\\dlvle.wav", "music\\sintro.wav", }; /** Maps from track ID to track name. */ const char *const MusicTracks[NUM_MUSIC] = { "music\\dtowne.wav", "music\\dlvla.wav", "music\\dlvlb.wav", "music\\dlvlc.wav", "music\\dlvld.wav", "music\\dlvlf.wav", "music\\dlvle.wav", "music\\dintro.wav", }; int CapVolume(int volume) { return std::clamp(volume, VOLUME_MIN, VOLUME_MAX); } void OptionAudioChanged() { effects_cleanup_sfx(false); music_stop(); snd_deinit(); snd_init(); music_start(TMUSIC_INTRO); if (gbRunGame) sound_init(); else ui_sound_init(); } const auto OptionChangeSampleRate = (GetOptions().Audio.sampleRate.SetValueChangedCallback(OptionAudioChanged), true); const auto OptionChangeChannels = (GetOptions().Audio.channels.SetValueChangedCallback(OptionAudioChanged), true); const auto OptionChangeBufferSize = (GetOptions().Audio.bufferSize.SetValueChangedCallback(OptionAudioChanged), true); const auto OptionChangeResamplingQuality = (GetOptions().Audio.resamplingQuality.SetValueChangedCallback(OptionAudioChanged), true); const auto OptionChangeResampler = (GetOptions().Audio.resampler.SetValueChangedCallback(OptionAudioChanged), true); const auto OptionChangeDevice = (GetOptions().Audio.device.SetValueChangedCallback(OptionAudioChanged), true); } // namespace void ClearDuplicateSounds() { // Move sound samples to a temporary list, // avoiding a deadlock that involves SDL's // mixer lock being taken by finalizers std::list> drain; { const std::lock_guard lock(*duplicateSoundsMutex); drain = std::move(duplicateSounds); duplicateSounds.clear(); } } void snd_play_snd(TSnd *pSnd, int lVolume, int lPan, int userVolume) { if (pSnd == nullptr || !gbSoundOn) { return; } const uint32_t tc = SDL_GetTicks(); if (tc - pSnd->start_tc < 80) { return; } SoundSample *sound = &pSnd->DSB; if (sound->IsPlaying()) { sound = DuplicateSound(*sound); if (sound == nullptr) return; } sound->PlayWithVolumeAndPan(lVolume, userVolume, lPan); pSnd->start_tc = tc; } tl::expected, std::string> SoundFileLoadWithStatus(const char *path, bool stream) { auto snd = std::make_unique(); snd->start_tc = SDL_GetTicks() - 80 - 1; #ifndef NOSOUND RETURN_IF_ERROR(LoadAudioFile(path, stream, snd->DSB)); #endif return snd; } std::unique_ptr sound_file_load(const char *path, bool stream) { tl::expected, std::string> result = SoundFileLoadWithStatus(path, stream); if (!result.has_value()) app_fatal(result.error()); return std::move(result).value(); } TSnd::~TSnd() { if (DSB.IsLoaded()) DSB.Stop(); DSB.Release(); } void snd_init() { GetOptions().Audio.soundVolume.SetValue(CapVolume(*GetOptions().Audio.soundVolume)); gbSoundOn = *GetOptions().Audio.soundVolume > VOLUME_MIN; sgbSaveSoundOn = gbSoundOn; GetOptions().Audio.musicVolume.SetValue(CapVolume(*GetOptions().Audio.musicVolume)); gbMusicOn = *GetOptions().Audio.musicVolume > VOLUME_MIN; // Initialize the SDL_audiolib library. Set the output sample rate to // 22kHz, the audio format to 16-bit signed, use 2 output channels // (stereo), and a 2KiB output buffer. #ifdef USE_SDL3 const AudioOptions &audioOptions = GetOptions().Audio; SDL_AudioSpec specHint = {}; specHint.format = SDL_AUDIO_S16LE; specHint.channels = *audioOptions.channels; specHint.freq = static_cast(*audioOptions.sampleRate); const SDL_AudioDeviceID resolvedId = SDL_OpenAudioDevice(audioOptions.device.id(), &specHint); if (resolvedId == 0) { LogError(LogCategory::Audio, "Failed to open audio device: {}", SDL_GetError()); SDL_ClearError(); return; } CurrentAudioDeviceId = resolvedId; #else if (!Aulib::init(*GetOptions().Audio.sampleRate, AUDIO_S16, *GetOptions().Audio.channels, *GetOptions().Audio.bufferSize, *GetOptions().Audio.device)) { LogError(LogCategory::Audio, "Failed to initialize audio (Aulib::init): {}", SDL_GetError()); return; } LogVerbose(LogCategory::Audio, "Aulib sampleRate={} channels={} frameSize={} format={:#x}", Aulib::sampleRate(), Aulib::channelCount(), Aulib::frameSize(), Aulib::sampleFormat()); #endif duplicateSoundsMutex.emplace(); gbSndInited = true; } void snd_deinit() { if (gbSndInited) { #ifdef USE_SDL3 const AudioOptions &audioOptions = GetOptions().Audio; SDL_CloseAudioDevice(audioOptions.device.id()); #else Aulib::quit(); #endif duplicateSoundsMutex = std::nullopt; } gbSndInited = false; } _music_id GetLevelMusic(dungeon_type dungeonType) { switch (dungeonType) { case DTYPE_TOWN: return TMUSIC_TOWN; case DTYPE_CATHEDRAL: return TMUSIC_CATHEDRAL; case DTYPE_CATACOMBS: return TMUSIC_CATACOMBS; case DTYPE_CAVES: return TMUSIC_CAVES; case DTYPE_HELL: return TMUSIC_HELL; case DTYPE_NEST: return TMUSIC_NEST; case DTYPE_CRYPT: return TMUSIC_CRYPT; default: return TMUSIC_INTRO; } } void music_stop() { music.Release(); sgnMusicTrack = NUM_MUSIC; } void music_start(_music_id nTrack) { const char *trackPath; assert(nTrack < NUM_MUSIC); music_stop(); if (!gbMusicOn) return; if (HaveFullMusic()) trackPath = MusicTracks[nTrack]; else trackPath = SpawnMusicTracks[nTrack]; #ifdef DISABLE_STREAMING_MUSIC const bool stream = false; #else const bool stream = true; #endif if (!LoadAudioFile(trackPath, stream, music).has_value()) { music_stop(); return; } music.SetVolume(*GetOptions().Audio.musicVolume, VOLUME_MIN, VOLUME_MAX); if (!music.Play(/*numIterations=*/0)) { LogError(LogCategory::Audio, "Aulib::Stream::play (from music_start): {}", SDL_GetError()); music_stop(); return; } sgnMusicTrack = nTrack; } void sound_disable_music(bool disable) { if (disable) { music_stop(); } else if (sgnMusicTrack != NUM_MUSIC) { music_start(sgnMusicTrack); } } int sound_get_or_set_music_volume(int volume) { if (volume == 1) return *GetOptions().Audio.musicVolume; GetOptions().Audio.musicVolume.SetValue(volume); if (music.IsLoaded()) music.SetVolume(*GetOptions().Audio.musicVolume, VOLUME_MIN, VOLUME_MAX); return *GetOptions().Audio.musicVolume; } int sound_get_or_set_sound_volume(int volume) { if (volume == 1) return *GetOptions().Audio.soundVolume; GetOptions().Audio.soundVolume.SetValue(volume); return *GetOptions().Audio.soundVolume; } int SoundGetOrSetAudioCuesVolume(int volume) { if (volume == 1) return *GetOptions().Audio.audioCuesVolume; GetOptions().Audio.audioCuesVolume.SetValue(volume); return *GetOptions().Audio.audioCuesVolume; } void music_mute() { if (music.IsLoaded()) music.Mute(); } void music_unmute() { if (music.IsLoaded()) music.Unmute(); } } // namespace devilution ================================================ FILE: Source/engine/sound.h ================================================ /** * @file sound.h * * Interface of functions setting up the audio pipeline. */ #pragma once #include #include #include #include #include #include "levels/gendung.h" #include "utils/attributes.h" #ifndef NOSOUND #ifdef USE_SDL3 #include #endif #include "utils/soundsample.h" #endif namespace devilution { enum _music_id : uint8_t { TMUSIC_TOWN, TMUSIC_CATHEDRAL, TMUSIC_CATACOMBS, TMUSIC_CAVES, TMUSIC_HELL, TMUSIC_NEST, TMUSIC_CRYPT, TMUSIC_INTRO, NUM_MUSIC, }; struct TSnd { uint32_t start_tc; #ifndef NOSOUND SoundSample DSB; bool isPlaying() { return DSB.IsPlaying(); } #else bool isPlaying() { return false; } #endif ~TSnd(); }; extern bool gbSndInited; #ifdef USE_SDL3 extern SDL_AudioDeviceID CurrentAudioDeviceId; #endif extern _music_id sgnMusicTrack; void ClearDuplicateSounds(); void snd_play_snd(TSnd *pSnd, int lVolume, int lPan, int userVolume); std::unique_ptr sound_file_load(const char *path, bool stream = false); tl::expected, std::string> SoundFileLoadWithStatus(const char *path, bool stream = false); void snd_init(); void snd_deinit(); _music_id GetLevelMusic(dungeon_type dungeonType); void music_stop(); void music_start(_music_id nTrack); void sound_disable_music(bool disable); int sound_get_or_set_music_volume(int volume); int sound_get_or_set_sound_volume(int volume); int SoundGetOrSetAudioCuesVolume(int volume); void music_mute(); void music_unmute(); /* data */ extern DVL_API_FOR_TEST bool gbMusicOn; extern DVL_API_FOR_TEST bool gbSoundOn; } // namespace devilution ================================================ FILE: Source/engine/sound_defs.hpp ================================================ #pragma once #ifdef USE_SDL3 #include #else #include #endif #define VOLUME_MIN -1600 #define VOLUME_MAX 0 #define VOLUME_STEPS 64 #define ATTENUATION_MIN -6400 #define ATTENUATION_MAX 0 #define PAN_MIN -6400 #define PAN_MAX 6400 #if SDL_VERSION_ATLEAST(2, 0, 7) && defined(DEVILUTIONX_RESAMPLER_SDL) #define DVL_AULIB_SUPPORTS_SDL_RESAMPLER #endif ================================================ FILE: Source/engine/sound_position.cpp ================================================ #include "engine/sound_position.hpp" #include "engine/sound_defs.hpp" #include "player.h" namespace devilution { bool CalculateSoundPosition(Point soundPosition, int *plVolume, int *plPan) { const Point playerPosition { MyPlayer->position.tile }; const Displacement delta = soundPosition - playerPosition; const int pan = (delta.deltaX - delta.deltaY) * 256; *plPan = std::clamp(pan, PAN_MIN, PAN_MAX); const int volume = playerPosition.ApproxDistance(soundPosition) * -64; if (volume <= ATTENUATION_MIN) return false; *plVolume = volume; return true; } } // namespace devilution ================================================ FILE: Source/engine/sound_position.hpp ================================================ #pragma once #include "engine/point.hpp" namespace devilution { bool CalculateSoundPosition(Point soundPosition, int *plVolume, int *plPan); } // namespace devilution ================================================ FILE: Source/engine/sound_stubs.cpp ================================================ // Stubbed implementations of sound functions for the NOSOUND mode. #include "engine/sound.h" namespace devilution { bool gbSndInited; bool gbMusicOn; bool gbSoundOn; _music_id sgnMusicTrack = NUM_MUSIC; void ClearDuplicateSounds() { } void snd_play_snd(TSnd *pSnd, int lVolume, int lPan, int userVolume) { } std::unique_ptr sound_file_load(const char *path, bool stream) { return nullptr; } tl::expected, std::string> SoundFileLoadWithStatus(const char *path, bool stream) { return nullptr; } TSnd::~TSnd() { } void snd_init() { } void snd_deinit() { } void music_stop() { } void music_start(_music_id nTrack) { } void sound_disable_music(bool disable) { } int sound_get_or_set_music_volume(int volume) { return 0; } int sound_get_or_set_sound_volume(int volume) { return 0; } int SoundGetOrSetAudioCuesVolume(int volume) { return 0; } void music_mute() { } void music_unmute() { } _music_id GetLevelMusic(dungeon_type dungeonType) { return TMUSIC_TOWN; } } // namespace devilution ================================================ FILE: Source/engine/surface.cpp ================================================ #include "engine/surface.hpp" #include #include namespace devilution { namespace { template void SurfaceBlit(const Surface &src, SDL_Rect srcRect, const Surface &dst, Point dstPosition) { // We do not use `SDL_BlitSurface` here because the palettes may be different objects // and SDL would attempt to map them. dst.Clip(&srcRect, &dstPosition); if (srcRect.w <= 0 || srcRect.h <= 0) return; const std::uint8_t *srcBuf = src.at(srcRect.x, srcRect.y); const auto srcPitch = src.pitch(); std::uint8_t *dstBuf = &dst[dstPosition]; const auto dstPitch = dst.pitch(); for (unsigned h = srcRect.h; h != 0; --h) { if (SkipColorIndexZero) { for (unsigned w = srcRect.w; w != 0; --w) { if (*srcBuf != 0) *dstBuf = *srcBuf; ++srcBuf, ++dstBuf; } srcBuf += srcPitch - srcRect.w; dstBuf += dstPitch - srcRect.w; } else { std::memcpy(dstBuf, srcBuf, srcRect.w); srcBuf += srcPitch; dstBuf += dstPitch; } } } } // namespace void Surface::BlitFrom(const Surface &src, SDL_Rect srcRect, Point targetPosition) const { SurfaceBlit(src, srcRect, *this, targetPosition); } void Surface::BlitFromSkipColorIndexZero(const Surface &src, SDL_Rect srcRect, Point targetPosition) const { SurfaceBlit(src, srcRect, *this, targetPosition); } } // namespace devilution ================================================ FILE: Source/engine/surface.hpp ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #include #else #include #if SDL_VERSION_ATLEAST(2, 0, 0) #include #include #else #include "utils/sdl2_to_1_2_backports.h" #include #endif #endif #include "engine/point.hpp" #include "utils/sdl_geometry.h" #include "utils/sdl_wrap.h" namespace devilution { /** * @brief 8-bit surface. */ struct Surface { SDL_Surface *surface; SDL_Rect region; Surface() : surface(nullptr) , region(SDL_Rect { 0, 0, 0, 0 }) { } explicit Surface(SDL_Surface *surface) : surface(surface) , region(MakeSdlRect(0, 0, surface->w, surface->h)) { } Surface(SDL_Surface *surface, SDL_Rect region) : surface(surface) , region(region) { } Surface(const Surface &other) = default; Surface &operator=(const Surface &other) = default; int w() const { return region.w; } int h() const { return region.h; } std::uint8_t &operator[](Point p) const { return *at(p.x, p.y); } std::uint8_t *at(int x, int y) const { return static_cast(surface->pixels) + region.x + x + surface->pitch * (region.y + y); } std::uint8_t *begin() const { return at(0, 0); } std::uint8_t *end() const { return at(0, region.h); } /** * @brief Set the value of a single pixel if it is in bounds. * @param position Target buffer coordinate * @param col Color index from current palette */ void SetPixel(Point position, std::uint8_t col) const { if (InBounds(position)) (*this)[position] = col; } /** * @brief Line width of the raw underlying byte buffer. * May be wider than its logical width (for power-of-2 alignment). */ [[nodiscard]] uint16_t pitch() const { return surface->pitch; } bool InBounds(Point position) const { return position.x >= 0 && position.y >= 0 && position.x < region.w && position.y < region.h; } /** * @brief Returns a subregion of the given buffer. */ Surface subregion(int x, int y, int w, int h) const { return Surface(surface, MakeSdlRect(region.x + x, region.y + y, w, h)); } /** * @brief Returns a buffer that starts at `x` of width `w`. */ Surface subregionX(int x, int w) const { SDL_Rect subregion = region; subregion.x += static_cast(x); subregion.w = static_cast(w); return Surface(surface, subregion); } /** * @brief Returns a buffer that starts at `y` of height `h`. */ Surface subregionY(int y, int h) const { SDL_Rect subregion = region; subregion.y += static_cast(y); subregion.h = static_cast(h); return Surface(surface, subregion); } /** * @brief Clips srcRect and targetPosition to this output buffer. */ void Clip(SDL_Rect *srcRect, Point *targetPosition) const { if (targetPosition->x < 0) { srcRect->x -= targetPosition->x; srcRect->w += targetPosition->x; targetPosition->x = 0; } if (targetPosition->y < 0) { srcRect->y -= targetPosition->y; srcRect->h += targetPosition->y; targetPosition->y = 0; } if (targetPosition->x + srcRect->w > region.w) { srcRect->w = region.w - targetPosition->x; } if (targetPosition->y + srcRect->h > region.h) { srcRect->h = region.h - targetPosition->y; } } /** * @brief Copies the `srcRect` portion of the given buffer to this buffer at `targetPosition`. */ void BlitFrom(const Surface &src, SDL_Rect srcRect, Point targetPosition) const; /** * @brief Copies the `srcRect` portion of the given buffer to this buffer at `targetPosition`. * Source pixels with index 0 are not copied. */ void BlitFromSkipColorIndexZero(const Surface &src, SDL_Rect srcRect, Point targetPosition) const; }; class OwnedSurface : public Surface { SDLSurfaceUniquePtr pinnedSurface; public: explicit OwnedSurface(SDLSurfaceUniquePtr surface) : Surface(surface.get()) , pinnedSurface(std::move(surface)) { } OwnedSurface(int width, int height) : OwnedSurface(SDLWrap::CreateRGBSurfaceWithFormat(0, width, height, 8, SDL_PIXELFORMAT_INDEX8)) { } explicit OwnedSurface(Size size) : OwnedSurface(size.width, size.height) { } }; } // namespace devilution ================================================ FILE: Source/engine/ticks.cpp ================================================ #include "engine/ticks.hpp" #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { uint32_t GetAnimationFrame(uint32_t frames, uint32_t fps) { return (SDL_GetTicks() / fps) % frames; } } // namespace devilution ================================================ FILE: Source/engine/ticks.hpp ================================================ #pragma once #include namespace devilution { uint32_t GetAnimationFrame(uint32_t frames, uint32_t fps = 60); } // namespace devilution ================================================ FILE: Source/engine/trn.cpp ================================================ #include "engine/trn.hpp" #include #include #ifdef _DEBUG #include "debug.h" #endif #include "engine/load_file.hpp" #include "lighting.h" #include "utils/str_cat.hpp" namespace devilution { uint8_t *GetInfravisionTRN() { return InfravisionTable.data(); } uint8_t *GetStoneTRN() { return StoneTable.data(); } uint8_t *GetPauseTRN() { return PauseTable.data(); } std::optional> GetClassTRN(Player &player) { std::array trn; char path[64]; const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(player._pClass); *BufCopy(path, "plrgfx\\", spriteData.trn, ".trn") = '\0'; #ifdef _DEBUG if (!debugTRN.empty()) { *BufCopy(path, debugTRN.c_str()) = '\0'; } #endif if (LoadOptionalFileInMem(path, &trn[0], 256)) { return trn; } return std::nullopt; } std::optional> GetPlayerGraphicTRN(const char *pszName) { char path[MaxMpqPathSize]; *BufCopy(path, pszName, ".trn") = '\0'; std::array trn; if (LoadOptionalFileInMem(path, &trn[0], 256)) { return trn; } return std::nullopt; } } // namespace devilution ================================================ FILE: Source/engine/trn.hpp ================================================ /** * @file trn.hpp * * Contains most of trn logic */ #pragma once #include #include #include "player.h" namespace devilution { uint8_t *GetInfravisionTRN(); uint8_t *GetStoneTRN(); uint8_t *GetPauseTRN(); std::optional> GetClassTRN(Player &player); std::optional> GetPlayerGraphicTRN(const char *pszName); } // namespace devilution ================================================ FILE: Source/engine/world_tile.hpp ================================================ #pragma once #include #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/size.hpp" namespace devilution { using WorldTileCoord = uint8_t; using WorldTilePosition = PointOf; using WorldTileOffset = int8_t; using WorldTileDisplacement = DisplacementOf; using WorldTileSize = SizeOf; using WorldTileRectangle = RectangleOf; } // namespace devilution namespace std { /** * @brief Allows using WorldTilePosition as a map key for contexts where we want to lookup an entity by physical dungeon location */ template <> struct hash { size_t operator()(const devilution::WorldTilePosition &position) const noexcept { return static_cast(position.x) << 8 | position.y; } }; } // namespace std ================================================ FILE: Source/game_mode.cpp ================================================ #include "game_mode.hpp" #include #include "options.h" namespace devilution { namespace { void OptionSharewareChanged() { gbIsSpawn = *GetOptions().GameMode.shareware; } const auto OptionChangeHandlerShareware = (GetOptions().GameMode.shareware.SetValueChangedCallback(OptionSharewareChanged), true); } // namespace bool gbRunGame; bool gbIsSpawn; bool gbIsHellfire; bool gbVanilla; bool forceHellfire; } // namespace devilution ================================================ FILE: Source/game_mode.hpp ================================================ #pragma once #include "utils/attributes.h" namespace devilution { /* Are we in-game? If false, we're in the main menu. */ extern DVL_API_FOR_TEST bool gbRunGame; /** Indicate if we only have access to demo data */ extern DVL_API_FOR_TEST bool gbIsSpawn; /** Indicate if we have loaded the Hellfire expansion data */ extern DVL_API_FOR_TEST bool gbIsHellfire; /** Indicate if we want vanilla savefiles */ extern DVL_API_FOR_TEST bool gbVanilla; /** Whether the Hellfire mode is required (forced). */ extern bool forceHellfire; } // namespace devilution ================================================ FILE: Source/gamemenu.cpp ================================================ /** * @file gamemenu.cpp * * Implementation of the in-game menu functions. */ #include "gamemenu.h" #ifdef USE_SDL3 #include #endif #include "cursor.h" #include "diablo_msg.hpp" #include "engine/backbuffer_state.hpp" #include "engine/demomode.h" #include "engine/events.hpp" #include "engine/sound.h" #include "engine/sound_defs.hpp" #include "game_mode.hpp" #include "gmenu.h" #include "headless_mode.hpp" #include "loadsave.h" #include "multi.h" #include "options.h" #include "pfile.h" #include "qol/floatingnumbers.h" #include "utils/language.h" #ifndef USE_SDL1 #include "controls/touch/renderers.h" #endif namespace devilution { bool isGameMenuOpen = false; namespace { // Forward-declare menu handlers, used by the global menu structs below. void GamemenuPrevious(bool bActivate); void GamemenuNewGame(bool bActivate); void GamemenuOptions(bool bActivate); void GamemenuMusicVolume(bool bActivate); void GamemenuSoundVolume(bool bActivate); void GamemenuBrightness(bool bActivate); void GamemenuSpeed(bool bActivate); /** Contains the game menu items of the single player menu. */ TMenuItem sgSingleMenu[] = { // clang-format off // dwFlags, pszStr, fnMenu { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, { GMENU_ENABLED, N_("Save Game"), &gamemenu_save_game }, { GMENU_ENABLED, N_("Load Game"), &gamemenu_load_game }, { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, { GMENU_ENABLED, nullptr, nullptr }, // clang-format on }; /** Contains the game menu items of the multi player menu. */ TMenuItem sgMultiMenu[] = { // clang-format off // dwFlags, pszStr, fnMenu { GMENU_ENABLED, N_("Options"), &GamemenuOptions }, { GMENU_ENABLED, N_("Exit to Main Menu"), &GamemenuNewGame }, { GMENU_ENABLED, N_("Quit Game"), &gamemenu_quit_game }, { GMENU_ENABLED, nullptr, nullptr }, // clang-format on }; TMenuItem sgOptionsMenu[] = { // clang-format off // dwFlags, pszStr, fnMenu { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, { GMENU_ENABLED , nullptr, nullptr }, // clang-format on }; /** Specifies the menu names for music enabled and disabled. */ const char *const MusicToggleNames[] = { N_("Music"), N_("Music Disabled"), }; /** Specifies the menu names for sound enabled and disabled. */ const char *const SoundToggleNames[] = { N_("Sound"), N_("Sound Disabled"), }; void GamemenuUpdateSingle() { sgSingleMenu[2].setEnabled(gbValidSaveFile); const bool enable = MyPlayer->_pmode != PM_DEATH && !MyPlayerIsDead; sgSingleMenu[0].setEnabled(enable); } void GamemenuPrevious(bool /*bActivate*/) { gamemenu_on(); } void GamemenuNewGame(bool /*bActivate*/) { for (Player &player : Players) { player._pmode = PM_QUIT; player._pInvincible = true; } MyPlayerIsDead = false; if (!HeadlessMode) { RedrawEverything(); scrollrt_draw_game_screen(); } CornerStone.activated = false; gbRunGame = false; gamemenu_off(); } void GamemenuSoundMusicToggle(const char *const *names, TMenuItem *menuItem, int volume) { if (gbSndInited) { menuItem->addFlags(GMENU_ENABLED | GMENU_SLIDER); menuItem->pszStr = names[0]; gmenu_slider_steps(menuItem, VOLUME_STEPS); gmenu_slider_set(menuItem, VOLUME_MIN, VOLUME_MAX, volume); return; } menuItem->removeFlags(GMENU_ENABLED | GMENU_SLIDER); menuItem->pszStr = names[1]; } int GamemenuSliderMusicSound(TMenuItem *menuItem) { return gmenu_slider_get(menuItem, VOLUME_MIN, VOLUME_MAX); } void GamemenuGetMusic() { GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); } void GamemenuGetSound() { GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); } void GamemenuGetBrightness() { gmenu_slider_steps(&sgOptionsMenu[2], 21); gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); } void GamemenuGetSpeed() { if (gbIsMultiplayer) { sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); if (sgGameInitInfo.nTickRate >= 50) sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); else if (sgGameInitInfo.nTickRate >= 40) sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); else if (sgGameInitInfo.nTickRate >= 30) sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); else if (sgGameInitInfo.nTickRate == 20) sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); return; } sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); sgOptionsMenu[3].pszStr = _("Speed").data(); gmenu_slider_steps(&sgOptionsMenu[3], 46); gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); } int GamemenuSliderBrightness() { return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); } void GamemenuOptions(bool /*bActivate*/) { GamemenuGetMusic(); GamemenuGetSound(); GamemenuGetBrightness(); GamemenuGetSpeed(); gmenu_set_items(sgOptionsMenu, nullptr); } void GamemenuMusicVolume(bool bActivate) { if (bActivate) { if (gbMusicOn) { gbMusicOn = false; music_stop(); sound_get_or_set_music_volume(VOLUME_MIN); } else { gbMusicOn = true; sound_get_or_set_music_volume(VOLUME_MAX); music_start(GetLevelMusic(leveltype)); } } else { const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[0]); sound_get_or_set_music_volume(volume); if (volume == VOLUME_MIN) { if (gbMusicOn) { gbMusicOn = false; music_stop(); } } else if (!gbMusicOn) { gbMusicOn = true; music_start(GetLevelMusic(leveltype)); } } GamemenuGetMusic(); } void GamemenuSoundVolume(bool bActivate) { if (bActivate) { if (gbSoundOn) { gbSoundOn = false; sound_stop(); sound_get_or_set_sound_volume(VOLUME_MIN); } else { gbSoundOn = true; sound_get_or_set_sound_volume(VOLUME_MAX); } } else { const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[1]); sound_get_or_set_sound_volume(volume); if (volume == VOLUME_MIN) { if (gbSoundOn) { gbSoundOn = false; sound_stop(); } } else if (!gbSoundOn) { gbSoundOn = true; } } PlaySFX(SfxID::MenuMove); GamemenuGetSound(); } void GamemenuBrightness(bool bActivate) { int brightness; if (bActivate) { brightness = UpdateBrightness(-1); brightness = (brightness == 0) ? 100 : 0; } else { brightness = GamemenuSliderBrightness(); } UpdateBrightness(brightness); GamemenuGetBrightness(); } void GamemenuSpeed(bool bActivate) { if (bActivate) { if (sgGameInitInfo.nTickRate != 20) sgGameInitInfo.nTickRate = 20; else sgGameInitInfo.nTickRate = 50; gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); } else { sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); } GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; } } // namespace void gamemenu_exit_game(bool bActivate) { GamemenuNewGame(bActivate); } void gamemenu_quit_game(bool bActivate) { GamemenuNewGame(bActivate); #ifndef NOEXIT gbRunGameResult = false; #else ReturnToMainMenu = true; #endif } void gamemenu_load_game(bool /*bActivate*/) { EventHandler saveProc = SetEventHandler(DisableInputEventHandler); gamemenu_off(); ClearFloatingNumbers(); NewCursor(CURSOR_NONE); InitDiabloMsg(EMSG_LOADING); RedrawEverything(); DrawAndBlit(); const std::array prevPalette = logical_palette; #ifndef USE_SDL1 DeactivateVirtualGamepad(); FreeVirtualGamepadTextures(); #endif if (tl::expected result = LoadGame(false); !result.has_value()) { app_fatal(result.error()); } #if !defined(USE_SDL1) && !defined(__vita__) if (renderer != nullptr) { InitVirtualGamepadTextures(*renderer); } #endif ClrDiabloMsg(); PaletteFadeOut(8, prevPalette); LoadPWaterPalette(); NewCursor(CURSOR_HAND); CornerStone.activated = false; MyPlayerIsDead = false; RedrawEverything(); DrawAndBlit(); PaletteFadeIn(8); NewCursor(CURSOR_HAND); interface_msg_pump(); SetEventHandler(saveProc); } void gamemenu_save_game(bool /*bActivate*/) { if (pcurs != CURSOR_HAND) { return; } if (MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead) { gamemenu_off(); return; } EventHandler saveProc = SetEventHandler(DisableInputEventHandler); NewCursor(CURSOR_NONE); gamemenu_off(); InitDiabloMsg(EMSG_SAVING); RedrawEverything(); DrawAndBlit(); const uint32_t currentTime = SDL_GetTicks(); SaveGame(); ClrDiabloMsg(); InitDiabloMsg(EMSG_GAME_SAVED, currentTime + 1000 - SDL_GetTicks()); RedrawEverything(); NewCursor(CURSOR_HAND); if (CornerStone.activated) { CornerstoneSave(); if (!demo::IsRunning()) SaveOptions(); } interface_msg_pump(); SetEventHandler(saveProc); } void gamemenu_on() { isGameMenuOpen = true; if (!gbIsMultiplayer) { gmenu_set_items(sgSingleMenu, GamemenuUpdateSingle); } else { gmenu_set_items(sgMultiMenu, nullptr); } PressEscKey(); } void gamemenu_off() { isGameMenuOpen = false; gmenu_set_items(nullptr, nullptr); } void gamemenu_handle_previous() { if (gmenu_is_active()) gamemenu_off(); else gamemenu_on(); } } // namespace devilution ================================================ FILE: Source/gamemenu.h ================================================ /** * @file gamemenu.h * * Interface of the in-game menu functions. */ #pragma once namespace devilution { void gamemenu_on(); void gamemenu_off(); void gamemenu_handle_previous(); void gamemenu_exit_game(bool bActivate); void gamemenu_quit_game(bool bActivate); void gamemenu_load_game(bool bActivate); void gamemenu_save_game(bool bActivate); extern bool isGameMenuOpen; } // namespace devilution ================================================ FILE: Source/gmenu.cpp ================================================ /** * @file gmenu.cpp * * Implementation of the in-game navigation and interaction. */ #include "gmenu.h" #include #include #include #ifdef USE_SDL3 #include #include #include #include #else #include #endif #include "DiabloUI/ui_flags.hpp" #include "appfat.h" #include "control/control.hpp" #include "controls/axis_direction.h" #include "controls/controller_motion.h" #include "engine/clx_sprite.hpp" #include "engine/demomode.h" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "headless_mode.hpp" #include "options.h" #include "stores.h" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/ui_fwd.h" namespace devilution { namespace { // Width of the slider menu item, including the label. constexpr int SliderItemWidth = 490; // Horizontal dimensions of the slider value constexpr int SliderValueBoxLeft = 16 + SliderItemWidth / 2; constexpr int SliderValueBoxWidth = 287; constexpr int SliderValueBorderWidth = 2; constexpr int SliderValueLeft = SliderValueBoxLeft + SliderValueBorderWidth; constexpr int SliderValueWidth = SliderValueBoxWidth - 2 * SliderValueBorderWidth; constexpr int SliderValueHeight = 29; constexpr int SliderValuePaddingTop = 10; constexpr int SliderMarkerWidth = 27; constexpr int SliderFillMin = SliderMarkerWidth / 2; constexpr int SliderFillMax = SliderValueWidth - SliderMarkerWidth / 2 - 1; constexpr int GMenuTop = 117; constexpr int GMenuItemHeight = 45; OptionalOwnedClxSpriteList optbar_cel; OptionalOwnedClxSpriteList PentSpin_cel; OptionalOwnedClxSpriteList option_cel; OptionalOwnedClxSpriteList sgpLogo; bool isDraggingSlider; TMenuItem *sgpCurrItem; int LogoAnim_tick; uint8_t LogoAnim_frame; void (*gmenu_current_option)(); int sgCurrentMenuIdx; void GmenuUpDown(bool isDown) { if (sgpCurrItem == nullptr) { return; } isDraggingSlider = false; int i = sgCurrentMenuIdx; if (sgCurrentMenuIdx != 0) { while (i != 0) { i--; if (isDown) { sgpCurrItem++; if (sgpCurrItem->fnMenu == nullptr) sgpCurrItem = &sgpCurrentMenu[0]; } else { if (sgpCurrItem == sgpCurrentMenu) sgpCurrItem = &sgpCurrentMenu[sgCurrentMenuIdx]; sgpCurrItem--; } if (sgpCurrItem->enabled()) { if (i != 0) PlaySFX(SfxID::MenuMove); return; } } } } void GmenuLeftRight(bool isRight) { if (!sgpCurrItem->isSlider()) return; uint16_t step = sgpCurrItem->sliderStep(); if (isRight) { if (step == sgpCurrItem->sliderSteps()) return; step++; } else { if (step == 0) return; step--; } sgpCurrItem->setSliderStep(step); sgpCurrItem->fnMenu(false); } int GmenuGetLineWidth(TMenuItem *pItem) { if (pItem->isSlider()) return SliderItemWidth; return GetLineWidth(_(pItem->pszStr), GameFont46, 2); } void GmenuDrawMenuItem(const Surface &out, TMenuItem *pItem, int y) { const int w = GmenuGetLineWidth(pItem); if (pItem->isSlider()) { const int uiPositionX = GetUIRectangle().position.x; ClxDraw(out, { SliderValueBoxLeft + uiPositionX, y + 40 }, (*optbar_cel)[0]); const uint16_t step = pItem->dwFlags & 0xFFF; const uint16_t steps = std::max(pItem->sliderSteps(), 2); const uint16_t pos = SliderFillMin + step * (SliderFillMax - SliderFillMin) / steps; SDL_Rect rect = MakeSdlRect(SliderValueLeft + uiPositionX, y + SliderValuePaddingTop, pos, SliderValueHeight); SDL_FillSurfaceRect(out.surface, &rect, 205); ClxDraw(out, { SliderValueLeft + pos - SliderMarkerWidth / 2 + uiPositionX, y + SliderValuePaddingTop + SliderValueHeight - 1 }, (*option_cel)[0]); } const int x = (gnScreenWidth - w) / 2; const UiFlags style = pItem->enabled() ? UiFlags::ColorGold : UiFlags::ColorBlack; DrawString(out, _(pItem->pszStr), Point { x, y }, { .flags = style | UiFlags::FontSize46, .spacing = 2 }); if (pItem == sgpCurrItem) { const ClxSprite sprite = (*PentSpin_cel)[PentSpn2Spin()]; ClxDraw(out, { x - 54, y + 51 }, sprite); ClxDraw(out, { x + 4 + w, y + 51 }, sprite); } } void GameMenuMove() { static AxisDirectionRepeater repeater; const AxisDirection moveDir = repeater.Get(GetLeftStickOrDpadDirection(false)); if (moveDir.x != AxisDirectionX_NONE) GmenuLeftRight(moveDir.x == AxisDirectionX_RIGHT); if (moveDir.y != AxisDirectionY_NONE) GmenuUpDown(moveDir.y == AxisDirectionY_DOWN); } bool GmenuMouseIsOverSlider() { const int uiPositionX = GetUIRectangle().position.x; if (MousePosition.x < SliderValueLeft + uiPositionX) { return false; } if (MousePosition.x >= SliderValueLeft + SliderValueWidth + uiPositionX) { return false; } return true; } int GmenuGetSliderFill() { return std::clamp(MousePosition.x - SliderValueLeft - GetUIRectangle().position.x, SliderFillMin, SliderFillMax); } } // namespace TMenuItem *sgpCurrentMenu; void gmenu_draw_pause(const Surface &out) { if (leveltype != DTYPE_TOWN) RedBack(out); if (sgpCurrentMenu == nullptr) { DrawString(out, _("Pause"), { { 0, 0 }, { gnScreenWidth, GetMainPanel().position.y } }, { .flags = UiFlags::FontSize46 | UiFlags::ColorGold | UiFlags::AlignCenter | UiFlags::VerticalCenter, .spacing = 2 }); } } void FreeGMenu() { sgpLogo = std::nullopt; PentSpin_cel = std::nullopt; option_cel = std::nullopt; optbar_cel = std::nullopt; } void gmenu_init_menu() { LogoAnim_frame = 0; sgpCurrentMenu = nullptr; sgpCurrItem = nullptr; gmenu_current_option = nullptr; sgCurrentMenuIdx = 0; isDraggingSlider = false; if (HeadlessMode) return; sgpLogo = LoadOptionalCel("data\\hf_logo3", 430); if (!sgpLogo.has_value()) sgpLogo = LoadCel("data\\diabsmal", 296); PentSpin_cel = LoadCel("data\\pentspin", 48); option_cel = LoadCel("data\\option", SliderMarkerWidth); optbar_cel = LoadCel("data\\optbar", SliderValueBoxWidth); } bool gmenu_is_active() { return sgpCurrentMenu != nullptr; } void gmenu_set_items(TMenuItem *pItem, void (*gmFunc)()) { PauseMode = 0; isDraggingSlider = false; sgpCurrentMenu = pItem; gmenu_current_option = gmFunc; if (gmenu_current_option != nullptr) { gmenu_current_option(); } sgCurrentMenuIdx = 0; if (sgpCurrentMenu != nullptr) { for (int i = 0; sgpCurrentMenu[i].fnMenu != nullptr; i++) { sgCurrentMenuIdx++; } } // BUGFIX: OOB access when sgCurrentMenuIdx is 0; should be set to NULL instead. (fixed) sgpCurrItem = sgCurrentMenuIdx > 0 ? &sgpCurrentMenu[sgCurrentMenuIdx - 1] : nullptr; GmenuUpDown(true); if (sgpCurrentMenu == nullptr && !demo::IsRunning()) { SaveOptions(); } } void gmenu_draw(const Surface &out) { if (sgpCurrentMenu != nullptr) { GameMenuMove(); if (gmenu_current_option != nullptr) gmenu_current_option(); if (sgpLogo->numSprites() > 1) { const uint32_t ticks = SDL_GetTicks(); if ((int)(ticks - LogoAnim_tick) > 25) { ++LogoAnim_frame; LogoAnim_frame = LogoAnim_frame % sgpLogo->numSprites(); LogoAnim_tick = ticks; } } const int uiPositionY = GetUIRectangle().position.y; const ClxSprite sprite = (*sgpLogo)[LogoAnim_frame]; ClxDraw(out, { (gnScreenWidth - sprite.width()) / 2, 102 + uiPositionY }, sprite); int y = 110 + uiPositionY; TMenuItem *i = sgpCurrentMenu; if (sgpCurrentMenu->fnMenu != nullptr) { while (i->fnMenu != nullptr) { GmenuDrawMenuItem(out, i, y); i++; y += GMenuItemHeight; } } } } bool gmenu_presskeys(SDL_Keycode vkey) { if (sgpCurrentMenu == nullptr) return false; switch (vkey) { case SDLK_KP_ENTER: case SDLK_RETURN: if (sgpCurrItem->enabled()) { PlaySFX(SfxID::MenuMove); sgpCurrItem->fnMenu(true); } break; case SDLK_ESCAPE: PlaySFX(SfxID::MenuMove); gmenu_set_items(nullptr, nullptr); break; case SDLK_SPACE: return false; case SDLK_LEFT: GmenuLeftRight(false); break; case SDLK_RIGHT: GmenuLeftRight(true); break; case SDLK_UP: GmenuUpDown(false); break; case SDLK_DOWN: GmenuUpDown(true); break; default: break; } return true; } bool gmenu_on_mouse_move() { if (!isDraggingSlider) return false; const uint16_t step = sgpCurrItem->sliderSteps() * (GmenuGetSliderFill() - SliderFillMin) / (SliderFillMax - SliderFillMin); sgpCurrItem->setSliderStep(step); sgpCurrItem->fnMenu(false); return true; } bool gmenu_left_mouse(bool isDown) { if (!isDown) { if (isDraggingSlider) { isDraggingSlider = false; return true; } return false; } if (sgpCurrentMenu == nullptr) { return false; } const Point uiPosition = GetUIRectangle().position; if (MousePosition.y >= GetMainPanel().position.y) { return false; } if (MousePosition.y - (GMenuTop + uiPosition.y) < 0) { return true; } const int i = (MousePosition.y - (GMenuTop + uiPosition.y)) / GMenuItemHeight; if (i >= sgCurrentMenuIdx) { return true; } TMenuItem *pItem = &sgpCurrentMenu[i]; if (!pItem->enabled()) { return true; } const int w = GmenuGetLineWidth(pItem); const uint16_t screenWidth = GetScreenWidth(); if (MousePosition.x < screenWidth / 2 - w / 2) { return true; } if (MousePosition.x > screenWidth / 2 + w / 2) { return true; } sgpCurrItem = pItem; PlaySFX(SfxID::MenuMove); if (pItem->isSlider()) { isDraggingSlider = GmenuMouseIsOverSlider(); gmenu_on_mouse_move(); } else { sgpCurrItem->fnMenu(true); } return true; } void gmenu_slider_set(TMenuItem *pItem, int min, int max, int value) { assert(pItem); const uint16_t nSteps = std::max(pItem->sliderSteps(), 2); pItem->setSliderStep(((max - min - 1) / 2 + (value - min) * nSteps) / (max - min)); } int gmenu_slider_get(TMenuItem *pItem, int min, int max) { const uint16_t step = pItem->sliderStep(); const uint16_t steps = std::max(pItem->sliderSteps(), 2); return min + (step * (max - min) + (steps - 1) / 2) / steps; } void gmenu_slider_steps(TMenuItem *pItem, int steps) { pItem->dwFlags &= 0xFF000FFF; pItem->setSliderSteps(steps); } } // namespace devilution ================================================ FILE: Source/gmenu.h ================================================ /** * @file gmenu.h * * Interface of the in-game navigation and interaction. */ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif #include "engine/surface.hpp" namespace devilution { #define GMENU_SLIDER 0x40000000 #define GMENU_ENABLED 0x80000000 struct TMenuItem { uint32_t dwFlags; const char *pszStr; void (*fnMenu)(bool); [[nodiscard]] bool enabled() const { return (dwFlags & GMENU_ENABLED) != 0; } [[nodiscard]] bool isSlider() const { return (dwFlags & GMENU_SLIDER) != 0; } [[nodiscard]] uint16_t sliderStep() const { return dwFlags & 0xFFF; } void setSliderStep(uint16_t step) { dwFlags &= 0xFFFFF000; dwFlags |= step; } [[nodiscard]] uint16_t sliderSteps() const { return (dwFlags & 0xFFF000) >> 12; } void setSliderSteps(uint16_t steps) { dwFlags |= (steps << 12) & 0xFFF000; } void addFlags(uint32_t flags) { dwFlags |= flags; } void removeFlags(uint32_t flags) { dwFlags &= ~flags; } void setEnabled(bool enabled) { if (enabled) { addFlags(GMENU_ENABLED); } else { removeFlags(GMENU_ENABLED); } } }; extern TMenuItem *sgpCurrentMenu; void gmenu_draw_pause(const Surface &out); void FreeGMenu(); void gmenu_init_menu(); bool gmenu_is_active(); void gmenu_set_items(TMenuItem *pItem, void (*gmFunc)()); void gmenu_draw(const Surface &out); bool gmenu_presskeys(SDL_Keycode vkey); bool gmenu_on_mouse_move(); bool gmenu_left_mouse(bool isDown); /** * @brief Set the TMenuItem slider position based on the given value */ void gmenu_slider_set(TMenuItem *pItem, int min, int max, int value); /** * @brief Get the current value for the slider */ int gmenu_slider_get(TMenuItem *pItem, int min, int max); /** * @brief Set the number of steps for the slider */ void gmenu_slider_steps(TMenuItem *pItem, int steps); } // namespace devilution ================================================ FILE: Source/headless_mode.cpp ================================================ #include "headless_mode.hpp" namespace devilution { bool HeadlessMode; } // namespace devilution ================================================ FILE: Source/headless_mode.hpp ================================================ #pragma once #include "utils/attributes.h" namespace devilution { /** * @brief Don't load UI or show Messageboxes or other user-interaction. Needed for unit tests. */ extern DVL_API_FOR_TEST bool HeadlessMode; } // namespace devilution ================================================ FILE: Source/help.cpp ================================================ /** * @file help.cpp * * Implementation of the in-game help text. */ #include #include #include #include "DiabloUI/ui_flags.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "game_mode.hpp" #include "minitext.h" #include "qol/chatlog.h" #include "stores.h" #include "utils/language.h" namespace devilution { bool HelpFlag; namespace { unsigned int SkipLines; const char *const HelpText[] = { N_("$Keyboard Shortcuts:"), N_("F1: Open Help Screen"), N_("Esc: Display Main Menu"), N_("Tab: Display Auto-map"), N_("Space: Hide all info screens"), N_("S: Open Speedbook"), N_("B: Open Spellbook"), N_("I: Open Inventory screen"), N_("C: Open Character screen"), N_("Q: Open Quest log"), N_("F: Reduce screen brightness"), N_("G: Increase screen brightness"), N_("Z: Zoom Game Screen"), N_("+ / -: Zoom Automap"), N_("1 - 8: Use Belt item"), N_("F5, F6, F7, F8: Set hotkey for skill or spell"), N_("Shift + Left Mouse Button: Attack without moving"), N_("Shift + Left Mouse Button (on character screen): Assign all stat points"), N_("Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip item"), N_("Shift + Left Mouse Button (on belt): Move item to inventory"), "", N_("$Movement:"), N_("If you hold the mouse button down while moving, the character " "will continue to move in that direction."), "", N_("$Combat:"), N_("Holding down the shift key and then left-clicking allows the " "character to attack without moving."), "", N_("$Auto-map:"), N_("To access the auto-map, click the 'MAP' button on the " "Information Bar or press 'TAB' on the keyboard. Zooming in and " "out of the map is done with the + and - keys. Scrolling the map " "uses the arrow keys."), "", N_("$Picking up Objects:"), N_("Useable items that are small in size, such as potions or scrolls, " "are automatically placed in your 'belt' located at the top of " "the Interface bar . When an item is placed in the belt, a small " "number appears in that box. Items may be used by either pressing " "the corresponding number or right-clicking on the item."), "", N_("$Gold:"), N_("You can select a specific amount of gold to drop by " "right-clicking on a pile of gold in your inventory."), "", N_("$Skills & Spells:"), N_("You can access your list of skills and spells by left-clicking on " "the 'SPELLS' button in the interface bar. Memorized spells and " "those available through staffs are listed here. Left-clicking on " "the spell you wish to cast will ready the spell. A readied spell " "may be cast by simply right-clicking in the play area."), "", N_("$Using the Speedbook for Spells:"), N_("Left-clicking on the 'readied spell' button will open the 'Speedbook' " "which allows you to select a skill or spell for immediate use. " "To use a readied skill or spell, simply right-click in the main play " "area."), N_("Shift + Left-clicking on the 'select current spell' button will clear the readied spell."), "", N_("$Setting Spell Hotkeys:"), N_("You can assign up to four Hotkeys for skills, spells or scrolls. " "Start by opening the 'speedbook' as described in the section above. " "Press the F5, F6, F7 or F8 keys after highlighting the spell you " "wish to assign."), "", N_("$Spell Books:"), N_("Reading more than one book increases your knowledge of that " "spell, allowing you to cast the spell more effectively."), }; std::vector HelpTextLines; constexpr int PaddingTop = 32; constexpr int PaddingLeft = 32; constexpr int PanelHeight = 297; constexpr int ContentTextWidth = 565; int LineHeight() { return IsSmallFontTall() ? 18 : 14; } int BlankLineHeight() { return 12; } int DividerLineMarginY() { return BlankLineHeight() / 2; } int HeaderHeight() { return PaddingTop + LineHeight() + 2 * BlankLineHeight() + DividerLineMarginY(); } int ContentPaddingY() { return BlankLineHeight(); } int ContentsTextHeight() { return PanelHeight - HeaderHeight() - DividerLineMarginY() - 2 * ContentPaddingY() - BlankLineHeight(); } int NumVisibleLines() { return (ContentsTextHeight() - 1) / LineHeight() + 1; // Ceil } void DrawHelpSlider(const Surface &out) { const Point uiPosition = GetUIRectangle().position; const int sliderXPos = ContentTextWidth + uiPosition.x + 36; int sliderStart = uiPosition.y + HeaderHeight() + LineHeight() + 3; const int sliderEnd = uiPosition.y + PaddingTop + PanelHeight - 12; ClxDraw(out, { sliderXPos, sliderStart }, (*pSTextSlidCels)[11]); sliderStart += 12; int sliderCurrent = sliderStart; for (; sliderCurrent < sliderEnd; sliderCurrent += 12) { ClxDraw(out, { sliderXPos, sliderCurrent }, (*pSTextSlidCels)[13]); } ClxDraw(out, { sliderXPos, sliderCurrent }, (*pSTextSlidCels)[10]); // Subtract visible lines from the total number of lines to get the actual // scroll range const int scrollRange = static_cast(HelpTextLines.size()) - NumVisibleLines(); // Subtract the size of the arrow buttons to get the length of the interior // part of the slider const int sliderLength = sliderCurrent - 12 - sliderStart; ClxDraw(out, { sliderXPos, sliderStart + ((static_cast(SkipLines) * sliderLength) / scrollRange) }, (*pSTextSlidCels)[12]); } } // namespace void InitHelp() { static bool Initialized = false; if (Initialized) return; HelpFlag = false; for (const auto *text : HelpText) { const std::string paragraph = WordWrapString(_(text), ContentTextWidth); size_t previous = 0; while (true) { const size_t next = paragraph.find('\n', previous); HelpTextLines.emplace_back(paragraph.substr(previous, next - previous)); if (next == std::string::npos) break; previous = next + 1; } } Initialized = true; } void DrawHelp(const Surface &out) { DrawSTextHelp(); DrawQTextBack(out); const int lineHeight = LineHeight(); const int blankLineHeight = BlankLineHeight(); std::string_view title; if (gbIsHellfire) title = gbIsSpawn ? _("Shareware Hellfire Help") : _("Hellfire Help"); else title = gbIsSpawn ? _("Shareware Diablo Help") : _("Diablo Help"); const Point uiPosition = GetUIRectangle().position; const int sx = uiPosition.x + PaddingLeft; const int sy = uiPosition.y; DrawString(out, title, { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter }); const int titleBottom = sy + HeaderHeight(); DrawSLine(out, titleBottom); const int numLines = NumVisibleLines(); const int contentY = titleBottom + DividerLineMarginY() + ContentPaddingY(); for (int i = 0; i < numLines; i++) { const std::string_view line = HelpTextLines[i + SkipLines]; if (line.empty()) { continue; } int offset = 0; UiFlags style = UiFlags::ColorWhite; if (line[0] == '$') { offset = 1; style = UiFlags::ColorBlue; } DrawString(out, line.substr(offset), { { sx, contentY + i * lineHeight }, { ContentTextWidth, lineHeight } }, { .flags = style, .lineHeight = lineHeight }); } DrawString(out, _("Press ESC to end or the arrow keys to scroll."), { { sx, contentY + ContentsTextHeight() + ContentPaddingY() + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter }); DrawHelpSlider(out); } void DisplayHelp() { SkipLines = 0; HelpFlag = true; ChatLogFlag = false; } void HelpScrollUp() { if (SkipLines > 0) SkipLines--; } void HelpScrollDown() { if (SkipLines + NumVisibleLines() < HelpTextLines.size()) SkipLines++; } } // namespace devilution ================================================ FILE: Source/help.h ================================================ /** * @file help.h * * Interface of the in-game help text. */ #pragma once #include "engine/surface.hpp" namespace devilution { extern bool HelpFlag; void InitHelp(); void DrawHelp(const Surface &out); void DisplayHelp(); void HelpScrollUp(); void HelpScrollDown(); } // namespace devilution ================================================ FILE: Source/hwcursor.cpp ================================================ #include "hwcursor.hpp" #include #include #ifdef USE_SDL3 #include #include #include #include #include #else #include #ifndef USE_SDL1 #include #include #include #endif #endif #include "DiabloUI/diabloui.h" #include "appfat.h" #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/render/clx_render.hpp" #include "engine/surface.hpp" #include "utils/display.h" #include "utils/sdl_bilinear_scale.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_wrap.h" namespace devilution { namespace { CursorInfo CurrentCursorInfo; #if SDL_VERSION_ATLEAST(2, 0, 0) SDLCursorUniquePtr CurrentCursor; enum class HotpointPosition : uint8_t { TopLeft, Center, }; Size ScaledSize(Size size) { if (renderer != nullptr) { #ifdef USE_SDL3 SDL_FRect logicalDstRect; int logicalWidth; int logicalHeight; if (!SDL_GetRenderLogicalPresentation(renderer, &logicalWidth, &logicalHeight, /*mode=*/nullptr)) { LogError("SDL_GetRenderOutputSize: {}", SDL_GetError()); SDL_ClearError(); return size; } if (!SDL_GetRenderLogicalPresentationRect(renderer, &logicalDstRect)) { LogError("SDL_GetRenderLogicalPresentationRect: {}", SDL_GetError()); SDL_ClearError(); return size; } const float dispScale = SDL_GetWindowDisplayScale(ghMainWnd); if (dispScale == 0.0F) { LogError("SDL_GetWindowDisplayScale: {}", SDL_GetError()); SDL_ClearError(); return size; } const float scaleX = logicalDstRect.w / static_cast(logicalWidth); const float scaleY = logicalDstRect.h / static_cast(logicalHeight); size.width = static_cast(static_cast(size.width) * scaleX / dispScale); size.height = static_cast(static_cast(size.height) * scaleY / dispScale); #else float scaleX = 1.0F; float scaleY = 1.0F; if (!SDL_GetRenderScale(renderer, &scaleX, &scaleY)) { LogError("SDL_GetRenderScale: {}", SDL_GetError()); SDL_ClearError(); } size.width = static_cast(size.width * scaleX); size.height = static_cast(size.height * scaleY); #endif } return size; } bool IsCursorSizeAllowed(Size size) { if (*GetOptions().Graphics.hardwareCursorMaxSize <= 0) return true; size = ScaledSize(size); return size.width <= *GetOptions().Graphics.hardwareCursorMaxSize && size.height <= *GetOptions().Graphics.hardwareCursorMaxSize; } Point GetHotpointPosition(const SDL_Surface &surface, HotpointPosition position) { switch (position) { case HotpointPosition::TopLeft: return { 0, 0 }; case HotpointPosition::Center: return { surface.w / 2, surface.h / 2 }; } app_fatal("Unhandled enum value"); } bool ShouldUseBilinearScaling() { return *GetOptions().Graphics.scaleQuality != ScalingQuality::NearestPixel; } bool SetHardwareCursorFromSurface(SDL_Surface *surface, HotpointPosition hotpointPosition) { SDLCursorUniquePtr newCursor; const Size size { surface->w, surface->h }; const Size scaledSize = ScaledSize(size); if (size == scaledSize) { #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursorFromSurface {}x{}", size.width, size.height); #endif const Point hotpoint = GetHotpointPosition(*surface, hotpointPosition); newCursor = SDLCursorUniquePtr { SDL_CreateColorCursor(surface, hotpoint.x, hotpoint.y) }; } else { // SDL does not support BlitScaled from 8-bit to RGBA. const SDLSurfaceUniquePtr converted { #ifdef USE_SDL3 SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888) #else SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_ARGB8888, 0) #endif }; const SDLSurfaceUniquePtr scaledSurface = SDLWrap::CreateRGBSurfaceWithFormat(0, scaledSize.width, scaledSize.height, 32, SDL_PIXELFORMAT_ARGB8888); if (ShouldUseBilinearScaling()) { #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursorFromSurface {}x{} scaled to {}x{} using bilinear scaling", size.width, size.height, scaledSize.width, scaledSize.height); #endif BilinearScale32(converted.get(), scaledSurface.get()); } else { #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursorFromSurface {}x{} scaled to {}x{} using nearest neighbour scaling", size.width, size.height, scaledSize.width, scaledSize.height); #endif #ifdef USE_SDL3 SDL_BlitSurfaceScaled(converted.get(), nullptr, scaledSurface.get(), nullptr, SDL_SCALEMODE_PIXELART); #else SDL_BlitScaled(converted.get(), nullptr, scaledSurface.get(), nullptr); #endif } const Point hotpoint = GetHotpointPosition(*scaledSurface, hotpointPosition); newCursor = SDLCursorUniquePtr { SDL_CreateColorCursor(scaledSurface.get(), hotpoint.x, hotpoint.y) }; } if (newCursor == nullptr) { LogError("SDL_CreateColorCursor: {}", SDL_GetError()); SDL_ClearError(); return false; } #ifdef USE_SDL3 if (!SDL_SetCursor(newCursor.get())) { LogError("SDL_SetCursor: {}", SDL_GetError()); SDL_ClearError(); return false; } #else SDL_SetCursor(newCursor.get()); #endif CurrentCursor = std::move(newCursor); return true; } bool SetHardwareCursorFromClxSprite(ClxSprite sprite, HotpointPosition hotpointPosition) { const OwnedSurface surface { sprite.width(), sprite.height() }; if (!SDLC_SetSurfacePalette(surface.surface, Palette.get())) { LogError("SDL_SetSurfacePalette: {}", SDL_GetError()); SDL_ClearError(); return false; } if (!SDL_SetSurfaceColorKey(surface.surface, true, 0)) { LogError("SDL_SetSurfaceColorKey: {}", SDL_GetError()); SDL_ClearError(); return false; } RenderClxSprite(surface, sprite, { 0, 0 }); return SetHardwareCursorFromSurface(surface.surface, hotpointPosition); } bool SetHardwareCursorFromSprite(int pcurs) { const bool isItem = !MyPlayer->HoldItem.isEmpty(); if (isItem && !*GetOptions().Graphics.hardwareCursorForItems) return false; const int outlineWidth = isItem ? 1 : 0; auto size = GetInvItemSize(pcurs); size.width += 2 * outlineWidth; size.height += 2 * outlineWidth; if (!IsCursorSizeAllowed(size)) return false; const OwnedSurface out { size }; SDL_SetSurfacePalette(out.surface, Palette.get()); // Transparent color must not be used in the sprite itself. // Colors 1-127 are outside of the UI palette so are safe to use. constexpr std::uint8_t TransparentColor = 1; if (!SDL_FillSurfaceRect(out.surface, nullptr, TransparentColor)) { LogError("SDL_FillSurfaceRect: {}", SDL_GetError()); SDL_ClearError(); return false; } if (!SDL_SetSurfaceColorKey(out.surface, true, TransparentColor)) { LogError("SDL_SetSurfaceColorKey: {}", SDL_GetError()); SDL_ClearError(); return false; } DrawSoftwareCursor(out, { outlineWidth, size.height - outlineWidth - 1 }, pcurs); const bool result = SetHardwareCursorFromSurface( out.surface, isItem ? HotpointPosition::Center : HotpointPosition::TopLeft); return result; } #endif } // namespace CursorInfo &GetCurrentCursorInfo() { return CurrentCursorInfo; } void SetHardwareCursor(CursorInfo cursorInfo) { #if SDL_VERSION_ATLEAST(2, 0, 0) CurrentCursorInfo = cursorInfo; CurrentCursorInfo.setNeedsReinitialization(false); switch (cursorInfo.type()) { case CursorType::Game: #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursor Game"); #endif CurrentCursorInfo.SetEnabled(SetHardwareCursorFromSprite(cursorInfo.id())); break; case CursorType::UserInterface: #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursor UserInterface"); #endif // ArtCursor is null while loading the game on the progress screen, // called via palette fade from ShowProgress. CurrentCursorInfo.SetEnabled( ArtCursor && IsCursorSizeAllowed(Size { (*ArtCursor)[0].width(), (*ArtCursor)[0].height() }) && SetHardwareCursorFromClxSprite((*ArtCursor)[0], HotpointPosition::TopLeft)); break; case CursorType::Unknown: #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursor Unknown"); #endif CurrentCursorInfo.SetEnabled(false); break; } if (!CurrentCursorInfo.Enabled()) SetHardwareCursorVisible(false); #endif } } // namespace devilution ================================================ FILE: Source/hwcursor.hpp ================================================ /** * @file hwcursor.hpp * * Hardware cursor (SDL2 only). */ #pragma once #include #ifdef USE_SDL3 #include #include #else #include #endif #include "options.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" // Set this to 1 to log the hardware cursor state changes. #define LOG_HWCURSOR 0 namespace devilution { // Whether the hardware cursor is enabled in settings. inline bool IsHardwareCursorEnabled() { #if SDL_VERSION_ATLEAST(2, 0, 0) return *GetOptions().Graphics.hardwareCursor && HardwareCursorSupported(); #else return false; #endif } enum class CursorType : uint8_t { Unknown, UserInterface, Game, }; class CursorInfo { public: CursorInfo() = default; static CursorInfo UserInterfaceCursor() { return CursorInfo { CursorType::UserInterface }; } static CursorInfo GameCursor(int gameSpriteId) { return CursorInfo { CursorType::Game, gameSpriteId }; } static CursorInfo UnknownCursor() { return CursorInfo { CursorType::Unknown }; } [[nodiscard]] CursorType type() const { return type_; } [[nodiscard]] int id() const { return id_; } [[nodiscard]] bool Enabled() const { return enabled_; } void SetEnabled(bool value) { #if LOG_HWCURSOR if (enabled_ != value) { Log("hwcursor: SetEnabled {}", value); } #endif enabled_ = value; } [[nodiscard]] bool needsReinitialization() const { return needs_reinitialization_; } void setNeedsReinitialization(bool value) { #if LOG_HWCURSOR if (needs_reinitialization_ != value) { Log("hwcursor: setNeedsReinitialization {}", value); } #endif needs_reinitialization_ = value; } bool operator==(const CursorInfo &other) const { return type_ == other.type_ && (type_ != CursorType::Game || id_ == other.id_); } bool operator!=(const CursorInfo &other) const { return !(*this == other); } private: explicit CursorInfo(CursorType type, int id = 0) : type_(type) , id_(id) , enabled_(false) { } CursorType type_ = CursorType::Unknown; // ID for CursorType::Game int id_; bool enabled_ = false; bool needs_reinitialization_ = false; }; CursorInfo &GetCurrentCursorInfo(); // Whether the current cursor is a hardware cursor. inline bool IsHardwareCursor() { return GetCurrentCursorInfo().Enabled(); } void SetHardwareCursor(CursorInfo cursorInfo); inline void DoReinitializeHardwareCursor() { #if LOG_HWCURSOR Log("hwcursor: DoReinitializeHardwareCursor"); #endif SetHardwareCursor(GetCurrentCursorInfo()); } inline bool IsHardwareCursorVisible() { #ifndef USE_SDL1 return SDL_CursorVisible(); #else return false; #endif } inline void SetHardwareCursorVisible(bool visible) { #if SDL_VERSION_ATLEAST(2, 0, 0) if (IsHardwareCursorVisible() == visible) return; if (visible && GetCurrentCursorInfo().needsReinitialization()) { DoReinitializeHardwareCursor(); } #if LOG_HWCURSOR Log("hwcursor: SetHardwareCursorVisible {}", visible); #endif if (!(visible ? SDLC_ShowCursor() : SDLC_HideCursor())) { LogError("{}", SDL_GetError()); SDL_ClearError(); } #endif } inline void ReinitializeHardwareCursor() { if (IsHardwareCursorVisible()) { DoReinitializeHardwareCursor(); } else { GetCurrentCursorInfo().setNeedsReinitialization(true); } } } // namespace devilution ================================================ FILE: Source/init.cpp ================================================ /** * @file init.cpp * * Implementation of routines for initializing the environment, disable screen saver, load MPQ. */ #include "init.hpp" #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "DiabloUI/diabloui.h" #include "engine/assets.hpp" #include "engine/backbuffer_state.hpp" #include "engine/dx.h" #include "engine/events.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "options.h" #include "pfile.h" #include "utils/file_util.h" #include "utils/language.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/str_split.hpp" #include "utils/ui_fwd.h" #include "utils/utf8.hpp" #ifndef UNPACKED_MPQS #include "mpq/mpq_common.hpp" #include "mpq/mpq_reader.hpp" #endif #ifdef __vita__ // increase default allowed heap size on Vita int _newlib_heap_size_user = 100 * 1024 * 1024; #endif namespace devilution { bool gbActive; namespace { constexpr char DevilutionXMpqVersion[] = "1\n"; constexpr char ExtraFontsVersion[] = "1\n"; bool AssetContentsEq(AssetRef &&ref, std::string_view expected) { const size_t size = ref.size(); AssetHandle handle = OpenAsset(std::move(ref), false); if (!handle.ok()) return false; const std::unique_ptr contents { new char[size] }; if (!handle.read(contents.get(), size)) return false; return std::string_view { contents.get(), size } == expected; } bool CheckDevilutionXMpqVersion(AssetRef &&ref) { return !AssetContentsEq(std::move(ref), DevilutionXMpqVersion); } bool CheckExtraFontsVersion(AssetRef &&ref) { return !AssetContentsEq(std::move(ref), ExtraFontsVersion); } } // namespace bool IsDevilutionXMpqOutOfDate() { AssetRef ref = FindAsset("ASSETS_VERSION"); if (!ref.ok()) return true; return CheckDevilutionXMpqVersion(std::move(ref)); } #ifdef UNPACKED_MPQS bool AreExtraFontsOutOfDate(std::string_view path) { const std::string versionPath = StrCat(path, "fonts" DIRECTORY_SEPARATOR_STR "VERSION"); if (versionPath.size() + 1 > AssetRef::PathBufSize) app_fatal("Path too long"); AssetRef ref; *BufCopy(ref.path, versionPath) = '\0'; return CheckExtraFontsVersion(std::move(ref)); } #else bool AreExtraFontsOutOfDate(MpqArchive &archive) { const char filename[] = "fonts\\VERSION"; const MpqFileHash fileHash = CalculateMpqFileHash(filename); uint32_t fileNumber; if (!archive.GetFileNumber(fileHash, fileNumber)) return true; AssetRef ref; ref.archive = &archive; ref.fileNumber = fileNumber; ref.filename = filename; return CheckExtraFontsVersion(std::move(ref)); } #endif bool AreExtraFontsOutOfDate() { const auto it = MpqArchives.find(FontMpqPriority); return it != MpqArchives.end() && AreExtraFontsOutOfDate(it->second); } void init_cleanup() { if (gbIsMultiplayer && gbRunGame) { pfile_write_hero(/*writeGameData=*/false); sfile_write_stash(); } MpqArchives.clear(); HasHellfireMpq = false; NetClose(); } void init_create_window() { if (!SpawnWindow(PROJECT_NAME)) app_fatal(_("Unable to create main window")); dx_init(); gbActive = true; #ifndef USE_SDL1 SDL_DisableScreenSaver(); #endif } void MainWndProc(const SDL_Event &event) { #ifndef USE_SDL1 #ifdef USE_SDL3 switch (event.type) #else if (event.type != SDL_WINDOWEVENT) return; switch (event.window.event) #endif { #ifdef USE_SDL3 case SDL_EVENT_WINDOW_HIDDEN: case SDL_EVENT_WINDOW_MINIMIZED: #else case SDL_WINDOWEVENT_HIDDEN: case SDL_WINDOWEVENT_MINIMIZED: #endif gbActive = false; break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_SHOWN: case SDL_EVENT_WINDOW_EXPOSED: case SDL_EVENT_WINDOW_RESTORED: #else case SDL_WINDOWEVENT_SHOWN: case SDL_WINDOWEVENT_EXPOSED: case SDL_WINDOWEVENT_RESTORED: #endif gbActive = true; RedrawEverything(); break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: #else case SDL_WINDOWEVENT_SIZE_CHANGED: #endif ReinitializeHardwareCursor(); break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_MOUSE_LEAVE: #else case SDL_WINDOWEVENT_LEAVE: #endif sgbMouseDown = CLICK_NONE; LastPlayerAction = PlayerActionType::None; RedrawEverything(); break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_CLOSE_REQUESTED: #else case SDL_WINDOWEVENT_CLOSE: #endif diablo_quit(0); break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_FOCUS_LOST: #else case SDL_WINDOWEVENT_FOCUS_LOST: #endif if (*GetOptions().Gameplay.pauseOnFocusLoss) diablo_focus_pause(); break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_FOCUS_GAINED: #else case SDL_WINDOWEVENT_FOCUS_GAINED: #endif if (*GetOptions().Gameplay.pauseOnFocusLoss) diablo_focus_unpause(); break; #ifdef USE_SDL3 default: break; #else case SDL_WINDOWEVENT_MOVED: case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_MAXIMIZED: case SDL_WINDOWEVENT_ENTER: case SDL_WINDOWEVENT_TAKE_FOCUS: break; default: LogVerbose("Unhandled SDL_WINDOWEVENT event: {:d}", event.window.event); break; #endif } #else if (event.type != SDL_ACTIVEEVENT) return; if ((event.active.state & SDL_APPINPUTFOCUS) != 0) { if (event.active.gain == 0) diablo_focus_pause(); else diablo_focus_unpause(); } #endif } } // namespace devilution ================================================ FILE: Source/init.hpp ================================================ /** * @file init.hpp * * Interface of routines for initializing the environment, disable screen saver, load MPQ. */ #pragma once // Unused here but must be included before SDL.h, see: // https://github.com/bebbo/amiga-gcc/issues/413 #include #ifdef USE_SDL3 #include #else #include #endif #ifdef UNPACKED_MPQS #include #else #include "mpq/mpq_reader.hpp" #endif namespace devilution { /** True if the game is the current active window */ extern bool gbActive; [[nodiscard]] bool IsDevilutionXMpqOutOfDate(); #ifdef UNPACKED_MPQS bool AreExtraFontsOutOfDate(std::string_view path); #else bool AreExtraFontsOutOfDate(MpqArchive &archive); #endif [[nodiscard]] bool AreExtraFontsOutOfDate(); void init_cleanup(); void init_create_window(); void MainWndProc(const SDL_Event &event); } // namespace devilution ================================================ FILE: Source/interfac.cpp ================================================ /** * @file interfac.cpp * * Implementation of load screens. */ #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #else #include #endif #include #include "control/control.hpp" #include "controls/input.h" #include "engine/clx_sprite.hpp" #include "engine/dx.h" #include "engine/events.hpp" #include "engine/load_cel.hpp" #include "engine/load_clx.hpp" #include "engine/palette.h" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "loadsave.h" #include "multi.h" #include "pfile.h" #include "plrmsg.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/sdl_thread.h" #ifndef USE_SDL1 #include "controls/touch/renderers.h" #endif #ifdef __DJGPP__ #define LOAD_ON_MAIN_THREAD #endif namespace devilution { namespace { constexpr uint32_t MaxProgress = 534; constexpr uint32_t ProgressStepSize = 23; OptionalOwnedClxSpriteList sgpBackCel; bool IsProgress; uint32_t sgdwProgress; int progress_id; /** The color used for the progress bar as an index into the palette. */ const uint8_t BarColor[3] = { 138, 43, 254 }; /** The screen position of the top left corner of the progress bar. */ const int BarPos[3][2] = { { 53, 37 }, { 53, 421 }, { 53, 37 } }; OptionalOwnedClxSpriteList ArtCutsceneWidescreen; SdlEventType CustomEventType = SDL_EVENT_USER; Cutscenes GetCutSceneFromLevelType(dungeon_type type) { switch (type) { case DTYPE_TOWN: return CutTown; case DTYPE_CATHEDRAL: return CutLevel1; case DTYPE_CATACOMBS: return CutLevel2; case DTYPE_CAVES: return CutLevel3; case DTYPE_HELL: return CutLevel4; case DTYPE_NEST: return CutLevel6; case DTYPE_CRYPT: return CutLevel5; default: return CutLevel1; } } Cutscenes PickCutscene(interface_mode uMsg) { switch (uMsg) { case WM_DIABLOADGAME: case WM_DIABNEWGAME: return CutStart; case WM_DIABRETOWN: return CutTown; case WM_DIABNEXTLVL: case WM_DIABPREVLVL: case WM_DIABTOWNWARP: case WM_DIABTWARPUP: { const int lvl = MyPlayer->plrlevel; if (lvl == 1 && uMsg == WM_DIABNEXTLVL) return CutTown; if (lvl == 16 && uMsg == WM_DIABNEXTLVL) return CutGate; return GetCutSceneFromLevelType(GetLevelType(lvl)); } case WM_DIABWARPLVL: return CutPortal; case WM_DIABSETLVL: case WM_DIABRTNLVL: if (setlvlnum == SL_BONECHAMB) return CutLevel2; if (setlvlnum == SL_VILEBETRAYER) return CutPortalRed; if (IsArenaLevel(setlvlnum)) { if (uMsg == WM_DIABSETLVL) return GetCutSceneFromLevelType(setlvltype); return CutTown; } return CutLevel1; default: app_fatal("Unknown progress mode"); } } void LoadCutsceneBackground(interface_mode uMsg) { const char *celPath; const char *palPath; switch (PickCutscene(uMsg)) { case CutStart: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutstartw.clx"); celPath = "gendata\\cutstart"; palPath = "gendata\\cutstart.pal"; progress_id = 1; break; case CutTown: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutttw.clx"); celPath = "gendata\\cuttt"; palPath = "gendata\\cuttt.pal"; progress_id = 1; break; case CutLevel1: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutl1dw.clx"); celPath = "gendata\\cutl1d"; palPath = "gendata\\cutl1d.pal"; progress_id = 0; break; case CutLevel2: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut2w.clx"); celPath = "gendata\\cut2"; palPath = "gendata\\cut2.pal"; progress_id = 2; break; case CutLevel3: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut3w.clx"); celPath = "gendata\\cut3"; palPath = "gendata\\cut3.pal"; progress_id = 1; break; case CutLevel4: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut4w.clx"); celPath = "gendata\\cut4"; palPath = "gendata\\cut4.pal"; progress_id = 1; break; case CutLevel5: ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl5w.clx"); celPath = "nlevels\\cutl5"; palPath = "nlevels\\cutl5.pal"; progress_id = 1; break; case CutLevel6: ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl6w.clx"); celPath = "nlevels\\cutl6"; palPath = "nlevels\\cutl6.pal"; progress_id = 1; break; case CutPortal: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutportlw.clx"); celPath = "gendata\\cutportl"; palPath = "gendata\\cutportl.pal"; progress_id = 1; break; case CutPortalRed: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutportrw.clx"); celPath = "gendata\\cutportr"; palPath = "gendata\\cutportr.pal"; progress_id = 1; break; case CutGate: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutgatew.clx"); celPath = "gendata\\cutgate"; palPath = "gendata\\cutgate.pal"; progress_id = 1; break; } assert(!sgpBackCel); sgpBackCel = LoadCel(celPath, 640); LoadPalette(palPath); UpdateSystemPalette(logical_palette); sgdwProgress = 0; } void FreeCutsceneBackground() { sgpBackCel = std::nullopt; ArtCutsceneWidescreen = std::nullopt; } void DrawCutsceneBackground() { const Rectangle &uiRectangle = GetUIRectangle(); const Surface &out = GlobalBackBuffer(); SDL_FillSurfaceRect(out.surface, nullptr, 0); if (ArtCutsceneWidescreen) { const ClxSprite sprite = (*ArtCutsceneWidescreen)[0]; RenderClxSprite(out, sprite, { uiRectangle.position.x - (sprite.width() - uiRectangle.size.width) / 2, uiRectangle.position.y }); } ClxDraw(out, { uiRectangle.position.x, 480 - 1 + uiRectangle.position.y }, (*sgpBackCel)[0]); } void DrawCutsceneForeground() { const Rectangle &uiRectangle = GetUIRectangle(); const Surface &out = GlobalBackBuffer(); constexpr int ProgressHeight = 22; SDL_Rect rect = MakeSdlRect( out.region.x + BarPos[progress_id][0] + uiRectangle.position.x, out.region.y + BarPos[progress_id][1] + uiRectangle.position.y, sgdwProgress, ProgressHeight); SDL_FillSurfaceRect(out.surface, &rect, BarColor[progress_id]); if (DiabloUiSurface() == PalSurface) BltFast(&rect, &rect); RenderPresent(); } struct { uint32_t loadStartedAt; EventHandler prevHandler; bool skipRendering; bool done; uint32_t drawnProgress; std::array palette; } ProgressEventHandlerState; void InitRendering() { // Blit the background once and then free it. DrawCutsceneBackground(); if (RenderDirectlyToOutputSurface && PalSurface != nullptr) { // Render into all the backbuffers if there are multiple. const void *initialPixels = PalSurface->pixels; if (DiabloUiSurface() == PalSurface) BltFast(nullptr, nullptr); RenderPresent(); while (PalSurface->pixels != initialPixels) { DrawCutsceneBackground(); if (DiabloUiSurface() == PalSurface) BltFast(nullptr, nullptr); RenderPresent(); } } FreeCutsceneBackground(); // The loading thread sets `logical_palette`, so we make sure to use // our own palette for the fade-in. PaletteFadeIn(8, ProgressEventHandlerState.palette); } void CheckShouldSkipRendering() { if (!ProgressEventHandlerState.skipRendering) return; const bool shouldSkip = ProgressEventHandlerState.loadStartedAt + *GetOptions().Gameplay.skipLoadingScreenThresholdMs > SDL_GetTicks(); if (shouldSkip) return; ProgressEventHandlerState.skipRendering = false; if (!HeadlessMode) InitRendering(); } bool HandleProgressBarUpdate() { CheckShouldSkipRendering(); SDL_Event event; // We use the real `PollEvent` here instead of `FetchMessage` // to process real events rather than the recorded ones in demo mode. while (PollEvent(&event)) { CheckShouldSkipRendering(); if (event.type != SDL_EVENT_QUIT) { HandleMessage(event, SDL_GetModState()); } if (ProgressEventHandlerState.done) return false; } return true; } void DoLoad(interface_mode uMsg) { IncProgress(); sound_init(); IncProgress(); Player &myPlayer = *MyPlayer; tl::expected loadResult; switch (uMsg) { case WM_DIABLOADGAME: IncProgress(2); loadResult = LoadGame(true); if (loadResult.has_value()) IncProgress(2); break; case WM_DIABNEWGAME: myPlayer.pOriginalCathedral = !gbIsHellfire; IncProgress(); FreeGameMem(); IncProgress(); pfile_remove_temp_files(); IncProgress(); loadResult = LoadGameLevel(true, ENTRY_MAIN); if (loadResult.has_value()) IncProgress(); break; case WM_DIABNEXTLVL: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); setlevel = false; currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_MAIN); if (loadResult.has_value()) IncProgress(); break; case WM_DIABPREVLVL: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); currlevel--; leveltype = GetLevelType(currlevel); assert(myPlayer.isOnActiveLevel()); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_PREV); if (loadResult.has_value()) IncProgress(); break; case WM_DIABSETLVL: // Note: ReturnLevel, ReturnLevelType and ReturnLvlPosition is only set to ensure vanilla compatibility ReturnLevel = GetMapReturnLevel(); ReturnLevelType = GetLevelType(ReturnLevel); ReturnLvlPosition = GetMapReturnPosition(); IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); setlevel = true; leveltype = setlvltype; currlevel = static_cast(setlvlnum); FreeGameMem(); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_SETLVL); if (loadResult.has_value()) IncProgress(); break; case WM_DIABRTNLVL: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); setlevel = false; FreeGameMem(); IncProgress(); currlevel = GetMapReturnLevel(); leveltype = GetLevelType(currlevel); loadResult = LoadGameLevel(false, ENTRY_RTNLVL); if (loadResult.has_value()) IncProgress(); break; case WM_DIABWARPLVL: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); GetPortalLevel(); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_WARPLVL); if (loadResult.has_value()) IncProgress(); break; case WM_DIABTOWNWARP: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); setlevel = false; currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_TWARPDN); if (loadResult.has_value()) IncProgress(); break; case WM_DIABTWARPUP: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_TWARPUP); if (loadResult.has_value()) IncProgress(); break; case WM_DIABRETOWN: IncProgress(); if (!gbIsMultiplayer) { pfile_save_level(); } else { DeltaSaveLevel(); } IncProgress(); FreeGameMem(); setlevel = false; currlevel = myPlayer.plrlevel; leveltype = GetLevelType(currlevel); IncProgress(); loadResult = LoadGameLevel(false, ENTRY_MAIN); if (loadResult.has_value()) IncProgress(); break; default: loadResult = tl::make_unexpected("Unknown progress mode"); break; } if (!loadResult.has_value()) { SDL_Event event; CustomEventToSdlEvent(event, WM_ERROR); event.user.data1 = new std::string(std::move(loadResult).error()); if (!SDLC_PushEvent(&event)) { LogError("Failed to send WM_ERROR {}", SDL_GetError()); SDL_ClearError(); } #ifdef LOAD_ON_MAIN_THREAD HandleProgressBarUpdate(); #endif return; } SDL_Event event; CustomEventToSdlEvent(event, WM_DONE); if (!SDLC_PushEvent(&event)) { LogError("Failed to send WM_DONE {}", SDL_GetError()); SDL_ClearError(); } #ifdef LOAD_ON_MAIN_THREAD HandleProgressBarUpdate(); #endif } void ProgressEventHandler(const SDL_Event &event, uint16_t modState) { DisableInputEventHandler(event, modState); if (!IsCustomEvent(event.type)) return; const interface_mode customEvent = GetCustomEvent(event); switch (customEvent) { case WM_PROGRESS: if (!HeadlessMode && ProgressEventHandlerState.drawnProgress != sgdwProgress && !ProgressEventHandlerState.skipRendering) { DrawCutsceneForeground(); ProgressEventHandlerState.drawnProgress = sgdwProgress; } break; case WM_ERROR: app_fatal(*static_cast(event.user.data1)); break; case WM_DONE: { if (!ProgressEventHandlerState.skipRendering) { NewCursor(CURSOR_HAND); if (!HeadlessMode) { assert(ghMainWnd); // The loading thread sets `logical_palette`, so we make sure to use // our own palette for drawing the foreground. UpdateSystemPalette(ProgressEventHandlerState.palette); // Ensure that all back buffers have the full progress bar. const void *initialPixels = PalSurface->pixels; do { DrawCutsceneForeground(); if (DiabloUiSurface() == PalSurface) BltFast(nullptr, nullptr); RenderPresent(); } while (PalSurface->pixels != initialPixels); // The loading thread sets `logical_palette`, so we make sure to use // our own palette for the fade-out. PaletteFadeOut(8, ProgressEventHandlerState.palette); // Once the fade-out is done, restore the system palette. UpdateSystemPalette(logical_palette); } } [[maybe_unused]] EventHandler prevHandler = SetEventHandler(ProgressEventHandlerState.prevHandler); assert(prevHandler == ProgressEventHandler); ProgressEventHandlerState.prevHandler = nullptr; IsProgress = false; const Player &myPlayer = *MyPlayer; NetSendCmdLocParam2(true, CMD_PLAYER_JOINLEVEL, myPlayer.position.tile, myPlayer.plrlevel, myPlayer.plrIsOnSetLevel ? 1 : 0); DelayPlrMessages(SDL_GetTicks() - ProgressEventHandlerState.loadStartedAt); if (gbSomebodyWonGameKludge && myPlayer.isOnLevel(16)) { PrepDoEnding(); } gbSomebodyWonGameKludge = false; ProgressEventHandlerState.done = true; #if !defined(USE_SDL1) && !defined(__vita__) if (renderer != nullptr) { InitVirtualGamepadTextures(*renderer); } #endif } break; default: app_fatal("Unknown progress mode"); break; } #ifdef LOAD_ON_MAIN_THREAD HandleProgressBarUpdate(); #endif } } // namespace void RegisterCustomEvents() { #ifndef USE_SDL1 CustomEventType = SDL_RegisterEvents(1); #endif } bool IsCustomEvent(SdlEventType eventType) { return eventType == CustomEventType; } interface_mode GetCustomEvent(const SDL_Event &event) { return static_cast(event.user.code); } void CustomEventToSdlEvent(SDL_Event &event, interface_mode eventType) { event.type = CustomEventType; event.user.code = static_cast(eventType); } void interface_msg_pump() { SDL_Event event; uint16_t modState; while (FetchMessage(&event, &modState)) { if (event.type != SDL_EVENT_QUIT) { HandleMessage(event, modState); } } } void IncProgress(uint32_t steps) { if (!IsProgress) return; const uint32_t prevProgress = sgdwProgress; sgdwProgress += ProgressStepSize * steps; if (sgdwProgress > MaxProgress) sgdwProgress = MaxProgress; if (!HeadlessMode && sgdwProgress != prevProgress) { SDL_Event event; CustomEventToSdlEvent(event, WM_PROGRESS); if (!SDLC_PushEvent(&event)) { LogError("Failed to send WM_PROGRESS {}", SDL_GetError()); SDL_ClearError(); } #ifdef LOAD_ON_MAIN_THREAD HandleProgressBarUpdate(); #endif } } void CompleteProgress() { if (HeadlessMode) return; if (!IsProgress) return; if (sgdwProgress < MaxProgress) { IncProgress((MaxProgress - sgdwProgress) / ProgressStepSize); } } void ShowProgress(interface_mode uMsg) { IsProgress = true; gbSomebodyWonGameKludge = false; ProgressEventHandlerState.loadStartedAt = SDL_GetTicks(); ProgressEventHandlerState.prevHandler = SetEventHandler(ProgressEventHandler); ProgressEventHandlerState.skipRendering = true; ProgressEventHandlerState.done = false; ProgressEventHandlerState.drawnProgress = 0; #ifndef USE_SDL1 DeactivateVirtualGamepad(); FreeVirtualGamepadTextures(); #endif if (!HeadlessMode) { assert(ghMainWnd); interface_msg_pump(); ClearScreenBuffer(); scrollrt_draw_game_screen(); if (IsHardwareCursor()) SetHardwareCursorVisible(false); BlackPalette(); // Always load the background (even if we end up skipping rendering it). // This is because the MPQ archive can only be read by a single thread at a time. LoadCutsceneBackground(uMsg); // Save the palette at this point because the loading process may replace it. ProgressEventHandlerState.palette = logical_palette; } // Begin loading #ifdef LOAD_ON_MAIN_THREAD const uint32_t start = SDL_GetTicks(); DoLoad(uMsg); LogVerbose("Loading finished in {}ms", SDL_GetTicks() - start); #else static interface_mode loadTarget; loadTarget = uMsg; SdlThread loadThread = SdlThread([]() { const uint32_t start = SDL_GetTicks(); DoLoad(loadTarget); LogVerbose("Load thread finished in {}ms", SDL_GetTicks() - start); }); while (HandleProgressBarUpdate()) { } loadThread.join(); #endif } } // namespace devilution ================================================ FILE: Source/interfac.h ================================================ /** * @file interfac.h * * Interface of load screens. */ #pragma once #include #ifdef USE_SDL3 #include #include #else #include #endif #include "utils/ui_fwd.h" namespace devilution { /** * @brief Custom events. */ enum interface_mode : uint8_t { WM_DIABNEXTLVL = 0, WM_DIABPREVLVL, WM_DIABRTNLVL, WM_DIABSETLVL, WM_DIABWARPLVL, WM_DIABTOWNWARP, WM_DIABTWARPUP, WM_DIABRETOWN, WM_DIABNEWGAME, WM_DIABLOADGAME, // Asynchronous loading events. WM_PROGRESS, WM_ERROR, WM_DONE, WM_FIRST = WM_DIABNEXTLVL, WM_LAST = WM_DONE, }; void RegisterCustomEvents(); #if SDL_VERSION_ATLEAST(2, 0, 0) using SdlEventType = uint16_t; #else using SdlEventType = uint8_t; #endif bool IsCustomEvent(SdlEventType eventType); interface_mode GetCustomEvent(const SDL_Event &event); void CustomEventToSdlEvent(SDL_Event &event, interface_mode eventType); enum Cutscenes : uint8_t { CutStart, CutTown, CutLevel1, CutLevel2, CutLevel3, CutLevel4, CutLevel5, CutLevel6, CutPortal, CutPortalRed, CutGate, }; void interface_msg_pump(); void IncProgress(uint32_t steps = 1); void CompleteProgress(); void ShowProgress(interface_mode uMsg); } // namespace devilution ================================================ FILE: Source/inv.cpp ================================================ /** * @file inv.cpp * * Implementation of player inventory. */ #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "DiabloUI/ui_flags.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/palette.h" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/size.hpp" #include "hwcursor.hpp" #include "inv_iterators.hpp" #include "levels/tile_properties.hpp" #include "levels/town.h" #include "minitext.h" #include "options.h" #include "panels/ui_panels.hpp" #include "player.h" #include "plrmsg.h" #include "qol/stash.h" #include "stores.h" #include "towners.h" #include "utils/display.h" #include "utils/format_int.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/sdl_geometry.h" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" namespace devilution { bool invflag; /** * Maps from inventory slot to screen position. The inventory slots are * arranged as follows: * * @code{.unparsed} * 00 00 * 00 00 03 * * 04 04 06 06 05 05 * 04 04 06 06 05 05 * 04 04 06 06 05 05 * * 01 02 * * 07 08 09 10 11 12 13 14 15 16 * 17 18 19 20 21 22 23 24 25 26 * 27 28 29 30 31 32 33 34 35 36 * 37 38 39 40 41 42 43 44 45 46 * * 47 48 49 50 51 52 53 54 * @endcode */ const Rectangle InvRect[] = { // clang-format off //{ X, Y }, { W, H } { { 132, 2 }, { 58, 59 } }, // helmet { { 47, 177 }, { 28, 29 } }, // left ring { { 248, 177 }, { 28, 29 } }, // right ring { { 205, 32 }, { 28, 29 } }, // amulet { { 17, 75 }, { 58, 86 } }, // left hand { { 248, 75 }, { 58, 87 } }, // right hand { { 132, 75 }, { 58, 87 } }, // chest { { 17, 222 }, { 29, 29 } }, // inv row 1 { { 46, 222 }, { 29, 29 } }, // inv row 1 { { 75, 222 }, { 29, 29 } }, // inv row 1 { { 104, 222 }, { 29, 29 } }, // inv row 1 { { 133, 222 }, { 29, 29 } }, // inv row 1 { { 162, 222 }, { 29, 29 } }, // inv row 1 { { 191, 222 }, { 29, 29 } }, // inv row 1 { { 220, 222 }, { 29, 29 } }, // inv row 1 { { 249, 222 }, { 29, 29 } }, // inv row 1 { { 278, 222 }, { 29, 29 } }, // inv row 1 { { 17, 251 }, { 29, 29 } }, // inv row 2 { { 46, 251 }, { 29, 29 } }, // inv row 2 { { 75, 251 }, { 29, 29 } }, // inv row 2 { { 104, 251 }, { 29, 29 } }, // inv row 2 { { 133, 251 }, { 29, 29 } }, // inv row 2 { { 162, 251 }, { 29, 29 } }, // inv row 2 { { 191, 251 }, { 29, 29 } }, // inv row 2 { { 220, 251 }, { 29, 29 } }, // inv row 2 { { 249, 251 }, { 29, 29 } }, // inv row 2 { { 278, 251 }, { 29, 29 } }, // inv row 2 { { 17, 280 }, { 29, 29 } }, // inv row 3 { { 46, 280 }, { 29, 29 } }, // inv row 3 { { 75, 280 }, { 29, 29 } }, // inv row 3 { { 104, 280 }, { 29, 29 } }, // inv row 3 { { 133, 280 }, { 29, 29 } }, // inv row 3 { { 162, 280 }, { 29, 29 } }, // inv row 3 { { 191, 280 }, { 29, 29 } }, // inv row 3 { { 220, 280 }, { 29, 29 } }, // inv row 3 { { 249, 280 }, { 29, 29 } }, // inv row 3 { { 278, 280 }, { 29, 29 } }, // inv row 3 { { 17, 309 }, { 29, 29 } }, // inv row 4 { { 46, 309 }, { 29, 29 } }, // inv row 4 { { 75, 309 }, { 29, 29 } }, // inv row 4 { { 104, 309 }, { 29, 29 } }, // inv row 4 { { 133, 309 }, { 29, 29 } }, // inv row 4 { { 162, 309 }, { 29, 29 } }, // inv row 4 { { 191, 309 }, { 29, 29 } }, // inv row 4 { { 220, 309 }, { 29, 29 } }, // inv row 4 { { 249, 309 }, { 29, 29 } }, // inv row 4 { { 278, 309 }, { 29, 29 } }, // inv row 4 { { 205, 5 }, { 29, 29 } }, // belt { { 234, 5 }, { 29, 29 } }, // belt { { 263, 5 }, { 29, 29 } }, // belt { { 292, 5 }, { 29, 29 } }, // belt { { 321, 5 }, { 29, 29 } }, // belt { { 350, 5 }, { 29, 29 } }, // belt { { 379, 5 }, { 29, 29 } }, // belt { { 408, 5 }, { 29, 29 } } // belt // clang-format on }; namespace { OptionalOwnedClxSpriteList pInvCels; /** * @brief Adds an item to a player's InvGrid array * @param player The player reference * @param invGridIndex Item's position in InvGrid (this should be the item's topleft grid tile) * @param invListIndex The item's InvList index (it's expected this already has +1 added to it since InvGrid can't store a 0 index) * @param itemSize Size of item */ void AddItemToInvGrid(Player &player, int invGridIndex, int invListIndex, Size itemSize, bool sendNetworkMessage) { const int pitch = 10; for (int y = 0; y < itemSize.height; y++) { const int rowGridIndex = invGridIndex + pitch * y; for (int x = 0; x < itemSize.width; x++) { if (x == 0 && y == itemSize.height - 1) player.InvGrid[rowGridIndex + x] = invListIndex; else player.InvGrid[rowGridIndex + x] = -invListIndex; // use negative index to denote it's occupied but it's not the top-left cell. } } if (sendNetworkMessage) { NetSendCmdChInvItem(false, invGridIndex); } } /** * @brief Checks whether the given item can fit in a belt slot (i.e. the item's size in inventory cells is 1x1). * @param item The item to be checked. * @return 'True' in case the item can fit a belt slot and 'False' otherwise. */ bool FitsInBeltSlot(const Item &item) { return GetInventorySize(item) == Size { 1, 1 }; } /** * @brief Checks whether the given item can be equipped. Since this overload doesn't take player information, it only considers * general aspects about the item, like if its requirements are met and if the item's target location is valid for the body. * @param item The item to check. * @return 'True' in case the item could be equipped in a player, and 'False' otherwise. */ bool CanEquip(const Item &item) { return item.isEquipment() && item._iStatFlag; } /** * @brief A specialized version of 'CanEquip(int, Item&, int)' that specifically checks whether the item can be equipped * in one/both of the player's hands. * @param player The player whose inventory will be checked for compatibility with the item. * @param item The item to check. * @return 'True' if the player can currently equip the item in either one of his hands (i.e. the required hands are empty and * allow the item), and 'False' otherwise. */ bool CanWield(Player &player, const Item &item) { if (!CanEquip(item) || IsNoneOf(player.GetItemLocation(item), ILOC_ONEHAND, ILOC_TWOHAND)) return false; const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; if (leftHandItem.isEmpty() && rightHandItem.isEmpty()) { return true; } if (!leftHandItem.isEmpty() && !rightHandItem.isEmpty()) { return false; } const Item &occupiedHand = !leftHandItem.isEmpty() ? leftHandItem : rightHandItem; // Bard can dual wield swords and maces, so we allow equiping one-handed weapons in her free slot as long as her occupied // slot is another one-handed weapon. const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield)) { const bool occupiedHandIsOneHandedSwordOrMace = player.GetItemLocation(occupiedHand) == ILOC_ONEHAND && IsAnyOf(occupiedHand._itype, ItemType::Sword, ItemType::Mace); const bool weaponToEquipIsOneHandedSwordOrMace = player.GetItemLocation(item) == ILOC_ONEHAND && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace); if (occupiedHandIsOneHandedSwordOrMace && weaponToEquipIsOneHandedSwordOrMace) { return true; } } return player.GetItemLocation(item) == ILOC_ONEHAND && player.GetItemLocation(occupiedHand) == ILOC_ONEHAND && item._iClass != occupiedHand._iClass; } /** * @brief Checks whether the specified item can be equipped in the desired body location on the player. * @param player The player whose inventory will be checked for compatibility with the item. * @param item The item to check. * @param bodyLocation The location in the inventory to be checked against. * @return 'True' if the player can currently equip the item in the specified body location (i.e. the body location is empty and * allows the item), and 'False' otherwise. */ bool CanEquip(Player &player, const Item &item, inv_body_loc bodyLocation) { if (!CanEquip(item) || player._pmode > PM_WALK_SIDEWAYS || !player.InvBody[bodyLocation].isEmpty()) { return false; } switch (bodyLocation) { case INVLOC_AMULET: return item._iLoc == ILOC_AMULET; case INVLOC_CHEST: return item._iLoc == ILOC_ARMOR; case INVLOC_HAND_LEFT: case INVLOC_HAND_RIGHT: return CanWield(player, item); case INVLOC_HEAD: return item._iLoc == ILOC_HELM; case INVLOC_RING_LEFT: case INVLOC_RING_RIGHT: return item._iLoc == ILOC_RING; default: return false; } } void ChangeEquipment(Player &player, inv_body_loc bodyLocation, const Item &item, bool sendNetworkMessage) { player.InvBody[bodyLocation] = item; if (sendNetworkMessage) { NetSendCmdChItem(false, bodyLocation, true); } } bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool persistItem, bool sendNetworkMessage) { if (!CanEquip(player, item, bodyLocation)) { return false; } if (persistItem) { ChangeEquipment(player, bodyLocation, item, sendNetworkMessage); if (sendNetworkMessage && *GetOptions().Audio.autoEquipSound) { PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); } CalcPlrInv(player, true); } return true; } int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) { Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { if (InvRect[r].contains(cursorPosition + panelOffset)) return r; } for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { if (InvRect[r].contains(cursorPosition + panelOffset)) { // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. if (itemSize.height <= 1 && itemSize.width <= 1) { // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do return r; } // Otherwise work out how far the central cell is from the top-left cell Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left hotPixelCellOffset.deltaX++; } if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above hotPixelCellOffset.deltaY++; } // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) const int hotPixelCell = r - SLOTXY_INV_FIRST; const int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); const int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); return SLOTXY_INV_FIRST + targetRow * InventorySizeInSlots.width + targetColumn; } } panelOffset = Point { 0, 0 } - GetMainPanel().position; for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { if (InvRect[r].contains(cursorPosition + panelOffset)) return r; } return NUM_XY_SLOTS; } void ChangeBodyEquipment(Player &player, int slot, item_equip_type location) { const inv_body_loc bodyLocation = [&slot](item_equip_type location) { switch (location) { case ILOC_HELM: return INVLOC_HEAD; case ILOC_RING: return (slot == SLOTXY_RING_LEFT ? INVLOC_RING_LEFT : INVLOC_RING_RIGHT); case ILOC_AMULET: return INVLOC_AMULET; case ILOC_ARMOR: return INVLOC_CHEST; default: app_fatal("Unexpected equipment type"); } }(location); const Item previouslyEquippedItem = player.InvBody[slot]; ChangeEquipment(player, bodyLocation, player.HoldItem.pop(), &player == MyPlayer); if (!previouslyEquippedItem.isEmpty()) { player.HoldItem = previouslyEquippedItem; } } void ChangeEquippedItem(Player &player, uint8_t slot) { const inv_body_loc selectedHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_LEFT : INVLOC_HAND_RIGHT; const inv_body_loc otherHand = slot == SLOTXY_HAND_LEFT ? INVLOC_HAND_RIGHT : INVLOC_HAND_LEFT; const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); const bool pasteIntoSelectedHand = (player.InvBody[otherHand].isEmpty() || player.InvBody[otherHand]._iClass != player.HoldItem._iClass) || (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::DualWield) && player.InvBody[otherHand]._iClass == ICLASS_WEAPON && player.HoldItem._iClass == ICLASS_WEAPON); const bool dequipTwoHandedWeapon = (!player.InvBody[otherHand].isEmpty() && player.GetItemLocation(player.InvBody[otherHand]) == ILOC_TWOHAND); const inv_body_loc pasteHand = pasteIntoSelectedHand ? selectedHand : otherHand; const Item previouslyEquippedItem = dequipTwoHandedWeapon ? player.InvBody[otherHand] : player.InvBody[pasteHand]; if (dequipTwoHandedWeapon) { RemoveEquipment(player, otherHand, false); } ChangeEquipment(player, pasteHand, player.HoldItem.pop(), &player == MyPlayer); if (!previouslyEquippedItem.isEmpty()) { player.HoldItem = previouslyEquippedItem; } } void ChangeTwoHandItem(Player &player) { if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && !player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { inv_body_loc locationToUnequip = INVLOC_HAND_LEFT; if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { locationToUnequip = INVLOC_HAND_RIGHT; } if (!AutoPlaceItemInInventory(player, player.InvBody[locationToUnequip])) { return; } if (locationToUnequip == INVLOC_HAND_RIGHT) { RemoveEquipment(player, INVLOC_HAND_RIGHT, false); } else { player.InvBody[INVLOC_HAND_LEFT].clear(); } } if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_LEFT]; ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem.pop(), &player == MyPlayer); if (!previouslyEquippedItem.isEmpty()) { player.HoldItem = previouslyEquippedItem; } } else { const Item previouslyEquippedItem = player.InvBody[INVLOC_HAND_RIGHT]; RemoveEquipment(player, INVLOC_HAND_RIGHT, false); ChangeEquipment(player, INVLOC_HAND_LEFT, player.HoldItem, &player == MyPlayer); player.HoldItem = previouslyEquippedItem; } } int8_t CheckOverlappingItems(int slot, const Player &player, Size itemSize) { // check that the item we're pasting only overlaps one other item (or is going into empty space) const unsigned originCell = static_cast(slot - SLOTXY_INV_FIRST); int8_t overlappingId = 0; for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { const unsigned testCell = originCell + rowOffset + columnOffset; // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. assert(testCell < sizeof(player.InvGrid)); if (player.InvGrid[testCell] != 0) { const int8_t iv = std::abs(player.InvGrid[testCell]); if (overlappingId != 0) { if (overlappingId != iv) { // Found two different items that would be displaced by the held item, can't paste the item here. return -1; } } else { overlappingId = iv; } } } } return overlappingId; } int8_t GetPrevItemId(int slot, const Player &player, const Size &itemSize) { if (player.HoldItem._itype != ItemType::Gold) return CheckOverlappingItems(slot, player, itemSize); const int8_t item_cell_begin = player.InvGrid[slot - SLOTXY_INV_FIRST]; if (item_cell_begin == 0) return 0; if (item_cell_begin <= 0) return -item_cell_begin; if (player.InvList[item_cell_begin - 1]._itype != ItemType::Gold) return item_cell_begin; return 0; } bool ChangeInvItem(Player &player, int slot, Size itemSize) { int8_t prevItemId = GetPrevItemId(slot, player, itemSize); if (prevItemId < 0) return false; if (player.HoldItem._itype == ItemType::Gold && prevItemId == 0) { const int ii = slot - SLOTXY_INV_FIRST; if (player.InvGrid[ii] > 0) { const int invIndex = player.InvGrid[ii] - 1; const int gt = player.InvList[invIndex]._ivalue; int ig = player.HoldItem._ivalue + gt; if (ig <= MaxGold) { player.InvList[invIndex]._ivalue = ig; SetPlrHandGoldCurs(player.InvList[invIndex]); player._pGold += player.HoldItem._ivalue; player.HoldItem.clear(); } else { ig = MaxGold - gt; player._pGold += ig; player.HoldItem._ivalue -= ig; SetPlrHandGoldCurs(player.HoldItem); player.InvList[invIndex]._ivalue = MaxGold; player.InvList[invIndex]._iCurs = ICURS_GOLD_LARGE; } } else { const int invIndex = player._pNumInv; player._pGold += player.HoldItem._ivalue; player.InvList[invIndex] = player.HoldItem.pop(); player._pNumInv++; player.InvGrid[ii] = player._pNumInv; } if (&player == MyPlayer) { NetSendCmdChInvItem(false, ii); } } else { if (prevItemId == 0) { player.InvList[player._pNumInv] = player.HoldItem.pop(); player._pNumInv++; prevItemId = player._pNumInv; } else { const int invIndex = prevItemId - 1; if (player.HoldItem._itype == ItemType::Gold) player._pGold += player.HoldItem._ivalue; std::swap(player.InvList[invIndex], player.HoldItem); if (player.HoldItem._itype == ItemType::Gold) player._pGold = CalculateGold(player); for (int8_t &itemIndex : player.InvGrid) { if (itemIndex == prevItemId) itemIndex = 0; if (itemIndex == -prevItemId) itemIndex = 0; } } AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, prevItemId, itemSize, &player == MyPlayer); } return true; } void ChangeBeltItem(Player &player, int slot) { const int ii = slot - SLOTXY_BELT_FIRST; if (player.SpdList[ii].isEmpty()) { player.SpdList[ii] = player.HoldItem.pop(); } else { std::swap(player.SpdList[ii], player.HoldItem); if (player.HoldItem._itype == ItemType::Gold) player._pGold = CalculateGold(player); } if (&player == MyPlayer) { NetSendCmdChBeltItem(false, ii); } RedrawComponent(PanelDrawComponent::Belt); } item_equip_type GetItemEquipType(const Player &player, int slot, item_equip_type desiredLocation) { if (slot == SLOTXY_HEAD) return ILOC_HELM; if (slot == SLOTXY_RING_LEFT || slot == SLOTXY_RING_RIGHT) return ILOC_RING; if (slot == SLOTXY_AMULET) return ILOC_AMULET; if (slot == SLOTXY_HAND_LEFT || slot == SLOTXY_HAND_RIGHT) { if (desiredLocation == ILOC_TWOHAND) return ILOC_TWOHAND; return ILOC_ONEHAND; } if (slot == SLOTXY_CHEST) return ILOC_ARMOR; if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) return ILOC_BELT; return ILOC_UNEQUIPABLE; } void CheckInvPaste(Player &player, Point cursorPosition) { const Size itemSize = GetInventorySize(player.HoldItem); const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); if (slot == NUM_XY_SLOTS) return; const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); const item_equip_type location = GetItemEquipType(player, slot, desiredLocation); if (location == ILOC_BELT) { if (!CanBePlacedOnBelt(player, player.HoldItem)) return; } else if (location != ILOC_UNEQUIPABLE) { if (desiredLocation != location) return; } if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { if (!player.CanUseItem(player.HoldItem)) { player.Say(HeroSpeech::ICantUseThisYet); return; } if (player._pmode > PM_WALK_SIDEWAYS) return; } if (&player == MyPlayer) { PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); } // Select the parameters that go into // ChangeEquipment and add it to post switch switch (location) { case ILOC_HELM: case ILOC_RING: case ILOC_AMULET: case ILOC_ARMOR: ChangeBodyEquipment(player, slot, location); break; case ILOC_ONEHAND: ChangeEquippedItem(player, slot); break; case ILOC_TWOHAND: ChangeTwoHandItem(player); break; case ILOC_UNEQUIPABLE: if (!ChangeInvItem(player, slot, itemSize)) return; break; case ILOC_BELT: ChangeBeltItem(player, slot); break; case ILOC_NONE: case ILOC_INVALID: break; } CalcPlrInv(player, true); if (&player == MyPlayer) { NewCursor(player.HoldItem); } } inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) { assert(slot <= SLOTXY_CHEST); return static_cast(slot); } std::optional FindSlotUnderCursor(Point cursorPosition) { Point testPosition = static_cast(cursorPosition - GetRightPanel().position); for (std::underlying_type_t r = SLOTXY_EQUIPPED_FIRST; r != SLOTXY_BELT_FIRST; r++) { // check which body/inventory rectangle the mouse is in, if any if (InvRect[r].contains(testPosition)) { return static_cast(r); } } testPosition = static_cast(cursorPosition - GetMainPanel().position); for (std::underlying_type_t r = SLOTXY_BELT_FIRST; r != NUM_XY_SLOTS; r++) { // check which belt rectangle the mouse is in, if any if (InvRect[r].contains(testPosition)) { return static_cast(r); } } return {}; } /** * @brief Checks whether an item of the given size can be placed on the specified player's inventory slot. * @param player The player whose inventory will be checked. * @param slotIndex The 0-based index of the slot to put the item on. * @param itemSize The size of the item to be checked. * @param itemIndexToIgnore can be used to check if an item of the given size would fit if the item with the given (positive) ID was removed. * @return 'True' in case the item can be placed on the specified player's inventory slot and 'False' otherwise. */ bool CheckItemFitsInInventorySlot(const Player &player, int slotIndex, const Size &itemSize, int itemIndexToIgnore) { int yy = (slotIndex > 0) ? (10 * (slotIndex / 10)) : 0; for (int j = 0; j < itemSize.height; j++) { if (yy >= InventoryGridCells) { return false; } int xx = (slotIndex > 0) ? (slotIndex % 10) : 0; for (int i = 0; i < itemSize.width; i++) { if (xx >= 10 || !(player.InvGrid[xx + yy] == 0 || std::abs(player.InvGrid[xx + yy]) - 1 == itemIndexToIgnore)) { // The item is too wide to fit in the specified column, or one of the cells is occupied (and not by the item we're planning on removing) return false; } xx++; } yy += 10; } return true; } /** * @brief Finds the first slot that could fit an item of the given size * @param player Player whose inventory will be checked. * @param itemSize Dimensions of the item. * @param itemIndexToIgnore Can be used if you want to find whether the new item would fit with this item removed, without performing unnecessary actions. * @return The first slot that could fit the item or an empty optional. */ std::optional FindSlotForItem(const Player &player, const Size &itemSize, int itemIndexToIgnore = -1) { if (itemSize.height == 1) { for (int i = 30; i <= 39; i++) { if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) return i; } for (int x = 9; x >= 0; x--) { for (int y = 2; y >= 0; y--) { if (CheckItemFitsInInventorySlot(player, 10 * y + x, itemSize, itemIndexToIgnore)) return 10 * y + x; } } return {}; } if (itemSize.height == 2) { for (int x = 10 - itemSize.width; x >= 0; x--) { for (int y = 0; y < 3; y++) { if (CheckItemFitsInInventorySlot(player, 10 * y + x, itemSize, itemIndexToIgnore)) return 10 * y + x; } } return {}; } if (itemSize == Size { 1, 3 }) { for (int i = 0; i < 20; i++) { if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) return i; } return {}; } if (itemSize == Size { 2, 3 }) { for (int i = 0; i < 9; i++) { if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) return i; } for (int i = 10; i < 19; i++) { if (CheckItemFitsInInventorySlot(player, i, itemSize, itemIndexToIgnore)) return i; } return {}; } app_fatal(StrCat("Unknown item size: ", itemSize.width, "x", itemSize.height)); } /** * @brief Checks if the given item could be placed on the specified players inventory if the other item was removed. * @param player The player whose inventory will be checked. * @param item The item to be checked. * @param itemIndexToIgnore The inventory index of the item that we assume will be removed. * @return 'True' if the item could fit with the other item removed and 'False' otherwise. */ bool CouldFitItemInInventory(const Player &player, const Item &item, int itemIndexToIgnore) { return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); } void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) { if (player._pmode > PM_WALK_SIDEWAYS) { return; } CloseGoldDrop(); std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); if (!maybeSlot) { // not on an inventory slot rectangle return; } const inv_xy_slot r = *maybeSlot; Item &holdItem = player.HoldItem; holdItem.clear(); bool attemptedMove = false; bool automaticallyMoved = false; SfxID successSound = SfxID::None; HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { const inv_body_loc invloc = MapSlotToInvBodyLoc(r); if (!player.InvBody[invloc].isEmpty()) { if (automaticMove) { attemptedMove = true; automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); if (automaticallyMoved) { successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; RemoveEquipment(player, invloc, false); } else { failedSpeech = HeroSpeech::IHaveNoRoom; } } else { holdItem = player.InvBody[invloc]; RemoveEquipment(player, invloc, false); } } } if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { const unsigned ig = r - SLOTXY_INV_FIRST; const int iv = std::abs(player.InvGrid[ig]) - 1; if (iv >= 0) { if (automaticMove) { attemptedMove = true; if (CanBePlacedOnBelt(player, player.InvList[iv])) { automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); if (automaticallyMoved) { successSound = SfxID::GrabItem; player.RemoveInvItem(iv, false); } else { failedSpeech = HeroSpeech::IHaveNoRoom; } } else if (CanEquip(player.InvList[iv])) { failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails /* * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except * we don't want to leave the item on the hand if the equip attempt failed. We would end up * generating wasteful network messages if we did the lift first. Instead we work out whatever slot * needs to be unequipped (if any): */ int invloc = NUM_INVLOC; switch (player.GetItemLocation(player.InvList[iv])) { case ILOC_ARMOR: invloc = INVLOC_CHEST; break; case ILOC_HELM: invloc = INVLOC_HEAD; break; case ILOC_AMULET: invloc = INVLOC_AMULET; break; case ILOC_ONEHAND: if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { // The left hand is not empty and we're either trying to equip the same type of item or // it's holding a two handed weapon, so it must be unequipped invloc = INVLOC_HAND_LEFT; } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { // The right hand is not empty and we're trying to equip the same type of item, so we need // to unequip that item invloc = INVLOC_HAND_RIGHT; } // otherwise one hand is empty (and we can let the auto-equip code put the target item into // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip // a shield (in which case the attempt will fail). break; case ILOC_TWOHAND: // Moving a two-hand item from inventory to InvBody requires emptying both hands. if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { // If the right hand is empty then we can simply try equipping this item in the left hand, // we'll let the common code take care of unequipping anything held there. invloc = INVLOC_HAND_LEFT; } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { // We have an item in the right hand but nothing in the left, so let the common code // take care of unequipping whatever is held in the right hand. The auto-equip code // picks the most appropriate location for the item type (which in this case will be // the left hand), invloc isn't used there. invloc = INVLOC_HAND_RIGHT; } else { // Both hands are holding items, we must unequip one of the items and check that there's // space for the other before trying to auto-equip inv_body_loc mainHand = INVLOC_HAND_LEFT; inv_body_loc offHand = INVLOC_HAND_RIGHT; if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { // No space to move right hand item to inventory, can we move the left instead? std::swap(mainHand, offHand); if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { break; } } if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { // No space for the main hand item. Move the other item back to the off hand and abort. player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; player.RemoveInvItem(player._pNumInv - 1, false); break; } RemoveEquipment(player, offHand, false); invloc = mainHand; } break; default: // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. failedSpeech = HeroSpeech::ICantDoThat; break; } // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip if (invloc != NUM_INVLOC && !player.InvBody[invloc].isEmpty() && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { holdItem = player.InvBody[invloc].pop(); } automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); if (automaticallyMoved) { successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; player.RemoveInvItem(iv, false); // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { holdItem.clear(); } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. } else if (!holdItem.isEmpty()) { // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... player.InvBody[invloc] = holdItem.pop(); } } } else { holdItem = player.InvList[iv]; player.RemoveInvItem(iv, false); } } } if (r >= SLOTXY_BELT_FIRST) { const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; if (!beltItem.isEmpty()) { if (automaticMove) { attemptedMove = true; automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); if (automaticallyMoved) { successSound = SfxID::GrabItem; player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); } else { failedSpeech = HeroSpeech::IHaveNoRoom; } } else { holdItem = beltItem; player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); } } } if (!holdItem.isEmpty()) { if (holdItem._itype == ItemType::Gold) { player._pGold = CalculateGold(player); } CalcPlrInv(player, true); holdItem._iStatFlag = player.CanUseItem(holdItem); if (&player == MyPlayer) { PlaySFX(SfxID::GrabItem); NewCursor(holdItem); } if (dropItem) { TryDropItem(); } } else if (automaticMove) { if (automaticallyMoved) { CalcPlrInv(player, true); } if (attemptedMove && &player == MyPlayer) { if (automaticallyMoved) { PlaySFX(successSound); } else { player.SaySpecific(failedSpeech); } } } } void TryCombineNaKrulNotes(Player &player, Item ¬eItem) { const int idx = noteItem.IDidx; const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { return; } for (const _item_indexes note : notes) { if (idx != note && !HasInventoryItemWithId(player, note)) { return; // the player doesn't have all notes } } MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); for (const _item_indexes note : notes) { if (idx != note) { RemoveInventoryItemById(player, note); } } const Point position = noteItem.position; // copy the position to restore it after re-initialising the item noteItem = {}; GetItemAttrs(noteItem, IDI_FULLNOTE, 16); SetupItem(noteItem); noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table } void CheckQuestItem(Player &player, Item &questItem) { const Player &myPlayer = *MyPlayer; if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE && (questItem.IDidx == IDI_OPTAMULET || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { Quests[Q_BLIND]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_BLIND]); } if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); } if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, Quests[Q_ANVIL]); } if (Quests[Q_ANVIL]._qlog) { myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); } } if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); } if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { if (Quests[Q_ROCK]._qactive == QUEST_INIT) { Quests[Q_ROCK]._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, Quests[Q_ROCK]); } if (Quests[Q_ROCK]._qlog) { myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); } } if (Quests[Q_BLOOD]._qactive == QUEST_ACTIVE && (questItem.IDidx == IDI_ARMOFVAL || (Quests[Q_BLOOD].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 9, 3 })))) { Quests[Q_BLOOD]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_BLOOD]); myPlayer.Say(HeroSpeech::MayTheSpiritOfArkaineProtectMe, 20); } if (questItem.IDidx == IDI_MAPOFDOOM) { Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; if (Quests[Q_GRAVE]._qvar1 != 1) { MyPlayer->Say(HeroSpeech::UhHuh, 10); Quests[Q_GRAVE]._qvar1 = 1; } } TryCombineNaKrulNotes(player, questItem); } void CleanupItems(int ii) { const Item &item = Items[ii]; dItem[item.position.x][item.position.y] = 0; if (CornerStone.isAvailable() && item.position == CornerStone.position) { CornerStone.item.clear(); CornerStone.item.selectionRegion = SelectionRegion::None; CornerStone.item.position = { 0, 0 }; CornerStone.item._iAnimFlag = false; CornerStone.item._iIdentified = false; CornerStone.item._iPostDraw = false; } int i = 0; while (i < ActiveItemCount) { if (ActiveItems[i] == ii) { DeleteItem(i); i = 0; continue; } i++; } } bool CanUseStaff(Item &staff, SpellID spell) { return !staff.isEmpty() && IsAnyOf(staff._iMiscId, IMISC_STAFF, IMISC_UNIQUE) && staff._iSpell == spell && staff._iCharges > 0; } void StartGoldDrop() { CloseGoldWithdraw(); const int8_t invIndex = pcursinvitem; const Player &myPlayer = *MyPlayer; const int max = (invIndex <= INVITEM_INV_LAST) ? myPlayer.InvList[invIndex - INVITEM_INV_FIRST]._ivalue : myPlayer.SpdList[invIndex - INVITEM_BELT_FIRST]._ivalue; if (ChatFlag) ResetChat(); const Point start = GetPanelPosition(UiPanels::Inventory, { 67, 128 }); SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); OpenGoldDrop(invIndex, max); } int CreateGoldItemInInventorySlot(Player &player, int slotIndex, int value) { if (player.InvGrid[slotIndex] != 0) { return value; } Item &goldItem = player.InvList[player._pNumInv]; MakeGoldStack(goldItem, std::min(value, MaxGold)); player._pNumInv++; player.InvGrid[slotIndex] = player._pNumInv; if (&player == MyPlayer) { NetSendCmdChInvItem(false, slotIndex); } value -= goldItem._ivalue; return value; } } // namespace void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality) { SDL_Rect srcRect = MakeSdlRect(0, 0, size.width, size.height); out.Clip(&srcRect, &targetPosition); if (size.width <= 0 || size.height <= 0) return; uint8_t colorShift; switch (itemQuality) { case ITEM_QUALITY_MAGIC: colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BLUE : PAL16_ORANGE) - 1; break; case ITEM_QUALITY_UNIQUE: colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_YELLOW : PAL16_ORANGE) - 1; break; default: colorShift = PAL16_GRAY - (!IsInspectingPlayer() ? PAL16_BEIGE : PAL16_ORANGE) - 1; break; } uint8_t *dst = &out[targetPosition]; const auto dstPitch = out.pitch(); for (int y = size.height; y != 0; --y, dst -= dstPitch + size.width) { for (const uint8_t *end = dst + size.width; dst < end; ++dst) { uint8_t &pix = *dst; if (pix >= PAL16_GRAY) { pix -= colorShift; } } } } bool CanBePlacedOnBelt(const Player &player, const Item &item) { return FitsInBeltSlot(item) && item._itype != ItemType::Gold && player.CanUseItem(item) && item.isUsable(); } void FreeInvGFX() { pInvCels = std::nullopt; } void InitInv() { const PlayerData &playerClassData = GetPlayerDataForClass(MyPlayer->_pClass); const char *invName = playerClassData.inv.c_str(); if (gbIsSpawn && (playerClassData.inv == "inv_rog" || playerClassData.inv == "inv_sor")) { invName = "inv"; } pInvCels = LoadCel(StrCat("data\\inv\\", invName).c_str(), static_cast(SidePanelSize.width)); } void DrawInv(const Surface &out) { ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 0, 351 }), (*pInvCels)[0]); const Size slotSize[] = { { 2, 2 }, // head { 1, 1 }, // left ring { 1, 1 }, // right ring { 1, 1 }, // amulet { 2, 3 }, // left hand { 2, 3 }, // right hand { 2, 3 }, // chest }; const Point slotPos[] = { { 133, 59 }, // head { 48, 205 }, // left ring { 249, 205 }, // right ring { 205, 60 }, // amulet { 17, 160 }, // left hand { 248, 160 }, // right hand { 133, 160 }, // chest }; const Player &myPlayer = *InspectPlayer; for (int slot = INVLOC_HEAD; slot < NUM_INVLOC; slot++) { if (!myPlayer.InvBody[slot].isEmpty()) { int screenX = slotPos[slot].x; int screenY = slotPos[slot].y; InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, { screenX, screenY }), { slotSize[slot].width * InventorySlotSizeInPixels.width, slotSize[slot].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); const int cursId = myPlayer.InvBody[slot]._iCurs + CURSOR_FIRSTITEM; const Size frameSize = GetInvItemSize(cursId); // calc item offsets for weapons/armor smaller than 2x3 slots if (IsAnyOf(slot, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST)) { screenX += frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0; screenY += frameSize.height == (3 * InventorySlotSizeInPixels.height) ? 0 : -INV_SLOT_HALF_SIZE_PX; } const ClxSprite sprite = GetInvItemSprite(cursId); const Point position = GetPanelPosition(UiPanels::Inventory, { screenX, screenY }); if (pcursinvitem == slot) { ClxDrawOutline(out, GetOutlineColor(myPlayer.InvBody[slot], true), position, sprite); } DrawItem(myPlayer.InvBody[slot], out, position, sprite); if (slot == INVLOC_HAND_LEFT) { if (myPlayer.GetItemLocation(myPlayer.InvBody[slot]) == ILOC_TWOHAND) { InvDrawSlotBack(out, GetPanelPosition(UiPanels::Inventory, slotPos[INVLOC_HAND_RIGHT]), { slotSize[INVLOC_HAND_RIGHT].width * InventorySlotSizeInPixels.width, slotSize[INVLOC_HAND_RIGHT].height * InventorySlotSizeInPixels.height }, myPlayer.InvBody[slot]._iMagical); const int dstX = GetRightPanel().position.x + slotPos[INVLOC_HAND_RIGHT].x + (frameSize.width == InventorySlotSizeInPixels.width ? INV_SLOT_HALF_SIZE_PX : 0) - 1; const int dstY = GetRightPanel().position.y + slotPos[INVLOC_HAND_RIGHT].y; ClxDrawBlended(out, { dstX, dstY }, sprite); } } } } for (int i = 0; i < InventoryGridCells; i++) { if (myPlayer.InvGrid[i] != 0) { InvDrawSlotBack( out, GetPanelPosition(UiPanels::Inventory, InvRect[i + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }, InventorySlotSizeInPixels, myPlayer.InvList[std::abs(myPlayer.InvGrid[i]) - 1]._iMagical); } } for (int j = 0; j < InventoryGridCells; j++) { if (myPlayer.InvGrid[j] > 0) { // first slot of an item const int ii = myPlayer.InvGrid[j] - 1; const int cursId = myPlayer.InvList[ii]._iCurs + CURSOR_FIRSTITEM; const ClxSprite sprite = GetInvItemSprite(cursId); const Point position = GetPanelPosition(UiPanels::Inventory, InvRect[j + SLOTXY_INV_FIRST].position) + Displacement { 0, InventorySlotSizeInPixels.height }; if (pcursinvitem == ii + INVITEM_INV_FIRST) { ClxDrawOutline(out, GetOutlineColor(myPlayer.InvList[ii], true), position, sprite); } DrawItem(myPlayer.InvList[ii], out, position, sprite); } } } void DrawInvBelt(const Surface &out) { if (ChatFlag) { return; } const Point mainPanelPosition = GetMainPanel().position; DrawPanelBox(out, { 205, 21, 232, 28 }, mainPanelPosition + Displacement { 205, 5 }); const Player &myPlayer = *InspectPlayer; for (int i = 0; i < MaxBeltItems; i++) { if (myPlayer.SpdList[i].isEmpty()) { continue; } const Point position { InvRect[i + SLOTXY_BELT_FIRST].position.x + mainPanelPosition.x, InvRect[i + SLOTXY_BELT_FIRST].position.y + mainPanelPosition.y + InventorySlotSizeInPixels.height }; InvDrawSlotBack(out, position, InventorySlotSizeInPixels, myPlayer.SpdList[i]._iMagical); const int cursId = myPlayer.SpdList[i]._iCurs + CURSOR_FIRSTITEM; const ClxSprite sprite = GetInvItemSprite(cursId); if (pcursinvitem == i + INVITEM_BELT_FIRST) { if (ControlMode == ControlTypes::KeyboardAndMouse || invflag) { ClxDrawOutline(out, GetOutlineColor(myPlayer.SpdList[i], true), position, sprite); } } DrawItem(myPlayer.SpdList[i], out, position, sprite); if (myPlayer.SpdList[i].isUsable() && myPlayer.SpdList[i]._itype != ItemType::Gold) { auto beltKey = StrCat("BeltItem", i + 1); std::string_view keyName = ControlMode == ControlTypes::Gamepad ? GetOptions().Padmapper.InputNameForAction(beltKey, true) : GetOptions().Keymapper.KeyNameForAction(beltKey); if (keyName.length() > 2) keyName = {}; DrawString(out, keyName, { position - Displacement { 0, 12 }, InventorySlotSizeInPixels }, { .flags = UiFlags::ColorWhite | UiFlags::AlignRight }); } } } void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri) { if (&player == MyPlayer) { NetSendCmdDelItem(hiPri, bodyLocation); } player.InvBody[bodyLocation].clear(); } bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) { if (!CanBePlacedOnBelt(player, item)) { return false; } for (Item &beltItem : player.SpdList) { if (beltItem.isEmpty()) { if (persistItem) { beltItem = item; player.CalcScrolls(); RedrawComponent(PanelDrawComponent::Belt); if (sendNetworkMessage) { const auto beltIndex = static_cast(std::distance(&player.SpdList[0], &beltItem)); NetSendCmdChBeltItem(false, beltIndex); } } return true; } } return false; } bool AutoEquip(Player &player, const Item &item, bool persistItem, bool sendNetworkMessage) { if (!CanEquip(item)) { return false; } for (int bodyLocation = INVLOC_HEAD; bodyLocation < NUM_INVLOC; bodyLocation++) { if (AutoEquip(player, item, (inv_body_loc)bodyLocation, persistItem, sendNetworkMessage)) { return true; } } return false; } bool AutoEquipEnabled(const Player &player, const Item &item) { if (item.isWeapon()) { // Monk can use unarmed attack as an encouraged option, thus we do not automatically equip weapons on him so as to not // annoy players who prefer that playstyle. return player._pClass != HeroClass::Monk && *GetOptions().Gameplay.autoEquipWeapons; } if (item.isArmor()) { return *GetOptions().Gameplay.autoEquipArmor; } if (item.isHelm()) { return *GetOptions().Gameplay.autoEquipHelms; } if (item.isShield()) { return *GetOptions().Gameplay.autoEquipShields; } if (item.isJewelry()) { return *GetOptions().Gameplay.autoEquipJewelry; } return true; } bool CanFitItemInInventory(const Player &player, const Item &item) { return static_cast(FindSlotForItem(player, GetInventorySize(item))); } bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage) { const Size itemSize = GetInventorySize(item); std::optional targetSlot = FindSlotForItem(player, itemSize); if (targetSlot) { player.InvList[player._pNumInv] = item; player._pNumInv++; AddItemToInvGrid(player, *targetSlot, player._pNumInv, itemSize, sendNetworkMessage); player.CalcScrolls(); return true; } return false; } std::vector SortItemsBySize(Player &player) { std::vector> itemSizes; // Pair of item size and its index in InvList itemSizes.reserve(player._pNumInv); // Reserves space for the number of items in the player's inventory for (int i = 0; i < player._pNumInv; i++) { const Size size = GetInventorySize(player.InvList[i]); itemSizes.emplace_back(size, i); } // Sort items by height first, then by width std::sort(itemSizes.begin(), itemSizes.end(), [](const auto &a, const auto &b) { if (a.first.height == b.first.height) return a.first.width > b.first.width; return a.first.height > b.first.height; }); // Extract sorted indices std::vector sortedIndices; sortedIndices.reserve(itemSizes.size()); // Pre-allocate the necessary capacity for (const auto &itemSize : itemSizes) { sortedIndices.push_back(itemSize.second); } return sortedIndices; } void ReorganizeInventory(Player &player) { // Sort items by size const std::vector sortedIndices = SortItemsBySize(player); // Temporary storage for items and a copy of InvGrid std::vector tempStorage(player._pNumInv); std::array originalInvGrid; // Declare an array for InvGrid copy std::copy(std::begin(player.InvGrid), std::end(player.InvGrid), std::begin(originalInvGrid)); // Copy InvGrid to originalInvGrid // Move items to temporary storage and clear inventory slots for (int i = 0; i < player._pNumInv; ++i) { tempStorage[i] = player.InvList[i]; player.InvList[i] = {}; } player._pNumInv = 0; // Reset inventory count std::fill(std::begin(player.InvGrid), std::end(player.InvGrid), 0); // Clear InvGrid // Attempt to place items back, now from the temp storage bool reorganizationFailed = false; for (const int index : sortedIndices) { const Item &item = tempStorage[index]; if (!AutoPlaceItemInInventory(player, item, false)) { reorganizationFailed = true; break; } } // If reorganization failed, restore items and InvGrid from tempStorage and originalInvGrid if (reorganizationFailed) { for (const Item &item : tempStorage) { if (!item.isEmpty()) { player.InvList[player._pNumInv++] = item; } } std::copy(std::begin(originalInvGrid), std::end(originalInvGrid), std::begin(player.InvGrid)); // Restore InvGrid } } int RoomForGold() { int amount = 0; for (const int8_t &itemIndex : MyPlayer->InvGrid) { if (itemIndex < 0) { continue; } if (itemIndex == 0) { amount += MaxGold; continue; } const Item &goldItem = MyPlayer->InvList[itemIndex - 1]; if (goldItem._itype != ItemType::Gold || goldItem._ivalue == MaxGold) { continue; } amount += MaxGold - goldItem._ivalue; } return amount; } int AddGoldToInventory(Player &player, int value) { // Top off existing piles for (int i = 0; i < player._pNumInv && value > 0; i++) { Item &goldItem = player.InvList[i]; if (goldItem._itype != ItemType::Gold || goldItem._ivalue >= MaxGold) { continue; } if (goldItem._ivalue + value > MaxGold) { value -= MaxGold - goldItem._ivalue; goldItem._ivalue = MaxGold; } else { goldItem._ivalue += value; value = 0; } NetSyncInvItem(player, i); SetPlrHandGoldCurs(goldItem); } // Last row right to left for (int i = 39; i >= 30 && value > 0; i--) { value = CreateGoldItemInInventorySlot(player, i, value); } // Remaining inventory in columns, bottom to top, right to left for (int x = 9; x >= 0 && value > 0; x--) { for (int y = 2; y >= 0 && value > 0; y--) { value = CreateGoldItemInInventorySlot(player, 10 * y + x, value); } } return value; } bool GoldAutoPlace(Player &player, Item &goldStack) { goldStack._ivalue = AddGoldToInventory(player, goldStack._ivalue); SetPlrHandGoldCurs(goldStack); player._pGold = CalculateGold(player); return goldStack._ivalue == 0; } void CheckInvSwap(Player &player, inv_body_loc bLoc) { const Item &item = player.InvBody[bLoc]; if (bLoc == INVLOC_HAND_LEFT && player.GetItemLocation(item) == ILOC_TWOHAND) { player.InvBody[INVLOC_HAND_RIGHT].clear(); } else if (bLoc == INVLOC_HAND_RIGHT && player.GetItemLocation(item) == ILOC_TWOHAND) { player.InvBody[INVLOC_HAND_LEFT].clear(); } CalcPlrInv(player, true); } void inv_update_rem_item(Player &player, inv_body_loc iv) { player.InvBody[iv].clear(); CalcPlrInv(player, player._pmode != PM_DEATH); } void CheckInvSwap(Player &player, const Item &item, int invGridIndex) { Size itemSize = GetInventorySize(item); const int pitch = 10; const int invListIndex = [&]() -> int { for (int y = 0; y < itemSize.height; y++) { const int rowGridIndex = invGridIndex + pitch * y; for (int x = 0; x < itemSize.width; x++) { const int gridIndex = rowGridIndex + x; if (player.InvGrid[gridIndex] != 0) return std::abs(player.InvGrid[gridIndex]); } } player._pNumInv++; return player._pNumInv; }(); if (invListIndex < player._pNumInv) { for (int8_t &itemIndex : player.InvGrid) { if (itemIndex == invListIndex) itemIndex = 0; if (itemIndex == -invListIndex) itemIndex = 0; } } player.InvList[invListIndex - 1] = item; for (int y = 0; y < itemSize.height; y++) { const int rowGridIndex = invGridIndex + pitch * y; for (int x = 0; x < itemSize.width; x++) { if (x == 0 && y == itemSize.height - 1) player.InvGrid[rowGridIndex + x] = invListIndex; else player.InvGrid[rowGridIndex + x] = -invListIndex; } } CalcPlrInv(player, true); } void CheckInvRemove(Player &player, int invGridIndex) { const int invListIndex = std::abs(player.InvGrid[invGridIndex]) - 1; if (invListIndex >= 0) { player.RemoveInvItem(invListIndex); } } void TransferItemToStash(Player &player, int location) { if (location == -1) { return; } const Item &item = GetInventoryItem(player, location); if (!AutoPlaceItemInStash(player, item, true)) { player.SaySpecific(HeroSpeech::WhereWouldIPutThis); return; } PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); if (location < INVITEM_INV_FIRST) { RemoveEquipment(player, static_cast(location), false); CalcPlrInv(player, true); } else if (location <= INVITEM_INV_LAST) player.RemoveInvItem(location - INVITEM_INV_FIRST); else player.RemoveSpdBarItem(location - INVITEM_BELT_FIRST); } void CheckInvItem(bool isShiftHeld, bool isCtrlHeld) { if (IsInspectingPlayer()) return; if (!MyPlayer->HoldItem.isEmpty()) { CheckInvPaste(*MyPlayer, MousePosition); } else if (IsStashOpen && isCtrlHeld) { TransferItemToStash(*MyPlayer, pcursinvitem); } else { CheckInvCut(*MyPlayer, MousePosition, isShiftHeld, isCtrlHeld); } } void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld) { const Point mainPanelPosition = GetMainPanel().position; if (MousePosition.x > 190 + mainPanelPosition.x && MousePosition.x < 437 + mainPanelPosition.x && MousePosition.y > mainPanelPosition.y && MousePosition.y < 33 + mainPanelPosition.y) { CheckInvItem(isShiftHeld, isCtrlHeld); } } void InvGetItem(Player &player, int ii) { Item &item = Items[ii]; CloseGoldDrop(); if (dItem[item.position.x][item.position.y] == 0) return; item._iCreateInfo &= ~CF_PREGEN; CheckQuestItem(player, item); item.updateRequiredStatsCacheForPlayer(player); if (item._itype == ItemType::Gold && GoldAutoPlace(player, item)) { if (MyPlayer == &player) { // Non-gold items (or gold when you have a full inventory) go to the hand then provide audible feedback on // paste. To give the same feedback for auto-placed gold we play the sound effect now. PlaySFX(SfxID::ItemGold); } } else { // The item needs to go into the players hand if (MyPlayer == &player && !player.HoldItem.isEmpty()) { // drop whatever the player is currently holding NetSendCmdPItem(true, CMD_SYNCPUTITEM, player.position.tile, player.HoldItem); } // need to copy here instead of move so CleanupItems still has access to the position player.HoldItem = item; NewCursor(player.HoldItem); } // This potentially moves items in memory so must be done after we've made a copy CleanupItems(ii); pcursitem = -1; } std::optional FindAdjacentPositionForItem(Point origin, Direction facing) { if (ActiveItemCount >= MAXITEMS) return {}; if (CanPut(origin + facing)) return origin + facing; if (CanPut(origin + Left(facing))) return origin + Left(facing); if (CanPut(origin + Right(facing))) return origin + Right(facing); if (CanPut(origin + Left(Left(facing)))) return origin + Left(Left(facing)); if (CanPut(origin + Right(Right(facing)))) return origin + Right(Right(facing)); if (CanPut(origin + Left(Left(Left(facing))))) return origin + Left(Left(Left(facing))); if (CanPut(origin + Right(Right(Right(facing))))) return origin + Right(Right(Right(facing))); if (CanPut(origin + Opposite(facing))) return origin + Opposite(facing); if (CanPut(origin)) return origin; return {}; } void AutoGetItem(Player &player, Item *itemPointer, int ii) { Item &item = *itemPointer; CloseGoldDrop(); if (dItem[item.position.x][item.position.y] == 0) return; item._iCreateInfo &= ~CF_PREGEN; CheckQuestItem(player, item); item.updateRequiredStatsCacheForPlayer(player); bool done; bool autoEquipped = false; if (item._itype == ItemType::Gold) { done = GoldAutoPlace(player, item); if (!done) { SetPlrHandGoldCurs(item); } } else { done = AutoEquipEnabled(player, item) && AutoEquip(player, item, true, &player == MyPlayer); if (done) { autoEquipped = true; } if (!done) { done = AutoPlaceItemInBelt(player, item, true, &player == MyPlayer); } if (!done) { done = AutoPlaceItemInInventory(player, item, &player == MyPlayer); } } if (done) { if (!autoEquipped && *GetOptions().Audio.itemPickupSound && &player == MyPlayer) { PlaySFX(SfxID::GrabItem); } CleanupItems(ii); return; } if (&player == MyPlayer) { player.Say(HeroSpeech::ICantCarryAnymore); } RespawnItem(item, true); NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); } int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t createInfo) { for (uint8_t i = 0; i < ActiveItemCount; i++) { const Item &item = Items[ActiveItems[i]]; if (item.keyAttributesMatch(iseed, idx, createInfo)) { return i; } } return -1; } void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci) { // Check what the local client has at the target position int ii = dItem[position.x][position.y] - 1; if (ii >= 0 && ii < MAXITEMS) { // If there was an item there, check that it's the same item as the remote player has if (!Items[ii].keyAttributesMatch(iseed, idx, ci)) { // Key attributes don't match so we must've desynced, ignore this index and try find a matching item via lookup ii = -1; } } if (ii == -1) { // Either there's no item at the expected position or it doesn't match what is being picked up, so look for an item that matches the key attributes ii = FindGetItem(iseed, idx, ci); if (ii != -1) { // Translate to Items index for CleanupItems, FindGetItem returns an ActiveItems index ii = ActiveItems[ii]; } } if (ii == -1) { // Still can't find the expected item, assume it was collected earlier and this caused the desync return; } CleanupItems(ii); } bool CanPut(Point position) { if (!InDungeonBounds(position)) { return false; } if (IsTileSolid(position)) { return false; } if (dItem[position.x][position.y] != 0) { return false; } if (leveltype == DTYPE_TOWN) { if (dMonster[position.x][position.y] != 0) { return false; } if (dMonster[position.x + 1][position.y + 1] != 0) { return false; } } if (IsItemBlockingObjectAtPosition(position)) { return false; } return true; } int ClampDurability(const Item &item, int durability) { if (item._iMaxDur == 0) return 0; return std::clamp(durability, 1, item._iMaxDur); } int16_t ClampToHit(const Item &item, int16_t toHit) { if (toHit < item._iPLToHit || toHit > 51) return item._iPLToHit; return toHit; } uint8_t ClampMaxDam(const Item &item, uint8_t maxDam) { if (maxDam < item._iMaxDam || maxDam - item._iMinDam > 30) return item._iMaxDam; return maxDam; } int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam) { if (ActiveItemCount >= MAXITEMS) return -1; Item item; RecreateItem(*MyPlayer, item, idx, icreateinfo, iseed, ivalue, ibuff); if (id != 0) item._iIdentified = true; item._iMaxDur = mdur; item._iDurability = ClampDurability(item, dur); item._iMaxCharges = std::clamp(mch, 0, item._iMaxCharges); item._iCharges = std::clamp(ch, 0, item._iMaxCharges); if (gbIsHellfire) { item._iPLToHit = ClampToHit(item, toHit); item._iMaxDam = ClampMaxDam(item, maxDam); } return PlaceItemInWorld(std::move(item), position); } int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname) { if (ActiveItemCount >= MAXITEMS) return -1; Item item; RecreateEar(item, icreateinfo, iseed, cursval, heroname); return PlaceItemInWorld(std::move(item), position); } int8_t CheckInvHLight() { int8_t r = 0; for (; r < NUM_XY_SLOTS; r++) { int xo = GetRightPanel().position.x; int yo = GetRightPanel().position.y; if (r >= SLOTXY_BELT_FIRST) { xo = GetMainPanel().position.x; yo = GetMainPanel().position.y; } if (InvRect[r].contains(MousePosition - Displacement(xo, yo))) { break; } } if (r >= NUM_XY_SLOTS) return -1; int8_t rv = -1; InfoColor = UiFlags::ColorWhite; Item *pi = nullptr; Player &myPlayer = *InspectPlayer; if (r == SLOTXY_HEAD) { rv = INVLOC_HEAD; pi = &myPlayer.InvBody[rv]; } else if (r == SLOTXY_RING_LEFT) { rv = INVLOC_RING_LEFT; pi = &myPlayer.InvBody[rv]; } else if (r == SLOTXY_RING_RIGHT) { rv = INVLOC_RING_RIGHT; pi = &myPlayer.InvBody[rv]; } else if (r == SLOTXY_AMULET) { rv = INVLOC_AMULET; pi = &myPlayer.InvBody[rv]; } else if (r == SLOTXY_HAND_LEFT) { rv = INVLOC_HAND_LEFT; pi = &myPlayer.InvBody[rv]; } else if (r == SLOTXY_HAND_RIGHT) { pi = &myPlayer.InvBody[INVLOC_HAND_LEFT]; if (pi->isEmpty() || myPlayer.GetItemLocation(*pi) != ILOC_TWOHAND) { rv = INVLOC_HAND_RIGHT; pi = &myPlayer.InvBody[rv]; } else { rv = INVLOC_HAND_LEFT; } } else if (r == SLOTXY_CHEST) { rv = INVLOC_CHEST; pi = &myPlayer.InvBody[rv]; } else if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { const int8_t itemId = std::abs(myPlayer.InvGrid[r - SLOTXY_INV_FIRST]); if (itemId == 0) return -1; const int ii = itemId - 1; rv = ii + INVITEM_INV_FIRST; pi = &myPlayer.InvList[ii]; } else if (r >= SLOTXY_BELT_FIRST) { r -= SLOTXY_BELT_FIRST; RedrawComponent(PanelDrawComponent::Belt); pi = &myPlayer.SpdList[r]; if (pi->isEmpty()) return -1; rv = r + INVITEM_BELT_FIRST; } if (pi->isEmpty()) return -1; if (pi->_itype == ItemType::Gold) { const int nGold = pi->_ivalue; InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); FloatingInfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); } else { InfoColor = pi->getTextColor(); InfoString = pi->getName(); FloatingInfoString = pi->getName(); if (pi->_iIdentified) { PrintItemDetails(*pi); } else { PrintItemDur(*pi); } } return rv; } void ConsumeScroll(Player &player) { const SpellID spellId = player.executedSpell.spellId; const auto isCurrentSpell = [spellId](const Item &item) -> bool { return item.isScrollOf(spellId) || item.isRuneOf(spellId); }; // Try to remove the scroll from selected inventory slot const int8_t itemSlot = player.executedSpell.spellFrom; if (itemSlot >= INVITEM_INV_FIRST && itemSlot <= INVITEM_INV_LAST) { const int itemIndex = itemSlot - INVITEM_INV_FIRST; const Item *item = &player.InvList[itemIndex]; if (!item->isEmpty() && isCurrentSpell(*item)) { player.RemoveInvItem(itemIndex); return; } } else if (itemSlot >= INVITEM_BELT_FIRST && itemSlot <= INVITEM_BELT_LAST) { const int itemIndex = itemSlot - INVITEM_BELT_FIRST; const Item *item = &player.SpdList[itemIndex]; if (!item->isEmpty() && isCurrentSpell(*item)) { player.RemoveSpdBarItem(itemIndex); return; } } else if (itemSlot != 0) { app_fatal(StrCat("ConsumeScroll: Invalid item index ", itemSlot)); } // Didn't find it at the selected slot, take the first one we find // This path is always used when the scroll is consumed via spell selection RemoveInventoryOrBeltItem(player, isCurrentSpell); } bool CanUseScroll(Player &player, SpellID spell) { if (leveltype == DTYPE_TOWN && !GetSpellData(spell).isAllowedInTown()) return false; return HasInventoryOrBeltItem(player, [spell](const Item &item) { return item.isScrollOf(spell) || item.isRuneOf(spell); }); } void ConsumeStaffCharge(Player &player) { Item &staff = player.InvBody[INVLOC_HAND_LEFT]; if (!CanUseStaff(staff, player.executedSpell.spellId)) return; staff._iCharges--; CalcPlrInv(player, false); } bool CanUseStaff(Player &player, SpellID spellId) { return CanUseStaff(player.InvBody[INVLOC_HAND_LEFT], spellId); } Item &GetInventoryItem(Player &player, int location) { if (location < INVITEM_INV_FIRST) return player.InvBody[location]; if (location <= INVITEM_INV_LAST) return player.InvList[location - INVITEM_INV_FIRST]; return player.SpdList[location - INVITEM_BELT_FIRST]; } bool UseInvItem(int cii) { if (IsInspectingPlayer()) return false; Player &player = *MyPlayer; if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) return true; if (pcurs != CURSOR_HAND) return true; if (IsPlayerInStore()) return true; if (cii < INVITEM_INV_FIRST) return false; bool speedlist = false; int c; Item *item; if (cii <= INVITEM_INV_LAST) { c = cii - INVITEM_INV_FIRST; item = &player.InvList[c]; } else { if (ChatFlag) return true; c = cii - INVITEM_BELT_FIRST; item = &player.SpdList[c]; speedlist = true; // If selected speedlist item exists in InvList, use the InvList item. for (int i = 0; i < player._pNumInv && *GetOptions().Gameplay.autoRefillBelt; i++) { if (player.InvList[i]._iMiscId == item->_iMiscId && player.InvList[i]._iSpell == item->_iSpell) { c = i; item = &player.InvList[c]; cii = c + INVITEM_INV_FIRST; speedlist = false; break; } } // If speedlist item is not inventory, use same item at the end of the speedlist if exists. if (speedlist && *GetOptions().Gameplay.autoRefillBelt) { for (int i = INVITEM_BELT_LAST - INVITEM_BELT_FIRST; i > c; i--) { Item &candidate = player.SpdList[i]; if (!candidate.isEmpty() && candidate._iMiscId == item->_iMiscId && candidate._iSpell == item->_iSpell) { c = i; cii = c + INVITEM_BELT_FIRST; item = &candidate; break; } } } } constexpr int SpeechDelay = 10; if (item->IDidx == IDI_MUSHROOM) { player.Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); return true; } if (item->IDidx == IDI_FUNGALTM) { PlaySFX(SfxID::ItemBook); player.Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); return true; } if (player.isOnLevel(0)) { if (UseItemOpensHive(*item, player.position.tile)) { OpenHive(); player.RemoveInvItem(c); return true; } if (UseItemOpensGrave(*item, player.position.tile)) { OpenGrave(); player.RemoveInvItem(c); return true; } } if (!item->isUsable()) return false; if (!player.CanUseItem(*item)) { player.Say(HeroSpeech::ICantUseThisYet); return true; } if (item->_iMiscId == IMISC_NONE && item->_itype == ItemType::Gold) { StartGoldDrop(); return true; } CloseGoldDrop(); if (item->isScroll() && leveltype == DTYPE_TOWN && !GetSpellData(item->_iSpell).isAllowedInTown()) { return true; } if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { return true; } if (item->_iMiscId == IMISC_ARENAPOT && !player.isOnArenaLevel()) { player.Say(HeroSpeech::ThatWontWorkHere); return true; } const int idata = ItemCAnimTbl[item->_iCurs]; if (item->_iMiscId == IMISC_BOOK) PlaySFX(SfxID::ReadBook); else if (&player == MyPlayer) PlaySFX(ItemInvSnds[idata]); UseItem(player, item->_iMiscId, item->_iSpell, cii); if (speedlist) { if (player.SpdList[c]._iMiscId == IMISC_NOTE) { InitQTextMsg(TEXT_BOOK9); CloseInventory(); return true; } if (!item->isScroll() && !item->isRune()) player.RemoveSpdBarItem(c); return true; } if (player.InvList[c]._iMiscId == IMISC_MAPOFDOOM) return true; if (player.InvList[c]._iMiscId == IMISC_NOTE) { InitQTextMsg(TEXT_BOOK9); CloseInventory(); return true; } if (!item->isScroll() && !item->isRune()) player.RemoveInvItem(c); return true; } void CloseInventory() { CloseGoldWithdraw(); CloseStash(); invflag = false; } void CloseStash() { if (!IsStashOpen) return; Player &myPlayer = *MyPlayer; if (!myPlayer.HoldItem.isEmpty()) { std::optional itemTile = FindAdjacentPositionForItem(myPlayer.position.future, myPlayer._pdir); if (itemTile) { NetSendCmdPItem(true, CMD_PUTITEM, *itemTile, myPlayer.HoldItem); } else { if (!AutoPlaceItemInBelt(myPlayer, myPlayer.HoldItem, true, true) && !AutoPlaceItemInInventory(myPlayer, myPlayer.HoldItem, true) && !AutoPlaceItemInStash(myPlayer, myPlayer.HoldItem, true)) { // This can fail for max gold, arena potions and a stash that has been arranged // to not have room for the item all 3 cases are extremely unlikely app_fatal(_("No room for item")); } PlaySFX(ItemInvSnds[ItemCAnimTbl[myPlayer.HoldItem._iCurs]]); } myPlayer.HoldItem.clear(); NewCursor(CURSOR_HAND); } IsStashOpen = false; } void DoTelekinesis() { if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->IsDisabled()) NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJT, cursPosition); if (pcursitem != -1) NetSendCmdGItem(true, CMD_REQUESTAGITEM, *MyPlayer, pcursitem); if (pcursmonst != -1) { const Monster &monter = Monsters[pcursmonst]; if (!M_Talker(monter) && monter.talkMsg == TEXT_NONE) NetSendCmdParam1(true, CMD_KNOCKBACK, pcursmonst); } NewCursor(CURSOR_HAND); } int CalculateGold(Player &player) { int gold = 0; for (int i = 0; i < player._pNumInv; i++) { if (player.InvList[i]._itype == ItemType::Gold) gold += player.InvList[i]._ivalue; } return gold; } Size GetInventorySize(const Item &item) { const Size size = GetInvItemSize(item._iCurs + CURSOR_FIRSTITEM); return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; } } // namespace devilution ================================================ FILE: Source/inv.h ================================================ /** * @file inv.h * * Interface of player inventory. */ #pragma once #include #include "engine/palette.h" #include "engine/point.hpp" #include "inv_iterators.hpp" #include "items.h" #include "player.h" #include "utils/algorithm/container.hpp" namespace devilution { #define INV_SLOT_SIZE_PX 28 #define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) constexpr Size InventorySizeInSlots { 10, 4 }; #define INV_ROW_SLOT_SIZE InventorySizeInSlots.width constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; enum inv_item : int8_t { // clang-format off INVITEM_HEAD = 0, INVITEM_RING_LEFT = 1, INVITEM_RING_RIGHT = 2, INVITEM_AMULET = 3, INVITEM_HAND_LEFT = 4, INVITEM_HAND_RIGHT = 5, INVITEM_CHEST = 6, INVITEM_INV_FIRST = 7, INVITEM_INV_LAST = 46, INVITEM_BELT_FIRST = 47, INVITEM_BELT_LAST = 54, // clang-format on }; /** * identifiers for each of the inventory squares * @see InvRect */ enum inv_xy_slot : uint8_t { // clang-format off SLOTXY_HEAD = 0, SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, SLOTXY_RING_LEFT = 1, SLOTXY_RING_RIGHT = 2, SLOTXY_AMULET = 3, SLOTXY_HAND_LEFT = 4, SLOTXY_HAND_RIGHT = 5, SLOTXY_CHEST = 6, SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, // regular inventory SLOTXY_INV_FIRST = 7, SLOTXY_INV_ROW1_FIRST = SLOTXY_INV_FIRST, SLOTXY_INV_ROW1_LAST = 16, SLOTXY_INV_ROW2_FIRST = 17, SLOTXY_INV_ROW2_LAST = 26, SLOTXY_INV_ROW3_FIRST = 27, SLOTXY_INV_ROW3_LAST = 36, SLOTXY_INV_ROW4_FIRST = 37, SLOTXY_INV_ROW4_LAST = 46, SLOTXY_INV_LAST = SLOTXY_INV_ROW4_LAST, // belt items SLOTXY_BELT_FIRST = 47, SLOTXY_BELT_LAST = 54, NUM_XY_SLOTS = 55 // clang-format on }; enum item_color : uint8_t { // clang-format off ICOL_YELLOW = PAL16_YELLOW + 5, ICOL_WHITE = PAL16_GRAY + 5, ICOL_BLUE = PAL16_BLUE + 5, ICOL_RED = PAL16_RED + 5, // clang-format on }; extern bool invflag; extern const Rectangle InvRect[NUM_XY_SLOTS]; void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); /** * @brief Checks whether the given item can be placed on the belt. Takes item size as well as characteristics into account. Items * that cannot be placed on the belt have to be placed in the inventory instead. * @param item The item to be checked. * @return 'True' in case the item can be placed on the belt and 'False' otherwise. */ bool CanBePlacedOnBelt(const Player &player, const Item &item); /** * @brief Function type which performs an operation on the given item. */ using ItemFunc = void (*)(Item &); void CloseInventory(); void CloseStash(); void FreeInvGFX(); void InitInv(); /** * @brief Render the inventory panel to the given buffer. */ void DrawInv(const Surface &out); void DrawInvBelt(const Surface &out); /** * @brief Removes equipment from the specified location on the player's body. * @param player The player from which equipment will be removed. * @param bodyLocation The location from which equipment will be removed. * @param hiPri Priority of the network message to sync player equipment. */ void RemoveEquipment(Player &player, inv_body_loc bodyLocation, bool hiPri); /** * @brief Checks whether or not auto-equipping behavior is enabled for the given player and item. * @param player The player to check. * @param item The item to check. * @return 'True' if auto-equipping behavior is enabled for the player and item and 'False' otherwise. */ bool AutoEquipEnabled(const Player &player, const Item &item); /** * @brief Automatically attempts to equip the specified item in the most appropriate location in the player's body. * @note On success, this will broadcast an equipment_change event to let other players know about the equipment change. * @param player The player whose inventory will be checked for compatibility with the item. * @param item The item to equip. * @param persistItem Indicates whether or not the item should be persisted in the player's body. Pass 'False' to check * whether the player can equip the item but you don't want the item to actually be equipped. 'True' by default. * @param sendNetworkMessage Set to true if you want an equip sound and network message to be generated if the equipment * changes. Should only be set if a local player is equipping an item in a play session (not when creating a new game) * @return 'True' if the item was equipped and 'False' otherwise. */ bool AutoEquip(Player &player, const Item &item, bool persistItem = true, bool sendNetworkMessage = false); /** * @brief Checks whether the given item can be placed on the specified player's inventory. * @param player The player whose inventory will be checked. * @param item The item to be checked. * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. */ bool CanFitItemInInventory(const Player &player, const Item &item); /** * @brief Attempts to place the given item in the specified player's inventory. * @param player The player whose inventory will be used. * @param item The item to be placed. * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. * Should only be set if a local player is placing an item in a play session (not when creating a new game) * @return 'True' if the item was placed on the player's inventory and 'False' otherwise. */ bool AutoPlaceItemInInventory(Player &player, const Item &item, bool sendNetworkMessage = false); /** * @brief Checks whether the given item can be placed on the specified player's belt. Returns 'True' when the item can be placed * on belt slots and the player has at least one empty slot in his belt. * If 'persistItem' is 'True', the item is also placed in the belt. * @param player The player on whose belt will be checked. * @param item The item to be checked. * @param persistItem Pass 'True' to actually place the item in the belt. The default is 'False'. * @param sendNetworkMessage Set to true if you want a network message to be generated if the item is persisted. * Should only be set if a local player is placing an item in a play session (not when creating a new game) * @return 'True' in case the item can be placed on the player's belt and 'False' otherwise. */ bool AutoPlaceItemInBelt(Player &player, const Item &item, bool persistItem = false, bool sendNetworkMessage = false); /** * @brief Sort player inventory. */ void ReorganizeInventory(Player &player); /** * @brief Calculate the maximum additional gold that may fit in the user's inventory */ int RoomForGold(); /** * @return The leftover amount that didn't fit, if any */ int AddGoldToInventory(Player &player, int value); bool GoldAutoPlace(Player &player, Item &goldStack); void CheckInvSwap(Player &player, inv_body_loc bLoc); void inv_update_rem_item(Player &player, inv_body_loc iv); void CheckInvSwap(Player &player, const Item &item, int invGridIndex); void CheckInvRemove(Player &player, int invGridIndex); void TransferItemToStash(Player &player, int location); void CheckInvItem(bool isShiftHeld = false, bool isCtrlHeld = false); /** * Check for interactions with belt */ void CheckInvScrn(bool isShiftHeld, bool isCtrlHeld); void InvGetItem(Player &player, int ii); /** * @brief Returns the first free space that can take an item preferencing tiles in front of the current position * * The search starts with the adjacent tile in the desired direction and alternates sides until it ends up checking the * opposite tile, before finally checking the origin tile * * @param origin center tile of the search space * @param facing direction of the adjacent tile to check first * @return the first valid point or an empty optional */ std::optional FindAdjacentPositionForItem(Point origin, Direction facing); void AutoGetItem(Player &player, Item *itemPointer, int ii); /** * @brief Searches for a dropped item with the same type/createInfo/seed * @param iseed The value used to initialise the RNG when generating the item * @param idx The overarching type of the target item * @param ci Flags used to describe the specific subtype of the target item * @return An index into ActiveItems or -1 if no matching item was found */ int FindGetItem(uint32_t iseed, _item_indexes idx, uint16_t ci); void SyncGetItem(Point position, uint32_t iseed, _item_indexes idx, uint16_t ci); /** * @brief Checks if the tile has room for an item * @param position tile coordinates * @return True if the space is free of obstructions, false if blocked */ bool CanPut(Point position); int ClampDurability(const Item &item, int durability); int16_t ClampToHit(const Item &item, int16_t toHit); uint8_t ClampMaxDam(const Item &item, uint8_t maxDam); int SyncDropItem(Point position, _item_indexes idx, uint16_t icreateinfo, int iseed, int id, int dur, int mdur, int ch, int mch, int ivalue, uint32_t ibuff, int toHit, int maxDam); int SyncDropEar(Point position, uint16_t icreateinfo, uint32_t iseed, uint8_t cursval, std::string_view heroname); int8_t CheckInvHLight(); bool CanUseScroll(Player &player, SpellID spell); void ConsumeStaffCharge(Player &player); bool CanUseStaff(Player &player, SpellID spellId); Item &GetInventoryItem(Player &player, int location); bool UseInvItem(int cii); void DoTelekinesis(); int CalculateGold(Player &player); /** * @brief Gets the size, in inventory cells, of the given item. * @param item The item whose size is to be determined. * @return The size, in inventory cells, of the item. */ Size GetInventorySize(const Item &item); /** * @brief Checks whether the player has an inventory item matching the predicate. */ template bool HasInventoryItem(const Player &player, Predicate &&predicate) { const InventoryPlayerItemsRange items { player }; return c_find_if(items, std::forward(predicate)) != items.end(); } /** * @brief Checks whether the player has a belt item matching the predicate. */ template bool HasBeltItem(const Player &player, Predicate &&predicate) { const BeltPlayerItemsRange items { player }; return c_find_if(items, std::forward(predicate)) != items.end(); } /** * @brief Checks whether the player has an inventory or a belt item matching the predicate. */ template bool HasInventoryOrBeltItem(const Player &player, Predicate &&predicate) { return HasInventoryItem(player, predicate) || HasBeltItem(player, predicate); } /** * @brief Checks whether the player has an inventory item with the given ID (IDidx). */ inline bool HasInventoryItemWithId(const Player &player, _item_indexes id) { return HasInventoryItem(player, [id](const Item &item) { return item.IDidx == id; }); } /** * @brief Checks whether the player has a belt item with the given ID (IDidx). */ inline bool HasBeltItemWithId(const Player &player, _item_indexes id) { return HasBeltItem(player, [id](const Item &item) { return item.IDidx == id; }); } /** * @brief Checks whether the player has an inventory or a belt item with the given ID (IDidx). */ inline bool HasInventoryOrBeltItemWithId(const Player &player, _item_indexes id) { return HasInventoryItemWithId(player, id) || HasBeltItemWithId(player, id); } /** * @brief Removes the first inventory item matching the predicate. * * @return Whether an item was found and removed. */ template bool RemoveInventoryItem(Player &player, Predicate &&predicate) { const InventoryPlayerItemsRange items { player }; const auto it = c_find_if(items, std::forward(predicate)); if (it == items.end()) return false; player.RemoveInvItem(static_cast(it.index())); return true; } /** * @brief Removes the first belt item matching the predicate. * * @return Whether an item was found and removed. */ template bool RemoveBeltItem(Player &player, Predicate &&predicate) { const BeltPlayerItemsRange items { player }; const auto it = c_find_if(items, std::forward(predicate)); if (it == items.end()) return false; player.RemoveSpdBarItem(static_cast(it.index())); return true; } /** * @brief Removes the first inventory or belt item matching the predicate. * * @return Whether an item was found and removed. */ template bool RemoveInventoryOrBeltItem(Player &player, Predicate &&predicate) { return RemoveInventoryItem(player, predicate) || RemoveBeltItem(player, predicate); } /** * @brief Removes the first inventory item with the given id (IDidx). * * @return Whether an item was found and removed. */ inline bool RemoveInventoryItemById(Player &player, _item_indexes id) { return RemoveInventoryItem(player, [id](const Item &item) { return item.IDidx == id; }); } /** * @brief Removes the first belt item with the given id (IDidx). * * @return Whether an item was found and removed. */ inline bool RemoveBeltItemById(Player &player, _item_indexes id) { return RemoveBeltItem(player, [id](const Item &item) { return item.IDidx == id; }); } /** * @brief Removes the first inventory or belt item with the given id (IDidx). * * @return Whether an item was found and removed. */ inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) { return RemoveInventoryItemById(player, id) || RemoveBeltItemById(player, id); } /** * @brief Removes the first inventory or belt scroll with the player's current spell. */ void ConsumeScroll(Player &player); /* data */ } // namespace devilution ================================================ FILE: Source/inv_iterators.hpp ================================================ #pragma once #include #include #include #include #include #include "items.h" #include "player.h" namespace devilution { /** * @brief A range over non-empty items in a container. */ template class ItemsContainerRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Item` or `const Item`"); public: class Iterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ItemT; using pointer = value_type *; using reference = value_type &; Iterator() = default; Iterator(ItemT *items, std::size_t count, std::size_t index) : items_(items) , count_(count) , index_(index) { advancePastEmpty(); } pointer operator->() const { return &items_[index_]; } reference operator*() const { return items_[index_]; } Iterator &operator++() { ++index_; advancePastEmpty(); return *this; } Iterator operator++(int) { auto copy = *this; ++(*this); return copy; } bool operator==(const Iterator &other) const { return index_ == other.index_; } bool operator!=(const Iterator &other) const { return !(*this == other); } [[nodiscard]] bool atEnd() const { return index_ == count_; } [[nodiscard]] std::size_t index() const { return index_; } private: void advancePastEmpty() { while (index_ < count_ && items_[index_].isEmpty()) { ++index_; } } ItemT *items_ = nullptr; std::size_t count_ = 0; std::size_t index_ = 0; }; ItemsContainerRange(ItemT *items, std::size_t count) : items_(items) , count_(count) { } [[nodiscard]] Iterator begin() const { return Iterator { items_, count_, 0 }; } [[nodiscard]] Iterator end() const { return Iterator { nullptr, count_, count_ }; } private: ItemT *items_; std::size_t count_; }; /** * @brief A range over non-empty items in a list of containers. */ template class ItemsContainerListRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Item` or `const Item`"); public: class Iterator { public: using iterator_category = std::forward_iterator_tag; using difference_type = int; using value_type = ItemT; using pointer = value_type *; using reference = value_type &; Iterator() = default; explicit Iterator(std::vector::Iterator> iterators) : iterators_(std::move(iterators)) { advancePastEmpty(); } pointer operator->() const { return iterators_[current_].operator->(); } reference operator*() const { return iterators_[current_].operator*(); } Iterator &operator++() { ++iterators_[current_]; advancePastEmpty(); return *this; } Iterator operator++(int) { auto copy = *this; ++(*this); return copy; } bool operator==(const Iterator &other) const { return current_ == other.current_ && iterators_[current_] == other.iterators_[current_]; } bool operator!=(const Iterator &other) const { return !(*this == other); } private: void advancePastEmpty() { while (current_ + 1 < iterators_.size() && iterators_[current_].atEnd()) { ++current_; } } std::vector::Iterator> iterators_; std::size_t current_ = 0; }; }; /** * @brief A range over equipped player items. */ template class EquippedPlayerItemsRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Player` or `const Player`"); using ItemT = std::conditional_t, const Item, Item>; using Iterator = typename ItemsContainerRange::Iterator; public: explicit EquippedPlayerItemsRange(PlayerT &player) : player_(&player) { } [[nodiscard]] Iterator begin() const { return Iterator { &player_->InvBody[0], containerSize(), 0 }; } [[nodiscard]] Iterator end() const { return Iterator { nullptr, containerSize(), containerSize() }; } private: [[nodiscard]] std::size_t containerSize() const { return sizeof(player_->InvBody) / sizeof(player_->InvBody[0]); } PlayerT *player_; }; /** * @brief A range over non-equipped inventory player items. */ template class InventoryPlayerItemsRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Player` or `const Player`"); using ItemT = std::conditional_t, const Item, Item>; using Iterator = typename ItemsContainerRange::Iterator; public: explicit InventoryPlayerItemsRange(PlayerT &player) : player_(&player) { } [[nodiscard]] Iterator begin() const { return Iterator { &player_->InvList[0], containerSize(), 0 }; } [[nodiscard]] Iterator end() const { return Iterator { nullptr, containerSize(), containerSize() }; } private: [[nodiscard]] std::size_t containerSize() const { return static_cast(player_->_pNumInv); } PlayerT *player_; }; /** * @brief A range over belt player items. */ template class BeltPlayerItemsRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Player` or `const Player`"); using ItemT = std::conditional_t, const Item, Item>; using Iterator = typename ItemsContainerRange::Iterator; public: explicit BeltPlayerItemsRange(PlayerT &player) : player_(&player) { } [[nodiscard]] Iterator begin() const { return Iterator { &player_->SpdList[0], containerSize(), 0 }; } [[nodiscard]] Iterator end() const { return Iterator { nullptr, containerSize(), containerSize() }; } private: [[nodiscard]] std::size_t containerSize() const { return sizeof(player_->SpdList) / sizeof(player_->SpdList[0]); } PlayerT *player_; }; /** * @brief A range over non-equipped player items in the following order: Inventory, Belt. */ template class InventoryAndBeltPlayerItemsRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Player` or `const Player`"); using ItemT = std::conditional_t, const Item, Item>; using Iterator = typename ItemsContainerListRange::Iterator; public: explicit InventoryAndBeltPlayerItemsRange(PlayerT &player) : player_(&player) { } [[nodiscard]] Iterator begin() const { return Iterator({ InventoryPlayerItemsRange(*player_).begin(), BeltPlayerItemsRange(*player_).begin(), }); } [[nodiscard]] Iterator end() const { return Iterator({ InventoryPlayerItemsRange(*player_).end(), BeltPlayerItemsRange(*player_).end(), }); } private: PlayerT *player_; }; /** * @brief A range over non-empty player items in the following order: Equipped, Inventory, Belt. */ template class PlayerItemsRange { static_assert(std::is_same_v || std::is_same_v, "The template argument must be `Player` or `const Player`"); using ItemT = std::conditional_t, const Item, Item>; using Iterator = typename ItemsContainerListRange::Iterator; public: explicit PlayerItemsRange(PlayerT &player) : player_(&player) { } [[nodiscard]] Iterator begin() const { return Iterator({ EquippedPlayerItemsRange(*player_).begin(), InventoryPlayerItemsRange(*player_).begin(), BeltPlayerItemsRange(*player_).begin(), }); } [[nodiscard]] Iterator end() const { return Iterator({ EquippedPlayerItemsRange(*player_).end(), InventoryPlayerItemsRange(*player_).end(), BeltPlayerItemsRange(*player_).end(), }); } private: PlayerT *player_; }; } // namespace devilution ================================================ FILE: Source/items/validation.cpp ================================================ /** * @file items/validation.cpp * * Implementation of functions for validation of player and item data. */ #include "items/validation.h" #include #include "items.h" #include "msg.h" #include "player.h" #include "spells.h" #include "tables/monstdat.h" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" namespace devilution { namespace { bool hasMultipleFlags(uint16_t flags) { return (flags & (flags - 1)) > 0; } } // namespace bool IsCreationFlagComboValid(uint16_t iCreateInfo) { iCreateInfo = iCreateInfo & ~CF_LEVEL; const bool isTownItem = (iCreateInfo & CF_TOWN) != 0; const bool isPregenItem = (iCreateInfo & CF_PREGEN) != 0; const bool isUsefulItem = (iCreateInfo & CF_USEFUL) == CF_USEFUL; if (isPregenItem) { // Pregen flags are discarded when an item is picked up, therefore impossible to have in the inventory return false; } if (isUsefulItem && (iCreateInfo & ~CF_USEFUL) != 0) return false; if (isTownItem && hasMultipleFlags(iCreateInfo)) { // Items from town can only have 1 towner flag return false; } return true; } bool IsTownItemValid(uint16_t iCreateInfo, const Player &player) { const uint8_t level = iCreateInfo & CF_LEVEL; const bool isBoyItem = (iCreateInfo & CF_BOY) != 0; const uint8_t maxTownItemLevel = 30; // Wirt items in multiplayer are equal to the level of the player, therefore they cannot exceed the max character level if (isBoyItem && level <= player.getMaxCharacterLevel()) return true; return level <= maxTownItemLevel; } bool IsShopPriceValid(const Item &item) { const int boyPriceLimit = MaxBoyValue; if (!gbIsHellfire && (item._iCreateInfo & CF_BOY) != 0 && item._iIvalue > boyPriceLimit) return false; const int premiumPriceLimit = MaxVendorValue; if (!gbIsHellfire && (item._iCreateInfo & CF_SMITHPREMIUM) != 0 && item._iIvalue > premiumPriceLimit) return false; const uint16_t smithOrWitch = CF_SMITH | CF_WITCH; const int smithAndWitchPriceLimit = gbIsHellfire ? MaxVendorValueHf : MaxVendorValue; if ((item._iCreateInfo & smithOrWitch) != 0 && item._iIvalue > smithAndWitchPriceLimit) return false; return true; } bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff) { const uint8_t level = iCreateInfo & CF_LEVEL; // Check all unique monster levels to see if they match the item level for (const UniqueMonsterData &uniqueMonsterData : UniqueMonstersData) { if (IsAnyOf(uniqueMonsterData.mtype, MT_DEFILER, MT_NAKRUL, MT_HORKDMN)) { // These monsters don't use their mlvl for item generation continue; } const auto &uniqueMonsterLevel = static_cast(MonstersData[uniqueMonsterData.mtype].level); if (level == uniqueMonsterLevel) { // If the ilvl matches the mlvl, we confirm the item is legitimate return true; } } return false; } bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff) { const uint8_t level = iCreateInfo & CF_LEVEL; const bool isHellfireItem = (dwBuff & CF_HELLFIRE) != 0; // Check all monster levels to see if they match the item level int type = -1; for (const MonsterData &monsterData : MonstersData) { type++; auto monsterLevel = static_cast(monsterData.level); if (type != MT_DIABLO && monsterData.availability == MonsterAvailability::Never) { // Skip monsters that are unable to appear in the game continue; } if (type == MT_DIABLO && isHellfireItem) { // Adjust The Dark Lord's mlvl if the item is a Hellfire item to match the Diablo mlvl monsterLevel += 15; } if (level == monsterLevel) { // If the ilvl matches the mlvl, we confirm the item is legitimate return true; } } if (isHellfireItem) { uint8_t hellfireMaxDungeonLevel = 24; // Hellfire adjusts the currlevel minus 7 in dungeon levels 20-24 for generating items hellfireMaxDungeonLevel -= 7; return level <= (hellfireMaxDungeonLevel * 2); } uint8_t diabloMaxDungeonLevel = 16; // Diablo doesn't have containers that drop items in dungeon level 16, therefore we decrement by 1 diabloMaxDungeonLevel--; return level <= (diabloMaxDungeonLevel * 2); } bool IsHellfireSpellBookValid(const Item &spellBook) { // Hellfire uses the spell book level when generating items via CreateSpellBook() int spellBookLevel = GetSpellBookLevel(spellBook._iSpell); // CreateSpellBook() adds 1 to the spell level for ilvl spellBookLevel++; if (spellBookLevel >= 1 && (spellBook._iCreateInfo & CF_LEVEL) == spellBookLevel * 2) { // The ilvl matches the result for a spell book drop, so we confirm the item is legitimate return true; } return IsDungeonItemValid(spellBook._iCreateInfo, spellBook.dwBuff); } bool IsItemValid(const Player &player, const Item &item) { if (!gbIsMultiplayer) return true; if (item.IDidx == IDI_EAR) return true; if (item.IDidx != IDI_GOLD && !IsCreationFlagComboValid(item._iCreateInfo)) return false; if ((item._iCreateInfo & CF_TOWN) != 0) return IsTownItemValid(item._iCreateInfo, player) && IsShopPriceValid(item); if ((item._iCreateInfo & CF_USEFUL) == CF_UPER15) return IsUniqueMonsterItemValid(item._iCreateInfo, item.dwBuff); if ((item.dwBuff & CF_HELLFIRE) != 0 && AllItemsList[item.IDidx].iMiscId == IMISC_BOOK) return IsHellfireSpellBookValid(item); return IsDungeonItemValid(item._iCreateInfo, item.dwBuff); } bool IsItemDeltaValid(const TCmdPItem &itemDelta) { if (itemDelta.bCmd == CMD_INVALID) return true; if (IsNoneOf(itemDelta.bCmd, TCmdPItem::FloorItem, TCmdPItem::PickedUpItem, TCmdPItem::DroppedItem)) return false; if (!InDungeonBounds({ itemDelta.x, itemDelta.y })) return false; const _item_indexes idx = static_cast<_item_indexes>(Swap16LE(itemDelta.def.wIndx)); if (idx == IDI_EAR) return true; if (!IsItemAvailable(idx)) return false; Item item = {}; RecreateItem(*MyPlayer, itemDelta.item, item); return IsItemValid(*MyPlayer, item); } } // namespace devilution ================================================ FILE: Source/items/validation.h ================================================ /** * @file items/validation.h * * Interface of functions for validation of player and item data. */ #pragma once #include namespace devilution { // Forward declared structs to avoid circular dependencies struct Item; struct TCmdPItem; struct Player; bool IsCreationFlagComboValid(uint16_t iCreateInfo); bool IsTownItemValid(uint16_t iCreateInfo, const Player &player); bool IsShopPriceValid(const Item &item); bool IsUniqueMonsterItemValid(uint16_t iCreateInfo, uint32_t dwBuff); bool IsDungeonItemValid(uint16_t iCreateInfo, uint32_t dwBuff); bool IsItemValid(const Player &player, const Item &item); bool IsItemDeltaValid(const TCmdPItem &itemDelta); } // namespace devilution ================================================ FILE: Source/items.cpp ================================================ /** * @file items.cpp * * Implementation of item functionality. */ #include "items.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "DiabloUI/ui_flags.hpp" #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/controller_buttons.h" #include "cursor.h" #include "diablo.h" #include "doom.h" #include "effects.h" #include "engine/animationinfo.h" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "engine/surface.hpp" #include "engine/world_tile.hpp" #include "function_ref.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "inv.h" #include "inv_iterators.hpp" #include "items/validation.h" #include "levels/gendung.h" #include "levels/gendung_defs.hpp" #include "levels/tile_properties.hpp" #include "levels/town.h" #include "lighting.h" #include "minitext.h" #include "monster.h" #include "msg.h" #include "multi.h" #include "objects.h" #include "options.h" #include "pack.h" #include "panels/info_box.hpp" #include "panels/ui_panels.hpp" #include "player.h" #include "qol/stash.h" #include "quests.h" #include "sound_effect_enums.h" #include "spells.h" #include "stores.h" #include "tables/itemdat.h" #include "tables/monstdat.h" #include "tables/objdat.h" #include "tables/playerdat.hpp" #include "tables/spelldat.h" #include "tables/textdat.h" #include "utils/enum_traits.h" #include "utils/format_int.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/math.h" #include "utils/sdl_geometry.h" #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #include "utils/string_or_view.hpp" #include "utils/utf8.hpp" namespace devilution { Item Items[MAXITEMS + 1]; uint8_t ActiveItems[MAXITEMS]; uint8_t ActiveItemCount; int8_t dItem[MAXDUNX][MAXDUNY]; bool ShowUniqueItemInfoBox; CornerStoneStruct CornerStone; bool UniqueItemFlags[128]; int MaxGold = GOLD_MAX_LIMIT; /** Maps from item_cursor_graphic to in-memory item type. */ int8_t ItemCAnimTbl[] = { 20, 16, 16, 16, 4, 4, 4, 12, 12, 12, 12, 12, 12, 12, 12, 21, 21, 25, 12, 28, 28, 28, 38, 38, 38, 32, 38, 38, 38, 24, 24, 26, 2, 25, 22, 23, 24, 21, 27, 27, 29, 0, 0, 0, 12, 12, 12, 12, 12, 0, 8, 8, 0, 8, 8, 8, 8, 8, 8, 6, 8, 8, 8, 6, 8, 8, 6, 8, 8, 6, 6, 6, 8, 8, 8, 5, 9, 13, 13, 13, 5, 5, 5, 15, 5, 5, 18, 18, 18, 30, 5, 5, 14, 5, 14, 13, 16, 18, 5, 5, 7, 1, 3, 17, 1, 15, 10, 14, 3, 11, 8, 0, 1, 7, 0, 7, 15, 7, 3, 3, 3, 6, 6, 11, 11, 11, 31, 14, 14, 14, 6, 6, 7, 3, 8, 14, 0, 14, 14, 0, 33, 1, 1, 1, 1, 1, 7, 7, 7, 14, 14, 17, 17, 17, 0, 34, 1, 0, 3, 17, 8, 8, 6, 1, 3, 3, 11, 3, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 35, 39, 36, 36, 36, 37, 38, 38, 38, 38, 38, 41, 42, 8, 8, 8, 17, 0, 6, 8, 11, 11, 3, 3, 1, 6, 6, 6, 1, 8, 6, 11, 3, 6, 8, 1, 6, 6, 17, 40, 0, 0 }; /** Maps of drop sounds effect of placing the item in the inventory. */ SfxID ItemInvSnds[] = { SfxID::ItemArmor, SfxID::ItemAxe, SfxID::ItemPotion, SfxID::ItemBow, SfxID::ItemGold, SfxID::ItemCap, SfxID::ItemSword, SfxID::ItemShield, SfxID::ItemSword, SfxID::ItemRock, SfxID::ItemAxe, SfxID::ItemStaff, SfxID::ItemRing, SfxID::ItemCap, SfxID::ItemLeather, SfxID::ItemShield, SfxID::ItemScroll, SfxID::ItemArmor, SfxID::ItemBook, SfxID::ItemArmor, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemPotion, SfxID::ItemBodyPart, SfxID::ItemBodyPart, SfxID::ItemMushroom, SfxID::ItemSign, SfxID::ItemBloodStone, SfxID::ItemAnvil, SfxID::ItemStaff, SfxID::ItemRock, SfxID::ItemScroll, SfxID::ItemScroll, SfxID::ItemRock, SfxID::ItemMushroom, SfxID::ItemArmor, SfxID::ItemLeather, SfxID::ItemLeather, }; namespace { OptionalOwnedClxSpriteList itemanims[ITEMTYPES]; enum class PlayerArmorGraphic : uint8_t { // clang-format off Light = 0, Medium = 1 << 4, Heavy = 1 << 5, // clang-format on }; Item curruitem; /** Holds item get records, tracking items being recently looted. This is in an effort to prevent items being picked up more than once. */ ItemGetRecordStruct itemrecord[MAXITEMS]; bool itemhold[3][3]; /** Specifies the number of active item get records. */ int gnNumGetRecords; int OilLevels[] = { 1, 10, 1, 10, 4, 1, 5, 17, 1, 10 }; int OilValues[] = { 500, 2500, 500, 2500, 1500, 100, 2500, 15000, 500, 2500 }; item_misc_id OilMagic[] = { IMISC_OILACC, IMISC_OILMAST, IMISC_OILSHARP, IMISC_OILDEATH, IMISC_OILSKILL, IMISC_OILBSMTH, IMISC_OILFORT, IMISC_OILPERM, IMISC_OILHARD, IMISC_OILIMP, }; char OilNames[10][25] = { N_("Oil of Accuracy"), N_("Oil of Mastery"), N_("Oil of Sharpness"), N_("Oil of Death"), N_("Oil of Skill"), N_("Blacksmith Oil"), N_("Oil of Fortitude"), N_("Oil of Permanence"), N_("Oil of Hardening"), N_("Oil of Imperviousness") }; /** Map of item type .cel file names. */ const char *const ItemDropNames[] = { "armor2", "axe", "fbttle", "bow", "goldflip", "helmut", "mace", "shield", "swrdflip", "rock", "cleaver", "staff", "ring", "crownf", "larmor", "wshield", "scroll", "fplatear", "fbook", "food", "fbttlebb", "fbttledy", "fbttleor", "fbttlebr", "fbttlebl", "fbttleby", "fbttlewh", "fbttledb", "fear", "fbrain", "fmush", "innsign", "bldstn", "fanvil", "flazstaf", "bombs1", "halfps1", "wholeps1", "runes1", "teddys1", "cows1", "donkys1", "mooses1", }; /** Maps of item drop animation length. */ int8_t ItemAnimLs[] = { 15, 13, 16, 13, 10, 13, 13, 13, 13, 20, 13, 13, 13, 13, 13, 13, 13, 13, 13, 1, 16, 16, 16, 16, 16, 16, 16, 16, 13, 12, 12, 13, 13, 13, 8, 10, 16, 16, 10, 10, 15, 15, 15, }; /** Maps of drop sounds effect of dropping the item on ground. */ SfxID ItemDropSnds[] = { SfxID::ItemArmorFlip, SfxID::ItemAxeFlip, SfxID::ItemPotionFlip, SfxID::ItemBowFlip, SfxID::ItemGold, SfxID::ItemCapFlip, SfxID::ItemSwordFlip, SfxID::ItemShieldFlip, SfxID::ItemSwordFlip, SfxID::ItemRockFlip, SfxID::ItemAxeFlip, SfxID::ItemStaffFlip, SfxID::ItemRingFlip, SfxID::ItemCapFlip, SfxID::ItemLeatherFlip, SfxID::ItemShieldFlip, SfxID::ItemScrollFlip, SfxID::ItemArmorFlip, SfxID::ItemBookFlip, SfxID::ItemLeatherFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemPotionFlip, SfxID::ItemBodyPartFlip, SfxID::ItemBodyPartFlip, SfxID::ItemMushroomFlip, SfxID::ItemSignFlip, SfxID::ItemBloodStoneFlip, SfxID::ItemAnvilFlip, SfxID::ItemStaffFlip, SfxID::ItemRockFlip, SfxID::ItemScrollFlip, SfxID::ItemScrollFlip, SfxID::ItemRockFlip, SfxID::ItemMushroomFlip, SfxID::ItemArmorFlip, SfxID::ItemLeatherFlip, SfxID::ItemLeatherFlip, }; /** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */ int itemLevelAdd[] = { // clang-format off -1, -1, 0, 0, 1, 2, // clang-format on }; /** Maps from Griswold premium item number to a quality level delta as added to the base quality level. */ int itemLevelAddHf[] = { // clang-format off -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 3, // clang-format on }; int ItemsGetCurrlevel() { if (setlevel) { switch (setlvlnum) { case SL_SKELKING: return Quests[Q_SKELKING]._qlevel; case SL_BONECHAMB: return Quests[Q_SCHAMB]._qlevel; case SL_POISONWATER: return Quests[Q_PWATER]._qlevel; case SL_VILEBETRAYER: return Quests[Q_BETRAYER]._qlevel; default: return 1; } } if (leveltype == DTYPE_NEST) return currlevel - 8; if (leveltype == DTYPE_CRYPT) return currlevel - 7; return currlevel; } bool ItemPlace(Point position) { if (dMonster[position.x][position.y] != 0) return false; if (dPlayer[position.x][position.y] != 0) return false; if (dItem[position.x][position.y] != 0) return false; if (IsObjectAtPosition(position)) return false; if (TileContainsSetPiece(position)) return false; if (IsTileSolid(position)) return false; return true; } Point GetRandomAvailableItemPosition() { Point position = {}; do { position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 }; } while (!ItemPlace(position)); return position; } void AddInitItems() { const int curlv = ItemsGetCurrlevel(); const int rnd = GenerateRnd(3) + 3; for (int j = 0; j < rnd; j++) { const int ii = AllocateItem(); auto &item = Items[ii]; const Point position = GetRandomAvailableItemPosition(); item.position = position; dItem[position.x][position.y] = ii + 1; item._iSeed = AdvanceRndSeed(); SetRndSeed(item._iSeed); GetItemAttrs(item, PickRandomlyAmong({ IDI_MANA, IDI_HEAL }), curlv); item._iCreateInfo = curlv | CF_PREGEN; SetupItem(item); item.AnimInfo.currentFrame = static_cast(item.AnimInfo.numberOfFrames - 1); item._iAnimFlag = false; item.selectionRegion = SelectionRegion::Bottom; DeltaAddItem(ii); } } void SpawnNote() { _item_indexes id; switch (currlevel) { case 22: id = IDI_NOTE2; break; case 23: id = IDI_NOTE3; break; default: id = IDI_NOTE1; break; } const Point position = GetRandomAvailableItemPosition(); SpawnQuestItem(id, position, 0, SelectionRegion::Bottom, false); } void CalcSelfItems(Player &player) { int sa = 0; int ma = 0; int da = 0; // first iteration is used for collecting stat bonuses from items for (Item &equipment : EquippedPlayerItemsRange(player)) { equipment._iStatFlag = true; if (equipment._iIdentified) { sa += equipment._iPLStr; ma += equipment._iPLMag; da += equipment._iPLDex; } } bool changeflag; do { // cap stats to 0 const int currstr = std::max(0, sa + player._pBaseStr); const int currmag = std::max(0, ma + player._pBaseMag); const int currdex = std::max(0, da + player._pBaseDex); changeflag = false; // Iterate over equipped items and remove stat bonuses if they are not valid for (Item &equipment : EquippedPlayerItemsRange(player)) { if (!equipment._iStatFlag) continue; bool isValid = IsItemValid(player, equipment); if (currstr < equipment._iMinStr || currmag < equipment._iMinMag || currdex < equipment._iMinDex) isValid = false; if (isValid) continue; changeflag = true; equipment._iStatFlag = false; if (equipment._iIdentified) { sa -= equipment._iPLStr; ma -= equipment._iPLMag; da -= equipment._iPLDex; } } } while (changeflag); } bool GetItemSpace(Point position, int8_t inum) { int xx = 0; int yy = 0; for (int j = position.y - 1; j <= position.y + 1; j++) { xx = 0; for (int i = position.x - 1; i <= position.x + 1; i++) { itemhold[xx][yy] = ItemSpaceOk({ i, j }); xx++; } yy++; } bool savail = false; for (int j = 0; j < 3; j++) { for (int i = 0; i < 3; i++) { // NOLINT(modernize-loop-convert) if (itemhold[i][j]) savail = true; } } int rs = GenerateRnd(15) + 1; if (!savail) return false; xx = 0; yy = 0; while (rs > 0) { if (itemhold[xx][yy]) rs--; if (rs <= 0) continue; xx++; if (xx != 3) continue; xx = 0; yy++; if (yy == 3) yy = 0; } xx += position.x - 1; yy += position.y - 1; Items[inum].position = { xx, yy }; dItem[xx][yy] = static_cast(inum + 1); return true; } void CalcItemValue(Item &item) { int v = item._iVMult1 + item._iVMult2; if (v > 0) { v *= item._ivalue; } if (v < 0) { v = item._ivalue / v; } v = item._iVAdd1 + item._iVAdd2 + v; item._iIvalue = std::max(v, 1); } void GetBookSpell(Item &item, int lvl) { if (lvl == 0) lvl = 1; int rv = GenerateRnd(static_cast(SpellsData.size())) + 1; if (gbIsSpawn && lvl > 5) lvl = 5; int s = static_cast(SpellID::Firebolt); SpellID bs = SpellID::Firebolt; while (rv > 0) { const int sLevel = GetSpellBookLevel(static_cast(s)); if (sLevel != -1 && lvl >= sLevel) { rv--; bs = static_cast(s); } s++; if (!gbIsMultiplayer) { if (s == static_cast(SpellID::Resurrect)) s = static_cast(SpellID::Telekinesis); } if (!gbIsMultiplayer) { if (s == static_cast(SpellID::HealOther)) s = static_cast(SpellID::BloodStar); } if (static_cast(s) == SpellsData.size()) s = 1; } const std::string_view spellName = GetSpellData(bs).sNameText; const size_t iNameLen = std::string_view(item._iName).size(); const size_t iINameLen = std::string_view(item._iIName).size(); CopyUtf8(item._iName + iNameLen, spellName, ItemNameLength - iNameLen); CopyUtf8(item._iIName + iINameLen, spellName, ItemNameLength - iINameLen); item._iSpell = bs; const SpellData &spellData = GetSpellData(bs); item._iMinMag = spellData.minInt; item._ivalue += static_cast(spellData.bookCost()); item._iIvalue += static_cast(spellData.bookCost()); switch (spellData.type()) { case MagicType::Fire: item._iCurs = ICURS_BOOK_RED; break; case MagicType::Lightning: item._iCurs = ICURS_BOOK_BLUE; break; case MagicType::Magic: item._iCurs = ICURS_BOOK_GREY; break; } } int RndPL(int param1, int param2) { return param1 + GenerateRnd(param2 - param1 + 1); } int CalculateToHitBonus(int level) { switch (level) { case -50: return -RndPL(6, 10); case -25: return -RndPL(1, 5); case 20: return RndPL(1, 5); case 36: return RndPL(6, 10); case 51: return RndPL(11, 15); case 66: return RndPL(16, 20); case 81: return RndPL(21, 30); case 96: return RndPL(31, 40); case 111: return RndPL(41, 50); case 126: return RndPL(51, 75); case 151: return RndPL(76, 100); default: app_fatal("Unknown to hit bonus"); } } int SaveItemPower(const Player &player, Item &item, ItemPower &power) { int r = RndPL(power.param1, power.param2); switch (power.type) { case IPL_TOHIT: item._iPLToHit += static_cast(r); break; case IPL_TOHIT_CURSE: item._iPLToHit -= static_cast(r); break; case IPL_DAMP: item._iPLDam += static_cast(r); break; case IPL_DAMP_CURSE: item._iPLDam -= static_cast(r); break; case IPL_DOPPELGANGER: item._iDamAcFlags |= ItemSpecialEffectHf::Doppelganger; [[fallthrough]]; case IPL_TOHIT_DAMP: r = RndPL(power.param1, power.param2); item._iPLDam += static_cast(r); item._iPLToHit += CalculateToHitBonus(power.param1); break; case IPL_TOHIT_DAMP_CURSE: item._iPLDam -= static_cast(r); item._iPLToHit += CalculateToHitBonus(-power.param1); break; case IPL_ACP: item._iPLAC += r; break; case IPL_ACP_CURSE: item._iPLAC -= r; break; case IPL_SETAC: item._iAC = r; break; case IPL_AC_CURSE: item._iAC -= r; break; case IPL_FIRERES: item._iPLFR += r; break; case IPL_LIGHTRES: item._iPLLR += r; break; case IPL_MAGICRES: item._iPLMR += r; break; case IPL_ALLRES: item._iPLFR = std::max(item._iPLFR + r, 0); item._iPLLR = std::max(item._iPLLR + r, 0); item._iPLMR = std::max(item._iPLMR + r, 0); break; case IPL_SPLLVLADD: item._iSplLvlAdd = r; break; case IPL_CHARGES: item._iCharges *= power.param1; item._iMaxCharges = item._iCharges; break; case IPL_SPELL: item._iSpell = static_cast(power.param1); item._iCharges = power.param2; item._iMaxCharges = power.param2; break; case IPL_FIREDAM: item._iFlags |= ItemSpecialEffect::FireDamage; item._iFlags &= ~ItemSpecialEffect::LightningDamage; item._iFMinDam = power.param1; item._iFMaxDam = power.param2; item._iLMinDam = 0; item._iLMaxDam = 0; break; case IPL_LIGHTDAM: item._iFlags |= ItemSpecialEffect::LightningDamage; item._iFlags &= ~ItemSpecialEffect::FireDamage; item._iLMinDam = power.param1; item._iLMaxDam = power.param2; item._iFMinDam = 0; item._iFMaxDam = 0; break; case IPL_STR: item._iPLStr += r; break; case IPL_STR_CURSE: item._iPLStr -= r; break; case IPL_MAG: item._iPLMag += r; break; case IPL_MAG_CURSE: item._iPLMag -= r; break; case IPL_DEX: item._iPLDex += r; break; case IPL_DEX_CURSE: item._iPLDex -= r; break; case IPL_VIT: item._iPLVit += r; break; case IPL_VIT_CURSE: item._iPLVit -= r; break; case IPL_ATTRIBS: item._iPLStr += r; item._iPLMag += r; item._iPLDex += r; item._iPLVit += r; break; case IPL_ATTRIBS_CURSE: item._iPLStr -= r; item._iPLMag -= r; item._iPLDex -= r; item._iPLVit -= r; break; case IPL_GETHIT_CURSE: item._iPLGetHit += r; break; case IPL_GETHIT: item._iPLGetHit -= r; break; case IPL_LIFE: item._iPLHP += r << 6; break; case IPL_LIFE_CURSE: item._iPLHP -= r << 6; break; case IPL_MANA: item._iPLMana += r << 6; RedrawComponent(PanelDrawComponent::Mana); break; case IPL_MANA_CURSE: item._iPLMana -= r << 6; RedrawComponent(PanelDrawComponent::Mana); break; case IPL_DUR: { const int bonus = r * item._iMaxDur / 100; item._iMaxDur += bonus; item._iDurability += bonus; } break; case IPL_CRYSTALLINE: item._iPLDam += 140 + r * 2; [[fallthrough]]; case IPL_DUR_CURSE: item._iMaxDur -= r * item._iMaxDur / 100; item._iMaxDur = std::max(item._iMaxDur, 1); item._iDurability = item._iMaxDur; break; case IPL_INDESTRUCTIBLE: item._iDurability = DUR_INDESTRUCTIBLE; item._iMaxDur = DUR_INDESTRUCTIBLE; break; case IPL_LIGHT: item._iPLLight += power.param1; break; case IPL_LIGHT_CURSE: item._iPLLight -= power.param1; break; case IPL_MULT_ARROWS: item._iFlags |= ItemSpecialEffect::MultipleArrows; break; case IPL_FIRE_ARROWS: item._iFlags |= (ItemSpecialEffect::FireArrows | ItemSpecialEffect::FireDamage); item._iFlags &= ~ItemSpecialEffect::LightningArrows; item._iFMinDam = power.param1; item._iFMaxDam = power.param2; item._iLMinDam = 0; item._iLMaxDam = 0; break; case IPL_LIGHT_ARROWS: item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::LightningDamage); item._iFlags &= ~ItemSpecialEffect::FireArrows; item._iLMinDam = power.param1; item._iLMaxDam = power.param2; item._iFMinDam = 0; item._iFMaxDam = 0; break; case IPL_FIREBALL: item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::FireArrows); item._iFMinDam = power.param1; item._iFMaxDam = power.param2; item._iLMinDam = 0; item._iLMaxDam = 0; break; case IPL_THORNS: item._iFlags |= ItemSpecialEffect::Thorns; break; case IPL_NOMANA: item._iFlags |= ItemSpecialEffect::NoMana; RedrawComponent(PanelDrawComponent::Mana); break; case IPL_ABSHALFTRAP: item._iFlags |= ItemSpecialEffect::HalfTrapDamage; break; case IPL_KNOCKBACK: item._iFlags |= ItemSpecialEffect::Knockback; break; case IPL_3XDAMVDEM: item._iFlags |= ItemSpecialEffect::TripleDemonDamage; break; case IPL_ALLRESZERO: item._iFlags |= ItemSpecialEffect::ZeroResistance; break; case IPL_STEALMANA: if (power.param1 == 3) item._iFlags |= ItemSpecialEffect::StealMana3; if (power.param1 == 5) item._iFlags |= ItemSpecialEffect::StealMana5; RedrawComponent(PanelDrawComponent::Mana); break; case IPL_STEALLIFE: if (power.param1 == 3) item._iFlags |= ItemSpecialEffect::StealLife3; if (power.param1 == 5) item._iFlags |= ItemSpecialEffect::StealLife5; RedrawComponent(PanelDrawComponent::Health); break; case IPL_TARGAC: if (gbIsHellfire) item._iPLEnAc = power.param1; else item._iPLEnAc += r; break; case IPL_FASTATTACK: if (power.param1 == 1) item._iFlags |= ItemSpecialEffect::QuickAttack; if (power.param1 == 2) item._iFlags |= ItemSpecialEffect::FastAttack; if (power.param1 == 3) item._iFlags |= ItemSpecialEffect::FasterAttack; if (power.param1 == 4) item._iFlags |= ItemSpecialEffect::FastestAttack; break; case IPL_FASTRECOVER: if (power.param1 == 1) item._iFlags |= ItemSpecialEffect::FastHitRecovery; if (power.param1 == 2) item._iFlags |= ItemSpecialEffect::FasterHitRecovery; if (power.param1 == 3) item._iFlags |= ItemSpecialEffect::FastestHitRecovery; break; case IPL_FASTBLOCK: item._iFlags |= ItemSpecialEffect::FastBlock; break; case IPL_DAMMOD: item._iPLDamMod += r; break; case IPL_RNDARROWVEL: item._iFlags |= ItemSpecialEffect::RandomArrowVelocity; break; case IPL_SETDAM: item._iMinDam = power.param1; item._iMaxDam = power.param2; break; case IPL_SETDUR: item._iDurability = power.param1; item._iMaxDur = power.param1; break; case IPL_ONEHAND: item._iLoc = ILOC_ONEHAND; break; case IPL_DRAINLIFE: item._iFlags |= ItemSpecialEffect::DrainLife; break; case IPL_RNDSTEALLIFE: item._iFlags |= ItemSpecialEffect::RandomStealLife; break; case IPL_NOMINSTR: item._iMinStr = 0; break; case IPL_ADDACLIFE: item._iFlags |= (ItemSpecialEffect::LightningArrows | ItemSpecialEffect::FireArrows); item._iFMinDam = power.param1; item._iFMaxDam = power.param2; item._iLMinDam = 1; item._iLMaxDam = 0; break; case IPL_ADDMANAAC: item._iFlags |= (ItemSpecialEffect::LightningDamage | ItemSpecialEffect::FireDamage); item._iFMinDam = power.param1; item._iFMaxDam = power.param2; item._iLMinDam = 2; item._iLMaxDam = 0; break; case IPL_FIRERES_CURSE: item._iPLFR -= r; break; case IPL_LIGHTRES_CURSE: item._iPLLR -= r; break; case IPL_MAGICRES_CURSE: item._iPLMR -= r; break; case IPL_DEVASTATION: item._iDamAcFlags |= ItemSpecialEffectHf::Devastation; break; case IPL_DECAY: item._iDamAcFlags |= ItemSpecialEffectHf::Decay; item._iPLDam += r; break; case IPL_PERIL: item._iDamAcFlags |= ItemSpecialEffectHf::Peril; break; case IPL_JESTERS: item._iDamAcFlags |= ItemSpecialEffectHf::Jesters; break; case IPL_ACDEMON: item._iDamAcFlags |= ItemSpecialEffectHf::ACAgainstDemons; break; case IPL_ACUNDEAD: item._iDamAcFlags |= ItemSpecialEffectHf::ACAgainstUndead; break; case IPL_MANATOLIFE: { const int portion = ((player._pMaxManaBase >> 6) * 50 / 100) << 6; item._iPLMana -= portion; item._iPLHP += portion; } break; case IPL_LIFETOMANA: { const int portion = ((player._pMaxHPBase >> 6) * 40 / 100) << 6; item._iPLHP -= portion; item._iPLMana += portion; } break; default: break; } return r; } bool StringInPanel(const char *str) { return GetLineWidth(str, GameFont12, 2) < 254; } int PLVal(int pv, int p1, int p2, int minv, int maxv) { if (p1 == p2) return minv; if (minv == maxv) return minv; return minv + ((maxv - minv) * (100 * (pv - p1) / (p2 - p1)) / 100); } void SaveItemAffix(const Player &player, Item &item, const PLStruct &affix) { auto power = affix.power; int value = SaveItemPower(player, item, power); value = PLVal(value, power.param1, power.param2, affix.minVal, affix.maxVal); if (item._iVAdd1 != 0 || item._iVMult1 != 0) { item._iVAdd2 = value; item._iVMult2 = affix.multVal; } else { item._iVAdd1 = value; item._iVMult1 = affix.multVal; } } std::optional SelectAffix( const std::vector &affixList, AffixItemType type, int minlvl, int maxlvl, bool onlygood, goodorevil goe, bool excludeChargesForStaffs) { StaticVector eligibleAffixes; for (const PLStruct &affix : affixList) { if (!HasAnyOf(type, affix.PLIType)) continue; if (affix.PLMinLvl < minlvl || affix.PLMinLvl > maxlvl) continue; if (onlygood && !affix.PLOk) continue; if ((goe == GOE_GOOD && affix.PLGOE == GOE_EVIL) || (goe == GOE_EVIL && affix.PLGOE == GOE_GOOD)) continue; if (excludeChargesForStaffs && type == AffixItemType::Staff && affix.power.type == IPL_CHARGES) continue; for (int i = 0; i < affix.PLChance; ++i) { eligibleAffixes.push_back(&affix); } } if (eligibleAffixes.empty()) return std::nullopt; return eligibleAffixes[GenerateRnd(static_cast(eligibleAffixes.size()))]; } std::optional GetStaffPrefix(int maxlvl, bool onlygood) { if (!FlipCoin(10) && !onlygood) { return std::nullopt; } return SelectAffix(ItemPrefixes, AffixItemType::Staff, 0, maxlvl, onlygood, GOE_ANY, false); } std::string GenerateStaffName(const ItemData &baseItemData, SpellID spellId, bool translate) { std::string_view baseName = translate ? _(baseItemData.iName) : baseItemData.iName; std::string_view spellName = translate ? pgettext("spell", GetSpellData(spellId).sNameText) : GetSpellData(spellId).sNameText; const std::string_view normalFmt = translate ? pgettext("spell", /* TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall */ "{0} of {1}") : "{0} of {1}"; std::string name = fmt::format(fmt::runtime(normalFmt), baseName, spellName); if (!StringInPanel(name.c_str())) { std::string_view shortName = translate ? _(baseItemData.iSName) : baseItemData.iSName; name = fmt::format(fmt::runtime(normalFmt), shortName, spellName); } return name; } std::string GenerateStaffNameMagical(const ItemData &baseItemData, SpellID spellId, const PLStruct &power, bool translate, std::optional forceNameLengthCheck) { std::string_view baseName = translate ? _(baseItemData.iName) : baseItemData.iName; const std::string_view magicFmt = translate ? pgettext("spell", /* TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall */ "{0} {1} of {2}") : "{0} {1} of {2}"; std::string_view spellName = translate ? pgettext("spell", GetSpellData(spellId).sNameText) : GetSpellData(spellId).sNameText; std::string_view prefixName = translate ? _(power.PLName) : power.PLName; std::string identifiedName = fmt::format(fmt::runtime(magicFmt), prefixName, baseName, spellName); if (forceNameLengthCheck ? *forceNameLengthCheck : !StringInPanel(identifiedName.c_str())) { std::string_view shortName = translate ? _(baseItemData.iSName) : baseItemData.iSName; identifiedName = fmt::format(fmt::runtime(magicFmt), prefixName, shortName, spellName); } return identifiedName; } void GetStaffPower(const Player &player, Item &item, int maxlvl, bool onlygood) { std::optional prefix = GetStaffPrefix(maxlvl, onlygood); if (prefix.has_value()) { item._iMagical = ITEM_QUALITY_MAGIC; SaveItemAffix(player, item, **prefix); item._iPrePower = (*prefix)->power.type; } const ItemData &baseItemData = AllItemsList[item.IDidx]; const std::string staffName = GenerateStaffName(baseItemData, item._iSpell, false); CopyUtf8(item._iName, staffName, ItemNameLength); if (prefix.has_value()) { const std::string staffNameMagical = GenerateStaffNameMagical(baseItemData, item._iSpell, **prefix, false, std::nullopt); CopyUtf8(item._iIName, staffNameMagical, ItemNameLength); } else { CopyUtf8(item._iIName, item._iName, ItemNameLength); } CalcItemValue(item); } std::string GenerateMagicItemName(const std::string_view &baseNamel, const PLStruct *pPrefix, const PLStruct *pSufix, bool translate) { if (pPrefix != nullptr && pSufix != nullptr) { const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale */ "{0} {1} of {2}") : "{0} {1} of {2}"; return fmt::format(fmt::runtime(fmt), translate ? _(pPrefix->PLName) : pPrefix->PLName, baseNamel, translate ? _(pSufix->PLName) : pSufix->PLName); } if (pPrefix != nullptr) { const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword */ "{0} {1}") : "{0} {1}"; return fmt::format(fmt::runtime(fmt), translate ? _(pPrefix->PLName) : pPrefix->PLName, baseNamel); } if (pSufix != nullptr) { const std::string_view fmt = translate ? _(/* TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale */ "{0} of {1}") : "{0} of {1}"; return fmt::format(fmt::runtime(fmt), baseNamel, translate ? _(pSufix->PLName) : pSufix->PLName); } return std::string(baseNamel); } void GetItemPowerPrefixAndSuffix( int minlvl, int maxlvl, AffixItemType flgs, bool onlygood, tl::function_ref prefixFound, tl::function_ref suffixFound) { bool allocatePrefix = FlipCoin(4); bool allocateSuffix = !FlipCoin(3); if (!allocatePrefix && !allocateSuffix) { // At least try and give each item a prefix or suffix if (FlipCoin()) allocatePrefix = true; else allocateSuffix = true; } goodorevil goe = GOE_ANY; if (!onlygood && !FlipCoin(3)) onlygood = true; if (allocatePrefix) { std::optional prefix = SelectAffix(ItemPrefixes, flgs, minlvl, maxlvl, onlygood, goe, true); if (prefix.has_value()) { goe = (*prefix)->PLGOE; prefixFound(**prefix); } } if (allocateSuffix) { std::optional suffix = SelectAffix(ItemSuffixes, flgs, minlvl, maxlvl, onlygood, goe, true); if (suffix.has_value()) { suffixFound(**suffix); } } } void GetItemPower(const Player &player, Item &item, int minlvl, int maxlvl, AffixItemType flgs, bool onlygood) { const PLStruct *pPrefix = nullptr; const PLStruct *pSufix = nullptr; GetItemPowerPrefixAndSuffix( minlvl, maxlvl, flgs, onlygood, [&item, &player, &pPrefix](const PLStruct &prefix) { item._iMagical = ITEM_QUALITY_MAGIC; SaveItemAffix(player, item, prefix); item._iPrePower = prefix.power.type; pPrefix = &prefix; }, [&item, &player, &pSufix](const PLStruct &suffix) { item._iMagical = ITEM_QUALITY_MAGIC; SaveItemAffix(player, item, suffix); item._iSufPower = suffix.power.type; pSufix = &suffix; }); CopyUtf8(item._iIName, GenerateMagicItemName(item._iName, pPrefix, pSufix, false), ItemNameLength); if (!StringInPanel(item._iIName)) { CopyUtf8(item._iIName, GenerateMagicItemName(AllItemsList[item.IDidx].iSName, pPrefix, pSufix, false), ItemNameLength); } if (pPrefix != nullptr || pSufix != nullptr) CalcItemValue(item); } void GetStaffSpell(const Player &player, Item &item, int lvl, bool onlygood) { if (!gbIsHellfire && FlipCoin(4)) { GetItemPower(player, item, lvl / 2, lvl, AffixItemType::Staff, onlygood); return; } int l = lvl / 2; if (l == 0) l = 1; int rv = GenerateRnd(static_cast(SpellsData.size())) + 1; if (gbIsSpawn && lvl > 10) lvl = 10; int s = static_cast(SpellID::Firebolt); SpellID bs = SpellID::Null; while (rv > 0) { const int sLevel = GetSpellStaffLevel(static_cast(s)); if (sLevel != -1 && l >= sLevel) { rv--; bs = static_cast(s); } s++; if (!gbIsMultiplayer && s == static_cast(SpellID::Resurrect)) s = static_cast(SpellID::Telekinesis); if (!gbIsMultiplayer && s == static_cast(SpellID::HealOther)) s = static_cast(SpellID::BloodStar); if (static_cast(s) == SpellsData.size()) s = static_cast(SpellID::Firebolt); } const int minc = GetSpellData(bs).sStaffMin; const int maxc = GetSpellData(bs).sStaffMax - minc + 1; item._iSpell = bs; item._iCharges = minc + GenerateRnd(maxc); item._iMaxCharges = item._iCharges; item._iMinMag = GetSpellData(bs).minInt; const int v = item._iCharges * GetSpellData(bs).staffCost() / 5; item._ivalue += v; item._iIvalue += v; GetStaffPower(player, item, lvl, onlygood); } void GetOilType(Item &item, int maxLvl) { int cnt = 2; int8_t rnd[32] = { 5, 6 }; if (!gbIsMultiplayer) { if (maxLvl == 0) maxLvl = 1; cnt = 0; for (size_t j = 0; j < sizeof(OilLevels) / sizeof(OilLevels[0]); j++) { if (OilLevels[j] <= maxLvl) { rnd[cnt] = static_cast(j); cnt++; } } } const int8_t t = rnd[GenerateRnd(cnt)]; CopyUtf8(item._iName, OilNames[t], ItemNameLength); CopyUtf8(item._iIName, OilNames[t], ItemNameLength); item._iMiscId = OilMagic[t]; item._ivalue = OilValues[t]; item._iIvalue = OilValues[t]; } void GetItemBonus(const Player &player, Item &item, int minlvl, int maxlvl, bool onlygood, bool allowspells) { minlvl = std::min(minlvl, 25); switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Mace: GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Weapon, onlygood); break; case ItemType::Bow: GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Bow, onlygood); break; case ItemType::Shield: GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Shield, onlygood); break; case ItemType::LightArmor: case ItemType::Helm: case ItemType::MediumArmor: case ItemType::HeavyArmor: GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Armor, onlygood); break; case ItemType::Staff: if (allowspells) GetStaffSpell(player, item, maxlvl, onlygood); else GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Staff, onlygood); break; case ItemType::Ring: case ItemType::Amulet: GetItemPower(player, item, minlvl, maxlvl, AffixItemType::Misc, onlygood); break; case ItemType::None: case ItemType::Misc: case ItemType::Gold: break; } } struct WeightedItemIndex { _item_indexes index; unsigned cumulativeWeight; }; _item_indexes GetItemIndexForDroppableItem(bool considerDropRate, tl::function_ref isItemOkay) { static std::vector ril; ril.clear(); unsigned cumulativeWeight = 0; for (size_t i = 0; i < AllItemsList.size(); i++) { if (!IsItemAvailable(static_cast(i))) continue; const ItemData &item = AllItemsList[i]; if (item.dropRate == 0) continue; if (IsAnyOf(item.iSpell, SpellID::Resurrect, SpellID::HealOther) && !gbIsMultiplayer) continue; if (!isItemOkay(item)) continue; cumulativeWeight += considerDropRate ? item.dropRate : 1; ril.push_back({ static_cast<_item_indexes>(i), cumulativeWeight }); } const auto targetWeight = static_cast(RandomIntLessThan(static_cast(cumulativeWeight))); return std::upper_bound(ril.begin(), ril.end(), targetWeight, [](unsigned target, const WeightedItemIndex &value) { return target < value.cumulativeWeight; })->index; } _item_indexes RndUItem(Monster *monster) { int itemMaxLevel = ItemsGetCurrlevel() * 2; if (monster != nullptr) itemMaxLevel = monster->level(sgGameInitInfo.nDifficulty); return GetItemIndexForDroppableItem(false, [&itemMaxLevel](const ItemData &item) { if (item.itype == ItemType::Misc && item.iMiscId == IMISC_BOOK) return true; if (itemMaxLevel < item.iMinMLvl) return false; if (IsAnyOf(item.itype, ItemType::Gold, ItemType::Misc)) return false; return true; }); } _item_indexes RndAllItems() { if (GenerateRnd(100) > 25) return IDI_GOLD; int itemMaxLevel = ItemsGetCurrlevel() * 2; return GetItemIndexForDroppableItem(false, [&itemMaxLevel](const ItemData &item) { if (itemMaxLevel < item.iMinMLvl) return false; return true; }); } _item_indexes RndTypeItems(ItemType itemType, int imid, int lvl) { int itemMaxLevel = lvl * 2; return GetItemIndexForDroppableItem(false, [&itemMaxLevel, &itemType, &imid](const ItemData &item) { if (itemMaxLevel < item.iMinMLvl) return false; if (item.itype != itemType) return false; if (imid != -1 && item.iMiscId != imid) return false; return true; }); } std::vector GetValidUniques(int lvl, unique_base_item baseItemId) { std::vector validUniques; int index = 0; for (const UniqueItem &itemData : UniqueItems) { if (itemData.UIItemId == baseItemId && lvl >= itemData.UIMinLvl) { validUniques.push_back(index); } index++; } return validUniques; } _unique_items CheckUnique(Item &item, int lvl, int uper, int uidOffset = 0) { if (GenerateRnd(100) > uper) return UITEM_INVALID; auto validUniques = GetValidUniques(lvl, AllItemsList[item.IDidx].iItemId); if (validUniques.empty()) return UITEM_INVALID; DiscardRandomValues(1); // Check if uidOffset is out of bounds if (static_cast(uidOffset) >= validUniques.size()) { return UITEM_INVALID; } const uint8_t selectedUniqueIndex = validUniques[validUniques.size() - 1 - uidOffset]; return static_cast<_unique_items>(selectedUniqueIndex); } void GetUniqueItem(const Player &player, Item &item, _unique_items uid) { const auto &uniqueItemData = UniqueItems[uid]; for (auto power : uniqueItemData.powers) { if (power.type == IPL_INVALID) break; SaveItemPower(player, item, power); } CopyUtf8(item._iIName, uniqueItemData.UIName, ItemNameLength); if (uniqueItemData.UICurs != ICURS_DEFAULT) item._iCurs = uniqueItemData.UICurs; item._iIvalue = uniqueItemData.UIValue; if (item._iMiscId == IMISC_UNIQUE) item._iSeed = uid; item._iUid = uid; item._iMagical = ITEM_QUALITY_UNIQUE; item._iCreateInfo |= CF_UNIQUE; } void ItemRndDur(Item &item) { if (item._iDurability > 0 && item._iDurability != DUR_INDESTRUCTIBLE) item._iDurability = GenerateRnd(item._iMaxDur / 2) + (item._iMaxDur / 4) + 1; } int GetItemBLevel(int lvl, item_misc_id miscId, bool onlygood, bool uper15) { int iblvl = -1; if (GenerateRnd(100) <= 10 || GenerateRnd(100) <= lvl || onlygood || IsAnyOf(miscId, IMISC_STAFF, IMISC_RING, IMISC_AMULET)) { iblvl = lvl; } if (uper15) iblvl = lvl + 4; return iblvl; } void SetupBaseItem(Point position, _item_indexes idx, bool onlygood, bool sendmsg, bool delta, bool spawn = false) { if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; GetSuperItemSpace(position, ii); const int curlv = ItemsGetCurrlevel(); SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * curlv, 1, onlygood, delta); TryRandomUniqueItem(item, idx, 2 * curlv, 1, onlygood, delta); SetupItem(item); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); if (delta) DeltaAddItem(ii); if (spawn) NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); } void SetupAllUseful(Item &item, int iseed, int lvl) { item._iSeed = iseed; SetRndSeed(iseed); _item_indexes idx; if (gbIsHellfire) { switch (GenerateRnd(7)) { case 0: idx = IDI_PORTAL; if (lvl <= 1) idx = IDI_HEAL; break; case 1: case 2: idx = IDI_HEAL; break; case 3: idx = IDI_PORTAL; if (lvl <= 1) idx = IDI_MANA; break; case 4: case 5: idx = IDI_MANA; break; default: idx = IDI_OIL; break; } } else { idx = PickRandomlyAmong({ IDI_MANA, IDI_HEAL }); if (lvl > 1 && FlipCoin(3)) idx = IDI_PORTAL; } GetItemAttrs(item, idx, lvl); item._iCreateInfo = lvl | CF_USEFUL; SetupItem(item); } uint8_t Char2int(uint8_t input) { if (input >= '0' && input <= '9') return input - '0'; if (input >= 'A' && input <= 'F') return input - 'A' + 10; return 0; } void Hex2bin(const char *src, int bytes, uint8_t *target) { for (int i = 0; i < bytes; i++, src += 2) { target[i] = (Char2int(src[0]) << 4) | Char2int(src[1]); } } void SpawnRock() { if (ActiveItemCount >= MAXITEMS) return; const Object *stand = nullptr; for (int i = 0; i < ActiveObjectCount; i++) { const Object &object = Objects[ActiveObjects[i]]; if (object._otype == OBJ_STAND) { stand = &object; break; } } if (stand == nullptr) return; const int ii = AllocateItem(); Item &item = Items[ii]; item.position = stand->position; dItem[item.position.x][item.position.y] = ii + 1; const int curlv = ItemsGetCurrlevel(); GetItemAttrs(item, IDI_ROCK, curlv); SetupItem(item); item.selectionRegion = SelectionRegion::Middle; item._iPostDraw = true; item.AnimInfo.currentFrame = 10; item._iAnimFlag = true; item._iCreateInfo |= CF_PREGEN; DeltaAddItem(ii); } void ItemDoppel() { if (!gbIsMultiplayer) return; static int idoppely = 16; for (int idoppelx = 16; idoppelx < 96; idoppelx++) { if (dItem[idoppelx][idoppely] != 0) { Item *i = &Items[dItem[idoppelx][idoppely] - 1]; if (i->position.x != idoppelx || i->position.y != idoppely) dItem[idoppelx][idoppely] = 0; } } idoppely++; if (idoppely == 96) idoppely = 16; } void AddItemInfoBoxString(const std::string_view str) { const bool floatingInfoBoxEnabled = *GetOptions().Gameplay.floatingInfoBox; AddInfoBoxString(str, floatingInfoBoxEnabled); } void AddItemInfoBoxString(std::string &&str) { const bool floatingInfoBoxEnabled = *GetOptions().Gameplay.floatingInfoBox; AddInfoBoxString(std::move(str), floatingInfoBoxEnabled); } void PrintItemOil(char iDidx) { switch (iDidx) { case IMISC_OILACC: AddItemInfoBoxString(_("increases a weapon's")); AddItemInfoBoxString(_("chance to hit")); break; case IMISC_OILMAST: AddItemInfoBoxString(_("greatly increases a")); AddItemInfoBoxString(_("weapon's chance to hit")); break; case IMISC_OILSHARP: AddItemInfoBoxString(_("increases a weapon's")); AddItemInfoBoxString(_("damage potential")); break; case IMISC_OILDEATH: AddItemInfoBoxString(_("greatly increases a weapon's")); AddItemInfoBoxString(_("damage potential - not bows")); break; case IMISC_OILSKILL: AddItemInfoBoxString(_("reduces attributes needed")); AddItemInfoBoxString(_("to use armor or weapons")); break; case IMISC_OILBSMTH: AddItemInfoBoxString(/*xgettext:no-c-format*/ _("restores 20% of an")); AddItemInfoBoxString(_("item's durability")); break; case IMISC_OILFORT: AddItemInfoBoxString(_("increases an item's")); AddItemInfoBoxString(_("current and max durability")); break; case IMISC_OILPERM: AddItemInfoBoxString(_("makes an item indestructible")); break; case IMISC_OILHARD: AddItemInfoBoxString(_("increases the armor class")); AddItemInfoBoxString(_("of armor and shields")); break; case IMISC_OILIMP: AddItemInfoBoxString(_("greatly increases the armor")); AddItemInfoBoxString(_("class of armor and shields")); break; case IMISC_RUNEF: AddItemInfoBoxString(_("sets fire trap")); break; case IMISC_RUNEL: case IMISC_GR_RUNEL: AddItemInfoBoxString(_("sets lightning trap")); break; case IMISC_GR_RUNEF: AddItemInfoBoxString(_("sets fire trap")); break; case IMISC_RUNES: AddItemInfoBoxString(_("sets petrification trap")); break; case IMISC_FULLHEAL: AddItemInfoBoxString(_("restore all life")); break; case IMISC_HEAL: AddItemInfoBoxString(_("restore some life")); break; case IMISC_MANA: AddItemInfoBoxString(_("restore some mana")); break; case IMISC_FULLMANA: AddItemInfoBoxString(_("restore all mana")); break; case IMISC_ELIXSTR: AddItemInfoBoxString(_("increase strength")); break; case IMISC_ELIXMAG: AddItemInfoBoxString(_("increase magic")); break; case IMISC_ELIXDEX: AddItemInfoBoxString(_("increase dexterity")); break; case IMISC_ELIXVIT: AddItemInfoBoxString(_("increase vitality")); break; case IMISC_REJUV: AddItemInfoBoxString(_("restore some life and mana")); break; case IMISC_FULLREJUV: AddItemInfoBoxString(_("restore all life and mana")); break; case IMISC_ARENAPOT: AddItemInfoBoxString(_("restore all life and mana")); AddItemInfoBoxString(_("(works only in arenas)")); break; } } Point DrawUniqueInfoWindow(const Surface &out) { const bool isInStash = IsStashOpen && GetLeftPanel().contains(MousePosition); int panelX, panelY; if (isInStash) { ClxDraw(out, GetPanelPosition(UiPanels::Stash, { 24 + SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]); panelX = GetLeftPanel().position.x + SidePanelSize.width + 27; panelY = GetLeftPanel().position.y + 28; } else { ClxDraw(out, GetPanelPosition(UiPanels::Inventory, { 24 - SidePanelSize.width, 327 }), (*pSTextBoxCels)[0]); panelX = GetRightPanel().position.x - SidePanelSize.width + 27; panelY = GetRightPanel().position.y + 28; } const Point rightInfoPos = GetRightPanel().position - Displacement { SidePanelSize.width, 0 }; const Point leftInfoPos = GetLeftPanel().position + Displacement { SidePanelSize.width, 0 }; const bool isInfoOverlapping = IsLeftPanelOpen() && IsRightPanelOpen() && GetLeftPanel().contains(rightInfoPos); const int fadeLevel = isInfoOverlapping ? 3 : 1; for (int i = 0; i < fadeLevel; ++i) { DrawHalfTransparentRectTo(out, panelX, panelY, 265, 297); } return isInStash ? leftInfoPos : rightInfoPos; } void printItemMiscKBM(const Item &item, const bool isOil, const bool isCastOnTarget) { if (item._iMiscId == IMISC_MAPOFDOOM) { AddItemInfoBoxString(_("Right-click to view")); } else if (isOil) { PrintItemOil(item._iMiscId); AddItemInfoBoxString(_("Right-click to use")); } else if (isCastOnTarget) { AddItemInfoBoxString(_("Right-click to read, then\nleft-click to target")); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { AddItemInfoBoxString(_("Right-click to read")); } } void printItemMiscGenericGamepad(const Item &item, const bool isOil, bool isCastOnTarget) { if (item._iMiscId == IMISC_MAPOFDOOM) { AddItemInfoBoxString(_("Activate to view")); } else if (isOil) { PrintItemOil(item._iMiscId); if (!invflag) { AddItemInfoBoxString(_("Open inventory to use")); } else { AddItemInfoBoxString(_("Activate to use")); } } else if (isCastOnTarget) { AddItemInfoBoxString(_("Select from spell book, then\ncast spell to read")); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { AddItemInfoBoxString(_("Activate to read")); } } void printItemMiscGamepad(const Item &item, bool isOil, bool isCastOnTarget) { if (GamepadType == GamepadLayout::Generic) { printItemMiscGenericGamepad(item, isOil, isCastOnTarget); return; } const std::string_view activateButton = GetOptions().Padmapper.InputNameForAction("SecondaryAction"); const std::string_view castButton = GetOptions().Padmapper.InputNameForAction("SpellAction"); if (item._iMiscId == IMISC_MAPOFDOOM) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to view")), activateButton)); } else if (isOil) { PrintItemOil(item._iMiscId); if (!invflag) { AddItemInfoBoxString(_("Open inventory to use")); } else { AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to use")), activateButton)); } } else if (isCastOnTarget) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("Select from spell book,\nthen {} to read")), castButton)); } else if (IsAnyOf(item._iMiscId, IMISC_BOOK, IMISC_NOTE, IMISC_SCROLL, IMISC_SCROLLT)) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("{} to read")), activateButton)); } } void PrintItemMisc(const Item &item) { if (item._iMiscId == IMISC_EAR) { AddItemInfoBoxString(fmt::format(fmt::runtime(pgettext("player", "Level: {:d}")), item._ivalue)); return; } if (item._iMiscId == IMISC_AURIC) { AddItemInfoBoxString(_("Doubles gold capacity")); return; } const bool isOil = (item._iMiscId >= IMISC_USEFIRST && item._iMiscId <= IMISC_USELAST) || (item._iMiscId > IMISC_OILFIRST && item._iMiscId < IMISC_OILLAST) || (item._iMiscId > IMISC_RUNEFIRST && item._iMiscId < IMISC_RUNELAST) || item._iMiscId == IMISC_ARENAPOT; const bool mouseRequiresTarget = (item._iMiscId == IMISC_SCROLLT && item._iSpell != SpellID::Flash) || (item._iMiscId == IMISC_SCROLL && IsAnyOf(item._iSpell, SpellID::TownPortal, SpellID::Identify)); const bool gamepadRequiresTarget = item.isScroll() && TargetsMonster(item._iSpell); switch (ControlMode) { case ControlTypes::None: break; case ControlTypes::KeyboardAndMouse: printItemMiscKBM(item, isOil, mouseRequiresTarget); break; case ControlTypes::VirtualGamepad: printItemMiscGenericGamepad(item, isOil, gamepadRequiresTarget); break; case ControlTypes::Gamepad: printItemMiscGamepad(item, isOil, gamepadRequiresTarget); break; } } void PrintItemInfo(const Item &item) { PrintItemMisc(item); uint8_t str = item._iMinStr; uint8_t dex = item._iMinDex; uint8_t mag = item._iMinMag; if (str != 0 || mag != 0 || dex != 0) { std::string text = std::string(_("Required:")); if (str != 0) text.append(fmt::format(fmt::runtime(_(" {:d} Str")), str)); if (mag != 0) text.append(fmt::format(fmt::runtime(_(" {:d} Mag")), mag)); if (dex != 0) text.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex)); AddItemInfoBoxString(text); } } bool SmithItemOk(const Player & /*player*/, const ItemData &item) { if (item.itype == ItemType::Misc) return false; if (item.itype == ItemType::Gold) return false; if (item.itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(item.iSpell))) return false; if (item.itype == ItemType::Ring) return false; if (item.itype == ItemType::Amulet) return false; return true; } template _item_indexes RndVendorItem(const Player &player, int minlvl, int maxlvl) { return GetItemIndexForDroppableItem(ConsiderDropRate, [&player, &minlvl, &maxlvl](const ItemData &item) { if (!Ok(player, item)) return false; if (item.iMinMLvl < minlvl || item.iMinMLvl > maxlvl) return false; return true; }); } _item_indexes RndSmithItem(const Player &player, int lvl) { return RndVendorItem(player, 0, lvl); } void SortVendor(std::span itemList, size_t PinnedItemCount) { auto cmp = [](const Item &a, const Item &b) { return a.IDidx < b.IDidx; }; std::sort(itemList.begin() + PinnedItemCount, itemList.end(), cmp); } bool PremiumItemOk(const Player & /*player*/, const ItemData &item) { if (item.itype == ItemType::Misc) return false; if (item.itype == ItemType::Gold) return false; if (!gbIsHellfire && item.itype == ItemType::Staff) return false; if (gbIsMultiplayer) { if (item.iMiscId == IMISC_OILOF) return false; if (item.itype == ItemType::Ring) return false; if (item.itype == ItemType::Amulet) return false; } return true; } _item_indexes RndPremiumItem(const Player &player, int minlvl, int maxlvl) { return RndVendorItem(player, minlvl, maxlvl); } void SpawnOnePremium(Item &premiumItem, int plvl, const Player &player) { int strength = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Strength), player._pStrength); int dexterity = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Dexterity), player._pDexterity); int magic = std::max(player.GetMaximumAttributeValue(CharacterAttribute::Magic), player._pMagic); strength += strength / 5; dexterity += dexterity / 5; magic += magic / 5; plvl = std::clamp(plvl, 1, 30); const int maxCount = 150; const bool unlimited = !gbIsHellfire; // TODO: This could lead to an infinite loop if a suitable item can never be generated for (int count = 0; unlimited || count < maxCount; count++) { premiumItem = {}; premiumItem._iSeed = AdvanceRndSeed(); SetRndSeed(premiumItem._iSeed); const _item_indexes itemType = RndPremiumItem(player, plvl / 4, plvl); GetItemAttrs(premiumItem, itemType, plvl); GetItemBonus(player, premiumItem, plvl / 2, plvl, true, !gbIsHellfire); if (!gbIsHellfire) { if (premiumItem._iIvalue <= MaxVendorValue) { break; } } else { int itemValue = 0; switch (premiumItem._itype) { case ItemType::LightArmor: case ItemType::MediumArmor: case ItemType::HeavyArmor: { const auto *const mostValuablePlayerArmor = player.GetMostValuableItem( [](const Item &item) { return IsAnyOf(item._itype, ItemType::LightArmor, ItemType::MediumArmor, ItemType::HeavyArmor); }); itemValue = mostValuablePlayerArmor == nullptr ? 0 : mostValuablePlayerArmor->_iIvalue; break; } case ItemType::Shield: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Sword: case ItemType::Helm: case ItemType::Staff: case ItemType::Ring: case ItemType::Amulet: { const auto *const mostValuablePlayerItem = player.GetMostValuableItem( [filterType = premiumItem._itype](const Item &item) { return item._itype == filterType; }); itemValue = mostValuablePlayerItem == nullptr ? 0 : mostValuablePlayerItem->_iIvalue; break; } default: itemValue = 0; break; } itemValue = itemValue * 4 / 5; // avoids forced int > float > int conversion if (premiumItem._iIvalue <= MaxVendorValueHf && premiumItem._iMinStr <= strength && premiumItem._iMinMag <= magic && premiumItem._iMinDex <= dexterity && premiumItem._iIvalue >= itemValue) { break; } } } premiumItem._iCreateInfo = plvl | CF_SMITHPREMIUM; premiumItem._iIdentified = true; premiumItem._iStatFlag = player.CanUseItem(premiumItem); } bool WitchItemOk(const Player & /*player*/, const ItemData &item) { if (IsNoneOf(item.itype, ItemType::Misc, ItemType::Staff)) return false; if (item.iMiscId == IMISC_MANA) return false; if (item.iMiscId == IMISC_FULLMANA) return false; if (item.iSpell == SpellID::TownPortal) return false; if (item.iMiscId == IMISC_FULLHEAL) return false; if (item.iMiscId == IMISC_HEAL) return false; if (item.iMiscId > IMISC_OILFIRST && item.iMiscId < IMISC_OILLAST) return false; if (item.iSpell == SpellID::Resurrect && !gbIsMultiplayer) return false; if (item.iSpell == SpellID::HealOther && !gbIsMultiplayer) return false; return true; } _item_indexes RndWitchItem(const Player &player, int lvl) { return RndVendorItem(player, 0, lvl); } _item_indexes RndBoyItem(const Player &player, int lvl) { return RndVendorItem(player, 0, lvl); } bool HealerItemOk(const Player &player, const ItemData &item) { if (item.itype != ItemType::Misc) return false; if (item.iMiscId == IMISC_SCROLL) return item.iSpell == SpellID::Healing; if (item.iMiscId == IMISC_SCROLLT) return item.iSpell == SpellID::HealOther && gbIsMultiplayer; if (!gbIsMultiplayer) { if (item.iMiscId == IMISC_ELIXSTR) return !gbIsHellfire || player._pBaseStr < player.GetMaximumAttributeValue(CharacterAttribute::Strength); if (item.iMiscId == IMISC_ELIXMAG) return !gbIsHellfire || player._pBaseMag < player.GetMaximumAttributeValue(CharacterAttribute::Magic); if (item.iMiscId == IMISC_ELIXDEX) return !gbIsHellfire || player._pBaseDex < player.GetMaximumAttributeValue(CharacterAttribute::Dexterity); if (item.iMiscId == IMISC_ELIXVIT) return !gbIsHellfire || player._pBaseVit < player.GetMaximumAttributeValue(CharacterAttribute::Vitality); } if (item.iMiscId == IMISC_REJUV) return true; if (item.iMiscId == IMISC_FULLREJUV) return true; return false; } _item_indexes RndHealerItem(const Player &player, int lvl) { return RndVendorItem(player, 0, lvl); } void RecreateSmithItem(const Player &player, Item &item, int lvl, int iseed) { SetRndSeed(iseed); const _item_indexes itype = RndSmithItem(player, lvl); GetItemAttrs(item, itype, lvl); item._iSeed = iseed; item._iCreateInfo = lvl | CF_SMITH; item._iIdentified = true; } void RecreatePremiumItem(const Player &player, Item &item, int plvl, int iseed) { SetRndSeed(iseed); const _item_indexes itype = RndPremiumItem(player, plvl / 4, plvl); GetItemAttrs(item, itype, plvl); GetItemBonus(player, item, plvl / 2, plvl, true, !gbIsHellfire); item._iSeed = iseed; item._iCreateInfo = plvl | CF_SMITHPREMIUM; item._iIdentified = true; } void RecreateBoyItem(const Player &player, Item &item, int lvl, int iseed) { SetRndSeed(iseed); const _item_indexes itype = RndBoyItem(player, lvl); GetItemAttrs(item, itype, lvl); GetItemBonus(player, item, lvl, 2 * lvl, true, true); item._iSeed = iseed; item._iCreateInfo = lvl | CF_BOY; item._iIdentified = true; } void RecreateWitchItem(const Player &player, Item &item, _item_indexes idx, int lvl, int iseed) { if (IsAnyOf(idx, IDI_MANA, IDI_FULLMANA, IDI_PORTAL)) { GetItemAttrs(item, idx, lvl); } else if (gbIsHellfire && idx >= 114 && idx <= 117) { SetRndSeed(iseed); DiscardRandomValues(1); GetItemAttrs(item, idx, lvl); } else { SetRndSeed(iseed); const _item_indexes itype = RndWitchItem(player, lvl); GetItemAttrs(item, itype, lvl); int iblvl = -1; if (GenerateRnd(100) <= 5) iblvl = 2 * lvl; if (iblvl == -1 && item._iMiscId == IMISC_STAFF) iblvl = 2 * lvl; if (iblvl != -1) GetItemBonus(player, item, iblvl / 2, iblvl, true, true); } item._iSeed = iseed; item._iCreateInfo = lvl | CF_WITCH; item._iIdentified = true; } void RecreateHealerItem(const Player &player, Item &item, _item_indexes idx, int lvl, int iseed) { if (IsAnyOf(idx, IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT)) { GetItemAttrs(item, idx, lvl); } else { SetRndSeed(iseed); const _item_indexes itype = RndHealerItem(player, lvl); GetItemAttrs(item, itype, lvl); } item._iSeed = iseed; item._iCreateInfo = lvl | CF_HEALER; item._iIdentified = true; } void RecreateTownItem(const Player &player, Item &item, _item_indexes idx, uint16_t icreateinfo, int iseed) { if ((icreateinfo & CF_SMITH) != 0) RecreateSmithItem(player, item, icreateinfo & CF_LEVEL, iseed); else if ((icreateinfo & CF_SMITHPREMIUM) != 0) RecreatePremiumItem(player, item, icreateinfo & CF_LEVEL, iseed); else if ((icreateinfo & CF_BOY) != 0) RecreateBoyItem(player, item, icreateinfo & CF_LEVEL, iseed); else if ((icreateinfo & CF_WITCH) != 0) RecreateWitchItem(player, item, idx, icreateinfo & CF_LEVEL, iseed); else if ((icreateinfo & CF_HEALER) != 0) RecreateHealerItem(player, item, idx, icreateinfo & CF_LEVEL, iseed); } void CreateMagicItem(Point position, int lvl, ItemType itemType, int imid, int icurs, bool sendmsg, bool delta, bool spawn = false) { if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; _item_indexes idx = RndTypeItems(itemType, imid, lvl); while (true) { item = {}; SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta); TryRandomUniqueItem(item, idx, 2 * lvl, 1, true, delta); SetupItem(item); if (item._iCurs == icurs) break; idx = RndTypeItems(itemType, imid, lvl); } GetSuperItemSpace(position, ii); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); if (delta) DeltaAddItem(ii); if (spawn) NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); } void NextItemRecord(int i) { gnNumGetRecords--; if (gnNumGetRecords == 0) { return; } itemrecord[i].dwTimestamp = itemrecord[gnNumGetRecords].dwTimestamp; itemrecord[i].nSeed = itemrecord[gnNumGetRecords].nSeed; itemrecord[i].wCI = itemrecord[gnNumGetRecords].wCI; itemrecord[i].nIndex = itemrecord[gnNumGetRecords].nIndex; } StringOrView GetTranslatedItemName(const Item &item) { const auto &baseItemData = AllItemsList[static_cast(item.IDidx)]; if (item._iCreateInfo == 0) { return _(baseItemData.iName); } if (item._iMiscId == IMISC_BOOK) { const std::string_view spellName = pgettext("spell", GetSpellData(item._iSpell).sNameText); return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a spell name */ "Book of {:s}")), spellName); } if (item._iMiscId == IMISC_EAR) { return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a Character Name */ "Ear of {:s}")), item._iIName); } if (item._iMiscId > IMISC_OILFIRST && item._iMiscId < IMISC_OILLAST) { for (size_t i = 0; i < 10; i++) { if (OilMagic[i] != item._iMiscId) continue; return _(OilNames[i]); } app_fatal("unknown oil"); } else if (item._itype == ItemType::Staff && item._iSpell != SpellID::Null && item._iMagical != ITEM_QUALITY_UNIQUE) { return GenerateStaffName(baseItemData, item._iSpell, true); } else { return _(baseItemData.iName); } } std::string GetTranslatedItemNameMagical(const Item &item, bool hellfireItem, bool translate, std::optional forceNameLengthCheck) { std::string identifiedName; const auto &baseItemData = AllItemsList[static_cast(item.IDidx)]; const int lvl = item._iCreateInfo & CF_LEVEL; const bool onlygood = (item._iCreateInfo & (CF_ONLYGOOD | CF_SMITHPREMIUM | CF_BOY | CF_WITCH)) != 0; const uint32_t currentSeed = GetLCGEngineState(); SetRndSeed(item._iSeed); int minlvl; int maxlvl; if ((item._iCreateInfo & CF_SMITHPREMIUM) != 0) { DiscardRandomValues(2); // RndVendorItem and GetItemAttrs minlvl = lvl / 2; maxlvl = lvl; } else if ((item._iCreateInfo & CF_BOY) != 0) { DiscardRandomValues(2); // RndVendorItem and GetItemAttrs minlvl = lvl; maxlvl = lvl * 2; } else if ((item._iCreateInfo & CF_WITCH) != 0) { DiscardRandomValues(2); // RndVendorItem and GetItemAttrs int iblvl = -1; if (GenerateRnd(100) <= 5) iblvl = 2 * lvl; if (iblvl == -1 && item._iMiscId == IMISC_STAFF) iblvl = 2 * lvl; minlvl = iblvl / 2; maxlvl = iblvl; } else { DiscardRandomValues(1); // GetItemAttrs const int iblvl = GetItemBLevel(lvl, item._iMiscId, onlygood, (item._iCreateInfo & CF_UPER15) != 0); minlvl = iblvl / 2; maxlvl = iblvl; DiscardRandomValues(1); // CheckUnique } minlvl = std::min(minlvl, 25); AffixItemType affixItemType = AffixItemType::None; switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Mace: affixItemType = AffixItemType::Weapon; break; case ItemType::Bow: affixItemType = AffixItemType::Bow; break; case ItemType::Shield: affixItemType = AffixItemType::Shield; break; case ItemType::LightArmor: case ItemType::Helm: case ItemType::MediumArmor: case ItemType::HeavyArmor: affixItemType = AffixItemType::Armor; break; case ItemType::Staff: { const bool allowspells = !hellfireItem || ((item._iCreateInfo & CF_SMITHPREMIUM) == 0); if (!allowspells) affixItemType = AffixItemType::Staff; else if (!hellfireItem && FlipCoin(4)) { affixItemType = AffixItemType::Staff; minlvl = maxlvl / 2; } else { DiscardRandomValues(2); // Spell and Charges std::optional prefix = GetStaffPrefix(maxlvl, onlygood); if (!prefix.has_value() || item._iSpell == SpellID::Null) { if (forceNameLengthCheck) { // We generate names to check if it's a diablo or hellfire item. This checks fails => invalid item => don't generate a item name identifiedName.clear(); } else { // This can happen, if the item is hacked or a bug in the logic exists LogWarn("GetTranslatedItemNameMagical failed for item '{}' with prefix '{}' and spellid '{}'", item._iIName, prefix.has_value() ? (*prefix)->PLName : "NULL", static_cast>(item._iSpell)); identifiedName = item._iIName; } } else { identifiedName = GenerateStaffNameMagical(baseItemData, item._iSpell, **prefix, translate, forceNameLengthCheck); } } break; } case ItemType::Ring: case ItemType::Amulet: affixItemType = AffixItemType::Misc; break; case ItemType::None: case ItemType::Misc: case ItemType::Gold: break; } if (affixItemType != AffixItemType::None) { const PLStruct *pPrefix = nullptr; const PLStruct *pSufix = nullptr; GetItemPowerPrefixAndSuffix( minlvl, maxlvl, affixItemType, onlygood, [&pPrefix](const PLStruct &prefix) { pPrefix = &prefix; // GenerateRnd(prefix.power.param2 - prefix.power.param2 + 1) DiscardRandomValues(1); switch (pPrefix->power.type) { case IPL_DOPPELGANGER: case IPL_TOHIT_DAMP: DiscardRandomValues(2); break; case IPL_TOHIT_DAMP_CURSE: DiscardRandomValues(1); break; default: break; } }, [&pSufix](const PLStruct &suffix) { pSufix = &suffix; }); identifiedName = GenerateMagicItemName(_(baseItemData.iName), pPrefix, pSufix, translate); if (forceNameLengthCheck ? *forceNameLengthCheck : !StringInPanel(identifiedName.c_str())) { identifiedName = GenerateMagicItemName(_(baseItemData.iSName), pPrefix, pSufix, translate); } } SetRndSeed(currentSeed); return identifiedName; } } // namespace bool IsItemAvailable(int i) { if (i < 0 || i >= static_cast(AllItemsList.size())) return false; if (gbIsSpawn) { if (i >= 62 && i <= 70) return false; // Medium and heavy armors if (IsAnyOf(i, 105, 107, 108, 110, 111, 113)) return false; // Unavailable scrolls } if (gbIsHellfire) return true; return ( i != IDI_MAPOFDOOM // Cathedral Map && i != IDI_LGTFORGE // Bovine Plate && (i < IDI_OIL || i > IDI_GREYSUIT) // Hellfire exclusive items && (i < 83 || i > 86) // Oils && i != 92 // Scroll of Search && (i < 161 || i > 165) // Runes && i != IDI_SORCERER // Short Staff of Mana ) || ( // Bard items are technically Hellfire-exclusive // but are just normal items with adjusted stats. *GetOptions().Gameplay.testBard && IsAnyOf(i, IDI_BARDSWORD, IDI_BARDDAGGER)); } uint8_t GetOutlineColor(const Item &item, bool checkReq) { if (checkReq && !item._iStatFlag) return ICOL_RED; if (item._itype == ItemType::Gold) return ICOL_YELLOW; if (item._iMagical == ITEM_QUALITY_MAGIC) return ICOL_BLUE; if (item._iMagical == ITEM_QUALITY_UNIQUE) return ICOL_YELLOW; return ICOL_WHITE; } void ClearUniqueItemFlags() { memset(UniqueItemFlags, 0, sizeof(UniqueItemFlags)); } void InitItemGFX() { char arglist[64]; const int itemTypes = gbIsHellfire ? ITEMTYPES : 35; for (int i = 0; i < itemTypes; i++) { *BufCopy(arglist, "items\\", ItemDropNames[i]) = '\0'; itemanims[i] = LoadCel(arglist, ItemAnimWidth); } } void InitItems() { ActiveItemCount = 0; memset(dItem, 0, sizeof(dItem)); for (auto &item : Items) { item.clear(); item.position = { 0, 0 }; item._iAnimFlag = false; item.selectionRegion = SelectionRegion::None; item._iIdentified = false; item._iPostDraw = false; } for (uint8_t i = 0; i < MAXITEMS; i++) { ActiveItems[i] = i; } if (!setlevel) { DiscardRandomValues(1); if (Quests[Q_ROCK].IsAvailable()) SpawnRock(); if (Quests[Q_ANVIL].IsAvailable()) SpawnQuestItem(IDI_ANVIL, SetPiece.position.megaToWorld() + Displacement { 11, 11 }, 0, SelectionRegion::Bottom, false); if (sgGameInitInfo.bCowQuest != 0 && currlevel == 20) SpawnQuestItem(IDI_BROWNSUIT, { 25, 25 }, 3, SelectionRegion::Bottom, false); if (sgGameInitInfo.bCowQuest != 0 && currlevel == 19) SpawnQuestItem(IDI_GREYSUIT, { 25, 25 }, 3, SelectionRegion::Bottom, false); // In multiplayer items spawn during level generation to avoid desyncs if (gbIsMultiplayer) { if (Quests[Q_MUSHROOM].IsAvailable()) SpawnQuestItem(IDI_FUNGALTM, { 0, 0 }, 5, SelectionRegion::Bottom, false); if (currlevel == Quests[Q_VEIL]._qlevel + 1 && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) SpawnQuestItem(IDI_GLDNELIX, { 0, 0 }, 5, SelectionRegion::Bottom, false); } if (currlevel > 0 && currlevel < 16) AddInitItems(); if (currlevel >= 21 && currlevel <= 23) SpawnNote(); } ShowUniqueItemInfoBox = false; initItemGetRecords(); } int GetBonusAC(const Item &item) { if (item._iPLAC != 0) { int tempAc = item._iAC; tempAc *= item._iPLAC; tempAc /= 100; if (tempAc == 0) tempAc = math::Sign(item._iPLAC); return tempAc; } return 0; } void CalcPlrDamage(Player &player, int minDamage, int maxDamage) { const uint8_t playerLevel = player.getCharacterLevel(); if (minDamage == 0 && maxDamage == 0) { minDamage = 1; maxDamage = 1; if (player.isHoldingItem(ItemType::Shield)) { maxDamage = 3; } if (player._pClass == HeroClass::Monk) { minDamage = std::max(minDamage, playerLevel / 2); maxDamage = std::max(maxDamage, playerLevel); } } player._pIMinDam = minDamage; player._pIMaxDam = maxDamage; } void CalcPlrPrimaryStats(Player &player, int strength, int &magic, int dexterity, int &vitality) { const uint8_t playerLevel = player.getCharacterLevel(); if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) { strength += 2 * playerLevel; dexterity += playerLevel + playerLevel / 2; vitality += 2 * playerLevel; } if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) { strength -= 2 * playerLevel; dexterity -= playerLevel + playerLevel / 2; vitality -= 2 * playerLevel; } player._pStrength = std::clamp(strength + player._pBaseStr, 0, 750); player._pMagic = std::clamp(magic + player._pBaseMag, 0, 750); player._pDexterity = std::clamp(dexterity + player._pBaseDex, 0, 750); player._pVitality = std::clamp(vitality + player._pBaseVit, 0, 750); } void CalcPlrLightRadius(Player &player, int lrad) { lrad = std::clamp(lrad, 2, 15); if (player._pLightRad != lrad) { if (player.isOnActiveLevel()) { ChangeLightRadius(player.lightId, lrad); ChangeVisionRadius(player.getId(), lrad); } player._pLightRad = lrad; } } void CalcPlrDamageMod(Player &player) { const uint8_t playerLevel = player.getCharacterLevel(); const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; const int strMod = playerLevel * player._pStrength; const int strDexMod = playerLevel * (player._pStrength + player._pDexterity); switch (player._pClass) { case HeroClass::Rogue: player._pDamageMod = strDexMod / 200; break; case HeroClass::Monk: if (player.isHoldingItem(ItemType::Staff) || (leftHandItem.isEmpty() && rightHandItem.isEmpty())) { player._pDamageMod = strDexMod / 150; } else { player._pDamageMod = strDexMod / 300; } break; case HeroClass::Bard: if (player.isHoldingItem(ItemType::Sword)) { player._pDamageMod = strDexMod / 150; } else if (player.isHoldingItem(ItemType::Bow)) { player._pDamageMod = strDexMod / 250; } else { player._pDamageMod = strMod / 100; } break; case HeroClass::Barbarian: if (player.isHoldingItem(ItemType::Axe) || player.isHoldingItem(ItemType::Mace)) { player._pDamageMod = strMod / 75; } else if (player.isHoldingItem(ItemType::Bow)) { player._pDamageMod = strMod / 300; } else { player._pDamageMod = strMod / 100; } if (player.isHoldingItem(ItemType::Shield)) { if (leftHandItem._itype == ItemType::Shield) player._pIAC -= leftHandItem._iAC / 2; else if (rightHandItem._itype == ItemType::Shield) player._pIAC -= rightHandItem._iAC / 2; } else if (!player.isHoldingItem(ItemType::Staff) && !player.isHoldingItem(ItemType::Bow)) { player._pDamageMod += playerLevel * player._pVitality / 100; } break; default: player._pDamageMod = strMod / 100; break; } const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::IronSkin)) { player._pIAC += playerLevel / 4; } } void CalcPlrResistances(Player &player, ItemSpecialEffect iflgs, int fire, int lightning, int magic) { const uint8_t playerLevel = player.getCharacterLevel(); const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::NaturalResistance)) { magic += playerLevel; fire += playerLevel; lightning += playerLevel; } if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) { magic -= playerLevel; fire -= playerLevel; lightning -= playerLevel; } if (HasAnyOf(iflgs, ItemSpecialEffect::ZeroResistance)) { // reset resistances to zero if the respective special effect is active magic = 0; fire = 0; lightning = 0; } player._pMagResist = std::clamp(magic, 0, MaxResistance); player._pFireResist = std::clamp(fire, 0, MaxResistance); player._pLghtResist = std::clamp(lightning, 0, MaxResistance); } void CalcPlrLifeMana(Player &player, int vitality, int magic, int life, int mana) { const ClassAttributes &playerClassAttributes = player.getClassAttributes(); vitality = (vitality * playerClassAttributes.itmLife) >> 6; life += (vitality << 6); magic = (magic * playerClassAttributes.itmMana) >> 6; mana += (magic << 6); player._pMaxHP = std::clamp(life + player._pMaxHPBase, 1 << 6, 2000 << 6); player._pHitPoints = std::min(life + player._pHPBase, player._pMaxHP); if (&player == MyPlayer && player.hasNoLife()) { SetPlayerHitPoints(player, 0); } player._pMaxMana = std::clamp(mana + player._pMaxManaBase, 0, 2000 << 6); player._pMana = std::min(mana + player._pManaBase, player._pMaxMana); } void CalcPlrBlockFlag(Player &player) { const auto &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; const auto &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; player._pBlockFlag = false; if (player._pClass == HeroClass::Monk) { if (player.isHoldingItem(ItemType::Staff)) { player._pBlockFlag = true; player._pIFlags |= ItemSpecialEffect::FastBlock; } else if ((leftHandItem.isEmpty() && rightHandItem.isEmpty()) || (leftHandItem._iClass == ICLASS_WEAPON && leftHandItem._iLoc != ILOC_TWOHAND && rightHandItem.isEmpty()) || (rightHandItem._iClass == ICLASS_WEAPON && rightHandItem._iLoc != ILOC_TWOHAND && leftHandItem.isEmpty())) { player._pBlockFlag = true; } } player._pBlockFlag = player._pBlockFlag || player.isHoldingItem(ItemType::Shield); } PlayerWeaponGraphic GetPlrAnimWeaponId(const Player &player) { const Item &leftHandItem = player.InvBody[INVLOC_HAND_LEFT]; const Item &rightHandItem = player.InvBody[INVLOC_HAND_RIGHT]; const bool holdsShield = player.isHoldingItem(ItemType::Shield); const bool leftHandUsable = player.CanUseItem(leftHandItem); const bool rightHandUsable = player.CanUseItem(rightHandItem); ItemType weaponItemType = ItemType::None; if (!leftHandItem.isEmpty() && leftHandItem._iClass == ICLASS_WEAPON && leftHandUsable) { weaponItemType = leftHandItem._itype; } if (!rightHandItem.isEmpty() && rightHandItem._iClass == ICLASS_WEAPON && rightHandUsable) { weaponItemType = rightHandItem._itype; } switch (weaponItemType) { case ItemType::Sword: return holdsShield ? PlayerWeaponGraphic::SwordShield : PlayerWeaponGraphic::Sword; case ItemType::Axe: return PlayerWeaponGraphic::Axe; case ItemType::Bow: return PlayerWeaponGraphic::Bow; case ItemType::Mace: return holdsShield ? PlayerWeaponGraphic::MaceShield : PlayerWeaponGraphic::Mace; case ItemType::Staff: return PlayerWeaponGraphic::Staff; default: return holdsShield ? PlayerWeaponGraphic::UnarmedShield : PlayerWeaponGraphic::Unarmed; } } PlayerArmorGraphic GetPlrAnimArmorId(Player &player) { const Item &chestItem = player.InvBody[INVLOC_CHEST]; const bool chestUsable = player.CanUseItem(chestItem); const uint8_t playerLevel = player.getCharacterLevel(); if (chestUsable) { switch (chestItem._itype) { case ItemType::HeavyArmor: if (player._pClass == HeroClass::Monk) { if (chestItem._iMagical == ITEM_QUALITY_UNIQUE) player._pIAC += playerLevel / 2; } return PlayerArmorGraphic::Heavy; case ItemType::MediumArmor: if (player._pClass == HeroClass::Monk) { if (chestItem._iMagical == ITEM_QUALITY_UNIQUE) player._pIAC += playerLevel * 2; else player._pIAC += playerLevel / 2; } return PlayerArmorGraphic::Medium; default: if (player._pClass == HeroClass::Monk) player._pIAC += playerLevel * 2; return PlayerArmorGraphic::Light; } } return PlayerArmorGraphic::Light; } void CalcPlrGraphics(Player &player, PlayerWeaponGraphic animWeaponId, PlayerArmorGraphic animArmorId, bool loadgfx) { const uint8_t gfxNum = static_cast(animWeaponId) | static_cast(animArmorId); if (player._pgfxnum != gfxNum && loadgfx) { player._pgfxnum = gfxNum; ResetPlayerGFX(player); SetPlrAnims(player); player.previewCelSprite = std::nullopt; const player_graphic graphic = player.getGraphic(); int8_t numberOfFrames; int8_t ticksPerFrame; player.getAnimationFramesAndTicksPerFrame(graphic, numberOfFrames, ticksPerFrame); LoadPlrGFX(player, graphic); OptionalClxSpriteList sprites; if (!HeadlessMode) sprites = player.AnimationData[static_cast(graphic)].spritesForDirection(player._pdir); player.AnimInfo.changeAnimationData(sprites, numberOfFrames, ticksPerFrame); } else { player._pgfxnum = gfxNum; } } void CalcPlrAuricBonus(Player &player) { if (&player == MyPlayer) { if (player.InvBody[INVLOC_AMULET].isEmpty() || player.InvBody[INVLOC_AMULET].IDidx != IDI_AURIC) { const int half = MaxGold; MaxGold = GOLD_MAX_LIMIT; if (half != MaxGold) StripTopGold(player); } else { MaxGold = GOLD_MAX_LIMIT * 2; } } } void CalcPlrItemVals(Player &player, bool loadgfx) { int minDamage = 0; int maxDamage = 0; int ac = 0; int dam = 0; int toHit = 0; int bonusAc = 0; ItemSpecialEffect flags = ItemSpecialEffect::None; ItemSpecialEffectHf damAcFlags = ItemSpecialEffectHf::None; int strength = 0; int magic = 0; int dexterity = 0; int vitality = 0; uint64_t spells = 0; int fireRes = 0; int lightRes = 0; int magicRes = 0; int damMod = 0; int getHit = 0; int lightRadius = 10; int life = 0; int mana = 0; int8_t splLvlAdd = 0; int targetAc = 0; int minFireDam = 0; int maxFireDam = 0; int minLightDam = 0; int maxLightDam = 0; for (const Item &item : player.InvBody) { if (!item.isEmpty() && item._iStatFlag) { minDamage += item._iMinDam; maxDamage += item._iMaxDam; ac += item._iAC; if (IsValidSpell(item._iSpell) && item._iCharges != 0) { spells |= GetSpellBitmask(item._iSpell); } if (item._iMagical == ITEM_QUALITY_NORMAL || item._iIdentified) { dam += item._iPLDam; toHit += item._iPLToHit; bonusAc += GetBonusAC(item); flags |= item._iFlags; damAcFlags |= item._iDamAcFlags; strength += item._iPLStr; magic += item._iPLMag; dexterity += item._iPLDex; vitality += item._iPLVit; fireRes += item._iPLFR; lightRes += item._iPLLR; magicRes += item._iPLMR; damMod += item._iPLDamMod; getHit += item._iPLGetHit; lightRadius += item._iPLLight; life += item._iPLHP; mana += item._iPLMana; splLvlAdd += item._iSplLvlAdd; targetAc += item._iPLEnAc; minFireDam += item._iFMinDam; maxFireDam += item._iFMaxDam; minLightDam += item._iLMinDam; maxLightDam += item._iLMaxDam; } } } CalcPlrDamage(player, minDamage, maxDamage); CalcPlrPrimaryStats(player, strength, magic, dexterity, vitality); player._pIAC = ac; player._pIBonusDam = dam; player._pIBonusToHit = toHit; player._pIBonusAC = bonusAc; player._pIFlags = flags; player.pDamAcFlags = damAcFlags; player._pIBonusDamMod = damMod; player._pIGetHit = getHit; CalcPlrLightRadius(player, lightRadius); CalcPlrDamageMod(player); player._pISpells = spells; EnsureValidReadiedSpell(player); player._pISplLvlAdd = splLvlAdd; player._pIEnAc = targetAc; CalcPlrResistances(player, flags, fireRes, lightRes, magicRes); CalcPlrLifeMana(player, vitality, magic, life, mana); player._pIFMinDam = minFireDam; player._pIFMaxDam = maxFireDam; player._pILMinDam = minLightDam; player._pILMaxDam = maxLightDam; CalcPlrBlockFlag(player); CalcPlrGraphics(player, GetPlrAnimWeaponId(player), GetPlrAnimArmorId(player), loadgfx); CalcPlrAuricBonus(player); RedrawComponent(PanelDrawComponent::Mana); RedrawComponent(PanelDrawComponent::Health); } void CalcPlrInv(Player &player, bool loadgfx) { // Determine the players current stats, this updates the statFlag on all equipped items that became unusable after // a change in equipment. CalcSelfItems(player); // Determine the current item bonuses gained from usable equipped items if (&player != MyPlayer && !player.isOnActiveLevel()) { // Ensure we don't load graphics for players that aren't on our level loadgfx = false; } CalcPlrItemVals(player, loadgfx); if (&player == MyPlayer) { // Now that stat gains from equipped items have been calculated, mark unusable scrolls etc for (Item &item : InventoryAndBeltPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } player.CalcScrolls(); if (IsStashOpen) { // If stash is open, ensure the items are displayed correctly Stash.RefreshItemStatFlags(); } if (!player.HoldItem.isEmpty()) player.HoldItem.updateRequiredStatsCacheForPlayer(player); } } void InitializeItem(Item &item, _item_indexes itemData) { auto &pAllItem = AllItemsList[static_cast(itemData)]; // zero-initialize struct item = {}; item._itype = pAllItem.itype; item._iCurs = pAllItem.iCurs; CopyUtf8(item._iName, pAllItem.iName, ItemNameLength); CopyUtf8(item._iIName, pAllItem.iName, ItemNameLength); item._iLoc = pAllItem.iLoc; item._iClass = pAllItem.iClass; item._iMinDam = pAllItem.iMinDam; item._iMaxDam = pAllItem.iMaxDam; item._iAC = pAllItem.iMinAC; item._iMiscId = pAllItem.iMiscId; item._iSpell = pAllItem.iSpell; if (pAllItem.iMiscId == IMISC_STAFF) { item._iCharges = gbIsHellfire ? 18 : 40; } item._iMaxCharges = item._iCharges; item._iDurability = pAllItem.iDurability; item._iMaxDur = pAllItem.iDurability; item._iMinStr = pAllItem.iMinStr; item._iMinMag = pAllItem.iMinMag; item._iMinDex = pAllItem.iMinDex; item._ivalue = pAllItem.iValue; item._iIvalue = pAllItem.iValue; item._iPrePower = IPL_INVALID; item._iSufPower = IPL_INVALID; item._iMagical = ITEM_QUALITY_NORMAL; item.IDidx = itemData; if (gbIsHellfire) item.dwBuff |= CF_HELLFIRE; } void GenerateNewSeed(Item &item) { item._iSeed = AdvanceRndSeed(); } int GetGoldCursor(int value) { if (value >= GOLD_MEDIUM_LIMIT) return ICURS_GOLD_LARGE; if (value <= GOLD_SMALL_LIMIT) return ICURS_GOLD_SMALL; return ICURS_GOLD_MEDIUM; } void SetPlrHandGoldCurs(Item &gold) { gold._iCurs = GetGoldCursor(gold._ivalue); } namespace { void CreateStartingItem(Player &player, _item_indexes itemData) { Item item; InitializeItem(item, itemData); GenerateNewSeed(item); item.updateRequiredStatsCacheForPlayer(player); AutoEquip(player, item) || AutoPlaceItemInBelt(player, item, true) || AutoPlaceItemInInventory(player, item); } } // namespace void CreatePlrItems(Player &player) { for (auto &item : player.InvBody) { item.clear(); } // converting this to a for loop creates a `rep stosd` instruction, // so this probably actually was a memset memset(&player.InvGrid, 0, sizeof(player.InvGrid)); for (auto &item : player.InvList) { item.clear(); } player._pNumInv = 0; for (auto &item : player.SpdList) { item.clear(); } const PlayerStartingLoadoutData &loadout = GetPlayerStartingLoadoutForClass(player._pClass); if (loadout.spell != SpellID::Null && loadout.spellLevel > 0) { player._pMemSpells = GetSpellBitmask(loadout.spell); player._pRSplType = SpellType::Spell; player._pRSpell = loadout.spell; player._pSplLvl[static_cast(loadout.spell)] = loadout.spellLevel; } else { player._pMemSpells = 0; } if (loadout.skill != SpellID::Null) { player._pAblSpells = GetSpellBitmask(loadout.skill); if (player._pRSplType == SpellType::Invalid) { player._pRSplType = SpellType::Skill; player._pRSpell = loadout.skill; } } InitCursor(); for (const _item_indexes itemData : loadout.items) { if (itemData != _item_indexes::IDI_NONE) CreateStartingItem(player, itemData); } FreeCursor(); if (loadout.gold > 0) { Item &goldItem = player.InvList[player._pNumInv]; MakeGoldStack(goldItem, loadout.gold); player._pNumInv++; player.InvGrid[30] = player._pNumInv; player._pGold = goldItem._ivalue; } CalcPlrItemVals(player, false); } bool ItemSpaceOk(Point position) { if (!InDungeonBounds(position)) { return false; } if (IsTileSolid(position)) { return false; } if (dItem[position.x][position.y] != 0) { return false; } if (dMonster[position.x][position.y] != 0) { return false; } if (dPlayer[position.x][position.y] != 0) { return false; } if (IsItemBlockingObjectAtPosition(position)) { return false; } return true; } int AllocateItem() { assert(ActiveItemCount < MAXITEMS); const int inum = ActiveItems[ActiveItemCount]; ActiveItemCount++; Items[inum] = {}; return inum; } uint8_t PlaceItemInWorld(Item &&item, WorldTilePosition position) { assert(ActiveItemCount < MAXITEMS); const uint8_t ii = ActiveItems[ActiveItemCount]; ActiveItemCount++; dItem[position.x][position.y] = static_cast(ii + 1); auto &newItem = Items[ii]; newItem = std::move(item); newItem.position = position; RespawnItem(newItem, true); if (CornerStone.isAvailable() && position == CornerStone.position) { CornerStone.item = newItem; InitQTextMsg(TEXT_CORNSTN); Quests[Q_CORNSTN]._qactive = QUEST_DONE; } return ii; } Point GetSuperItemLoc(Point position) { const std::optional itemPosition = FindClosestValidPosition(ItemSpaceOk, position, 1, 50); return itemPosition.value_or(Point { 0, 0 }); // TODO handle no space for dropping items } void GetItemAttrs(Item &item, _item_indexes itemData, int lvl) { auto &baseItemData = AllItemsList[static_cast(itemData)]; item._itype = baseItemData.itype; item._iCurs = baseItemData.iCurs; CopyUtf8(item._iName, baseItemData.iName, ItemNameLength); CopyUtf8(item._iIName, baseItemData.iName, ItemNameLength); item._iLoc = baseItemData.iLoc; item._iClass = baseItemData.iClass; item._iMinDam = baseItemData.iMinDam; item._iMaxDam = baseItemData.iMaxDam; item._iAC = baseItemData.iMinAC + GenerateRnd(baseItemData.iMaxAC - baseItemData.iMinAC + 1); item._iFlags = baseItemData.iFlags; item._iMiscId = baseItemData.iMiscId; item._iSpell = baseItemData.iSpell; item._iMagical = ITEM_QUALITY_NORMAL; item._ivalue = baseItemData.iValue; item._iIvalue = baseItemData.iValue; item._iDurability = baseItemData.iDurability; item._iMaxDur = baseItemData.iDurability; item._iMinStr = baseItemData.iMinStr; item._iMinMag = baseItemData.iMinMag; item._iMinDex = baseItemData.iMinDex; item.IDidx = itemData; if (gbIsHellfire) item.dwBuff |= CF_HELLFIRE; item._iPrePower = IPL_INVALID; item._iSufPower = IPL_INVALID; if (item._iMiscId == IMISC_BOOK) GetBookSpell(item, lvl); if (gbIsHellfire && item._iMiscId == IMISC_OILOF) GetOilType(item, lvl); if (item._itype != ItemType::Gold) return; int rndv; const int itemlevel = ItemsGetCurrlevel(); switch (sgGameInitInfo.nDifficulty) { case DIFF_NORMAL: rndv = 5 * itemlevel + GenerateRnd(10 * itemlevel); break; case DIFF_NIGHTMARE: rndv = 5 * (itemlevel + 16) + GenerateRnd(10 * (itemlevel + 16)); break; case DIFF_HELL: rndv = 5 * (itemlevel + 32) + GenerateRnd(10 * (itemlevel + 32)); break; } if (leveltype == DTYPE_HELL) rndv += rndv / 8; item._ivalue = std::min(rndv, GOLD_MAX_LIMIT); SetPlrHandGoldCurs(item); } void SetupItem(Item &item) { item.setNewAnimation(MyPlayer != nullptr && MyPlayer->pLvlLoad == 0); item._iIdentified = false; } Item *SpawnUnique(_unique_items uid, Point position, std::optional level /*= std::nullopt*/, bool sendmsg /*= true*/, bool exactPosition /*= false*/) { if (ActiveItemCount >= MAXITEMS) return nullptr; const int ii = AllocateItem(); auto &item = Items[ii]; if (exactPosition && CanPut(position)) { item.position = position; dItem[position.x][position.y] = ii + 1; } else { GetSuperItemSpace(position, ii); } int curlv = ItemsGetCurrlevel(); std::underlying_type_t<_item_indexes> idx = 0; while (AllItemsList[idx].iItemId != UniqueItems[uid].UIItemId) idx++; if (sgGameInitInfo.nDifficulty == DIFF_NORMAL) { GetItemAttrs(item, static_cast<_item_indexes>(idx), curlv); GetUniqueItem(*MyPlayer, item, uid); SetupItem(item); } else { if (level) curlv = *level; const ItemData &uniqueItemData = AllItemsList[idx]; const _item_indexes dropIdx = GetItemIndexForDroppableItem(false, [&uniqueItemData](const ItemData &item) { return item.itype == uniqueItemData.itype; }); SetupAllItems(*MyPlayer, item, dropIdx, AdvanceRndSeed(), curlv * 2, 15, true, false); TryRandomUniqueItem(item, dropIdx, curlv * 2, 15, true, false); SetupItem(item); } if (sendmsg) NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); return &item; } void GetSuperItemSpace(Point position, int8_t inum) { Point positionToCheck = position; if (GetItemSpace(positionToCheck, inum)) return; for (int k = 2; k < 50; k++) { for (int j = -k; j <= k; j++) { for (int i = -k; i <= k; i++) { const Displacement offset = { i, j }; positionToCheck = position + offset; if (!ItemSpaceOk(positionToCheck)) continue; Items[inum].position = positionToCheck; dItem[positionToCheck.x][positionToCheck.y] = inum + 1; return; } } } } _item_indexes RndItemForMonsterLevel(int8_t monsterLevel) { if (GenerateRnd(100) > 40) return IDI_NONE; if (GenerateRnd(100) > 25) return IDI_GOLD; return GetItemIndexForDroppableItem(true, [&monsterLevel](const ItemData &item) { return item.iMinMLvl <= monsterLevel; }); } void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t iseed, int lvl, int uper, bool onlygood, bool pregen, int uidOffset /*= 0*/, bool forceNotUnique /*= false*/) { item._iSeed = iseed; SetRndSeed(iseed); GetItemAttrs(item, idx, lvl / 2); item._iCreateInfo = lvl; if (pregen) item._iCreateInfo |= CF_PREGEN; if (onlygood) item._iCreateInfo |= CF_ONLYGOOD; if (uper == 15) item._iCreateInfo |= CF_UPER15; else if (uper == 1) item._iCreateInfo |= CF_UPER1; if (item._iMiscId != IMISC_UNIQUE) { const int iblvl = GetItemBLevel(lvl, item._iMiscId, onlygood, uper == 15); if (iblvl != -1) { _unique_items uid = UITEM_INVALID; if (!forceNotUnique) { uid = CheckUnique(item, iblvl, uper, uidOffset); } else { DiscardRandomValues(1); } if (uid == UITEM_INVALID) { GetItemBonus(player, item, iblvl / 2, iblvl, onlygood, true); } else { GetUniqueItem(player, item, uid); } } if (item._iMagical != ITEM_QUALITY_UNIQUE) ItemRndDur(item); } else { if (item._iLoc != ILOC_UNEQUIPABLE) { if (iseed > 109 || AllItemsList[static_cast(idx)].iItemId != UniqueItems[iseed].UIItemId) { item.clear(); return; } GetUniqueItem(player, item, (_unique_items)iseed); // uid is stored in iseed for uniques } } } void TryRandomUniqueItem(Item &item, _item_indexes idx, int8_t mLevel, int uper, bool onlygood, bool pregen) { // If the item is a non-quest unique, find a random valid uid and force generate items to get an item with that uid. if ((item._iCreateInfo & CF_UNIQUE) == 0 || item._iMiscId == IMISC_UNIQUE) return; SetRndSeed(item._iSeed); // Get item base level, which is used in CheckUnique to get the correct valid uniques for the base item. DiscardRandomValues(1); // GetItemAttrs const int blvl = GetItemBLevel(mLevel, item._iMiscId, onlygood, uper == 15); // Gather all potential unique items. uid is the index into UniqueItems. auto validUniques = GetValidUniques(blvl, AllItemsList[static_cast(idx)].iItemId); assert(!validUniques.empty()); std::vector uids; for (auto &possibleUid : validUniques) { // Verify item hasn't been dropped yet. We set this to true in MP, since uniques previously dropping shouldn't prevent further identical uniques from dropping. if (!UniqueItemFlags[possibleUid] || gbIsMultiplayer) { uids.emplace_back(possibleUid); } } // If we find at least one unique in uids that hasn't been obtained yet, we can proceed getting a random unique. if (uids.empty()) { // Set uper to 1 and make the level adjustment so we have better odds of not generating a unique item. if (uper == 15) mLevel += 4; uper = 1; const Point itemPos = item.position; // Force generate a non-unique item. DiabloGenerator itemGenerator(item._iSeed); do { item = {}; // Reset item data item.position = itemPos; SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), mLevel, uper, onlygood, pregen); } while (item._iMagical == ITEM_QUALITY_UNIQUE); return; } const int32_t uidsIdx = std::max(0, GenerateRnd(static_cast(uids.size()))); // Index into uids, used to get a random uid from the uids vector. const int uid = uids[uidsIdx]; // Actual unique id. const UniqueItem &uniqueItem = UniqueItems[uid]; // If the selected unique was already generated, there is no need to fiddle with its parameters. if (item._iUid == uid) { if (!gbIsMultiplayer) { UniqueItemFlags[uid] = true; } return; } // Find our own id to calculate the offset in validUniques and check if we can generate a reverse-compatible version of the item. int uidOffset = -1; bool canGenerateReverseCompatible = true; for (size_t i = 0; i < validUniques.size(); i++) { if (validUniques[i] == uid) { // Vanilla always picks the last unique, so the offset is calculated from the back of the valid unique list. uidOffset = static_cast(validUniques.size() - i - 1); } else if (uidOffset != -1 && UniqueItems[validUniques[i]].UIMinLvl <= uniqueItem.UIMinLvl) { // Found an item with same or lower level as our desired unique after our unique. // This means that we cannot possibly generate the item in reverse compatible mode and must rely on an offset. canGenerateReverseCompatible = false; } } assert(uidOffset != -1); const Point itemPos = item.position; if (canGenerateReverseCompatible) { int targetLvl = 1; // Target level for reverse compatibility, since vanilla always takes the last applicable uid in the list. // Set target level. Ideally we use uper 15 to have a 16% chance of generating a unique item. if (uniqueItem.UIMinLvl - 4 > 0) { // Negative level will underflow. Lvl 0 items may have unintended consequences. uper = 15; targetLvl = uniqueItem.UIMinLvl - 4; } else { uper = 1; targetLvl = uniqueItem.UIMinLvl; } // Force generate items until we find a uid match. DiabloGenerator itemGenerator(item._iSeed); do { item = {}; // Reset item data item.position = itemPos; // Set onlygood = true, to always get the required item base level for the unique. SetupAllItems(*MyPlayer, item, idx, itemGenerator.advanceRndSeed(), targetLvl, uper, true, pregen); } while (item._iUid != uid); } else { // Recreate the item with new offset, this creates the desired unique item but is not reverse compatible. const int seed = item._iSeed; item = {}; // Reset item data item.position = itemPos; SetupAllItems(*MyPlayer, item, idx, seed, mLevel, uper, onlygood, pregen, uidOffset); item.dwBuff |= (uidOffset << 1) & CF_UIDOFFSET; assert(item._iUid == uid); } // Set item as obtained to prevent it from being dropped again in SP. if (!gbIsMultiplayer) { UniqueItemFlags[uid] = true; } } void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn /*= false*/) { _item_indexes idx; bool onlygood = true; const bool dropsSpecialTreasure = (monster.data().treasure & T_UNIQ) != 0; const bool dropBrain = Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHGIVEN; if (dropsSpecialTreasure && !UseMultiplayerQuests()) { Item *uniqueItem = SpawnUnique(static_cast<_unique_items>(monster.data().treasure & T_MASK), position, std::nullopt, false); if (uniqueItem != nullptr && sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, uniqueItem->position, *uniqueItem); return; } if (monster.isUnique() || dropsSpecialTreasure) { // Unique monster is killed => use better item base (for example no gold) idx = RndUItem(&monster); } else if (dropBrain && !gbIsMultiplayer) { // Normal monster is killed => need to drop brain to progress the quest Quests[Q_MUSHROOM]._qvar1 = QS_BRAINSPAWNED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); // brain replaces normal drop idx = IDI_BRAIN; } else { if (dropBrain && gbIsMultiplayer && sendmsg) { Quests[Q_MUSHROOM]._qvar1 = QS_BRAINSPAWNED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); // Drop the brain as extra item to ensure that all clients see the brain drop // When executing SpawnItem is not reliable, because another client can already have the quest state updated before SpawnItem is executed const Point posBrain = GetSuperItemLoc(position); SpawnQuestItem(IDI_BRAIN, posBrain, 0, SelectionRegion::None, true); } // Normal monster if ((monster.data().treasure & T_NODROP) != 0) return; onlygood = false; idx = RndItemForMonsterLevel(static_cast(monster.level(sgGameInitInfo.nDifficulty))); } if (idx == IDI_NONE) return; if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; GetSuperItemSpace(position, ii); const int uper = monster.isUnique() ? 15 : 1; const int8_t mLevel = monster.data().level; SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), mLevel, uper, onlygood, false); TryRandomUniqueItem(item, idx, mLevel, uper, onlygood, false); SetupItem(item); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); if (spawn) NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); } void CreateRndItem(Point position, bool onlygood, bool sendmsg, bool delta) { const _item_indexes idx = onlygood ? RndUItem(nullptr) : RndAllItems(); SetupBaseItem(position, idx, onlygood, sendmsg, delta); } void CreateRndUseful(Point position, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; GetSuperItemSpace(position, ii); const int curlv = ItemsGetCurrlevel(); SetupAllUseful(item, AdvanceRndSeed(), curlv); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); } void CreateTypeItem(Point position, bool onlygood, ItemType itemType, int imisc, bool sendmsg, bool delta, bool spawn) { _item_indexes idx; const int curlv = ItemsGetCurrlevel(); if (itemType != ItemType::Gold) idx = RndTypeItems(itemType, imisc, curlv); else idx = IDI_GOLD; SetupBaseItem(position, idx, onlygood, sendmsg, delta, spawn); } void RecreateItem(const Player &player, Item &item, _item_indexes idx, uint16_t icreateinfo, uint32_t iseed, int ivalue, uint32_t dwBuff) { const bool tmpIsHellfire = gbIsHellfire; item.dwBuff = dwBuff; gbIsHellfire = (item.dwBuff & CF_HELLFIRE) != 0; if (idx == IDI_GOLD) { InitializeItem(item, IDI_GOLD); item._iSeed = iseed; item._iCreateInfo = icreateinfo; item._ivalue = ivalue; SetPlrHandGoldCurs(item); gbIsHellfire = tmpIsHellfire; return; } if (icreateinfo == 0) { InitializeItem(item, idx); item._iSeed = iseed; gbIsHellfire = tmpIsHellfire; return; } if ((icreateinfo & CF_UNIQUE) == 0) { if ((icreateinfo & CF_TOWN) != 0) { RecreateTownItem(player, item, idx, icreateinfo, iseed); gbIsHellfire = tmpIsHellfire; return; } if ((icreateinfo & CF_USEFUL) == CF_USEFUL) { SetupAllUseful(item, iseed, icreateinfo & CF_LEVEL); gbIsHellfire = tmpIsHellfire; return; } } const int level = icreateinfo & CF_LEVEL; int uper = 0; if ((icreateinfo & CF_UPER1) != 0) uper = 1; if ((icreateinfo & CF_UPER15) != 0) uper = 15; const bool onlygood = (icreateinfo & CF_ONLYGOOD) != 0; const bool forceNotUnique = (icreateinfo & CF_UNIQUE) == 0; const bool pregen = (icreateinfo & CF_PREGEN) != 0; auto uidOffset = static_cast((item.dwBuff & CF_UIDOFFSET) >> 1); SetupAllItems(player, item, idx, iseed, level, uper, onlygood, pregen, uidOffset, forceNotUnique); SetupItem(item); gbIsHellfire = tmpIsHellfire; } void RecreateEar(Item &item, uint16_t ic, uint32_t iseed, uint8_t bCursval, std::string_view heroName) { InitializeItem(item, IDI_EAR); const std::string itemName = fmt::format(fmt::runtime("Ear of {:s}"), heroName); CopyUtf8(item._iName, itemName, ItemNameLength); CopyUtf8(item._iIName, heroName, ItemNameLength); item._iCurs = ((bCursval >> 6) & 3) + ICURS_EAR_SORCERER; item._ivalue = bCursval & 0x3F; item._iCreateInfo = ic; item._iSeed = iseed; } void CornerstoneSave() { if (!CornerStone.activated) return; if (!CornerStone.item.isEmpty()) { ItemPack id; PackItem(id, CornerStone.item, (CornerStone.item.dwBuff & CF_HELLFIRE) != 0); const auto *buffer = reinterpret_cast(&id); for (size_t i = 0; i < sizeof(ItemPack); i++) { BufCopy(&GetOptions().Hellfire.szItem[i * 2], AsHexPad2(buffer[i], /*uppercase=*/true)); } GetOptions().Hellfire.szItem[sizeof(GetOptions().Hellfire.szItem) - 1] = '\0'; } else { GetOptions().Hellfire.szItem[0] = '\0'; } } void CornerstoneLoad(Point position) { ItemPack pkSItem; if (CornerStone.activated || position.x == 0 || position.y == 0) { return; } CornerStone.item.clear(); CornerStone.activated = true; if (dItem[position.x][position.y] != 0) { const int ii = dItem[position.x][position.y] - 1; for (int i = 0; i < ActiveItemCount; i++) { if (ActiveItems[i] == ii) { DeleteItem(i); break; } } dItem[position.x][position.y] = 0; } if (strlen(GetOptions().Hellfire.szItem) < sizeof(ItemPack) * 2) return; Hex2bin(GetOptions().Hellfire.szItem, sizeof(ItemPack), reinterpret_cast(&pkSItem)); const int ii = AllocateItem(); auto &item = Items[ii]; dItem[position.x][position.y] = static_cast(ii + 1); UnPackItem(pkSItem, *MyPlayer, item, (pkSItem.dwBuff & CF_HELLFIRE) != 0); item.position = position; RespawnItem(item, false); CornerStone.item = item; } void SpawnQuestItem(_item_indexes itemid, Point position, int randarea, SelectionRegion selectionRegion, bool sendmsg) { if (randarea > 0) { int tries = 0; while (true) { tries++; if (tries > 1000 && randarea > 1) randarea--; position.x = GenerateRnd(MAXDUNX); position.y = GenerateRnd(MAXDUNY); bool failed = false; for (int i = 0; i < randarea && !failed; i++) { for (int j = 0; j < randarea && !failed; j++) { failed = !ItemSpaceOk(position + Displacement { i, j }); } } if (!failed) break; } } if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; item.position = position; dItem[position.x][position.y] = ii + 1; const int curlv = ItemsGetCurrlevel(); GetItemAttrs(item, itemid, curlv); SetupItem(item); item._iSeed = AdvanceRndSeed(); SetRndSeed(item._iSeed); item._iPostDraw = true; if (selectionRegion != SelectionRegion::None) { item.selectionRegion = selectionRegion; item.AnimInfo.currentFrame = item.AnimInfo.numberOfFrames - 1; item._iAnimFlag = false; } if (sendmsg) NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); else { item._iCreateInfo |= CF_PREGEN; DeltaAddItem(ii); } } void SpawnRewardItem(_item_indexes itemid, Point position, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; item.position = position; dItem[position.x][position.y] = ii + 1; const int curlv = ItemsGetCurrlevel(); GetItemAttrs(item, itemid, curlv); item.setNewAnimation(true); item.selectionRegion = SelectionRegion::Middle; item._iPostDraw = true; item._iIdentified = true; GenerateNewSeed(item); if (sendmsg) { NetSendCmdPItem(true, CMD_SPAWNITEM, item.position, item); } } void SpawnMapOfDoom(Point position, bool sendmsg) { SpawnRewardItem(IDI_MAPOFDOOM, position, sendmsg); } void SpawnRuneBomb(Point position, bool sendmsg) { SpawnRewardItem(IDI_RUNEBOMB, position, sendmsg); } void SpawnTheodore(Point position, bool sendmsg) { SpawnRewardItem(IDI_THEODORE, position, sendmsg); } void RespawnItem(Item &item, bool flipFlag) { const int it = ItemCAnimTbl[item._iCurs]; item.setNewAnimation(flipFlag); item._iRequest = false; // Item isn't being picked up by a player switch (item._iCurs) { case ICURS_TAVERN_SIGN: case ICURS_ANVIL_OF_FURY: item.selectionRegion = SelectionRegion::Bottom; break; case ICURS_MAP_OF_THE_STARS: case ICURS_RUNE_BOMB: case ICURS_THEODORE: case ICURS_AURIC_AMULET: item.selectionRegion = SelectionRegion::Middle; // Item is selectable at elevated level break; case ICURS_MAGIC_ROCK: Object *stand = FindObjectAtPosition(item.position); if (stand != nullptr && stand->_otype == OBJ_STAND) { item.selectionRegion = SelectionRegion::Middle; // Item is selectable at elevated level and renders at elevated level item._iPostDraw = true; // Draw in front of stand item.AnimInfo.currentFrame = 10; // Frame 10 is the start of the elevated frames in the cel } else { item.selectionRegion = SelectionRegion::Bottom; // Item is selectable at floor level and renders at floor level } PlaySfxLoc(ItemDropSnds[it], item.position); // Play the drop sound (this item is perpetually in a dropping state, but can always be picked up) break; } } void DeleteItem(int i) { if (ActiveItemCount > 0) ActiveItemCount--; assert(i >= 0 && i < MAXITEMS && ActiveItemCount < MAXITEMS); if (pcursitem == ActiveItems[i]) // Unselect item if player has it highlighted pcursitem = -1; if (i < ActiveItemCount) { // If the deleted item was not already at the end of the active list, swap the indexes around to make the next item allocation simpler. std::swap(ActiveItems[i], ActiveItems[ActiveItemCount]); } } void ProcessItems() { for (int i = 0; i < ActiveItemCount; i++) { const int ii = ActiveItems[i]; auto &item = Items[ii]; if (!item._iAnimFlag) continue; item.AnimInfo.processAnimation(); if (item._iCurs == ICURS_MAGIC_ROCK) { if (item.selectionRegion == SelectionRegion::Bottom && item.AnimInfo.currentFrame == 10) // Reached end of floor frames + 1, cycle back item.AnimInfo.currentFrame = 0; // Beginning of floor frames if (item.selectionRegion == SelectionRegion::Middle && item.AnimInfo.currentFrame == 19) // Reached end of elevated frames, cycle back item.AnimInfo.currentFrame = 10; // Beginning of elevated frames } else { if (item.AnimInfo.currentFrame == (item.AnimInfo.numberOfFrames - 1) / 2) PlaySfxLoc(ItemDropSnds[ItemCAnimTbl[item._iCurs]], item.position); if (item.AnimInfo.isLastFrame()) { item.AnimInfo.currentFrame = item.AnimInfo.numberOfFrames - 1; item._iAnimFlag = false; item.selectionRegion = SelectionRegion::Bottom; } } } ItemDoppel(); } void FreeItemGFX() { for (auto &itemanim : itemanims) { itemanim = std::nullopt; } } void GetItemFrm(Item &item) { const int it = ItemCAnimTbl[item._iCurs]; if (itemanims[it]) item.AnimInfo.sprites.emplace(*itemanims[it]); } void GetItemStr(Item &item) { if (item._itype != ItemType::Gold) { InfoString = item.getName(); InfoColor = item.getTextColor(); } else { const int nGold = item._ivalue; InfoString = fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)); } } void CheckIdentify(Player &player, int cii) { Item *pi; if (cii >= NUM_INVLOC) pi = &player.InvList[cii - NUM_INVLOC]; else pi = &player.InvBody[cii]; pi->_iIdentified = true; CalcPlrInv(player, true); } void DoRepair(Player &player, int cii) { Item *pi; PlaySfxLoc(SfxID::SpellRepair, player.position.tile); if (cii >= NUM_INVLOC) { pi = &player.InvList[cii - NUM_INVLOC]; } else { pi = &player.InvBody[cii]; } RepairItem(*pi, player.getCharacterLevel()); CalcPlrInv(player, true); } void DoRecharge(Player &player, int cii) { Item *pi; if (cii >= NUM_INVLOC) { pi = &player.InvList[cii - NUM_INVLOC]; } else { pi = &player.InvBody[cii]; } RechargeItem(*pi, player); CalcPlrInv(player, true); } bool DoOil(Player &player, int cii) { Item *pi; if (cii >= NUM_INVLOC) { pi = &player.InvList[cii - NUM_INVLOC]; } else { pi = &player.InvBody[cii]; } if (!ApplyOilToItem(*pi, player)) return false; CalcPlrInv(player, true); return true; } [[nodiscard]] StringOrView PrintItemPower(char plidx, const Item &item) { switch (plidx) { case IPL_TOHIT: case IPL_TOHIT_CURSE: return fmt::format(fmt::runtime(_("chance to hit: {:+d}%")), item._iPLToHit); case IPL_DAMP: case IPL_DAMP_CURSE: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "{:+d}% damage")), item._iPLDam); case IPL_TOHIT_DAMP: case IPL_TOHIT_DAMP_CURSE: return fmt::format(fmt::runtime(_("to hit: {:+d}%, {:+d}% damage")), item._iPLToHit, item._iPLDam); case IPL_ACP: case IPL_ACP_CURSE: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "{:+d}% armor")), item._iPLAC); case IPL_SETAC: case IPL_AC_CURSE: return fmt::format(fmt::runtime(_("armor class: {:d}")), item._iAC); case IPL_FIRERES: case IPL_FIRERES_CURSE: if (item._iPLFR < MaxResistance) return fmt::format(fmt::runtime(_("Resist Fire: {:+d}%")), item._iPLFR); else return fmt::format(fmt::runtime(_("Resist Fire: {:+d}% MAX")), MaxResistance); case IPL_LIGHTRES: case IPL_LIGHTRES_CURSE: if (item._iPLLR < MaxResistance) return fmt::format(fmt::runtime(_("Resist Lightning: {:+d}%")), item._iPLLR); else return fmt::format(fmt::runtime(_("Resist Lightning: {:+d}% MAX")), MaxResistance); case IPL_MAGICRES: case IPL_MAGICRES_CURSE: if (item._iPLMR < MaxResistance) return fmt::format(fmt::runtime(_("Resist Magic: {:+d}%")), item._iPLMR); else return fmt::format(fmt::runtime(_("Resist Magic: {:+d}% MAX")), MaxResistance); case IPL_ALLRES: if (item._iPLFR < MaxResistance) return fmt::format(fmt::runtime(_("Resist All: {:+d}%")), item._iPLFR); else return fmt::format(fmt::runtime(_("Resist All: {:+d}% MAX")), MaxResistance); case IPL_SPLLVLADD: if (item._iSplLvlAdd > 0) return fmt::format(fmt::runtime(ngettext("spells are increased {:d} level", "spells are increased {:d} levels", item._iSplLvlAdd)), item._iSplLvlAdd); else if (item._iSplLvlAdd < 0) return fmt::format(fmt::runtime(ngettext("spells are decreased {:d} level", "spells are decreased {:d} levels", -item._iSplLvlAdd)), -item._iSplLvlAdd); else return _("spell levels unchanged (?)"); case IPL_CHARGES: return _("Extra charges"); case IPL_SPELL: return fmt::format(fmt::runtime(ngettext("{:d} {:s} charge", "{:d} {:s} charges", item._iMaxCharges)), item._iMaxCharges, pgettext("spell", GetSpellData(item._iSpell).sNameText)); case IPL_FIREDAM: if (item._iFMinDam == item._iFMaxDam) return fmt::format(fmt::runtime(_("Fire hit damage: {:d}")), item._iFMinDam); else return fmt::format(fmt::runtime(_("Fire hit damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam); case IPL_LIGHTDAM: if (item._iLMinDam == item._iLMaxDam) return fmt::format(fmt::runtime(_("Lightning hit damage: {:d}")), item._iLMinDam); else return fmt::format(fmt::runtime(_("Lightning hit damage: {:d}-{:d}")), item._iLMinDam, item._iLMaxDam); case IPL_STR: case IPL_STR_CURSE: return fmt::format(fmt::runtime(_("{:+d} to strength")), item._iPLStr); case IPL_MAG: case IPL_MAG_CURSE: return fmt::format(fmt::runtime(_("{:+d} to magic")), item._iPLMag); case IPL_DEX: case IPL_DEX_CURSE: return fmt::format(fmt::runtime(_("{:+d} to dexterity")), item._iPLDex); case IPL_VIT: case IPL_VIT_CURSE: return fmt::format(fmt::runtime(_("{:+d} to vitality")), item._iPLVit); case IPL_ATTRIBS: case IPL_ATTRIBS_CURSE: return fmt::format(fmt::runtime(_("{:+d} to all attributes")), item._iPLStr); case IPL_GETHIT_CURSE: case IPL_GETHIT: return fmt::format(fmt::runtime(_("{:+d} damage from enemies")), item._iPLGetHit); case IPL_LIFE: case IPL_LIFE_CURSE: return fmt::format(fmt::runtime(_("Hit Points: {:+d}")), item._iPLHP >> 6); case IPL_MANA: case IPL_MANA_CURSE: return fmt::format(fmt::runtime(_("Mana: {:+d}")), item._iPLMana >> 6); case IPL_DUR: return _("high durability"); case IPL_DUR_CURSE: return _("decreased durability"); case IPL_INDESTRUCTIBLE: return _("indestructible"); case IPL_LIGHT: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "+{:d}% light radius")), 10 * item._iPLLight); case IPL_LIGHT_CURSE: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "-{:d}% light radius")), -10 * item._iPLLight); case IPL_MULT_ARROWS: return _("multiple arrows per shot"); case IPL_FIRE_ARROWS: if (item._iFMinDam == item._iFMaxDam) return fmt::format(fmt::runtime(_("fire arrows damage: {:d}")), item._iFMinDam); else return fmt::format(fmt::runtime(_("fire arrows damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam); case IPL_LIGHT_ARROWS: if (item._iLMinDam == item._iLMaxDam) return fmt::format(fmt::runtime(_("lightning arrows damage {:d}")), item._iLMinDam); else return fmt::format(fmt::runtime(_("lightning arrows damage {:d}-{:d}")), item._iLMinDam, item._iLMaxDam); case IPL_FIREBALL: if (item._iFMinDam == item._iFMaxDam) return fmt::format(fmt::runtime(_("fireball damage: {:d}")), item._iFMinDam); else return fmt::format(fmt::runtime(_("fireball damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam); case IPL_THORNS: return _("attacker takes 1-3 damage"); case IPL_NOMANA: return _("user loses all mana"); case IPL_ABSHALFTRAP: return _("absorbs half of trap damage"); case IPL_KNOCKBACK: return _("knocks target back"); case IPL_3XDAMVDEM: return _(/*xgettext:no-c-format*/ "+200% damage vs. demons"); case IPL_ALLRESZERO: return _("All Resistance equals 0"); case IPL_STEALMANA: if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealMana3)) return _(/*xgettext:no-c-format*/ "hit steals 3% mana"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealMana5)) return _(/*xgettext:no-c-format*/ "hit steals 5% mana"); return {}; case IPL_STEALLIFE: if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealLife3)) return _(/*xgettext:no-c-format*/ "hit steals 3% life"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::StealLife5)) return _(/*xgettext:no-c-format*/ "hit steals 5% life"); return {}; case IPL_TARGAC: return _("penetrates target's armor"); case IPL_FASTATTACK: if (HasAnyOf(item._iFlags, ItemSpecialEffect::QuickAttack)) return _("quick attack"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastAttack)) return _("fast attack"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::FasterAttack)) return _("faster attack"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastestAttack)) return _("fastest attack"); return _("Another ability (NW)"); case IPL_FASTRECOVER: if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastHitRecovery)) return _("fast hit recovery"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::FasterHitRecovery)) return _("faster hit recovery"); if (HasAnyOf(item._iFlags, ItemSpecialEffect::FastestHitRecovery)) return _("fastest hit recovery"); return _("Another ability (NW)"); case IPL_FASTBLOCK: return _("fast block"); case IPL_DAMMOD: return fmt::format(fmt::runtime(ngettext("adds {:d} point to damage", "adds {:d} points to damage", item._iPLDamMod)), item._iPLDamMod); case IPL_RNDARROWVEL: return _("fires random speed arrows"); case IPL_SETDAM: return _("unusual item damage"); case IPL_SETDUR: return _("altered durability"); case IPL_ONEHAND: return _("one handed sword"); case IPL_DRAINLIFE: return _("constantly lose hit points"); case IPL_RNDSTEALLIFE: return _("life stealing"); case IPL_NOMINSTR: return _("no strength requirement"); case IPL_ADDACLIFE: if (item._iFMinDam == item._iFMaxDam) return fmt::format(fmt::runtime(_("lightning damage: {:d}")), item._iFMinDam); else return fmt::format(fmt::runtime(_("lightning damage: {:d}-{:d}")), item._iFMinDam, item._iFMaxDam); case IPL_ADDMANAAC: return _("charged bolts on hits"); case IPL_DEVASTATION: return _("occasional triple damage"); case IPL_DECAY: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "decaying {:+d}% damage")), item._iPLDam); case IPL_PERIL: return _("2x dmg to monst, 1x to you"); case IPL_JESTERS: return std::string(_(/*xgettext:no-c-format*/ "Random 0 - 600% damage")); case IPL_CRYSTALLINE: return fmt::format(fmt::runtime(_(/*xgettext:no-c-format*/ "low dur, {:+d}% damage")), item._iPLDam); case IPL_DOPPELGANGER: return fmt::format(fmt::runtime(_("to hit: {:+d}%, {:+d}% damage")), item._iPLToHit, item._iPLDam); case IPL_ACDEMON: return _("extra AC vs demons"); case IPL_ACUNDEAD: return _("extra AC vs undead"); case IPL_MANATOLIFE: return _("50% Mana moved to Health"); case IPL_LIFETOMANA: return _("40% Health moved to Mana"); default: return _("Another ability (NW)"); } } void DrawUniqueInfo(const Surface &out) { const Point position = DrawUniqueInfoWindow(out); Rectangle rect { position + Displacement { 32, 56 }, { 257, 0 } }; const UniqueItem &uitem = UniqueItems[curruitem._iUid]; DrawString(out, _(uitem.UIName), rect, { .flags = UiFlags::AlignCenter }); const Rectangle dividerLineRect { position + Displacement { 26, 25 }, { 267, 3 } }; out.BlitFrom(out, MakeSdlRect(dividerLineRect), dividerLineRect.position + Displacement { 0, (5 * 12) + 13 }); rect.position.y += (10 - uitem.UINumPL) * 12; assert(uitem.UINumPL <= sizeof(uitem.powers) / sizeof(*uitem.powers)); const TextRenderOptions textRenderOptions { .flags = UiFlags::ColorWhite | UiFlags::AlignCenter }; const GameFontTables fontSize = GetFontSizeFromUiFlags(textRenderOptions.flags); for (const auto &power : uitem.powers) { if (power.type == IPL_INVALID) break; rect.position.y += 2 * 12; // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words. const std::string wrapped = WordWrapString(PrintItemPower(power.type, curruitem), rect.size.width); DrawString(out, wrapped, rect, textRenderOptions); for (const std::string_view line : SplitByChar(wrapped, '\n')) { if (line.data() + line.size() == wrapped.data() + wrapped.size()) break; rect.position.y += GetLineHeight(line, fontSize); } } } void PrintItemDetails(const Item &item) { if (HeadlessMode) return; if (item._iClass == ICLASS_WEAPON) { if (item._iMinDam == item._iMaxDam) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur)); } else { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur)); } } if (item._iClass == ICLASS_ARMOR) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_(/* TRANSLATORS: Dur: is durability */ "armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur)); } if (item._iMiscId == IMISC_STAFF && item._iMaxCharges != 0) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); } if (item._iPrePower != -1) { AddItemInfoBoxString(PrintItemPower(item._iPrePower, item)); } if (item._iSufPower != -1) { AddItemInfoBoxString(PrintItemPower(item._iSufPower, item)); } if (item._iMagical == ITEM_QUALITY_UNIQUE) { AddItemInfoBoxString(_("unique item")); ShowUniqueItemInfoBox = true; curruitem = item; } PrintItemInfo(item); } void PrintItemDur(const Item &item) { if (HeadlessMode) return; if (item._iClass == ICLASS_WEAPON) { if (item._iMinDam == item._iMaxDam) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Indestructible")), item._iMinDam)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d} Dur: {:d}/{:d}")), item._iMinDam, item._iDurability, item._iMaxDur)); } else { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Indestructible")), item._iMinDam, item._iMaxDam)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_("damage: {:d}-{:d} Dur: {:d}/{:d}")), item._iMinDam, item._iMaxDam, item._iDurability, item._iMaxDur)); } if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); } if (item._iMagical != ITEM_QUALITY_NORMAL) AddItemInfoBoxString(_("Not Identified")); } if (item._iClass == ICLASS_ARMOR) { if (item._iMaxDur == DUR_INDESTRUCTIBLE) AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Indestructible")), item._iAC)); else AddItemInfoBoxString(fmt::format(fmt::runtime(_("armor: {:d} Dur: {:d}/{:d}")), item._iAC, item._iDurability, item._iMaxDur)); if (item._iMagical != ITEM_QUALITY_NORMAL) AddItemInfoBoxString(_("Not Identified")); if (item._iMiscId == IMISC_STAFF && item._iMaxCharges > 0) { AddItemInfoBoxString(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); } } if (IsAnyOf(item._itype, ItemType::Ring, ItemType::Amulet)) AddItemInfoBoxString(_("Not Identified")); PrintItemInfo(item); } void UseItem(Player &player, item_misc_id mid, SpellID spellID, int spellFrom) { std::optional prepareSpellID; switch (mid) { case IMISC_HEAL: player.RestorePartialLife(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } break; case IMISC_FULLHEAL: player.RestoreFullLife(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } break; case IMISC_MANA: player.RestorePartialMana(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Mana); } break; case IMISC_FULLMANA: player.RestoreFullMana(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Mana); } break; case IMISC_ELIXSTR: ModifyPlrStr(player, 1); break; case IMISC_ELIXMAG: ModifyPlrMag(player, 1); if (gbIsHellfire) { player.RestoreFullMana(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Mana); } } break; case IMISC_ELIXDEX: ModifyPlrDex(player, 1); break; case IMISC_ELIXVIT: ModifyPlrVit(player, 1); if (gbIsHellfire) { player.RestoreFullLife(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } } break; case IMISC_REJUV: { player.RestorePartialLife(); player.RestorePartialMana(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); } } break; case IMISC_FULLREJUV: case IMISC_ARENAPOT: player.RestoreFullLife(); player.RestoreFullMana(); if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); } break; case IMISC_SCROLL: case IMISC_SCROLLT: if (ControlMode == ControlTypes::KeyboardAndMouse && GetSpellData(spellID).isTargeted()) { prepareSpellID = spellID; } else { // Find a valid target for the spell because tile coords // will be validated when processing the network message Point target = cursPosition; if (!InDungeonBounds(target)) target = player.position.future + Displacement(player._pdir); // Use CMD_SPELLXY because it's the same behavior as normal casting assert(IsValidSpellFrom(spellFrom)); NetSendCmdLocParam3(true, CMD_SPELLXY, target, static_cast(spellID), static_cast(SpellType::Scroll), static_cast(spellFrom)); } break; case IMISC_BOOK: { const uint8_t newSpellLevel = player._pSplLvl[static_cast(spellID)] + 1; if (newSpellLevel <= MaxSpellLevel) { player._pSplLvl[static_cast(spellID)] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast(spellID), newSpellLevel); } if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) { player._pMana += GetSpellData(spellID).sManaCost << 6; player._pMana = std::min(player._pMana, player._pMaxMana); player._pManaBase += GetSpellData(spellID).sManaCost << 6; player._pManaBase = std::min(player._pManaBase, player._pMaxManaBase); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } RedrawComponent(PanelDrawComponent::Mana); } break; case IMISC_MAPOFDOOM: doom_init(); break; case IMISC_OILACC: case IMISC_OILMAST: case IMISC_OILSHARP: case IMISC_OILDEATH: case IMISC_OILSKILL: case IMISC_OILBSMTH: case IMISC_OILFORT: case IMISC_OILPERM: case IMISC_OILHARD: case IMISC_OILIMP: player._pOilType = mid; if (&player != MyPlayer) { return; } if (SpellbookFlag) { SpellbookFlag = false; } if (!invflag) { invflag = true; } NewCursor(CURSOR_OIL); break; case IMISC_SPECELIX: ModifyPlrStr(player, 3); ModifyPlrMag(player, 3); ModifyPlrDex(player, 3); ModifyPlrVit(player, 3); break; case IMISC_RUNEF: prepareSpellID = SpellID::RuneOfFire; break; case IMISC_RUNEL: prepareSpellID = SpellID::RuneOfLight; break; case IMISC_GR_RUNEL: prepareSpellID = SpellID::RuneOfNova; break; case IMISC_GR_RUNEF: prepareSpellID = SpellID::RuneOfImmolation; break; case IMISC_RUNES: prepareSpellID = SpellID::RuneOfStone; break; default: break; } if (prepareSpellID) { assert(IsValidSpellFrom(spellFrom)); player.inventorySpell = *prepareSpellID; player.spellFrom = spellFrom; if (&player == MyPlayer) NewCursor(CURSOR_TELEPORT); } } bool UseItemOpensHive(const Item &item, Point position) { if (item.IDidx != IDI_RUNEBOMB) return false; for (auto dir : PathDirs) { const Point adjacentPosition = position + dir; if (OpensHive(adjacentPosition)) return true; } return false; } bool UseItemOpensGrave(const Item &item, Point position) { if (item.IDidx != IDI_MAPOFDOOM) return false; for (auto dir : PathDirs) { const Point adjacentPosition = position + dir; if (OpensGrave(adjacentPosition)) return true; } return false; } void SpawnSmith(int lvl) { constexpr int PinnedItemCount = 0; int maxValue = MaxVendorValue; int maxItems = NumSmithBasicItems; if (gbIsHellfire) { maxValue = MaxVendorValueHf; maxItems = NumSmithBasicItemsHf; } const size_t iCnt = RandomIntBetween(10, maxItems); SmithItems.clear(); while (SmithItems.size() < iCnt) { Item newItem; do { newItem = {}; newItem._iSeed = AdvanceRndSeed(); SetRndSeed(newItem._iSeed); const _item_indexes itemData = RndSmithItem(*MyPlayer, lvl); GetItemAttrs(newItem, itemData, lvl); } while (newItem._iIvalue > maxValue); newItem._iCreateInfo = lvl | CF_SMITH; newItem._iIdentified = true; SmithItems.push_back(newItem); } SortVendor(SmithItems, PinnedItemCount); } void ReplacePremium(const Player &player, int idx) { int plvl = gbIsHellfire ? itemLevelAddHf[idx] : itemLevelAdd[idx]; plvl += player.getCharacterLevel(); SpawnOnePremium(PremiumItems[idx], plvl, player); } void SpawnPremium(const Player &player) { const int lvl = player.getCharacterLevel(); const size_t maxItems = gbIsHellfire ? NumSmithItemsHf : NumSmithItems; while (PremiumItems.size() < maxItems) { int plvl = PremiumItemLevel + (gbIsHellfire ? itemLevelAddHf[PremiumItems.size()] : itemLevelAdd[PremiumItems.size()]); Item item = {}; SpawnOnePremium(item, plvl, player); PremiumItems.push_back(item); } while (PremiumItemLevel < lvl) { PremiumItemLevel++; Item *ptr = PremiumItems.begin(); if (gbIsHellfire) { std::move(ptr + 3, ptr + 13, ptr); PremiumItems[11] = PremiumItems[13]; PremiumItems[13] = PremiumItems[14]; SpawnOnePremium(PremiumItems[10], PremiumItemLevel + itemLevelAddHf[10], player); SpawnOnePremium(PremiumItems[12], PremiumItemLevel + itemLevelAddHf[12], player); SpawnOnePremium(PremiumItems[14], PremiumItemLevel + itemLevelAddHf[14], player); } else { std::move(ptr + 2, ptr + 5, ptr); PremiumItems[4] = PremiumItems[5]; SpawnOnePremium(PremiumItems[3], PremiumItemLevel + itemLevelAdd[3], player); SpawnOnePremium(PremiumItems[5], PremiumItemLevel + itemLevelAdd[5], player); } } } void SpawnWitch(int lvl) { constexpr int PinnedItemCount = NumWitchPinnedItems; constexpr std::array<_item_indexes, PinnedItemCount> PinnedItemTypes = { IDI_MANA, IDI_FULLMANA, IDI_PORTAL }; constexpr int MaxPinnedBookCount = 4; constexpr std::array<_item_indexes, MaxPinnedBookCount> PinnedBookTypes = { IDI_BOOK1, IDI_BOOK2, IDI_BOOK3, IDI_BOOK4 }; int bookCount = 0; const int pinnedBookCount = gbIsHellfire ? RandomIntLessThan(MaxPinnedBookCount) : 0; const int itemCount = RandomIntBetween(10, gbIsHellfire ? NumWitchItemsHf : NumWitchItems); const int maxValue = gbIsHellfire ? MaxVendorValueHf : MaxVendorValue; WitchItems.clear(); for (int i = 0; i < itemCount; i++) { Item item = {}; if (i < PinnedItemCount) { item._iSeed = AdvanceRndSeed(); GetItemAttrs(item, PinnedItemTypes[i], 1); item._iCreateInfo = lvl; item._iStatFlag = true; WitchItems.push_back(item); continue; } if (gbIsHellfire) { if (i < PinnedItemCount + MaxPinnedBookCount && bookCount < pinnedBookCount) { const _item_indexes bookType = PinnedBookTypes[i - PinnedItemCount]; if (lvl >= AllItemsList[bookType].iMinMLvl) { item._iSeed = AdvanceRndSeed(); SetRndSeed(item._iSeed); DiscardRandomValues(1); GetItemAttrs(item, bookType, lvl); item._iCreateInfo = lvl | CF_WITCH; item._iIdentified = true; bookCount++; WitchItems.push_back(item); continue; } } } do { item = {}; item._iSeed = AdvanceRndSeed(); SetRndSeed(item._iSeed); const _item_indexes itemData = RndWitchItem(*MyPlayer, lvl); GetItemAttrs(item, itemData, lvl); int maxlvl = -1; if (GenerateRnd(100) <= 5) maxlvl = 2 * lvl; if (maxlvl == -1 && item._iMiscId == IMISC_STAFF) maxlvl = 2 * lvl; if (maxlvl != -1) GetItemBonus(*MyPlayer, item, maxlvl / 2, maxlvl, true, true); } while (item._iIvalue > maxValue); item._iCreateInfo = lvl | CF_WITCH; item._iIdentified = true; WitchItems.push_back(item); } SortVendor(WitchItems, PinnedItemCount); } void SpawnBoy(int lvl) { int ivalue = 0; bool keepgoing = false; int count = 0; const Player &myPlayer = *MyPlayer; const HeroClass pc = myPlayer._pClass; int strength = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength), myPlayer._pStrength); int dexterity = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity), myPlayer._pDexterity); int magic = std::max(myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic), myPlayer._pMagic); strength += strength / 5; dexterity += dexterity / 5; magic += magic / 5; if (BoyItemLevel >= (lvl / 2) && !BoyItem.isEmpty()) return; do { keepgoing = false; BoyItem = {}; BoyItem._iSeed = AdvanceRndSeed(); SetRndSeed(BoyItem._iSeed); const _item_indexes itype = RndBoyItem(*MyPlayer, lvl); GetItemAttrs(BoyItem, itype, lvl); GetItemBonus(*MyPlayer, BoyItem, lvl, 2 * lvl, true, true); if (!gbIsHellfire) { if (BoyItem._iIvalue > MaxBoyValue) { keepgoing = true; // prevent breaking the do/while loop too early by failing hellfire's condition in while continue; } break; } ivalue = 0; const ItemType itemType = BoyItem._itype; switch (itemType) { case ItemType::LightArmor: case ItemType::MediumArmor: case ItemType::HeavyArmor: { const auto *const mostValuablePlayerArmor = myPlayer.GetMostValuableItem( [](const Item &item) { return IsAnyOf(item._itype, ItemType::LightArmor, ItemType::MediumArmor, ItemType::HeavyArmor); }); ivalue = mostValuablePlayerArmor == nullptr ? 0 : mostValuablePlayerArmor->_iIvalue; break; } case ItemType::Shield: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Sword: case ItemType::Helm: case ItemType::Staff: case ItemType::Ring: case ItemType::Amulet: { const auto *const mostValuablePlayerItem = myPlayer.GetMostValuableItem( [itemType](const Item &item) { return item._itype == itemType; }); ivalue = mostValuablePlayerItem == nullptr ? 0 : mostValuablePlayerItem->_iIvalue; break; } default: app_fatal("Invalid item spawn"); } ivalue = ivalue * 4 / 5; // avoids forced int > float > int conversion count++; if (count < 200) { switch (pc) { case HeroClass::Warrior: if (IsAnyOf(itemType, ItemType::Bow, ItemType::Staff)) ivalue = INT_MAX; break; case HeroClass::Rogue: if (IsAnyOf(itemType, ItemType::Sword, ItemType::Staff, ItemType::Axe, ItemType::Mace, ItemType::Shield)) ivalue = INT_MAX; break; case HeroClass::Sorcerer: if (IsAnyOf(itemType, ItemType::Staff, ItemType::Axe, ItemType::Bow, ItemType::Mace)) ivalue = INT_MAX; break; case HeroClass::Monk: if (IsAnyOf(itemType, ItemType::Bow, ItemType::MediumArmor, ItemType::Shield, ItemType::Mace)) ivalue = INT_MAX; break; case HeroClass::Bard: if (IsAnyOf(itemType, ItemType::Axe, ItemType::Mace, ItemType::Staff)) ivalue = INT_MAX; break; case HeroClass::Barbarian: if (IsAnyOf(itemType, ItemType::Bow, ItemType::Staff)) ivalue = INT_MAX; break; default: break; } } } while (keepgoing || (( BoyItem._iIvalue > MaxBoyValueHf || BoyItem._iMinStr > strength || BoyItem._iMinMag > magic || BoyItem._iMinDex > dexterity || BoyItem._iIvalue < ivalue) && count < 250)); BoyItem._iCreateInfo = lvl | CF_BOY; BoyItem._iIdentified = true; BoyItemLevel = lvl / 2; } void SpawnHealer(int lvl) { constexpr size_t PinnedItemCount = NumHealerPinnedItems; constexpr std::array<_item_indexes, PinnedItemCount + 1> PinnedItemTypes = { IDI_HEAL, IDI_FULLHEAL, IDI_RESURRECT }; const size_t itemCount = static_cast(RandomIntBetween(10, gbIsHellfire ? NumHealerItemsHf : NumHealerItems)); HealerItems.clear(); for (size_t i = 0; i < itemCount; i++) { Item item = {}; if (i < PinnedItemCount || (gbIsMultiplayer && i < NumHealerPinnedItemsMp)) { item._iSeed = AdvanceRndSeed(); GetItemAttrs(item, PinnedItemTypes[i], 1); item._iCreateInfo = lvl; item._iStatFlag = true; } else { item._iSeed = AdvanceRndSeed(); SetRndSeed(item._iSeed); const _item_indexes itype = RndHealerItem(*MyPlayer, lvl); GetItemAttrs(item, itype, lvl); item._iCreateInfo = lvl | CF_HEALER; item._iIdentified = true; } HealerItems.push_back(item); } SortVendor(HealerItems, PinnedItemCount); } void MakeGoldStack(Item &goldItem, int value) { InitializeItem(goldItem, IDI_GOLD); GenerateNewSeed(goldItem); goldItem._iStatFlag = true; goldItem._ivalue = value; SetPlrHandGoldCurs(goldItem); } int ItemNoFlippy() { const int r = ActiveItems[ActiveItemCount - 1]; Items[r].AnimInfo.currentFrame = Items[r].AnimInfo.numberOfFrames - 1; Items[r]._iAnimFlag = false; Items[r].selectionRegion = SelectionRegion::Bottom; return r; } void CreateSpellBook(Point position, SpellID ispell, bool sendmsg, bool delta) { int lvl = currlevel; if (gbIsHellfire) { lvl = GetSpellBookLevel(ispell) + 1; if (lvl < 1) { return; } } const _item_indexes idx = RndTypeItems(ItemType::Misc, IMISC_BOOK, lvl); if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); auto &item = Items[ii]; while (true) { item = {}; SetupAllItems(*MyPlayer, item, idx, AdvanceRndSeed(), 2 * lvl, 1, true, delta); SetupItem(item); if (item._iMiscId == IMISC_BOOK && item._iSpell == ispell) break; } GetSuperItemSpace(position, ii); if (sendmsg) NetSendCmdPItem(false, CMD_DROPITEM, item.position, item); if (delta) DeltaAddItem(ii); } void CreateMagicArmor(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta) { const int lvl = ItemsGetCurrlevel(); CreateMagicItem(position, lvl, itemType, IMISC_NONE, icurs, sendmsg, delta); } void CreateAmulet(Point position, int lvl, bool sendmsg, bool delta, bool spawn /*= false*/) { CreateMagicItem(position, lvl, ItemType::Amulet, IMISC_AMULET, ICURS_AMULET, sendmsg, delta, spawn); } void CreateMagicWeapon(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta) { int imid = IMISC_NONE; if (itemType == ItemType::Staff) imid = IMISC_STAFF; const int curlv = ItemsGetCurrlevel(); CreateMagicItem(position, curlv, itemType, imid, icurs, sendmsg, delta); } bool GetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex) { const uint32_t ticks = SDL_GetTicks(); for (int i = 0; i < gnNumGetRecords; i++) { if (ticks - itemrecord[i].dwTimestamp > 6000) { // BUGFIX: loot actions for multiple quest items with same seed (e.g. blood stone) performed within less than 6 seconds will be ignored. NextItemRecord(i); i--; } else if (nSeed == itemrecord[i].nSeed && wCI == itemrecord[i].wCI && nIndex == itemrecord[i].nIndex) { return false; } } return true; } void SetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex) { const uint32_t ticks = SDL_GetTicks(); if (gnNumGetRecords == MAXITEMS) { return; } itemrecord[gnNumGetRecords].dwTimestamp = ticks; itemrecord[gnNumGetRecords].nSeed = nSeed; itemrecord[gnNumGetRecords].wCI = wCI; itemrecord[gnNumGetRecords].nIndex = nIndex; gnNumGetRecords++; } void PutItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex) { const uint32_t ticks = SDL_GetTicks(); for (int i = 0; i < gnNumGetRecords; i++) { if (ticks - itemrecord[i].dwTimestamp > 6000) { NextItemRecord(i); i--; } else if (nSeed == itemrecord[i].nSeed && wCI == itemrecord[i].wCI && nIndex == itemrecord[i].nIndex) { NextItemRecord(i); break; } } } bool Item::isUsable() const { if (IDidx == IDI_SPECELIX && Quests[Q_MUSHROOM]._qactive != QUEST_DONE) return false; return AllItemsList[IDidx].iUsable; } void Item::setNewAnimation(bool showAnimation) { const int8_t it = ItemCAnimTbl[_iCurs]; const int8_t numberOfFrames = ItemAnimLs[it]; const OptionalClxSpriteList sprite = itemanims[it] ? OptionalClxSpriteList { *itemanims[static_cast(it)] } : std::nullopt; if (_iCurs != ICURS_MAGIC_ROCK) AnimInfo.setNewAnimation(sprite, numberOfFrames, 1, AnimationDistributionFlags::ProcessAnimationPending, 0, numberOfFrames); else AnimInfo.setNewAnimation(sprite, numberOfFrames, 1); _iPostDraw = false; _iRequest = false; if (showAnimation) { _iAnimFlag = true; selectionRegion = SelectionRegion::None; } else { AnimInfo.currentFrame = AnimInfo.numberOfFrames - 1; _iAnimFlag = false; selectionRegion = SelectionRegion::Bottom; } } void Item::updateRequiredStatsCacheForPlayer(const Player &player) { if (_itype == ItemType::Misc && _iMiscId == IMISC_BOOK) { _iMinMag = GetSpellData(_iSpell).minInt; int8_t spellLevel = player._pSplLvl[static_cast(_iSpell)]; while (spellLevel != 0) { _iMinMag += 20 * _iMinMag / 100; spellLevel--; if (_iMinMag + 20 * _iMinMag / 100 > 255) { _iMinMag = 255; spellLevel = 0; } } } _iStatFlag = player.CanUseItem(*this); } StringOrView Item::getName() const { if (isEmpty()) { return std::string_view(""); } if (!_iIdentified || _iCreateInfo == 0 || _iMagical == ITEM_QUALITY_NORMAL) { return GetTranslatedItemName(*this); } if (_iMagical == ITEM_QUALITY_UNIQUE) { return _(UniqueItems[_iUid].UIName); } return GetTranslatedItemNameMagical(*this, dwBuff & CF_HELLFIRE, true, std::nullopt); } bool CornerStoneStruct::isAvailable() { return currlevel == 21 && !gbIsMultiplayer; } void initItemGetRecords() { memset(itemrecord, 0, sizeof(itemrecord)); gnNumGetRecords = 0; } void RepairItem(Item &item, int lvl) { if (item._iDurability == item._iMaxDur) { return; } if (item._iMaxDur <= 0) { item.clear(); return; } int rep = 0; do { rep += lvl + GenerateRnd(lvl); item._iMaxDur -= std::max(item._iMaxDur / (lvl + 9), 1); if (item._iMaxDur == 0) { item.clear(); return; } } while (rep + item._iDurability < item._iMaxDur); item._iDurability = std::min(item._iDurability + rep, item._iMaxDur); } void RechargeItem(Item &item, Player &player) { if (item._itype != ItemType::Staff || !IsValidSpell(item._iSpell)) return; if (item._iCharges == item._iMaxCharges) return; const int rechargeStrength = RandomIntBetween(1, player.getCharacterLevel() / GetSpellStaffLevel(item._iSpell)); do { item._iMaxCharges--; if (item._iMaxCharges == 0) { return; } item._iCharges += rechargeStrength; } while (item._iCharges < item._iMaxCharges); item._iCharges = std::min(item._iCharges, item._iMaxCharges); if (&player != MyPlayer) return; if (&item == &player.InvBody[INVLOC_HAND_LEFT]) { NetSendCmdChItem(true, INVLOC_HAND_LEFT); return; } if (&item == &player.InvBody[INVLOC_HAND_RIGHT]) { NetSendCmdChItem(true, INVLOC_HAND_RIGHT); return; } for (int i = 0; i < player._pNumInv; i++) { if (&item == &player.InvList[i]) { NetSyncInvItem(player, i); break; } } } bool ApplyOilToItem(Item &item, Player &player) { int r; if (item._iClass == ICLASS_MISC) { return false; } if (item._iClass == ICLASS_GOLD) { return false; } if (item._iClass == ICLASS_QUEST) { return false; } switch (player._pOilType) { case IMISC_OILACC: case IMISC_OILMAST: case IMISC_OILSHARP: if (item._iClass == ICLASS_ARMOR) { return false; } break; case IMISC_OILDEATH: if (item._iClass == ICLASS_ARMOR) { return false; } if (item._itype == ItemType::Bow) { return false; } break; case IMISC_OILHARD: case IMISC_OILIMP: if (item._iClass == ICLASS_WEAPON) { return false; } break; default: break; } switch (player._pOilType) { case IMISC_OILACC: if (item._iPLToHit < 50) { item._iPLToHit += RandomIntBetween(1, 2); } break; case IMISC_OILMAST: if (item._iPLToHit < 100) { item._iPLToHit += RandomIntBetween(3, 5); } break; case IMISC_OILSHARP: if (item._iMaxDam - item._iMinDam < 30 && item._iMaxDam < 255) { item._iMaxDam = item._iMaxDam + 1; } break; case IMISC_OILDEATH: if (item._iMaxDam - item._iMinDam < 30 && item._iMaxDam < 254) { item._iMinDam = item._iMinDam + 1; item._iMaxDam = item._iMaxDam + 2; } break; case IMISC_OILSKILL: r = RandomIntBetween(5, 10); item._iMinStr = std::max(0, item._iMinStr - r); item._iMinMag = std::max(0, item._iMinMag - r); item._iMinDex = std::max(0, item._iMinDex - r); break; case IMISC_OILBSMTH: if (item._iMaxDur == DUR_INDESTRUCTIBLE) return true; if (item._iDurability < item._iMaxDur) { item._iDurability = (item._iMaxDur + 4) / 5 + item._iDurability; item._iDurability = std::min(item._iDurability, item._iMaxDur); } else { if (item._iMaxDur >= 100) { return true; } item._iMaxDur++; item._iDurability = item._iMaxDur; } break; case IMISC_OILFORT: if (item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur < 200) { r = RandomIntBetween(10, 50); item._iMaxDur += r; item._iDurability += r; } break; case IMISC_OILPERM: item._iDurability = DUR_INDESTRUCTIBLE; item._iMaxDur = DUR_INDESTRUCTIBLE; break; case IMISC_OILHARD: if (item._iAC < 60) { item._iAC += RandomIntBetween(1, 2); } break; case IMISC_OILIMP: if (item._iAC < 120) { item._iAC += RandomIntBetween(3, 5); } break; default: return false; } return true; } void UpdateHellfireFlag(Item &item, const char *identifiedItemName) { // DevilutionX support vanilla and hellfire items in one save file and for that introduced CF_HELLFIRE // But vanilla hellfire items don't have CF_HELLFIRE set in Item::dwBuff // This functions tries to set this flag for vanilla hellfire items based on the item name // This ensures that Item::getName() returns the correct translated item name if ((item.dwBuff & CF_HELLFIRE) != 0U) return; // Item is already a hellfire item if (item._iMagical != ITEM_QUALITY_MAGIC) return; // Only magic item's name can differ between diablo and hellfire if (gbIsMultiplayer) return; // Vanilla hellfire multiplayer is not supported in devilutionX, so there can't be items with missing dwBuff from there // We need to test both short and long name, because StringInPanel can return a different result (other font and some bugfixes) const std::string diabloItemNameShort = GetTranslatedItemNameMagical(item, false, false, false); if (diabloItemNameShort == identifiedItemName) return; // Diablo item name is identical => not a hellfire specific item const std::string diabloItemNameLong = GetTranslatedItemNameMagical(item, false, false, true); if (diabloItemNameLong == identifiedItemName) return; // Diablo item name is identical => not a hellfire specific item const std::string hellfireItemNameShort = GetTranslatedItemNameMagical(item, true, false, false); const std::string hellfireItemNameLong = GetTranslatedItemNameMagical(item, true, false, true); if (hellfireItemNameShort == identifiedItemName || hellfireItemNameLong == identifiedItemName) { // This item should be a vanilla hellfire item that has CF_HELLFIRE missing, cause only then the item name matches item.dwBuff |= CF_HELLFIRE; } } } // namespace devilution ================================================ FILE: Source/items.h ================================================ /** * @file items.h * * Interface of item functionality. */ #pragma once #include #include #include "DiabloUI/ui_flags.hpp" #include "cursor.h" #include "engine/animationinfo.h" #include "engine/point.hpp" #include "engine/surface.hpp" #include "levels/dun_tile.hpp" #include "monster.h" #include "tables/itemdat.h" #include "utils/is_of.hpp" #include "utils/string_or_view.hpp" namespace devilution { #define MAXITEMS 127 #define ITEMTYPES 43 #define GOLD_SMALL_LIMIT 1000 #define GOLD_MEDIUM_LIMIT 2500 #define GOLD_MAX_LIMIT 5000 // Item indestructible durability #define DUR_INDESTRUCTIBLE 255 constexpr int ItemNameLength = 64; constexpr int MaxVendorValue = 140000; constexpr int MaxVendorValueHf = 200000; constexpr int MaxBoyValue = 90000; constexpr int MaxBoyValueHf = 200000; enum item_quality : uint8_t { ITEM_QUALITY_NORMAL, ITEM_QUALITY_MAGIC, ITEM_QUALITY_UNIQUE, }; enum _unique_items : int32_t { UITEM_CLEAVER, UITEM_SKCROWN, UITEM_INFRARING, UITEM_OPTAMULET, UITEM_TRING, UITEM_HARCREST, UITEM_STEELVEIL, UITEM_ARMOFVAL, UITEM_GRISWOLD, UITEM_BOVINE, UITEM_RIFTBOW, UITEM_NEEDLER, UITEM_CELESTBOW, UITEM_DEADLYHUNT, UITEM_BOWOFDEAD, UITEM_BLKOAKBOW, UITEM_FLAMEDART, UITEM_FLESHSTING, UITEM_WINDFORCE, UITEM_EAGLEHORN, UITEM_GONNAGALDIRK, UITEM_DEFENDER, UITEM_GRYPHONCLAW, UITEM_BLACKRAZOR, UITEM_GIBBOUSMOON, UITEM_ICESHANK, UITEM_EXECUTIONER, UITEM_BONESAW, UITEM_SHADHAWK, UITEM_WIZSPIKE, UITEM_LGTSABRE, UITEM_FALCONTALON, UITEM_INFERNO, UITEM_DOOMBRINGER, UITEM_GRIZZLY, UITEM_GRANDFATHER, UITEM_MANGLER, UITEM_SHARPBEAK, UITEM_BLOODLSLAYER, UITEM_CELESTAXE, UITEM_WICKEDAXE, UITEM_STONECLEAV, UITEM_AGUHATCHET, UITEM_HELLSLAYER, UITEM_MESSERREAVER, UITEM_CRACKRUST, UITEM_JHOLMHAMM, UITEM_CIVERBS, UITEM_CELESTSTAR, UITEM_BARANSTAR, UITEM_GNARLROOT, UITEM_CRANBASH, UITEM_SCHAEFHAMM, UITEM_DREAMFLANGE, UITEM_STAFFOFSHAD, UITEM_IMMOLATOR, UITEM_STORMSPIRE, UITEM_GLEAMSONG, UITEM_THUNDERCALL, UITEM_PROTECTOR, UITEM_NAJPUZZLE, UITEM_MINDCRY, UITEM_RODOFONAN, UITEM_SPIRITSHELM, UITEM_THINKINGCAP, UITEM_OVERLORDHELM, UITEM_FOOLSCREST, UITEM_GOTTERDAM, UITEM_ROYCIRCLET, UITEM_TORNFLESH, UITEM_GLADBANE, UITEM_RAINCLOAK, UITEM_LEATHAUT, UITEM_WISDWRAP, UITEM_SPARKMAIL, UITEM_SCAVCARAP, UITEM_NIGHTSCAPE, UITEM_NAJPLATE, UITEM_DEMONSPIKE, UITEM_DEFLECTOR, UITEM_SKULLSHLD, UITEM_DRAGONBRCH, UITEM_BLKOAKSHLD, UITEM_HOLYDEF, UITEM_STORMSHLD, UITEM_BRAMBLE, UITEM_REGHA, UITEM_BLEEDER, UITEM_CONSTRICT, UITEM_ENGAGE, UITEM_INVALID = -1, }; /* CF_LEVEL: Item Level (6 bits; value ranges from 0-63) CF_ONLYGOOD: Item is not able to have affixes with PLOK set to false CF_UPER15: Item is from a Unique Monster and has 15% chance of being a Unique Item CF_UPER1: Item is from the dungeon and has a 1% chance of being a Unique Item CF_UNIQUE: Item is a Unique Item CF_SMITH: Item is from Griswold (Basic) CF_SMITHPREMIUM: Item is from Griswold (Premium) CF_BOY: Item is from Wirt CF_WITCH: Item is from Adria CF_HEALER: Item is from Pepin CF_PREGEN: Item is pre-generated, mostly associated with Quest items found in the dungeon or potions on the dungeon floor Items that have both CF_UPER15 and CF_UPER1 are CF_USEFUL, which is used to generate Potions and Town Portal scrolls on the dungeon floor Items that have any of CF_SMITH, CF_SMITHPREMIUM, CF_BOY, CF_WICTH, and CF_HEALER are CF_TOWN, indicating the item is sourced from an NPC */ enum icreateinfo_flag { // clang-format off CF_LEVEL = (1 << 6) - 1, CF_ONLYGOOD = 1 << 6, CF_UPER15 = 1 << 7, CF_UPER1 = 1 << 8, CF_UNIQUE = 1 << 9, CF_SMITH = 1 << 10, CF_SMITHPREMIUM = 1 << 11, CF_BOY = 1 << 12, CF_WITCH = 1 << 13, CF_HEALER = 1 << 14, CF_PREGEN = 1 << 15, CF_USEFUL = CF_UPER15 | CF_UPER1, CF_TOWN = CF_SMITH | CF_SMITHPREMIUM | CF_BOY | CF_WITCH | CF_HEALER, // clang-format on }; enum icreateinfo_flag2 { // clang-format off CF_HELLFIRE = 1 << 0, CF_UIDOFFSET = ((1 << 4) - 1) << 1, // clang-format on }; // All item animation frames have this width. constexpr int ItemAnimWidth = 96; // Defined in player.h, forward declared here to allow for functions which operate in the context of a player. struct Player; struct Item { /** Randomly generated identifier */ uint32_t _iSeed = 0; uint16_t _iCreateInfo = 0; ItemType _itype = ItemType::None; bool _iAnimFlag = false; Point position = { 0, 0 }; /* * @brief Contains Information for current Animation */ AnimationInfo AnimInfo; bool _iDelFlag = false; // set when item is flagged for deletion, deprecated in 1.02 SelectionRegion selectionRegion = SelectionRegion::None; bool _iPostDraw = false; bool _iIdentified = false; item_quality _iMagical = ITEM_QUALITY_NORMAL; char _iName[ItemNameLength] = {}; char _iIName[ItemNameLength] = {}; item_equip_type _iLoc = ILOC_NONE; item_class _iClass = ICLASS_NONE; uint8_t _iCurs = 0; int _ivalue = 0; int _iIvalue = 0; uint8_t _iMinDam = 0; uint8_t _iMaxDam = 0; int16_t _iAC = 0; ItemSpecialEffect _iFlags = ItemSpecialEffect::None; item_misc_id _iMiscId = IMISC_NONE; SpellID _iSpell = SpellID::Null; _item_indexes IDidx = IDI_NONE; int _iCharges = 0; int _iMaxCharges = 0; int _iDurability = 0; int _iMaxDur = 0; int16_t _iPLDam = 0; int16_t _iPLToHit = 0; int16_t _iPLAC = 0; int16_t _iPLStr = 0; int16_t _iPLMag = 0; int16_t _iPLDex = 0; int16_t _iPLVit = 0; int16_t _iPLFR = 0; int16_t _iPLLR = 0; int16_t _iPLMR = 0; int16_t _iPLMana = 0; int16_t _iPLHP = 0; int16_t _iPLDamMod = 0; int16_t _iPLGetHit = 0; int16_t _iPLLight = 0; int8_t _iSplLvlAdd = 0; bool _iRequest = false; /** Unique item ID, used as an index into UniqueItemList */ int _iUid = 0; int16_t _iFMinDam = 0; int16_t _iFMaxDam = 0; int16_t _iLMinDam = 0; int16_t _iLMaxDam = 0; int16_t _iPLEnAc = 0; enum item_effect_type _iPrePower = IPL_INVALID; enum item_effect_type _iSufPower = IPL_INVALID; int _iVAdd1 = 0; int _iVMult1 = 0; int _iVAdd2 = 0; int _iVMult2 = 0; int8_t _iMinStr = 0; uint8_t _iMinMag = 0; int8_t _iMinDex = 0; bool _iStatFlag = false; ItemSpecialEffectHf _iDamAcFlags = ItemSpecialEffectHf::None; uint32_t dwBuff = 0; /** * @brief Clears this item and returns the old value */ Item pop() & { Item temp = std::move(*this); clear(); return temp; } /** * @brief Resets the item so isEmpty() returns true without needing to reinitialise the whole object */ DVL_REINITIALIZES void clear() { this->_itype = ItemType::None; } /** * @brief Checks whether this item is empty or not. * @return 'True' in case the item is empty and 'False' otherwise. */ bool isEmpty() const { return this->_itype == ItemType::None; } /** * @brief Checks whether this item is an equipment. * @return 'True' in case the item is an equipment and 'False' otherwise. */ bool isEquipment() const { if (this->isEmpty()) { return false; } switch (this->_iLoc) { case ILOC_AMULET: case ILOC_ARMOR: case ILOC_HELM: case ILOC_ONEHAND: case ILOC_RING: case ILOC_TWOHAND: return true; default: return false; } } /** * @brief Checks whether this item is a weapon. * @return 'True' in case the item is a weapon and 'False' otherwise. */ bool isWeapon() const { switch (this->_itype) { case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Staff: case ItemType::Sword: return true; default: return false; } } /** * @brief Checks whether this item is an armor. * @return 'True' in case the item is an armor and 'False' otherwise. */ bool isArmor() const { switch (this->_itype) { case ItemType::HeavyArmor: case ItemType::LightArmor: case ItemType::MediumArmor: return true; default: return false; } } /** * @brief Checks whether this item is gold. * @return 'True' in case the item is gold and 'False' otherwise. */ bool isGold() const { return this->_itype == ItemType::Gold; } /** * @brief Checks whether this item is a helm. * @return 'True' in case the item is a helm and 'False' otherwise. */ bool isHelm() const { return this->_itype == ItemType::Helm; } /** * @brief Checks whether this item is a shield. * @return 'True' in case the item is a shield and 'False' otherwise. */ bool isShield() const { return this->_itype == ItemType::Shield; } /** * @brief Checks whether this item is a jewelry. * @return 'True' in case the item is a jewelry and 'False' otherwise. */ bool isJewelry() const { switch (this->_itype) { case ItemType::Amulet: case ItemType::Ring: return true; default: return false; } } [[nodiscard]] bool isScroll() const { return IsAnyOf(_iMiscId, IMISC_SCROLL, IMISC_SCROLLT); } [[nodiscard]] bool isScrollOf(SpellID spellId) const { return isScroll() && _iSpell == spellId; } [[nodiscard]] bool isRune() const { return _iMiscId > IMISC_RUNEFIRST && _iMiscId < IMISC_RUNELAST; } [[nodiscard]] bool isRuneOf(SpellID spellId) const { if (!isRune()) return false; switch (_iMiscId) { case IMISC_RUNEF: return spellId == SpellID::RuneOfFire; case IMISC_RUNEL: return spellId == SpellID::RuneOfLight; case IMISC_GR_RUNEL: return spellId == SpellID::RuneOfNova; case IMISC_GR_RUNEF: return spellId == SpellID::RuneOfImmolation; case IMISC_RUNES: return spellId == SpellID::RuneOfStone; default: return false; } } [[nodiscard]] bool isUsable() const; [[nodiscard]] bool keyAttributesMatch(uint32_t seed, _item_indexes itemIndex, uint16_t createInfo) const { return _iSeed == seed && IDidx == itemIndex && _iCreateInfo == createInfo; } UiFlags getTextColor() const { switch (_iMagical) { case ITEM_QUALITY_MAGIC: return UiFlags::ColorBlue; case ITEM_QUALITY_UNIQUE: return UiFlags::ColorWhitegold; default: return UiFlags::ColorWhite; } } UiFlags getTextColorWithStatCheck() const { if (!_iStatFlag) return UiFlags::ColorRed; return getTextColor(); } /** * @brief Sets the current Animation for the Item * @param showAnimation Definies if the Animation (Flipping) is shown or if only the final Frame (item on the ground) is shown */ void setNewAnimation(bool showAnimation); /** * @brief If this item is a spell book, calculates the magic requirement to learn a new level, then for all items sets _iStatFlag * @param player Player to compare stats against requirements */ void updateRequiredStatsCacheForPlayer(const Player &player); /** @brief Returns the translated item name to display (respects identified flag) */ StringOrView getName() const; [[nodiscard]] Displacement getRenderingOffset(const ClxSprite sprite) const { return { -CalculateSpriteTileCenterX(sprite.width()), 0 }; } }; struct ItemGetRecordStruct { uint32_t nSeed; uint16_t wCI; int nIndex; uint32_t dwTimestamp; }; struct CornerStoneStruct { Point position; bool activated; Item item; bool isAvailable(); }; /** Contains the items on ground in the current game. */ extern Item Items[MAXITEMS + 1]; extern uint8_t ActiveItems[MAXITEMS]; extern uint8_t ActiveItemCount; /** Contains the location of dropped items. */ extern int8_t dItem[MAXDUNX][MAXDUNY]; extern bool ShowUniqueItemInfoBox; extern CornerStoneStruct CornerStone; extern DVL_API_FOR_TEST bool UniqueItemFlags[128]; uint8_t GetOutlineColor(const Item &item, bool checkReq); bool IsItemAvailable(int i); void ClearUniqueItemFlags(); void InitItemGFX(); void InitItems(); void CalcPlrItemVals(Player &player, bool Loadgfx); void CalcPlrInv(Player &player, bool Loadgfx); void InitializeItem(Item &item, _item_indexes itemData); void GenerateNewSeed(Item &item); int GetGoldCursor(int value); /** * @brief Update the gold cursor on the given gold item * @param gold The item to update */ void SetPlrHandGoldCurs(Item &gold); void CreatePlrItems(Player &player); bool ItemSpaceOk(Point position); int AllocateItem(); /** * @brief Moves the item onto the floor of the current dungeon level * @param item The source of the item data, should not be used after calling this function * @param position Coordinates of the tile to place the item on * @return The index assigned to the item */ uint8_t PlaceItemInWorld(Item &&item, WorldTilePosition position); Point GetSuperItemLoc(Point position); void GetItemAttrs(Item &item, _item_indexes itemData, int lvl); void SetupItem(Item &item); Item *SpawnUnique(_unique_items uid, Point position, std::optional level = std::nullopt, bool sendmsg = true, bool exactPosition = false); void GetSuperItemSpace(Point position, int8_t inum); _item_indexes RndItemForMonsterLevel(int8_t monsterLevel); void SetupAllItems(const Player &player, Item &item, _item_indexes idx, uint32_t iseed, int lvl, int uper, bool onlygood, bool pregen, int uidOffset = 0, bool forceNotUnique = false); void TryRandomUniqueItem(Item &item, _item_indexes idx, int8_t mLevel, int uper, bool onlygood, bool pregen); void SpawnItem(Monster &monster, Point position, bool sendmsg, bool spawn = false); void CreateRndItem(Point position, bool onlygood, bool sendmsg, bool delta); void CreateRndUseful(Point position, bool sendmsg); void CreateTypeItem(Point position, bool onlygood, ItemType itemType, int imisc, bool sendmsg, bool delta, bool spawn = false); void RecreateItem(const Player &player, Item &item, _item_indexes idx, uint16_t icreateinfo, uint32_t iseed, int ivalue, uint32_t dwBuff); void RecreateEar(Item &item, uint16_t ic, uint32_t iseed, uint8_t bCursval, std::string_view heroName); void CornerstoneSave(); void CornerstoneLoad(Point position); void SpawnQuestItem(_item_indexes itemid, Point position, int randarea, SelectionRegion selectionRegion, bool sendmsg); void SpawnRewardItem(_item_indexes itemid, Point position, bool sendmsg); void SpawnMapOfDoom(Point position, bool sendmsg); void SpawnRuneBomb(Point position, bool sendmsg); void SpawnTheodore(Point position, bool sendmsg); void RespawnItem(Item &item, bool FlipFlag); void DeleteItem(int i); void ProcessItems(); void FreeItemGFX(); void GetItemFrm(Item &item); void GetItemStr(Item &item); void CheckIdentify(Player &player, int cii); void DoRepair(Player &player, int cii); void DoRecharge(Player &player, int cii); bool DoOil(Player &player, int cii); [[nodiscard]] StringOrView PrintItemPower(char plidx, const Item &item); void DrawUniqueInfo(const Surface &out); void PrintItemDetails(const Item &item); void PrintItemDur(const Item &item); void UseItem(Player &player, item_misc_id Mid, SpellID spellID, int spellFrom); bool UseItemOpensHive(const Item &item, Point position); bool UseItemOpensGrave(const Item &item, Point position); void SpawnSmith(int lvl); void ReplacePremium(const Player &player, int idx); void SpawnPremium(const Player &player); void SpawnWitch(int lvl); void SpawnBoy(int lvl); void SpawnHealer(int lvl); void MakeGoldStack(Item &goldItem, int value); int ItemNoFlippy(); void CreateSpellBook(Point position, SpellID ispell, bool sendmsg, bool delta); void CreateMagicArmor(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta); void CreateAmulet(Point position, int lvl, bool sendmsg, bool delta, bool spawn = false); void CreateMagicWeapon(Point position, ItemType itemType, int icurs, bool sendmsg, bool delta); bool GetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex); void SetItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex); void PutItemRecord(uint32_t nSeed, uint16_t wCI, int nIndex); /** * @brief Resets item get records. */ void initItemGetRecords(); void RepairItem(Item &item, int lvl); void RechargeItem(Item &item, Player &player); bool ApplyOilToItem(Item &item, Player &player); /** * @brief Checks if the item is generated in vanilla hellfire. If yes it updates dwBuff to include CF_HELLFIRE. */ void UpdateHellfireFlag(Item &item, const char *identifiedItemName); /* data */ extern int MaxGold; extern int8_t ItemCAnimTbl[]; extern SfxID ItemInvSnds[]; } // namespace devilution ================================================ FILE: Source/levels/crypt.cpp ================================================ #include "levels/crypt.h" #include #include "engine/load_file.hpp" #include "engine/point.hpp" #include "items.h" #include "levels/drlg_l1.h" #include "lighting.h" namespace devilution { int UberRow; int UberCol; bool IsUberRoomOpened; bool IsUberLeverActivated; int UberDiabloMonsterIndex; /** Miniset: stairs up. */ const Miniset L5STAIRSUP { { 4, 4 }, { { 22, 22, 22, 22 }, { 2, 2, 2, 2 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 0, 66, 23, 0 }, { 63, 64, 65, 0 }, { 0, 67, 68, 0 }, { 0, 0, 0, 0 }, } }; namespace { const Miniset L5STAIRSUPHF { { 4, 5 }, { { 22, 22, 22, 22 }, { 22, 22, 22, 22 }, { 2, 2, 2, 2 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 0, 54, 23, 0 }, { 0, 53, 18, 0 }, { 55, 56, 57, 0 }, { 58, 59, 60, 0 }, { 0, 0, 0, 0 }, } }; const Miniset L5STAIRSDOWN { { 4, 5 }, { { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 0, 0, 52, 0 }, { 0, 48, 51, 0 }, { 0, 47, 50, 0 }, { 45, 46, 49, 0 }, { 0, 0, 0, 0 }, } }; const Miniset L5STAIRSTOWN { { 4, 5 }, { { 22, 22, 22, 22 }, { 22, 22, 22, 22 }, { 2, 2, 2, 2 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 0, 62, 23, 0 }, { 0, 61, 18, 0 }, { 63, 64, 65, 0 }, { 66, 67, 68, 0 }, { 0, 0, 0, 0 }, } }; const Miniset VWallSection { { 1, 3 }, { { 1 }, { 1 }, { 1 }, }, { { 91 }, { 90 }, { 89 }, } }; const Miniset HWallSection { { 3, 1 }, { { 2, 2, 2 } }, { { 94, 93, 92 } } }; const Miniset CryptFloorLave { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 101, 0 }, { 0, 0, 0 }, } }; const Miniset CryptPillar1 { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 167, 0 }, { 0, 0, 0 }, } }; const Miniset CryptPillar2 { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 168, 0 }, { 0, 0, 0 }, } }; const Miniset CryptPillar3 { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 169, 0 }, { 0, 0, 0 }, } }; const Miniset CryptPillar4 { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 170, 0 }, { 0, 0, 0 }, } }; const Miniset CryptPillar5 { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 171, 0 }, { 0, 0, 0 }, } }; const Miniset CryptStar { { 3, 3 }, { { 13, 13, 13 }, { 13, 13, 13 }, { 13, 13, 13 }, }, { { 0, 0, 0 }, { 0, 172, 0 }, { 0, 0, 0 }, } }; enum Tile : uint8_t { // clang-format off VWall = 1, HWall = 2, Corner = 3, DWall = 4, DArch = 5, VWallEnd = 6, HWallEnd = 7, HArchEnd = 8, VArchEnd = 9, HArchVWall = 10, VArch = 11, HArch = 12, Floor = 13, HWallVArch = 14, Pillar = 15, Pillar1 = 16, Pillar2 = 17, DirtHwall = 18, DirtVwall = 19, DirtCorner = 21, DirtHWallEnd = 23, DirtVWallEnd = 24, VDoor = 25, HDoor = 26, HFenceVWall = 27, HDoorVDoor = 28, DFence = 29, VDoorEnd = 30, HDoorEnd = 31, VFenceEnd = 32, VFence = 35, HFence = 36, HWallVFence = 37, HArchVFence = 38, HArchVDoor = 39, EntranceStairs = 64, DirtHWall2 = 82, DirtVWall2 = 83, DirtCorner2 = 85, DirtHWallEnd2 = 87, DirtVWallEnd2 = 88, VWall5 = 89, VWall6 = 90, VWall7 = 91, HWall5 = 92, HWall6 = 93, HWall7 = 94, VArch5 = 95, HArch5 = 96, Floor6 = 97, Floor7 = 98, Floor8 = 99, Floor9 = 100, Floor10 = 101, VWall2 = 112, HWall2 = 113, Corner2 = 114, DWall2 = 115, DArch2 = 116, VWallEnd2 = 117, HWallEnd2 = 118, HArchEnd2 = 119, VArchEnd2 = 120, HArchVWall2 = 121, VArch2 = 122, HArch2 = 123, Floor2 = 124, HWallVArch2 = 125, Pillar3 = 126, Pillar4 = 127, Pillar5 = 128, VWall3 = 129, HWall3 = 130, Corner3 = 131, DWall3 = 132, DArch3 = 133, VWallEnd3 = 134, HWallEnd3 = 135, HArchEnd3 = 136, VArchEnd3 = 137, HArchVWall3 = 138, VArch3 = 139, HArch3 = 140, Floor3 = 141, HWallVArch3 = 142, Pillar6 = 143, Pillar7 = 144, Pillar8 = 145, VWall4 = 146, HWall4 = 147, Corner4 = 148, DWall4 = 149, DArch4 = 150, VWallEnd4 = 151, HWallEnd4 = 152, HArchEnd4 = 153, VArchEnd4 = 154, HArchVWall4 = 155, VArch4 = 156, HArch4 = 157, Floor4 = 158, HWallVArch4 = 159, Pillar9 = 160, Pillar10 = 161, Pillar11 = 162, Floor11 = 163, Floor12 = 164, Floor13 = 165, Floor14 = 166, PillarHalf = 167, VWall8 = 173, VWall9 = 174, VWall10 = 175, VWall11 = 176, VWall12 = 177, VWall13 = 178, HWall8 = 179, HWall9 = 180, HWall10 = 181, HWall11 = 182, HWall12 = 183, HWall13 = 184, VArch6 = 185, VArch7 = 186, HArch6 = 187, HArch7 = 188, Floor15 = 189, Floor16 = 190, Floor17 = 191, Pillar12 = 192, Floor18 = 193, Floor19 = 194, Floor20 = 195, Floor21 = 196, Floor22 = 197, Floor23 = 198, VDemon = 199, HDemon = 200, VSuccubus = 201, HSuccubus = 202, Shadow1 = 203, Shadow2 = 204, Shadow3 = 205, Shadow4 = 206, Shadow5 = 207, Shadow6 = 208, Shadow7 = 209, Shadow8 = 210, Shadow9 = 211, Shadow10 = 212, Shadow11 = 213, Shadow12 = 214, Shadow13 = 215, Shadow14 = 216, Shadow15 = 217, // clang-format on }; struct ReplaceTile { Tile search; Tile replace; }; const ReplaceTile Statues[] { // clang-format off { VWall, VDemon }, { VWall, VSuccubus }, { HWall, HDemon }, { HWall, HSuccubus }, // clang-format on }; const ReplaceTile CrackedTiles[] { // clang-format off { VWall, VWall2 }, { HWall, HWall2 }, { Corner, Corner2 }, { DWall, DWall2 }, { DArch, DArch2 }, { VWallEnd, VWallEnd2 }, { HWallEnd, HWallEnd2 }, { HArchEnd, HArchEnd2 }, { VArchEnd, VArchEnd2 }, { HArchVWall, HArchVWall2 }, { VArch, VArch2 }, { HArch, HArch2 }, { Floor, Floor2 }, { HWallVArch, HWallVArch2 }, { Pillar, Pillar3 }, { Pillar1, Pillar4 }, { Pillar2, Pillar5 }, // clang-format on }; const ReplaceTile BrokenTiles[] { // clang-format off { VWall, VWall3 }, { HWall, HWall3 }, { Corner, Corner3 }, { DWall, DWall3 }, { DArch, DArch3 }, { VWallEnd, VWallEnd3 }, { HWallEnd, HWallEnd3 }, { HArchEnd, HArchEnd3 }, { VArchEnd, VArchEnd3 }, { HArchVWall, HArchVWall3 }, { VArch, VArch3 }, { HArch, HArch3 }, { Floor, Floor3 }, { HWallVArch, HWallVArch3 }, { Pillar, Pillar6 }, { Pillar1, Pillar7 }, { Pillar2, Pillar8 }, // clang-format on }; const ReplaceTile LeakingTiles[] { // clang-format off { VWall, VWall4 }, { HWall, HWall4 }, { Corner, Corner4 }, { DWall, DWall4 }, { DArch, DArch4 }, { VWallEnd, VWallEnd4 }, { HWallEnd, HWallEnd4 }, { HArchEnd, HArchEnd4 }, { VArchEnd, VArchEnd4 }, { HArchVWall, HArchVWall4 }, { VArch, VArch4 }, { HArch, HArch4 }, { Floor, Floor4 }, { HWallVArch, HWallVArch4 }, { Pillar, Pillar9 }, { Pillar1, Pillar10 }, { Pillar2, Pillar11 }, // clang-format on }; const ReplaceTile Substitions1Tiles[] { // clang-format off { VArch, VArch6 }, { HArch, HArch6 }, { VArch, VArch7 }, { HArch, HArch7 }, { VWall5, VWall8 }, { VWall5, VWall9 }, { VWall6, VWall10 }, { VWall6, VWall11 }, { VWall7, VWall12 }, { VWall7, VWall13 }, { HWall5, HWall8 }, { HWall5, HWall9 }, { HWall5, HWall10 }, { HWall5, HWall11 }, { HWall5, HWall12 }, { HWall5, HWall13 }, { Floor7, Floor15 }, { Floor7, Floor16 }, { Floor6, Floor17 }, { Pillar, Pillar12 }, { Floor8, Floor18 }, { Floor8, Floor19 }, { Floor9, Floor20 }, { Floor10, Floor21 }, { Floor10, Floor22 }, { Floor10, Floor23 }, // clang-format on }; const ReplaceTile Substition1Floor[] { // clang-format off { Floor, Floor11 }, { Floor, Floor12 }, { Floor, Floor13 }, { Floor, Floor14 }, // clang-format on }; const ReplaceTile Substition2Floor[] { // clang-format off { Floor, Floor6 }, { Floor, Floor7 }, { Floor, Floor8 }, { Floor, Floor9 }, // clang-format on }; void ApplyCryptShadowsPatterns() { for (int j = 1; j < DMAXY; j++) { for (int i = 1; i < DMAXX; i++) { switch (dungeon[i][j]) { case DArch: case DArch2: case DArch3: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow1; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow2; if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow3; break; case HWallEnd: case HWallEnd2: case HWallEnd3: case HWallEnd4: case Pillar: case Pillar2: case Pillar3: case Pillar5: case Pillar9: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow4; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow5; break; case HArchEnd: case HArchEnd2: case HArchEnd3: case HArchEnd4: case HWallVArch: case HWallVArch2: case HWallVArch3: case HWallVArch4: case VArch: case VArch4: case VArch5: case VArch6: case VArch7: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow1; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow2; break; case VArchEnd: case VArchEnd2: case VArchEnd4: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow4; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow5; if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow3; break; case HArch: case HArch2: case HArchVWall: case HArchVWall2: case HArchVWall3: case HArchVWall4: if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow3; break; case HArch5: case HArch6: if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow6; break; case VArch2: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow9; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow10; break; case VArchEnd3: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow11; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow12; if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow3; break; case VArch3: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow13; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow14; break; case HArch3: case HArch4: if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow15; break; case Pillar6: case Pillar8: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow11; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow12; break; case DArch4: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow1; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow2; if (dungeon[i][j - 1] == Floor) dungeon[i][j - 1] = Shadow15; break; case Pillar11: case Pillar12: case PillarHalf: if (dungeon[i - 1][j] == Floor) dungeon[i - 1][j] = Shadow7; if (dungeon[i - 1][j - 1] == Floor) dungeon[i - 1][j - 1] = Shadow8; break; } } } } void PlaceMiniSetRandom1x1(uint8_t search, uint8_t replace, int rndper) { PlaceMiniSetRandom({ { 1, 1 }, { { search } }, { { replace } } }, rndper); } void CryptCracked(int rndper) { for (const ReplaceTile pair : CrackedTiles) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } void CryptBroken(int rndper) { for (const ReplaceTile pair : BrokenTiles) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } void CryptLeaking(int rndper) { for (const ReplaceTile pair : LeakingTiles) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } void CryptSubstitions1(int rndper) { for (const ReplaceTile pair : Substitions1Tiles) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } void CryptSubstitions2(int rndper) { PlaceMiniSetRandom(CryptPillar1, rndper); PlaceMiniSetRandom(CryptPillar2, rndper); PlaceMiniSetRandom(CryptPillar3, rndper); PlaceMiniSetRandom(CryptPillar4, rndper); PlaceMiniSetRandom(CryptPillar5, rndper); PlaceMiniSetRandom(CryptStar, rndper); for (const ReplaceTile pair : Substition1Floor) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } void CryptFloor(int rndper) { for (const ReplaceTile pair : Substition2Floor) { PlaceMiniSetRandom1x1(pair.search, pair.replace, rndper); } } } // namespace void InitCryptPieces() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 76) { dSpecial[i][j] = 1; } else if (dPiece[i][j] == 79) { dSpecial[i][j] = 2; } } } } void SetCryptRoom() { const Point position = SelectChamber(); UberRow = 2 * position.x + 6; UberCol = 2 * position.y + 8; IsUberRoomOpened = false; IsUberLeverActivated = false; auto dunData = LoadFileInMem("nlevels\\l5data\\uberroom.dun"); SetPiece = { position, GetDunSize(dunData.get()) }; PlaceDunTiles(dunData.get(), position, 0); } void SetCornerRoom() { const Point position = SelectChamber(); auto dunData = LoadFileInMem("nlevels\\l5data\\cornerstone.dun"); SetPiece = { position, GetDunSize(dunData.get()) }; PlaceDunTiles(dunData.get(), position, 0); } void FixCryptDirtTiles() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { if (dungeon[i][j] == DirtVwall) dungeon[i][j] = DirtVWall2; if (dungeon[i][j] == DirtCorner) dungeon[i][j] = DirtCorner2; if (dungeon[i][j] == DirtHWallEnd) dungeon[i][j] = DirtHWallEnd2; if (dungeon[i][j] == DirtVWallEnd) dungeon[i][j] = DirtVWallEnd2; if (dungeon[i][j] == DirtHwall) dungeon[i][j] = DirtHWall2; } } } bool PlaceCryptStairs(lvl_entry entry) { bool success = true; std::optional position; // Place stairs up position = PlaceMiniSet(currlevel != 21 ? L5STAIRSUPHF : L5STAIRSTOWN, DMAXX * DMAXY, true); if (!position) { success = false; } else if (entry == ENTRY_MAIN || entry == ENTRY_TWARPDN) { ViewPosition = position->megaToWorld() + Displacement { 3, 5 }; } // Place stairs down if (currlevel != 24) { position = PlaceMiniSet(L5STAIRSDOWN, DMAXX * DMAXY, true); if (!position) success = false; else if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 3, 7 }; } return success; } void CryptSubstitution() { for (const ReplaceTile pair : Statues) { PlaceMiniSetRandom1x1(pair.search, pair.replace, 10); } PlaceMiniSetRandom1x1(VArch, VArch5, 95); PlaceMiniSetRandom1x1(HArch, HArch5, 95); PlaceMiniSetRandom(VWallSection, 100); PlaceMiniSetRandom(HWallSection, 100); PlaceMiniSetRandom(CryptFloorLave, 60); ApplyCryptShadowsPatterns(); switch (currlevel) { case 21: CryptCracked(30); CryptBroken(15); CryptLeaking(5); ApplyCryptShadowsPatterns(); CryptFloor(10); CryptSubstitions1(5); CryptSubstitions2(20); break; case 22: CryptFloor(10); CryptSubstitions1(10); CryptSubstitions2(20); CryptCracked(30); CryptBroken(20); CryptLeaking(10); ApplyCryptShadowsPatterns(); break; case 23: CryptFloor(10); CryptSubstitions1(15); CryptSubstitions2(30); CryptCracked(30); CryptBroken(20); CryptLeaking(15); ApplyCryptShadowsPatterns(); break; default: CryptFloor(10); CryptSubstitions1(20); CryptSubstitions2(30); CryptCracked(30); CryptBroken(20); CryptLeaking(20); ApplyCryptShadowsPatterns(); break; } } void SetCryptSetPieceRoom() { for (int j = dminPosition.y; j < dmaxPosition.y; j++) { for (int i = dminPosition.x; i < dmaxPosition.x; i++) { if (dPiece[i][j] == 289) { UberRow = i; UberCol = j; } if (dPiece[i][j] == 316) { CornerStone.position = { i, j }; } } } } void PlaceCryptLights() { constexpr int lavaTiles[] = { 124, 128, 130, 132, 133, 134, 135, 139, 141, 143, 145, 156, 164, 166, 167, 168, 169, 170, 182, 190, 192, 195, 196, 199, 200, 254, 266, 273, 276, 281, 282, 283, 284, 285, 286, 287, 288, 290, 302, 316, 434, 435, 436, 437, 445, 446, 447, 453, 457, 460, 466, 470, 477, 479, 484, 485, 486, 490, 507, 537, 557, 559, 561, 563, 564, 568, 569, 572, 578, 580, 584, 585, 589, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601 }; for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { for (const int lavaTile : lavaTiles) { if (dPiece[i][j] == lavaTile) { DoLighting({ i, j }, 3, {}); break; } } } } } } // namespace devilution ================================================ FILE: Source/levels/crypt.h ================================================ /** * @file levels/crypt.h * * Interface of the cathedral level generation algorithms. */ #pragma once #include "levels/gendung.h" namespace devilution { extern int UberRow; extern int UberCol; extern bool IsUberRoomOpened; extern bool IsUberLeverActivated; extern int UberDiabloMonsterIndex; extern const Miniset L5STAIRSUP; void InitCryptPieces(); void SetCryptRoom(); void SetCornerRoom(); void FixCryptDirtTiles(); bool PlaceCryptStairs(lvl_entry entry); void CryptSubstitution(); void SetCryptSetPieceRoom(); void PlaceCryptLights(); } // namespace devilution ================================================ FILE: Source/levels/drlg_l1.cpp ================================================ #include "levels/drlg_l1.h" #include #include "engine/load_file.hpp" #include "engine/point.hpp" #include "engine/random.hpp" #include "engine/rectangle.hpp" #include "levels/crypt.h" #include "levels/gendung.h" #include "player.h" #include "quests.h" #include "utils/bitset2d.hpp" #include "utils/is_of.hpp" namespace devilution { namespace { /** Marks where walls may not be added to the level */ Bitset2d Chamber; /** Specifies whether to generate a horizontal or vertical layout. */ bool VerticalLayout; /** Specifies whether to generate a room at position 1 in the Cathedral. */ bool HasChamber1; /** Specifies whether to generate a room at position 2 in the Cathedral. */ bool HasChamber2; /** Specifies whether to generate a room at position 3 in the Cathedral. */ bool HasChamber3; /** Miniset: stairs up on a corner wall. */ const Miniset STAIRSUP { { 4, 4 }, { { 13, 13, 13, 13 }, { 2, 2, 2, 2 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 0, 66, 6, 0 }, { 63, 64, 65, 0 }, { 0, 67, 68, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: stairs down. */ const Miniset STAIRSDOWN { { 4, 3 }, { { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, { 13, 13, 13, 13 }, }, { { 62, 57, 58, 0 }, { 61, 59, 60, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: candlestick. */ const Miniset LAMPS { { 2, 2 }, { { 13, 0 }, { 13, 13 }, }, { { 129, 0 }, { 130, 128 }, } }; /** Miniset: Poisoned Water Supply entrance. */ const Miniset PWATERIN { { 6, 6 }, { { 13, 13, 13, 13, 13, 13 }, { 13, 13, 13, 13, 13, 13 }, { 13, 13, 13, 13, 13, 13 }, { 13, 13, 13, 13, 13, 13 }, { 13, 13, 13, 13, 13, 13 }, { 13, 13, 13, 13, 13, 13 }, }, { { 0, 0, 0, 0, 0, 0 }, { 0, 202, 200, 200, 84, 0 }, { 0, 199, 203, 203, 83, 0 }, { 0, 85, 206, 80, 81, 0 }, { 0, 0, 134, 135, 0, 0 }, { 0, 0, 0, 0, 0, 0 }, } }; enum Tile : uint8_t { // clang-format off VWall = 1, HWall = 2, Corner = 3, DWall = 4, DArch = 5, VWallEnd = 6, HWallEnd = 7, HArchEnd = 8, VArchEnd = 9, HArchVWall = 10, VArch = 11, HArch = 12, Floor = 13, HWallVArch = 14, Pillar = 15, VCorner = 16, HCorner = 17, DirtHwall = 18, DirtVwall = 19, VDirtCorner = 20, HDirtCorner = 21, Dirt = 22, DirtHwallEnd = 23, DirtVwallEnd = 24, VDoor = 25, HDoor = 26, HFenceVWall = 27, HDoorVDoor = 28, DFence = 29, VDoorEnd = 30, HDoorEnd = 31, VFenceEnd = 32, VArchEnd2 = 33, HArchVWall2 = 34, VFence = 35, HFence = 36, HWallVFence = 37, HArchVFence = 38, HArchVDoor = 39, HArchVWall3 = 40, DWall2 = 41, HWallVArch2 = 42, DWall3 = 43, EntranceStairs = 64, VWall2 = 79, HWall2 = 80, DWall4 = 82, VWallEnd2 = 84, VWall4 = 89, VWall5 = 90, HWall4 = 91, HWall5 = 92, VWall8 = 100, Floor12 = 139, Floor13 = 140, Floor14 = 141, Floor15 = 142, Floor16 = 143, Floor17 = 144, Floor18 = 145, VWall17 = 146, VArch5 = 147, HWallShadow = 148, HArchShadow = 149, Floor19 = 150, Floor20 = 151, Floor21 = 152, HArchShadow2 = 153, HWallShadow2 = 154, Floor22 = 162, Floor23 = 163, DirtHWall2 = 199, DirtVWall2 = 200, DirtCorner2 = 202, DirtHWallEnd2 = 204, DirtVWallEnd2 = 205, // clang-format on }; /** Contains shadows for 2x2 blocks of base tile IDs in the Cathedral. */ const ShadowStruct ShadowPatterns[37] = { // clang-format off // strig, s1, s2, s3, nv1, nv2, nv3 { HWallEnd, Floor, 0, Floor, Floor17, 0, Floor15 }, { VCorner, Floor, 0, Floor, Floor17, 0, Floor15 }, { Pillar, Floor, 0, Floor, Floor18, 0, Floor15 }, { DArch, Floor, Floor, Floor, Floor21, Floor13, Floor12 }, { DArch, Floor, VWall, Floor, Floor16, VWall17, Floor12 }, { DArch, Floor, Floor, HWall, Floor16, Floor13, HWallShadow }, { DArch, 0, VWall, HWall, 0, VWall17, HWallShadow }, { DArch, Floor, VArch, Floor, Floor16, VArch5, Floor12 }, { DArch, Floor, Floor, HArch, Floor16, Floor13, HArchShadow }, { DArch, Floor, VArch, HArch, Floor19, VArch5, HArchShadow }, { DArch, Floor, VWall, HArch, Floor16, VWall17, HArchShadow }, { DArch, Floor, VArch, HWall, Floor16, VArch5, HWallShadow }, { VArchEnd, Floor, Floor, Floor, Floor17, Floor13, Floor15 }, { VArchEnd, Floor, VWall, Floor, Floor17, VWall17, Floor15 }, { VArchEnd, Floor, VArch, Floor, Floor20, VArch5, Floor15 }, { HArchEnd, Floor, 0, Floor, Floor17, 0, Floor12 }, { HArchEnd, Floor, 0, HArch, Floor16, 0, HArchShadow }, { HArchEnd, 0, 0, HWall, 0, 0, HWallShadow }, { VArch, 0, 0, Floor, 0, 0, Floor12 }, { VArch, Floor, 0, Floor, Floor12, 0, Floor12 }, { VArch, HWall, 0, Floor, HWallShadow, 0, Floor12 }, { VArch, HArch, 0, Floor, HArchShadow, 0, Floor12 }, { VArch, Floor, VArch, HArch, Floor12, 0, HArchShadow }, { HWallVArch, 0, 0, Floor, 0, 0, Floor12 }, { HWallVArch, Floor, 0, Floor, Floor12, 0, Floor12 }, { HWallVArch, HWall, 0, Floor, HWallShadow, 0, Floor12 }, { HWallVArch, HArch, 0, Floor, HArchShadow, 0, Floor12 }, { HWallVArch, Floor, VArch, HArch, Floor12, 0, HArchShadow }, { HArchVWall, 0, Floor, 0, 0, Floor13, 0 }, { HArchVWall, Floor, Floor, 0, Floor13, Floor13, 0 }, { HArchVWall, 0, VWall, 0, 0, VWall17, 0 }, { HArchVWall, Floor, VArch, 0, Floor13, VArch5, 0 }, { HArch, 0, Floor, 0, 0, Floor13, 0 }, { HArch, Floor, Floor, 0, Floor13, Floor13, 0 }, { HArch, 0, VWall, 0, 0, VWall17, 0 }, { HArch, Floor, VArch, 0, Floor13, VArch5, 0 }, { Corner, Floor, VArch, HArch, Floor19, 0, 0 } // clang-format on }; /** Maps tile IDs to their corresponding base tile ID. */ const uint8_t BaseTypes[207] = { 0, VWall, HWall, Corner, DWall, DArch, VWallEnd, HWallEnd, HArchEnd, VArchEnd, HArchVWall, VArch, HArch, Floor, HWallVArch, Pillar, VCorner, HCorner, 0, 0, 0, 0, 0, 0, 0, VWall, HWall, HArchVWall, DWall, DArch, VWallEnd, HWallEnd, HArchEnd, VArchEnd, HArchVWall, VArch, HArch, HWallVArch, DArch, HWallVArch, HArchVWall, DWall, HWallVArch, DWall, DArch, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, VWall, HWall, Corner, DWall, VWall, VWallEnd, HWallEnd, VCorner, HCorner, HWall, VWall, VWall, HWall, HWall, VWall, VWall, HWall, HWall, HWall, HWall, HWall, VWall, VWall, VArch, VWall, Floor, Floor, Floor, VWall, HWall, VWall, HWall, VWall, HWall, VWall, HWall, HWall, HWall, HWall, HArch, 0, 0, VArch, VWall, VArch, VWall, Floor, 0, 0, 0, 0, 0, 0, 0, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, Floor, VWall, VArch, HWall, HArch, Floor, Floor, Floor, HArch, HWall, VWall, HWall, HWall, DWall, HWallVArch, DWall, HArchVWall, Floor, Floor, DWall, DWall, VWall, VWall, DWall, HWall, HWall, Floor, Floor, Floor, Floor, VDoor, HDoor, HDoorVDoor, VDoorEnd, HDoorEnd, DWall2, DWall3, HArchVWall3, DWall2, HWallVArch2, DWall3, VDoor, DWall2, DWall3, HDoorVDoor, HDoorVDoor, VWall, HWall, VDoor, HDoor, Dirt, Dirt, VDoor, HDoor, 0, 0, 0, 0, 0, 0, 0, 0 }; /** Maps tile IDs to their corresponding undecorated tile ID. */ const uint8_t TileDecorations[207] = { 0, VWall, HWall, Corner, DWall, DArch, VWallEnd, HWallEnd, HArchEnd, VArchEnd, HArchVWall, VArch, HArch, Floor, HWallVArch, Pillar, VCorner, HCorner, 0, 0, 0, 0, 0, 0, 0, VDoor, HDoor, 0, HDoorVDoor, 0, VDoorEnd, HDoorEnd, 0, 0, 0, 0, 0, 0, 0, 0, HArchVWall3, DWall2, HWallVArch2, DWall3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, VWall2, HWall2, 0, DWall4, 0, 0, 0, 0, 0, 0, VWall2, 0, HWall2, 0, 0, VWall2, HWall2, 0, HWall, HWall, HWall, VWall, VWall, VArch, VDoor, Floor, Floor, Floor, VWall, HWall, VWall, HWall, VWall, HWall, VWall, HWall, HWall, HWall, HWall, HArch, 0, 0, VArch, VWall, VArch, VWall, Floor, 0, 0, 0, 0, 0, 0, 0, Floor, Floor, Floor, Floor, Floor, Floor, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; void ApplyShadowsPatterns() { uint8_t slice[2][2]; for (int y = 1; y < DMAXY; y++) { for (int x = 1; x < DMAXX; x++) { slice[0][0] = BaseTypes[dungeon[x][y]]; slice[1][0] = BaseTypes[dungeon[x - 1][y]]; slice[0][1] = BaseTypes[dungeon[x][y - 1]]; slice[1][1] = BaseTypes[dungeon[x - 1][y - 1]]; for (const auto &shadow : ShadowPatterns) { if (shadow.strig != slice[0][0]) continue; if (shadow.s1 != 0 && shadow.s1 != slice[1][1]) continue; if (shadow.s2 != 0 && shadow.s2 != slice[0][1]) continue; if (shadow.s3 != 0 && shadow.s3 != slice[1][0]) continue; if (shadow.nv1 != 0 && !Protected.test(x - 1, y - 1)) { dungeon[x - 1][y - 1] = shadow.nv1; } if (shadow.nv2 != 0 && !Protected.test(x, y - 1)) { dungeon[x][y - 1] = shadow.nv2; } if (shadow.nv3 != 0 && !Protected.test(x - 1, y)) { dungeon[x - 1][y] = shadow.nv3; } } } } for (int y = 1; y < DMAXY; y++) { for (int x = 1; x < DMAXX; x++) { if (Protected.test(x - 1, y)) continue; if (dungeon[x - 1][y] == Floor12) { Tile tnv3 = Floor12; if (IsAnyOf(dungeon[x][y], DFence, VFenceEnd, VFence, HWallVFence, HArchVFence, HArchVDoor)) { tnv3 = Floor14; } dungeon[x - 1][y] = tnv3; } if (dungeon[x - 1][y] == HArchShadow) { Tile tnv3 = HArchShadow; if (IsAnyOf(dungeon[x][y], DFence, VFenceEnd, VFence, HWallVFence, HArchVFence, HArchVDoor)) { tnv3 = HArchShadow2; } dungeon[x - 1][y] = tnv3; } if (dungeon[x - 1][y] == HWallShadow) { Tile tnv3 = HWallShadow; if (IsAnyOf(dungeon[x][y], DFence, VFenceEnd, VFence, HWallVFence, HArchVFence, HArchVDoor)) { tnv3 = HWallShadow2; } dungeon[x - 1][y] = tnv3; } } } } bool CanReplaceTile(uint8_t replace, Point tile) { if (replace < VWallEnd2 || replace > VWall8) { return true; } // BUGFIX: p2 is a workaround for a bug, only p1 should have been used (fixing this breaks compatibility) constexpr auto ComparisonWithBoundsCheck = [](Point p1, Point p2) { return (p1.x >= 0 && p1.x < DMAXX && p1.y >= 0 && p1.y < DMAXY) && (p2.x >= 0 && p2.x < DMAXX && p2.y >= 0 && p2.y < DMAXY) && (dungeon[p1.x][p1.y] >= VWallEnd2 && dungeon[p2.x][p2.y] <= VWall8); }; if (ComparisonWithBoundsCheck(tile + Direction::NorthWest, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::SouthEast, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::SouthWest, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::NorthEast, tile + Direction::NorthWest)) { return false; } return true; } void FillFloor() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] != Floor || Protected.test(i, j)) continue; const int rv = RandomIntLessThan(3); if (rv == 1) dungeon[i][j] = Floor22; else if (rv == 2) dungeon[i][j] = Floor23; } } } void InitSetPiece() { std::unique_ptr setPieceData; if (Quests[Q_BUTCHER].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l1data\\rnd6.dun"); } else if (Quests[Q_SKELKING].IsAvailable() && !UseMultiplayerQuests()) { setPieceData = LoadFileInMem("levels\\l1data\\skngdo.dun"); } else if (Quests[Q_LTBANNER].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l1data\\banner2.dun"); } else { return; // no setpiece needed for this level } const WorldTilePosition setPiecePosition = SelectChamber(); PlaceDunTiles(setPieceData.get(), setPiecePosition, Floor); SetPiece = { setPiecePosition, GetDunSize(setPieceData.get()) }; } void InitDungeonPieces() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { int8_t pc; if (IsAnyOf(dPiece[i][j], 11, 70, 320, 210, 340, 417)) { pc = 1; } else if (IsAnyOf(dPiece[i][j], 10, 248, 324, 343, 330, 420)) { pc = 2; } else if (dPiece[i][j] == 252) { pc = 3; } else if (dPiece[i][j] == 254) { pc = 4; } else if (dPiece[i][j] == 258) { pc = 5; } else if (dPiece[i][j] == 266) { pc = 6; } else { continue; } dSpecial[i][j] = pc; } } } void InitDungeonFlags() { memset(dungeon, Dirt, sizeof(dungeon)); Protected.reset(); Chamber.reset(); } void MapRoom(Rectangle room) { for (int y = 0; y < room.size.height; y++) { for (int x = 0; x < room.size.width; x++) { DungeonMask.set(room.position.x + x, room.position.y + y); } } } bool CheckRoom(Rectangle room) { for (int j = 0; j < room.size.height; j++) { for (int i = 0; i < room.size.width; i++) { if (i + room.position.x < 0 || i + room.position.x >= DMAXX || j + room.position.y < 0 || j + room.position.y >= DMAXY) { return false; } if (DungeonMask.test(i + room.position.x, j + room.position.y)) { return false; } } } return true; } void GenerateRoom(Rectangle area, bool verticalLayout) { const bool rotate = FlipCoin(4); verticalLayout = (!verticalLayout && rotate) || (verticalLayout && !rotate); bool placeRoom1; Rectangle room1; for (int num = 0; num < 20; num++) { const int32_t randomWidth = (GenerateRnd(5) + 2) & ~1; const int32_t randomHeight = (GenerateRnd(5) + 2) & ~1; room1.size = { randomWidth, randomHeight }; room1.position = area.position; if (verticalLayout) { room1.position += Displacement { -room1.size.width, area.size.height / 2 - room1.size.height / 2 }; placeRoom1 = CheckRoom({ room1.position + Displacement { -1, -1 }, { room1.size.height + 2, room1.size.width + 1 } }); /// BUGFIX: swap height and width ({ room1.size.width + 1, room1.size.height + 2 }) (workaround applied below) } else { room1.position += Displacement { area.size.width / 2 - room1.size.width / 2, -room1.size.height }; placeRoom1 = CheckRoom({ room1.position + Displacement { -1, -1 }, { room1.size.width + 2, room1.size.height + 1 } }); } if (placeRoom1) break; } if (placeRoom1) MapRoom({ room1.position, { std::min(DMAXX - room1.position.x, room1.size.width), std::min(DMAXX - room1.position.y, room1.size.height) } }); bool placeRoom2; Rectangle room2 = room1; if (verticalLayout) { room2.position.x = area.position.x + area.size.width; placeRoom2 = CheckRoom({ room2.position + Displacement { 0, -1 }, { room2.size.width + 1, room2.size.height + 2 } }); } else { room2.position.y = area.position.y + area.size.height; placeRoom2 = CheckRoom({ room2.position + Displacement { -1, 0 }, { room2.size.width + 2, room2.size.height + 1 } }); } if (placeRoom2) MapRoom(room2); if (placeRoom1) GenerateRoom(room1, !verticalLayout); if (placeRoom2) GenerateRoom(room2, !verticalLayout); } /** * @brief Generate a boolean dungoen room layout */ void FirstRoom() { DungeonMask.reset(); VerticalLayout = FlipCoin(); HasChamber1 = !FlipCoin(); HasChamber2 = !FlipCoin(); HasChamber3 = !FlipCoin(); if (!HasChamber1 || !HasChamber3) HasChamber2 = true; Rectangle chamber1 { { 1, 15 }, { 10, 10 } }; const Rectangle chamber2 { { 15, 15 }, { 10, 10 } }; Rectangle chamber3 { { 29, 15 }, { 10, 10 } }; Rectangle hallway { { 1, 17 }, { 38, 6 } }; if (!HasChamber1) { hallway.position.x += 17; hallway.size.width -= 17; } if (!HasChamber3) hallway.size.width -= 16; if (VerticalLayout) { std::swap(chamber1.position.x, chamber1.position.y); std::swap(chamber3.position.x, chamber3.position.y); std::swap(hallway.position.x, hallway.position.y); std::swap(hallway.size.width, hallway.size.height); } if (HasChamber1) MapRoom(chamber1); if (HasChamber2) MapRoom(chamber2); if (HasChamber3) MapRoom(chamber3); MapRoom(hallway); if (HasChamber1) GenerateRoom(chamber1, VerticalLayout); if (HasChamber2) GenerateRoom(chamber2, VerticalLayout); if (HasChamber3) GenerateRoom(chamber3, VerticalLayout); } /** * @brief Find the number of mega tiles used by layout */ inline size_t FindArea() { return DungeonMask.count(); } void MakeDmt() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { if (DungeonMask.test(i, j)) dungeon[i][j] = Floor; else if (!DungeonMask.test(i + 1, j + 1) && DungeonMask.test(i, j + 1) && DungeonMask.test(i + 1, j)) dungeon[i][j] = Floor; // Remove diagonal corners else if (DungeonMask.test(i + 1, j + 1) && DungeonMask.test(i, j + 1) && DungeonMask.test(i + 1, j)) dungeon[i][j] = VCorner; else if (DungeonMask.test(i, j + 1)) dungeon[i][j] = HWall; else if (DungeonMask.test(i + 1, j)) dungeon[i][j] = VWall; else if (DungeonMask.test(i + 1, j + 1)) dungeon[i][j] = DWall; else dungeon[i][j] = Dirt; } } } int HorizontalWallOk(Point position) { int length; for (length = 1; dungeon[position.x + length][position.y] == Floor; length++) { if (dungeon[position.x + length][position.y - 1] != Floor || dungeon[position.x + length][position.y + 1] != Floor || Protected.test(position.x + length, position.y) || Chamber.test(position.x + length, position.y)) break; } if (length == 1) return -1; auto tileId = static_cast(dungeon[position.x + length][position.y]); if (!IsAnyOf(tileId, Corner, DWall, DArch, VWallEnd, HWallEnd, VCorner, HCorner, DirtHwall, DirtVwall, VDirtCorner, HDirtCorner, DirtHwallEnd, DirtVwallEnd)) return -1; return length; } int VerticalWallOk(Point position) { int length; for (length = 1; dungeon[position.x][position.y + length] == Floor; length++) { if (dungeon[position.x - 1][position.y + length] != Floor || dungeon[position.x + 1][position.y + length] != Floor || Protected.test(position.x, position.y + length) || Chamber.test(position.x, position.y + length)) break; } if (length == 1) return -1; auto tileId = static_cast(dungeon[position.x][position.y + length]); if (!IsAnyOf(tileId, Corner, DWall, DArch, VWallEnd, HWallEnd, VCorner, HCorner, DirtHwall, DirtVwall, VDirtCorner, HDirtCorner, DirtHwallEnd, DirtVwallEnd)) return -1; return length; } void HorizontalWall(Point position, Tile start, int maxX) { Tile wallTile = HWall; Tile doorTile = HDoor; switch (GenerateRnd(4)) { case 2: // Add arch wallTile = HArch; doorTile = HArch; if (start == HWall) start = HArch; else if (start == DWall) start = HArchVWall; break; case 3: // Add Fence wallTile = HFence; if (start == HWall) start = HFence; else if (start == DWall) start = HFenceVWall; break; default: break; } if (GenerateRnd(6) == 5) doorTile = HArch; dungeon[position.x][position.y] = start; for (int x = 1; x < maxX; x++) { dungeon[position.x + x][position.y] = wallTile; } const int x = GenerateRnd(maxX - 1) + 1; dungeon[position.x + x][position.y] = doorTile; if (doorTile == HDoor) { Protected.set(position.x + x, position.y); } } void VerticalWall(Point position, Tile start, int maxY) { Tile wallTile = VWall; Tile doorTile = VDoor; switch (GenerateRnd(4)) { case 2: // Add arch wallTile = VArch; doorTile = VArch; if (start == VWall) start = VArch; else if (start == DWall) start = HWallVArch; break; case 3: // Add Fence wallTile = VFence; if (start == VWall) start = VFence; else if (start == DWall) start = HWallVFence; break; default: break; } if (GenerateRnd(6) == 5) doorTile = VArch; dungeon[position.x][position.y] = start; for (int y = 1; y < maxY; y++) { dungeon[position.x][position.y + y] = wallTile; } const int y = GenerateRnd(maxY - 1) + 1; dungeon[position.x][position.y + y] = doorTile; if (doorTile == VDoor) { Protected.set(position.x, position.y + y); } } void AddWall() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (Protected.test(i, j) || Chamber.test(i, j)) continue; if (dungeon[i][j] == Corner) { DiscardRandomValues(1); const int maxX = HorizontalWallOk({ i, j }); if (maxX != -1) { HorizontalWall({ i, j }, HWall, maxX); } } if (dungeon[i][j] == Corner) { DiscardRandomValues(1); const int maxY = VerticalWallOk({ i, j }); if (maxY != -1) { VerticalWall({ i, j }, VWall, maxY); } } if (dungeon[i][j] == VWallEnd) { DiscardRandomValues(1); const int maxX = HorizontalWallOk({ i, j }); if (maxX != -1) { HorizontalWall({ i, j }, DWall, maxX); } } if (dungeon[i][j] == HWallEnd) { DiscardRandomValues(1); const int maxY = VerticalWallOk({ i, j }); if (maxY != -1) { VerticalWall({ i, j }, DWall, maxY); } } if (dungeon[i][j] == HWall) { DiscardRandomValues(1); const int maxX = HorizontalWallOk({ i, j }); if (maxX != -1) { HorizontalWall({ i, j }, HWall, maxX); } } if (dungeon[i][j] == VWall) { DiscardRandomValues(1); const int maxY = VerticalWallOk({ i, j }); if (maxY != -1) { VerticalWall({ i, j }, VWall, maxY); } } } } } void GenerateChamber(Point position, bool connectPrevious, bool connectNext, bool verticalLayout) { if (connectPrevious) { if (verticalLayout) { dungeon[position.x + 2][position.y] = HArch; dungeon[position.x + 3][position.y] = HArch; dungeon[position.x + 4][position.y] = Corner; dungeon[position.x + 7][position.y] = VArchEnd; dungeon[position.x + 8][position.y] = HArch; dungeon[position.x + 9][position.y] = HWall; } else { dungeon[position.x][position.y + 2] = VArch; dungeon[position.x][position.y + 3] = VArch; dungeon[position.x][position.y + 4] = Corner; dungeon[position.x][position.y + 7] = HArchEnd; dungeon[position.x][position.y + 8] = VArch; dungeon[position.x][position.y + 9] = VWall; } } if (connectNext) { if (verticalLayout) { position.y += 11; dungeon[position.x + 2][position.y] = HArchVWall; dungeon[position.x + 3][position.y] = HArch; dungeon[position.x + 4][position.y] = HArchEnd; dungeon[position.x + 7][position.y] = DArch; dungeon[position.x + 8][position.y] = HArch; if (dungeon[position.x + 9][position.y] != DWall) dungeon[position.x + 9][position.y] = HDirtCorner; position.y -= 11; } else { position.x += 11; dungeon[position.x][position.y + 2] = HWallVArch; dungeon[position.x][position.y + 3] = VArch; dungeon[position.x][position.y + 4] = VArchEnd; dungeon[position.x][position.y + 7] = DArch; dungeon[position.x][position.y + 8] = VArch; if (dungeon[position.x][position.y + 9] != DWall) dungeon[position.x][position.y + 9] = HDirtCorner; position.x -= 11; } } for (int y = 1; y < 11; y++) { for (int x = 1; x < 11; x++) { dungeon[position.x + x][position.y + y] = Floor; Chamber.set(position.x + x, position.y + y); } } dungeon[position.x + 4][position.y + 4] = Pillar; dungeon[position.x + 7][position.y + 4] = Pillar; dungeon[position.x + 4][position.y + 7] = Pillar; dungeon[position.x + 7][position.y + 7] = Pillar; } void GenerateHall(Point start, int length, bool verticalLayout) { if (verticalLayout) { for (int i = start.y; i < start.y + length; i++) { dungeon[start.x][i] = VArch; dungeon[start.x + 3][i] = VArch; } } else { for (int i = start.x; i < start.x + length; i++) { dungeon[i][start.y] = HArch; dungeon[i][start.y + 3] = HArch; } } } void FixTilesPatterns() { // BUGFIX: Bounds checks are required in all loop bodies. // See https://github.com/diasurgical/devilutionX/pull/401 for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (i + 1 < DMAXX) { if (dungeon[i][j] == HWall && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = DirtHwallEnd; if (dungeon[i][j] == Floor && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = DirtHwall; if (dungeon[i][j] == Floor && dungeon[i + 1][j] == HWall) dungeon[i + 1][j] = HWallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = DirtVwallEnd; } if (j + 1 < DMAXY) { if (dungeon[i][j] == VWall && dungeon[i][j + 1] == Dirt) dungeon[i][j + 1] = DirtVwallEnd; if (dungeon[i][j] == Floor && dungeon[i][j + 1] == VWall) dungeon[i][j + 1] = VWallEnd; if (dungeon[i][j] == Floor && dungeon[i][j + 1] == Dirt) dungeon[i][j + 1] = DirtVwall; } } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (i + 1 < DMAXX) { if (dungeon[i][j] == Floor && dungeon[i + 1][j] == DirtVwall) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == Floor && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = VDirtCorner; if (dungeon[i][j] == HWallEnd && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = DirtHwallEnd; if (dungeon[i][j] == Floor && dungeon[i + 1][j] == DirtVwallEnd) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == DirtVwall && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = VDirtCorner; if (dungeon[i][j] == HWall && dungeon[i + 1][j] == DirtVwall) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == DirtVwall && dungeon[i + 1][j] == VWall) dungeon[i + 1][j] = VWallEnd; if (dungeon[i][j] == HWallEnd && dungeon[i + 1][j] == DirtVwall) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == HWall && dungeon[i + 1][j] == VWall) dungeon[i + 1][j] = VWallEnd; if (dungeon[i][j] == Corner && dungeon[i + 1][j] == Dirt) dungeon[i + 1][j] = DirtVwallEnd; if (dungeon[i][j] == HDirtCorner && dungeon[i + 1][j] == VWall) dungeon[i + 1][j] = VWallEnd; if (dungeon[i][j] == HWallEnd && dungeon[i + 1][j] == VWall) dungeon[i + 1][j] = VWallEnd; if (dungeon[i][j] == HWallEnd && dungeon[i + 1][j] == DirtVwallEnd) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == DWall && dungeon[i + 1][j] == VCorner) dungeon[i + 1][j] = HCorner; if (dungeon[i][j] == HWallEnd && dungeon[i + 1][j] == Floor) dungeon[i + 1][j] = HCorner; if (dungeon[i][j] == HWall && dungeon[i + 1][j] == DirtVwallEnd) dungeon[i + 1][j] = HDirtCorner; if (dungeon[i][j] == HWall && dungeon[i + 1][j] == Floor) dungeon[i + 1][j] = HCorner; } if (i > 0) { if (dungeon[i][j] == DirtHwallEnd && dungeon[i - 1][j] == Dirt) dungeon[i - 1][j] = DirtVwall; if (dungeon[i][j] == DirtVwall && dungeon[i - 1][j] == DirtHwallEnd) dungeon[i - 1][j] = HDirtCorner; if (dungeon[i][j] == VWallEnd && dungeon[i - 1][j] == Dirt) dungeon[i - 1][j] = DirtVwallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i - 1][j] == DirtHwallEnd) dungeon[i - 1][j] = HDirtCorner; } if (j + 1 < DMAXY) { if (dungeon[i][j] == VWall && dungeon[i][j + 1] == HWall) dungeon[i][j + 1] = HWallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i][j + 1] == DirtHwall) dungeon[i][j + 1] = HDirtCorner; if (dungeon[i][j] == DirtHwall && dungeon[i][j + 1] == HWall) dungeon[i][j + 1] = HWallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i][j + 1] == HWall) dungeon[i][j + 1] = HWallEnd; if (dungeon[i][j] == HDirtCorner && dungeon[i][j + 1] == HWall) dungeon[i][j + 1] = HWallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i][j + 1] == Dirt) dungeon[i][j + 1] = DirtVwallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i][j + 1] == Floor) dungeon[i][j + 1] = VCorner; if (dungeon[i][j] == VWall && dungeon[i][j + 1] == Floor) dungeon[i][j + 1] = VCorner; if (dungeon[i][j] == Floor && dungeon[i][j + 1] == VCorner) dungeon[i][j + 1] = HCorner; } if (j > 0) { if (dungeon[i][j] == VWallEnd && dungeon[i][j - 1] == Dirt) dungeon[i][j - 1] = HWallEnd; if (dungeon[i][j] == VWallEnd && dungeon[i][j - 1] == Dirt) dungeon[i][j - 1] = DirtVwallEnd; if (dungeon[i][j] == HWallEnd && dungeon[i][j - 1] == DirtVwallEnd) dungeon[i][j - 1] = HDirtCorner; if (dungeon[i][j] == DirtHwall && dungeon[i][j - 1] == DirtVwallEnd) dungeon[i][j - 1] = HDirtCorner; } } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (j + 1 < DMAXY && dungeon[i][j] == DWall && dungeon[i][j + 1] == HWall) dungeon[i][j + 1] = HWallEnd; if (i + 1 < DMAXX && dungeon[i][j] == HWall && dungeon[i + 1][j] == DirtVwall) dungeon[i + 1][j] = HDirtCorner; if (j + 1 < DMAXY && dungeon[i][j] == DirtHwall && dungeon[i][j + 1] == Dirt) dungeon[i][j + 1] = VDirtCorner; } } } void Substitution() { for (int y = 0; y < DMAXY; y++) { for (int x = 0; x < DMAXX; x++) { if (FlipCoin(4)) { const uint8_t c = TileDecorations[dungeon[x][y]]; if (c != 0 && !Protected.test(x, y)) { int rv = GenerateRnd(16); int i = -1; while (rv >= 0) { i++; if (i == sizeof(TileDecorations)) { i = 0; } if (c == TileDecorations[i]) { rv--; } } // BUGFIX: Add `&& y > 0` to the if statement. (fixed) if (i == VWall4 && y > 0) { if (TileDecorations[dungeon[x][y - 1]] != VWall2 || Protected.test(x, y - 1)) i = VWall2; else dungeon[x][y - 1] = VWall5; } // BUGFIX: Add `&& x + 1 < DMAXX` to the if statement. (fixed) if (i == HWall4 && x + 1 < DMAXX) { if (TileDecorations[dungeon[x + 1][y]] != HWall2 || Protected.test(x + 1, y)) i = HWall2; else dungeon[x + 1][y] = HWall5; } dungeon[x][y] = i; } } } } } void FillChambers() { Point chamber1 { 0, 14 }; Point chamber3 { 28, 14 }; Point hall1 { 12, 18 }; Point hall2 { 26, 18 }; if (VerticalLayout) { std::swap(chamber1.x, chamber1.y); std::swap(chamber3.x, chamber3.y); std::swap(hall1.x, hall1.y); std::swap(hall2.x, hall2.y); } if (HasChamber1) GenerateChamber(chamber1, false, true, VerticalLayout); if (HasChamber2) GenerateChamber({ 14, 14 }, HasChamber1, HasChamber3, VerticalLayout); if (HasChamber3) GenerateChamber(chamber3, true, false, VerticalLayout); if (HasChamber2) { if (HasChamber1) GenerateHall(hall1, 2, VerticalLayout); if (HasChamber3) GenerateHall(hall2, 2, VerticalLayout); } else { GenerateHall(hall1, 16, VerticalLayout); } if (leveltype == DTYPE_CRYPT) { if (currlevel == 24) { SetCryptRoom(); } else if (CornerStone.isAvailable()) { SetCornerRoom(); } } else { InitSetPiece(); } } void FixTransparency() { int yy = 16; for (int j = 0; j < DMAXY; j++) { int xx = 16; for (int i = 0; i < DMAXX; i++) { // BUGFIX: Should check for `j > 0` first. (fixed) if (dungeon[i][j] == DirtHwallEnd && j > 0 && dungeon[i][j - 1] == DirtHwall) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } // BUGFIX: Should check for `i + 1 < DMAXY` first. (fixed) if (dungeon[i][j] == DirtVwallEnd && i + 1 < DMAXY && dungeon[i + 1][j] == DirtVwall) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == DirtHwall) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == DirtVwall) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == VDirtCorner) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } xx += 2; } yy += 2; } } void FixDirtTiles() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { if (dungeon[i][j] == HDirtCorner && dungeon[i + 1][j] != DirtVwall) { dungeon[i][j] = DirtCorner2; } if (dungeon[i][j] == DirtVwall && dungeon[i + 1][j] != DirtVwall) { dungeon[i][j] = DirtVWall2; } if (dungeon[i][j] == DirtVwallEnd && dungeon[i + 1][j] != DirtVwall) { dungeon[i][j] = DirtVWallEnd2; } if (dungeon[i][j] == DirtHwall && dungeon[i][j + 1] != DirtHwall) { dungeon[i][j] = DirtHWall2; } if (dungeon[i][j] == HDirtCorner && dungeon[i][j + 1] != DirtHwall) { dungeon[i][j] = DirtCorner2; } if (dungeon[i][j] == DirtHwallEnd && dungeon[i][j + 1] != DirtHwall) { dungeon[i][j] = DirtHWallEnd2; } } } } void FixCornerTiles() { for (int j = 1; j < DMAXY - 1; j++) { for (int i = 1; i < DMAXX - 1; i++) { if (!Protected.test(i, j) && dungeon[i][j] == HCorner && dungeon[i - 1][j] == Floor && dungeon[i][j - 1] == VWall) { dungeon[i][j] = VCorner; // BUGFIX: Set tile as Protected } if (dungeon[i][j] == DirtCorner2 && dungeon[i + 1][j] == Floor && dungeon[i][j + 1] == VWall) { dungeon[i][j] = HArchEnd; } if (dungeon[i][j] == DirtCorner2 && dungeon[i][j + 1] == Floor && dungeon[i + 1][j] == HWall) { dungeon[i][j] = VArchEnd; } } } } bool PlaceCathedralStairs(lvl_entry entry) { bool success = true; std::optional position; // Place poison water entrance if (Quests[Q_PWATER].IsAvailable()) { position = PlaceMiniSet(PWATERIN, DMAXX * DMAXY, true); if (!position) { success = false; } else { const int8_t t = TransVal; TransVal = 0; const Point miniPosition = *position; DRLG_MRectTrans({ miniPosition + Displacement { 0, 2 }, { 5, 2 } }); TransVal = t; Quests[Q_PWATER].position = miniPosition.megaToWorld() + Displacement { 5, 6 }; if (entry == ENTRY_RTNLVL) ViewPosition = Quests[Q_PWATER].position; } } // Place stairs up position = PlaceMiniSet(MyPlayer->pOriginalCathedral && !Quests[Q_LTBANNER].IsAvailable() ? L5STAIRSUP : STAIRSUP, DMAXX * DMAXY, true); if (!position) { if (MyPlayer->pOriginalCathedral) return false; success = false; } else if (entry == ENTRY_MAIN) { ViewPosition = position->megaToWorld() + Displacement { 3, 4 }; } // Place stairs down if (Quests[Q_LTBANNER].IsAvailable()) { if (entry == ENTRY_PREV) ViewPosition = SetPiece.position.megaToWorld() + Displacement { 3, 11 }; } else { position = PlaceMiniSet(STAIRSDOWN, DMAXX * DMAXY, true); if (!position) { success = false; } else if (entry == ENTRY_PREV) { ViewPosition = position->megaToWorld() + Displacement { 3, 3 }; } } return success; } bool PlaceStairs(lvl_entry entry) { if (leveltype == DTYPE_CRYPT) { return PlaceCryptStairs(entry); } return PlaceCathedralStairs(entry); } void GenerateLevel(lvl_entry entry) { if (LevelSeeds[currlevel]) SetRndSeed(*LevelSeeds[currlevel]); size_t minarea = 761; switch (currlevel) { case 1: minarea = 533; break; case 2: minarea = 693; break; default: break; } while (true) { DRLG_InitTrans(); do { LevelSeeds[currlevel] = GetLCGEngineState(); FirstRoom(); } while (FindArea() < minarea); InitDungeonFlags(); MakeDmt(); FillChambers(); FixTilesPatterns(); AddWall(); FloodTransparencyValues(13); if (PlaceStairs(entry)) break; } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == EntranceStairs) { const int xx = 2 * i + 16; /* todo: fix loop */ const int yy = 2 * j + 16; DRLG_CopyTrans(xx, yy + 1, xx, yy); DRLG_CopyTrans(xx + 1, yy + 1, xx + 1, yy); } } } FixTransparency(); if (leveltype == DTYPE_CRYPT) { FixCryptDirtTiles(); } else { FixDirtTiles(); } FixCornerTiles(); if (leveltype == DTYPE_CRYPT) { CryptSubstitution(); } else { Substitution(); ApplyShadowsPatterns(); const int numt = GenerateRnd(5) + 5; for (int i = 0; i < numt; i++) { PlaceMiniSet(LAMPS, DMAXX * DMAXY, true); } FillFloor(); } memcpy(pdungeon, dungeon, sizeof(pdungeon)); DRLG_CheckQuests(SetPiece.position); } void Pass3() { DRLG_LPass3(Dirt - 1); if (leveltype == DTYPE_CRYPT) InitCryptPieces(); else InitDungeonPieces(); } } // namespace void PlaceMiniSetRandom(const Miniset &miniset, int rndper) { const WorldTileCoord sw = miniset.size.width; const WorldTileCoord sh = miniset.size.height; for (WorldTileCoord sy = 0; sy < DMAXY - sh; sy++) { for (WorldTileCoord sx = 0; sx < DMAXX - sw; sx++) { if (!miniset.matches({ sx, sy }, false)) continue; // BUGFIX: This code is copied from Cave and should not be applied for crypt if (!CanReplaceTile(miniset.replace[0][0], { sx, sy })) continue; if (GenerateRnd(100) >= rndper) continue; miniset.place({ sx, sy }); } } } WorldTilePosition SelectChamber() { int chamber; if (HasChamber1 && HasChamber2 && HasChamber3) { chamber = GenerateRnd(3) + 1; } else if (HasChamber1 && HasChamber2) { chamber = PickRandomlyAmong({ 2, 1 }); // Reverse order to match vanilla } else if (HasChamber1 && HasChamber3) { chamber = PickRandomlyAmong({ 3, 1 }); // Reverse order to match vanilla } else if (HasChamber2 && HasChamber3) { chamber = PickRandomlyAmong({ 2, 3 }); } else { // The dungeon generation logic ensures that chamber 2 is available if // either (or both of) 1 or 3 aren't, so if we ever end up with a single // chamber layout it's always chamber 2. chamber = 2; } switch (chamber) { case 1: return VerticalLayout ? WorldTilePosition { 16, 2 } : WorldTilePosition { 2, 16 }; case 3: return VerticalLayout ? WorldTilePosition { 16, 30 } : WorldTilePosition { 30, 16 }; default: return { 16, 16 }; } } void CreateL5Dungeon(uint32_t rseed, lvl_entry entry) { SetRndSeed(rseed); UberRow = 0; UberCol = 0; GenerateLevel(entry); Pass3(); if (leveltype == DTYPE_CRYPT) { PlaceCryptLights(); SetCryptSetPieceRoom(); } } void LoadPreL1Dungeon(const char *path) { InitDungeonFlags(); auto dunData = LoadFileInMem(path); PlaceDunTiles(dunData.get(), { 0, 0 }, Floor); if (setlvltype == DTYPE_CATHEDRAL) FillFloor(); memcpy(pdungeon, dungeon, sizeof(pdungeon)); } void LoadL1Dungeon(const char *path, Point spawn) { LoadDungeonBase(path, spawn, Floor, Dirt); if (setlvltype == DTYPE_CATHEDRAL) FillFloor(); Pass3(); if (setlvltype == DTYPE_CRYPT) { AddCryptObjects(0, 0, MAXDUNX, MAXDUNY); PlaceCryptLights(); } else { AddL1Objs(0, 0, MAXDUNX, MAXDUNY); } } } // namespace devilution ================================================ FILE: Source/levels/drlg_l1.h ================================================ /** * @file levels/drlg_l1.h * * Interface of the cathedral level generation algorithms. */ #pragma once #include #include "engine/world_tile.hpp" #include "levels/gendung.h" namespace devilution { void PlaceMiniSetRandom(const Miniset &miniset, int rndper); WorldTilePosition SelectChamber(); void CreateL5Dungeon(uint32_t rseed, lvl_entry entry); void LoadPreL1Dungeon(const char *path); void LoadL1Dungeon(const char *path, Point spawn); } // namespace devilution ================================================ FILE: Source/levels/drlg_l2.cpp ================================================ /** * @file levels/drlg_l2.cpp * * Implementation of the catacombs level generation algorithms. */ #include "levels/drlg_l2.h" #include #include #include #include #include #include "diablo.h" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "engine/size.hpp" #include "levels/gendung.h" #include "levels/setmaps.h" #include "player.h" #include "quests.h" #include "utils/is_of.hpp" namespace devilution { namespace { enum class HallDirection : int8_t { None, Up, Right, Down, Left, }; struct HallNode { WorldTilePosition beginning; WorldTilePosition end; HallDirection direction; }; struct RoomNode { WorldTilePosition topLeft; WorldTilePosition bottomRight; }; int nRoomCnt; RoomNode RoomList[81]; std::list HallList; // An ASCII representation of the level char predungeon[DMAXX][DMAXY]; const Displacement DirAdd[5] = { { 0, 0 }, { 0, -1 }, { 1, 0 }, { 0, 1 }, { -1, 0 } }; const ShadowStruct SPATSL2[2] = { { 6, 3, 0, 3, 48, 0, 50 }, { 9, 3, 0, 3, 48, 0, 50 } }; // short word_48489A = 0; const uint8_t BTYPESL2[161] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, 0, 17, 18, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 0, 0, 0, 0, 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, 1, 1, 1, 0, 0, 2, 2, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 0, 3, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; const uint8_t BSTYPESL2[161] = { 0, 1, 2, 3, 0, 0, 6, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 0, 0, 0, 0, 0, 6, 6, 6, 9, 0, 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, 0, 0, 2, 2, 2, 0, 0, 0, 1, 1, 1, 1, 6, 2, 2, 2, 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 2, 2, 3, 3, 3, 3, 1, 1, 2, 2, 3, 3, 3, 3, 1, 1, 3, 3, 2, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; /** Miniset: Arch vertical. */ const Miniset VARCH1 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 7 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH2 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 8 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH3 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 6 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH4 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 9 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH5 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 14 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH6 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 13 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH7 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 16 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH8 { { 2, 4 }, { { 3, 0 }, { 3, 1 }, { 3, 4 }, { 0, 15 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH9 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 7 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH10 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 8 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH11 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 6 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH12 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 9 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH13 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 14 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH14 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 13 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH15 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 16 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - corner. */ const Miniset VARCH16 { { 2, 4 }, { { 3, 0 }, { 3, 8 }, { 3, 4 }, { 0, 15 }, }, { { 48, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH17 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 7 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH18 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 8 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH19 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 6 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH20 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 9 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH21 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 14 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH22 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 13 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH23 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 16 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - open wall. */ const Miniset VARCH24 { { 2, 3 }, { { 2, 7 }, { 3, 4 }, { 0, 15 }, }, { { 141, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH25 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 7 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH26 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 8 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH27 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 6 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH28 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 9 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH29 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 14 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH30 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 13 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH31 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 16 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical. */ const Miniset VARCH32 { { 2, 4 }, { { 3, 0 }, { 3, 4 }, { 3, 1 }, { 0, 15 }, }, { { 48, 0 }, { 51, 39 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH33 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 7 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH34 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 8 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH35 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 6 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH36 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 9 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH37 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 14 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH38 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 13 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH39 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 16 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch vertical - room west entrance. */ const Miniset VARCH40 { { 2, 4 }, { { 2, 0 }, { 3, 8 }, { 3, 4 }, { 0, 15 }, }, { { 142, 0 }, { 51, 42 }, { 47, 44 }, { 0, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH1 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 9 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH2 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 6 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH3 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 8 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH4 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 7 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH5 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 15 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH6 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 16 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH7 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 13 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH8 { { 3, 2 }, { { 3, 3, 0 }, { 2, 5, 14 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH9 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 9 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH10 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 6 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH11 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 8 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH12 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 7 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH13 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 15 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH14 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 16 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH15 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 13 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - north corner. */ const Miniset HARCH16 { { 3, 2 }, { { 3, 3, 0 }, { 8, 5, 14 }, }, { { 49, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH17 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 9 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH18 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 6 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH19 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 8 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH20 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 7 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH21 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 15 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH22 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 16 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH23 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 13 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal - wall. */ const Miniset HARCH24 { { 3, 2 }, { { 1, 3, 0 }, { 8, 5, 14 }, }, { { 140, 46, 0 }, { 43, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH25 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 9 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH26 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 6 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH27 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 8 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH28 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 7 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH29 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 15 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH30 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 16 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH31 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 13 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal. */ const Miniset HARCH32 { { 3, 2 }, { { 3, 3, 0 }, { 5, 2, 14 }, }, { { 49, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH33 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 9 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH34 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 6 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH35 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 8 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH36 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 7 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH37 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 15 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH38 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 16 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH39 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 13 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Arch horizontal - west corner. */ const Miniset HARCH40 { { 3, 2 }, { { 1, 3, 0 }, { 9, 5, 14 }, }, { { 140, 46, 0 }, { 40, 45, 0 }, } }; /** Miniset: Stairs up. */ const Miniset USTAIRS { { 4, 4 }, { { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, }, { { 0, 0, 0, 0 }, { 0, 72, 77, 0 }, { 0, 76, 0, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stairs down. */ const Miniset DSTAIRS { { 4, 4 }, { { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, }, { { 0, 0, 0, 0 }, { 0, 48, 71, 0 }, { 0, 50, 78, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stairs to town. */ const Miniset WARPSTAIRS { { 4, 4 }, { { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, { 3, 3, 3, 3 }, }, { { 0, 0, 0, 0 }, { 0, 158, 160, 0 }, { 0, 159, 0, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Crumbled south pillar. */ const Miniset CRUSHCOL { { 3, 3 }, { { 3, 1, 3 }, { 2, 6, 3 }, { 3, 3, 3 }, }, { { 0, 0, 0 }, { 0, 83, 0 }, { 0, 0, 0 }, } }; /** Miniset: Vertical oil spill. */ const Miniset BIG1 { { 2, 2 }, { { 3, 3 }, { 3, 3 }, }, { { 113, 0 }, { 112, 0 }, } }; /** Miniset: Horizontal oil spill. */ const Miniset BIG2 { { 2, 2 }, { { 3, 3 }, { 3, 3 }, }, { { 114, 115 }, { 0, 0 }, } }; /** Miniset: Horizontal platform. */ const Miniset BIG3 { { 1, 2 }, { { 1 }, { 1 }, }, { { 117 }, { 116 }, } }; /** Miniset: Vertical platform. */ const Miniset BIG4 { { 2, 1 }, { { 2, 2 }, }, { { 118, 119 }, } }; /** Miniset: Large oil spill. */ const Miniset BIG5 { { 2, 2 }, { { 3, 3 }, { 3, 3 }, }, { { 120, 122 }, { 121, 123 }, } }; /** Miniset: Vertical wall with debris. */ const Miniset BIG6 { { 1, 2 }, { { 1 }, { 1 }, }, { { 125 }, { 124 }, } }; /** Miniset: Horizontal wall with debris. */ const Miniset BIG7 { { 2, 1 }, { { 2, 2 }, }, { { 126, 127 }, } }; /** Miniset: Rock pile. */ const Miniset BIG8 { { 2, 2 }, { { 3, 3 }, { 3, 3 }, }, { { 128, 130 }, { 129, 131 }, } }; /** Miniset: Vertical wall collapsed. */ const Miniset BIG9 { { 2, 2 }, { { 1, 3 }, { 1, 3 }, }, { { 133, 135 }, { 132, 134 }, } }; /** Miniset: Horizontal wall collapsed. */ const Miniset BIG10 { { 2, 2 }, { { 2, 2 }, { 3, 3 }, }, { { 136, 137 }, { 3, 3 }, } }; /** Miniset: Bloody gib 1. */ const Miniset PANCREAS1 { { 5, 3 }, { { 3, 3, 3, 3, 3 }, { 3, 3, 3, 3, 3 }, { 3, 3, 3, 3, 3 }, }, { { 0, 0, 0, 0, 0 }, { 0, 0, 108, 0, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Miniset: Bloody gib 2. */ const Miniset PANCREAS2 { { 5, 3 }, { { 3, 3, 3, 3, 3 }, { 3, 3, 3, 3, 3 }, { 3, 3, 3, 3, 3 }, }, { { 0, 0, 0, 0, 0 }, { 0, 0, 110, 0, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 1. */ const Miniset CTRDOOR1 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 9, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 2. */ const Miniset CTRDOOR2 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 8, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 3. */ const Miniset CTRDOOR3 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 6, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 4. */ const Miniset CTRDOOR4 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 7, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 5. */ const Miniset CTRDOOR5 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 15, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 6. */ const Miniset CTRDOOR6 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 13, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 7. */ const Miniset CTRDOOR7 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 16, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; /** Miniset: Move vertical doors away from west pillar 8. */ const Miniset CTRDOOR8 { { 3, 3 }, { { 3, 1, 3 }, { 0, 4, 0 }, { 0, 14, 0 }, }, { { 0, 4, 0 }, { 0, 1, 0 }, { 0, 0, 0 }, } }; int Patterns[100][10] = { { 0, 0, 0, 0, 0, 0, 0, 0, 0, 3 }, { 0, 0, 0, 0, 2, 0, 0, 0, 0, 3 }, { 0, 7, 0, 0, 1, 0, 0, 5, 0, 2 }, { 0, 5, 0, 0, 1, 0, 0, 7, 0, 2 }, { 0, 0, 0, 7, 1, 5, 0, 0, 0, 1 }, { 0, 0, 0, 5, 1, 7, 0, 0, 0, 1 }, { 0, 1, 0, 0, 3, 0, 0, 1, 0, 4 }, { 0, 0, 0, 1, 3, 1, 0, 0, 0, 5 }, { 0, 6, 0, 6, 1, 0, 0, 0, 0, 6 }, { 0, 6, 0, 0, 1, 6, 0, 0, 0, 9 }, { 0, 0, 0, 6, 1, 0, 0, 6, 0, 7 }, { 0, 0, 0, 0, 1, 6, 0, 6, 0, 8 }, { 0, 6, 0, 6, 6, 0, 8, 6, 0, 7 }, { 0, 6, 8, 6, 6, 6, 0, 0, 0, 9 }, { 0, 6, 0, 0, 6, 6, 0, 6, 8, 8 }, { 6, 6, 6, 6, 6, 6, 0, 6, 0, 8 }, { 2, 6, 6, 6, 6, 6, 0, 6, 0, 8 }, { 7, 7, 7, 6, 6, 6, 0, 6, 0, 8 }, { 6, 6, 2, 6, 6, 6, 0, 6, 0, 8 }, { 6, 2, 6, 6, 6, 6, 0, 6, 0, 8 }, { 2, 6, 6, 6, 6, 6, 0, 6, 0, 8 }, { 6, 7, 7, 6, 6, 6, 0, 6, 0, 8 }, { 4, 4, 6, 6, 6, 6, 2, 6, 2, 8 }, { 2, 2, 2, 2, 6, 2, 2, 6, 2, 7 }, { 2, 2, 2, 2, 6, 2, 6, 6, 6, 7 }, { 2, 2, 6, 2, 6, 6, 2, 2, 6, 9 }, { 2, 6, 2, 2, 6, 2, 2, 2, 2, 6 }, { 2, 2, 2, 2, 6, 6, 2, 2, 2, 9 }, { 2, 2, 2, 6, 6, 2, 2, 2, 2, 6 }, { 2, 2, 0, 2, 6, 6, 2, 2, 0, 9 }, { 0, 0, 0, 0, 4, 0, 0, 0, 0, 12 }, { 0, 1, 0, 0, 1, 4, 0, 1, 0, 10 }, { 0, 0, 0, 1, 1, 1, 0, 4, 0, 11 }, { 0, 0, 0, 6, 1, 4, 0, 1, 0, 14 }, { 0, 6, 0, 1, 1, 0, 0, 4, 0, 16 }, { 0, 6, 0, 0, 1, 1, 0, 4, 0, 15 }, { 0, 0, 0, 0, 1, 1, 0, 1, 4, 13 }, { 8, 8, 8, 8, 1, 1, 0, 1, 1, 13 }, { 8, 8, 4, 8, 1, 1, 0, 1, 1, 10 }, { 0, 0, 0, 1, 1, 1, 1, 1, 1, 11 }, { 1, 1, 1, 1, 1, 1, 2, 2, 8, 2 }, { 0, 1, 0, 1, 1, 4, 1, 1, 0, 16 }, { 0, 0, 0, 1, 1, 1, 1, 1, 4, 11 }, { 1, 1, 4, 1, 1, 1, 0, 2, 2, 2 }, { 1, 1, 1, 1, 1, 1, 6, 2, 6, 2 }, { 4, 1, 1, 1, 1, 1, 6, 2, 6, 2 }, { 2, 2, 2, 1, 1, 1, 4, 1, 1, 11 }, { 4, 1, 1, 1, 1, 1, 2, 2, 2, 2 }, { 1, 1, 4, 1, 1, 1, 2, 2, 1, 2 }, { 4, 1, 1, 1, 1, 1, 1, 2, 2, 2 }, { 2, 2, 6, 1, 1, 1, 4, 1, 1, 11 }, { 4, 1, 1, 1, 1, 1, 2, 2, 6, 2 }, { 1, 2, 2, 1, 1, 1, 4, 1, 1, 11 }, { 0, 1, 1, 0, 1, 1, 0, 1, 1, 10 }, { 2, 1, 1, 3, 1, 1, 2, 1, 1, 14 }, { 1, 1, 0, 1, 1, 2, 1, 1, 0, 1 }, { 0, 4, 0, 1, 1, 1, 0, 1, 1, 14 }, { 4, 1, 0, 1, 1, 0, 1, 1, 0, 1 }, { 0, 1, 0, 4, 1, 1, 0, 1, 1, 15 }, { 1, 1, 1, 1, 1, 1, 0, 2, 2, 2 }, { 0, 1, 1, 2, 1, 1, 2, 1, 4, 10 }, { 2, 1, 1, 1, 1, 1, 0, 4, 0, 16 }, { 1, 1, 4, 1, 1, 2, 0, 1, 2, 1 }, { 2, 1, 1, 2, 1, 1, 1, 1, 4, 10 }, { 1, 1, 2, 1, 1, 2, 4, 1, 8, 1 }, { 2, 1, 4, 1, 1, 1, 4, 4, 1, 16 }, { 2, 1, 1, 1, 1, 1, 1, 1, 1, 16 }, { 1, 1, 2, 1, 1, 1, 1, 1, 1, 15 }, { 1, 1, 1, 1, 1, 1, 2, 1, 1, 14 }, { 4, 1, 1, 1, 1, 1, 2, 1, 1, 14 }, { 1, 1, 1, 1, 1, 1, 1, 1, 2, 8 }, { 0, 0, 0, 0, 255, 0, 0, 0, 0, 0 }, }; void ApplyShadowsPatterns() { uint8_t sd[2][2]; for (int y = 1; y < DMAXY; y++) { for (int x = 1; x < DMAXX; x++) { sd[0][0] = BSTYPESL2[dungeon[x][y]]; sd[1][0] = BSTYPESL2[dungeon[x - 1][y]]; sd[0][1] = BSTYPESL2[dungeon[x][y - 1]]; sd[1][1] = BSTYPESL2[dungeon[x - 1][y - 1]]; for (const auto &shadow : SPATSL2) { if (shadow.strig != sd[0][0]) continue; if (shadow.s1 != 0 && shadow.s1 != sd[1][1]) continue; if (shadow.s2 != 0 && shadow.s2 != sd[0][1]) continue; if (shadow.s3 != 0 && shadow.s3 != sd[1][0]) continue; if (shadow.nv1 != 0) { dungeon[x - 1][y - 1] = shadow.nv1; } if (shadow.nv2 != 0) { dungeon[x][y - 1] = shadow.nv2; } if (shadow.nv3 != 0) { dungeon[x - 1][y] = shadow.nv3; } } } } } void PlaceMiniSetRandom(const Miniset &miniset, int rndper) { const WorldTileCoord sw = miniset.size.width; const WorldTileCoord sh = miniset.size.height; for (WorldTileCoord sy = 0; sy < DMAXY - sh; sy++) { for (WorldTileCoord sx = 0; sx < DMAXX - sw; sx++) { if (SetPieceRoom.contains(sx, sy)) continue; if (!miniset.matches({ sx, sy })) continue; bool found = true; for (int yy = std::max(sy - sh, 0); yy < std::min(sy + 2 * sh, DMAXY) && found; yy++) { for (int xx = std::max(sx - sw, 0); xx < std::min(sx + 2 * sw, DMAXX); xx++) { if (dungeon[xx][yy] == miniset.replace[0][0]) { found = false; break; } } } if (!found) continue; if (GenerateRnd(100) >= rndper) continue; miniset.place({ sx, sy }); } } } void PlaceMiniSetRandom1x1(uint8_t search, uint8_t replace, int rndper) { PlaceMiniSetRandom({ { 1, 1 }, { { search } }, { { replace } } }, rndper); } void InitSetPiece() { std::unique_ptr setPieceData; if (Quests[Q_BLIND].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l2data\\blind1.dun"); } else if (Quests[Q_BLOOD].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l2data\\blood1.dun"); } else if (Quests[Q_SCHAMB].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l2data\\bonestr2.dun"); } else { return; // no setpiece needed for this level } const WorldTilePosition setPiecePosition = SetPieceRoom.position; PlaceDunTiles(setPieceData.get(), setPiecePosition, 3); SetPiece = { setPiecePosition, GetDunSize(setPieceData.get()) }; } void InitDungeonPieces() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { int8_t pc; if (IsAnyOf(dPiece[i][j], 540, 177, 550)) { pc = 5; } else if (IsAnyOf(dPiece[i][j], 541, 552)) { pc = 6; } else { continue; } dSpecial[i][j] = pc; } } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 131) { dSpecial[i][j + 1] = 2; dSpecial[i][j + 2] = 1; } else if (dPiece[i][j] == 134 || dPiece[i][j] == 138) { dSpecial[i + 1][j] = 3; dSpecial[i + 2][j] = 4; } } } } void InitDungeonFlags() { Protected.reset(); memset(predungeon, ' ', sizeof(predungeon)); } void MapRoom(int x1, int y1, int x2, int y2) { for (int jj = y1; jj <= y2; jj++) { for (int ii = x1; ii <= x2; ii++) { predungeon[ii][jj] = '.'; } } for (int jj = y1; jj <= y2; jj++) { predungeon[x1][jj] = '#'; predungeon[x2][jj] = '#'; } for (int ii = x1; ii <= x2; ii++) { predungeon[ii][y1] = '#'; predungeon[ii][y2] = '#'; } } void DefineRoom(Point topLeft, Point bottomRight, bool forceHW) { predungeon[topLeft.x][topLeft.y] = 'C'; predungeon[topLeft.x][bottomRight.y] = 'E'; predungeon[bottomRight.x][topLeft.y] = 'B'; predungeon[bottomRight.x][bottomRight.y] = 'A'; nRoomCnt++; RoomList[nRoomCnt] = { topLeft, bottomRight }; if (forceHW) { for (int i = topLeft.x; i < bottomRight.x; i++) { /// BUGFIX: Should loop j between nY1 and nY2 instead of always using nY1. while (i < bottomRight.y) { Protected.set(i, topLeft.y); i++; } } } for (int i = topLeft.x + 1; i <= bottomRight.x - 1; i++) { predungeon[i][topLeft.y] = '#'; predungeon[i][bottomRight.y] = '#'; } bottomRight.y--; for (int j = topLeft.y + 1; j <= bottomRight.y; j++) { predungeon[topLeft.x][j] = '#'; predungeon[bottomRight.x][j] = '#'; for (int i = topLeft.x + 1; i < bottomRight.x; i++) { predungeon[i][j] = '.'; } } } void CreateDoorType(Point position) { if (predungeon[position.x - 1][position.y] == 'D') return; if (predungeon[position.x + 1][position.y] == 'D') return; if (predungeon[position.x][position.y - 1] == 'D') return; if (predungeon[position.x][position.y + 1] == 'D') return; if (IsAnyOf(predungeon[position.x][position.y], 'A', 'B', 'C', 'E')) return; predungeon[position.x][position.y] = 'D'; } void PlaceHallExt(Point position) { if (predungeon[position.x][position.y] == ' ') predungeon[position.x][position.y] = ','; } /** * Draws a random room rectangle, and then subdivides the rest of the passed in rectangle into 4 and recurses. * @param topLeft Lower X and Y boundaries of the area to draw into. * @param bottomRight Upper X and Y boundaries of the area to draw into. * @param nRDest The room number of the parent room this call was invoked for. Zero for empty * @param nHDir The direction of the hall from nRDest to this room. * @param size If set, is is used used for room size instead of random values. */ void CreateRoom(WorldTilePosition topLeft, WorldTilePosition bottomRight, int nRDest, HallDirection nHDir, std::optional size) { constexpr int AreaMin = 2; if (nRoomCnt >= 80 || topLeft.x + AreaMin > bottomRight.x || topLeft.y + AreaMin > bottomRight.y) return; const WorldTileDisplacement areaDisplacement = bottomRight - topLeft; const WorldTileSize area(areaDisplacement.deltaX, areaDisplacement.deltaY); constexpr WorldTileCoord RoomMax = 10; constexpr WorldTileCoord RoomMin = 4; WorldTileSize roomSize = area; if (area.width > RoomMin) roomSize.width = GenerateRnd(std::min(area.width, RoomMax) - RoomMin) + RoomMin; if (area.height > RoomMin) roomSize.height = GenerateRnd(std::min(area.height, RoomMax) - RoomMin) + RoomMin; if (size) roomSize = *size; const int32_t randomWidth = GenerateRnd(area.width); const int32_t randomHeight = GenerateRnd(area.height); WorldTilePosition roomTopLeft = topLeft + WorldTileDisplacement(randomWidth, randomHeight); WorldTilePosition roomBottomRight = roomTopLeft + WorldTileDisplacement(roomSize); if (roomBottomRight.x > bottomRight.x) { roomBottomRight.x = bottomRight.x; roomTopLeft.x = bottomRight.x - roomSize.width; } if (roomBottomRight.y > bottomRight.y) { roomBottomRight.y = bottomRight.y; roomTopLeft.y = bottomRight.y - roomSize.height; } roomTopLeft.x = std::clamp(roomTopLeft.x, 1, 38); roomTopLeft.y = std::clamp(roomTopLeft.y, 1, 38); roomBottomRight.x = std::clamp(roomBottomRight.x, 1, 38); roomBottomRight.y = std::clamp(roomBottomRight.y, 1, 38); DefineRoom(roomTopLeft, roomBottomRight, static_cast(size)); constexpr WorldTileDisplacement standoff { 2, 2 }; if (size) SetPieceRoom = { roomTopLeft + standoff, roomSize - 1 }; const int nRid = nRoomCnt; if (nRDest != 0) { WorldTileCoord nHx1 = 0; WorldTileCoord nHy1 = 0; WorldTileCoord nHx2 = 0; WorldTileCoord nHy2 = 0; if (nHDir == HallDirection::Up) { nHx1 = GenerateRnd(roomSize.width - 2) + roomTopLeft.x + 1; nHy1 = roomTopLeft.y; const int nHw = RoomList[nRDest].bottomRight.x - RoomList[nRDest].topLeft.x - 2; nHx2 = GenerateRnd(nHw) + RoomList[nRDest].topLeft.x + 1; nHy2 = RoomList[nRDest].bottomRight.y; } if (nHDir == HallDirection::Down) { nHx1 = GenerateRnd(roomSize.width - 2) + roomTopLeft.x + 1; nHy1 = roomBottomRight.y; const int nHw = RoomList[nRDest].bottomRight.x - RoomList[nRDest].topLeft.x - 2; nHx2 = GenerateRnd(nHw) + RoomList[nRDest].topLeft.x + 1; nHy2 = RoomList[nRDest].topLeft.y; } if (nHDir == HallDirection::Right) { nHx1 = roomBottomRight.x; nHy1 = GenerateRnd(roomSize.height - 2) + roomTopLeft.y + 1; nHx2 = RoomList[nRDest].topLeft.x; const int nHh = RoomList[nRDest].bottomRight.y - RoomList[nRDest].topLeft.y - 2; nHy2 = GenerateRnd(nHh) + RoomList[nRDest].topLeft.y + 1; } if (nHDir == HallDirection::Left) { nHx1 = roomTopLeft.x; nHy1 = GenerateRnd(roomSize.height - 2) + roomTopLeft.y + 1; nHx2 = RoomList[nRDest].bottomRight.x; const int nHh = RoomList[nRDest].bottomRight.y - RoomList[nRDest].topLeft.y - 2; nHy2 = GenerateRnd(nHh) + RoomList[nRDest].topLeft.y + 1; } HallList.push_back({ { nHx1, nHy1 }, { nHx2, nHy2 }, nHDir }); } const WorldTilePosition roomBottomLeft { roomTopLeft.x, roomBottomRight.y }; const WorldTilePosition roomTopRight { roomBottomRight.x, roomTopLeft.y }; if (roomSize.height > roomSize.width) { CreateRoom(topLeft + standoff, roomBottomLeft - standoff, nRid, HallDirection::Right, {}); CreateRoom(roomTopRight + standoff, bottomRight - standoff, nRid, HallDirection::Left, {}); CreateRoom(WorldTilePosition { topLeft.x, roomBottomRight.y } + standoff, WorldTilePosition { roomBottomRight.x, bottomRight.y } - standoff, nRid, HallDirection::Up, {}); CreateRoom(WorldTilePosition { roomTopLeft.x, topLeft.y } + standoff, WorldTilePosition { bottomRight.x, roomTopLeft.y } - standoff, nRid, HallDirection::Down, {}); } else { CreateRoom(topLeft + standoff, roomTopRight - standoff, nRid, HallDirection::Down, {}); CreateRoom(roomBottomLeft + standoff, bottomRight - standoff, nRid, HallDirection::Up, {}); CreateRoom(WorldTilePosition { topLeft.x, roomTopLeft.y } + standoff, WorldTilePosition { roomTopLeft.x, bottomRight.y } - standoff, nRid, HallDirection::Right, {}); CreateRoom(WorldTilePosition { roomBottomRight.x, topLeft.y } + standoff, WorldTilePosition { bottomRight.x, roomBottomRight.y } - standoff, nRid, HallDirection::Left, {}); } } void ConnectHall(const HallNode &node) { Point beginning = node.beginning; Point end = node.end; const bool fMinusFlag = GenerateRnd(100) < 50; const bool fPlusFlag = GenerateRnd(100) < 50; CreateDoorType(beginning); CreateDoorType(end); HallDirection nCurrd = node.direction; end -= DirAdd[static_cast(nCurrd)]; predungeon[end.x][end.y] = ','; bool fInroom = false; do { if (beginning.x >= 38 && nCurrd == HallDirection::Right) nCurrd = HallDirection::Left; if (beginning.y >= 38 && nCurrd == HallDirection::Down) nCurrd = HallDirection::Up; if (beginning.x <= 1 && nCurrd == HallDirection::Left) nCurrd = HallDirection::Right; if (beginning.y <= 1 && nCurrd == HallDirection::Up) nCurrd = HallDirection::Down; if (predungeon[beginning.x][beginning.y] == 'C' && IsAnyOf(nCurrd, HallDirection::Up, HallDirection::Left)) nCurrd = HallDirection::Right; if (predungeon[beginning.x][beginning.y] == 'B' && IsAnyOf(nCurrd, HallDirection::Up, HallDirection::Right)) nCurrd = HallDirection::Down; if (predungeon[beginning.x][beginning.y] == 'E' && IsAnyOf(nCurrd, HallDirection::Left, HallDirection::Down)) nCurrd = HallDirection::Up; if (predungeon[beginning.x][beginning.y] == 'A' && IsAnyOf(nCurrd, HallDirection::Right, HallDirection::Down)) nCurrd = HallDirection::Left; beginning += DirAdd[static_cast(nCurrd)]; if (predungeon[beginning.x][beginning.y] == ' ') { if (fInroom) { CreateDoorType(beginning - DirAdd[static_cast(nCurrd)]); fInroom = false; } else { if (fMinusFlag) { if (IsNoneOf(nCurrd, HallDirection::Up, HallDirection::Down)) PlaceHallExt(beginning + Displacement { 0, -1 }); // Up else PlaceHallExt(beginning + Displacement { -1, 0 }); // Left } if (fPlusFlag) { if (IsNoneOf(nCurrd, HallDirection::Up, HallDirection::Down)) PlaceHallExt(beginning + Displacement { 0, 1 }); // Down else PlaceHallExt(beginning + Displacement { 1, 0 }); // Right } } predungeon[beginning.x][beginning.y] = ','; } else { if (!fInroom && predungeon[beginning.x][beginning.y] == '#') CreateDoorType(beginning); if (predungeon[beginning.x][beginning.y] != ',') fInroom = true; } const int nDx = std::abs(end.x - beginning.x); const int nDy = std::abs(end.y - beginning.y); if (nDx > nDy) { const int nRp = std::min(2 * nDx, 30); if (GenerateRnd(100) < nRp) { if (end.x <= beginning.x || beginning.x >= DMAXX) nCurrd = HallDirection::Left; else nCurrd = HallDirection::Right; } } else { const int nRp = std::min(5 * nDy, 80); if (GenerateRnd(100) < nRp) { if (end.y <= beginning.y || beginning.y >= DMAXY) nCurrd = HallDirection::Up; else nCurrd = HallDirection::Down; } } if (nDy < 10 && beginning.x == end.x && IsAnyOf(nCurrd, HallDirection::Right, HallDirection::Left)) { if (end.y <= beginning.y || beginning.y >= DMAXY) nCurrd = HallDirection::Up; else nCurrd = HallDirection::Down; } if (nDx < 10 && beginning.y == end.y && IsAnyOf(nCurrd, HallDirection::Up, HallDirection::Down)) { if (end.x <= beginning.x || beginning.x >= DMAXX) nCurrd = HallDirection::Left; else nCurrd = HallDirection::Right; } if (nDy == 1 && nDx > 1 && IsAnyOf(nCurrd, HallDirection::Up, HallDirection::Down)) { if (end.x <= beginning.x || beginning.x >= DMAXX) nCurrd = HallDirection::Left; else nCurrd = HallDirection::Right; } if (nDx == 1 && nDy > 1 && IsAnyOf(nCurrd, HallDirection::Right, HallDirection::Left)) { if (end.y <= beginning.y || beginning.x >= DMAXX) nCurrd = HallDirection::Up; else nCurrd = HallDirection::Down; } if (nDx == 0 && predungeon[beginning.x][beginning.y] != ' ' && IsAnyOf(nCurrd, HallDirection::Right, HallDirection::Left)) { if (end.x <= node.beginning.x || beginning.x >= DMAXX) nCurrd = HallDirection::Up; else nCurrd = HallDirection::Down; } if (nDy == 0 && predungeon[beginning.x][beginning.y] != ' ' && IsAnyOf(nCurrd, HallDirection::Up, HallDirection::Down)) { if (end.y <= node.beginning.y || beginning.y >= DMAXY) nCurrd = HallDirection::Left; else nCurrd = HallDirection::Right; } } while (beginning != end); } void DoPatternCheck(int i, int j) { for (int k = 0; Patterns[k][4] != 255; k++) { int x = i - 1; int y = j - 1; int nOk = 254; for (int l = 0; l < 9 && nOk == 254; l++) { nOk = 255; if (l == 3 || l == 6) { y++; x = i - 1; } if (x >= 0 && x < DMAXX && y >= 0 && y < DMAXY) { switch (Patterns[k][l]) { case 0: nOk = 254; break; case 1: if (predungeon[x][y] == '#') { nOk = 254; } break; case 2: if (predungeon[x][y] == '.') { nOk = 254; } break; case 4: if (predungeon[x][y] == ' ') { nOk = 254; } break; case 3: if (predungeon[x][y] == 'D') { nOk = 254; } break; case 5: if (predungeon[x][y] == 'D' || predungeon[x][y] == '.') { nOk = 254; } break; case 6: if (predungeon[x][y] == 'D' || predungeon[x][y] == '#') { nOk = 254; } break; case 7: if (predungeon[x][y] == ' ' || predungeon[x][y] == '.') { nOk = 254; } break; case 8: if (predungeon[x][y] == 'D' || predungeon[x][y] == '#' || predungeon[x][y] == '.') { nOk = 254; } break; } } else { nOk = 254; } x++; } if (nOk == 254) { dungeon[i][j] = Patterns[k][9]; } } } void FixTilesPatterns() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 3) { dungeon[i][j + 1] = 1; } if (dungeon[i][j] == 3 && dungeon[i][j + 1] == 1) { dungeon[i][j + 1] = 3; } if (dungeon[i][j] == 3 && dungeon[i + 1][j] == 7) { dungeon[i + 1][j] = 3; } if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 3) { dungeon[i + 1][j] = 2; } if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 14) { dungeon[i + 1][j] = 16; } if (dungeon[i][j] == 15 && dungeon[i][j + 1] == 1) { dungeon[i][j + 1] = 8; } } } } void Substitution() { for (WorldTileCoord y = 0; y < DMAXY; y++) { for (WorldTileCoord x = 0; x < DMAXX; x++) { if (SetPieceRoom.contains(x, y)) continue; if (!FlipCoin(4)) continue; const uint8_t c = BTYPESL2[dungeon[x][y]]; if (c != 0) { int rv = GenerateRnd(16); int i = -1; while (rv >= 0) { i++; if (i == sizeof(BTYPESL2)) { i = 0; } if (c == BTYPESL2[i]) { rv--; } } int j; for (j = y - 2; j < y + 2; j++) { for (int k = x - 2; k < x + 2; k++) { if (dungeon[k][j] == i) { j = y + 3; k = x + 2; } } } if (j < y + 3) { dungeon[x][y] = i; } } } } } int CountEmptyTiles() { int t = 0; for (int jj = 0; jj < DMAXY; jj++) { for (int ii = 0; ii < DMAXX; ii++) { // NOLINT(modernize-loop-convert) if (predungeon[ii][jj] == ' ') { t++; } } } return t; } void KnockWalls(int x1, int y1, int x2, int y2) { for (int ii = x1 + 1; ii < x2; ii++) { if (predungeon[ii][y1 - 1] == '.' && predungeon[ii][y1 + 1] == '.') { predungeon[ii][y1] = '.'; } if (predungeon[ii][y2 - 1] == '.' && predungeon[ii][y2 + 1] == '.') { predungeon[ii][y2] = '.'; } if (predungeon[ii][y1 - 1] == 'D') { predungeon[ii][y1 - 1] = '.'; } if (predungeon[ii][y2 + 1] == 'D') { predungeon[ii][y2 + 1] = '.'; } } for (int jj = y1 + 1; jj < y2; jj++) { if (predungeon[x1 - 1][jj] == '.' && predungeon[x1 + 1][jj] == '.') { predungeon[x1][jj] = '.'; } if (predungeon[x2 - 1][jj] == '.' && predungeon[x2 + 1][jj] == '.') { predungeon[x2][jj] = '.'; } if (predungeon[x1 - 1][jj] == 'D') { predungeon[x1 - 1][jj] = '.'; } if (predungeon[x2 + 1][jj] == 'D') { predungeon[x2 + 1][jj] = '.'; } } } void FillVoid(bool xf1, bool yf1, bool xf2, bool yf2, int xx, int yy) { int x1 = xx; if (xf1) { x1--; } int x2 = xx; if (xf2) { x2++; } int y1 = yy; if (yf1) { y1--; } int y2 = yy; if (yf2) { y2++; } if (!xf1) { while (yf1 || yf2) { if (y1 == 0) { yf1 = false; } if (y2 == DMAXY - 1) { yf2 = false; } if (y2 - y1 >= 14) { yf1 = false; yf2 = false; } if (yf1) { y1--; } if (yf2) { y2++; } if (predungeon[x2][y1] != ' ') { yf1 = false; } if (predungeon[x2][y2] != ' ') { yf2 = false; } } y1 += 2; y2 -= 2; if (y2 - y1 > 5) { while (xf2) { if (x2 == 39) { xf2 = false; } if (x2 - x1 >= 12) { xf2 = false; } for (int jj = y1; jj <= y2; jj++) { if (predungeon[x2][jj] != ' ') { xf2 = false; } } if (xf2) { x2++; } } x2 -= 2; if (x2 - x1 > 5) { MapRoom(x1, y1, x2, y2); KnockWalls(x1, y1, x2, y2); } } } else if (!xf2) { while (yf1 || yf2) { if (y1 == 0) { yf1 = false; } if (y2 == DMAXY - 1) { yf2 = false; } if (y2 - y1 >= 14) { yf1 = false; yf2 = false; } if (yf1) { y1--; } if (yf2) { y2++; } if (predungeon[x1][y1] != ' ') { yf1 = false; } if (predungeon[x1][y2] != ' ') { yf2 = false; } } y1 += 2; y2 -= 2; if (y2 - y1 > 5) { while (xf1) { if (x1 == 0) { xf1 = false; } if (x2 - x1 >= 12) { xf1 = false; } for (int jj = y1; jj <= y2; jj++) { if (predungeon[x1][jj] != ' ') { xf1 = false; } } if (xf1) { x1--; } } x1 += 2; if (x2 - x1 > 5) { MapRoom(x1, y1, x2, y2); KnockWalls(x1, y1, x2, y2); } } } else if (!yf1) { while (xf1 || xf2) { if (x1 == 0) { xf1 = false; } if (x2 == DMAXX - 1) { xf2 = false; } if (x2 - x1 >= 14) { xf1 = false; xf2 = false; } if (xf1) { x1--; } if (xf2) { x2++; } if (predungeon[x1][y2] != ' ') { xf1 = false; } if (predungeon[x2][y2] != ' ') { xf2 = false; } } x1 += 2; x2 -= 2; if (x2 - x1 > 5) { while (yf2) { if (y2 == DMAXY - 1) { yf2 = false; } if (y2 - y1 >= 12) { yf2 = false; } for (int ii = x1; ii <= x2; ii++) { if (predungeon[ii][y2] != ' ') { yf2 = false; } } if (yf2) { y2++; } } y2 -= 2; if (y2 - y1 > 5) { MapRoom(x1, y1, x2, y2); KnockWalls(x1, y1, x2, y2); } } } else if (!yf2) { while (xf1 || xf2) { if (x1 == 0) { xf1 = false; } if (x2 == DMAXX - 1) { xf2 = false; } if (x2 - x1 >= 14) { xf1 = false; xf2 = false; } if (xf1) { x1--; } if (xf2) { x2++; } if (predungeon[x1][y1] != ' ') { xf1 = false; } if (predungeon[x2][y1] != ' ') { xf2 = false; } } x1 += 2; x2 -= 2; if (x2 - x1 > 5) { while (yf1) { if (y1 == 0) { yf1 = false; } if (y2 - y1 >= 12) { yf1 = false; } for (int ii = x1; ii <= x2; ii++) { if (predungeon[ii][y1] != ' ') { yf1 = false; } } if (yf1) { y1--; } } y1 += 2; if (y2 - y1 > 5) { MapRoom(x1, y1, x2, y2); KnockWalls(x1, y1, x2, y2); } } } } bool FillVoids() { int to = 0; while (CountEmptyTiles() > 700 && to < 100) { const int xx = GenerateRnd(38) + 1; const int yy = GenerateRnd(38) + 1; if (predungeon[xx][yy] != '#') { continue; } bool xf1 = false; bool xf2 = false; bool yf1 = false; bool yf2 = false; if (predungeon[xx - 1][yy] == ' ' && predungeon[xx + 1][yy] == '.') { if (predungeon[xx + 1][yy - 1] == '.' && predungeon[xx + 1][yy + 1] == '.' && predungeon[xx - 1][yy - 1] == ' ' && predungeon[xx - 1][yy + 1] == ' ') { xf1 = true; yf1 = true; yf2 = true; } } else if (predungeon[xx + 1][yy] == ' ' && predungeon[xx - 1][yy] == '.') { if (predungeon[xx - 1][yy - 1] == '.' && predungeon[xx - 1][yy + 1] == '.' && predungeon[xx + 1][yy - 1] == ' ' && predungeon[xx + 1][yy + 1] == ' ') { xf2 = true; yf1 = true; yf2 = true; } } else if (predungeon[xx][yy - 1] == ' ' && predungeon[xx][yy + 1] == '.') { if (predungeon[xx - 1][yy + 1] == '.' && predungeon[xx + 1][yy + 1] == '.' && predungeon[xx - 1][yy - 1] == ' ' && predungeon[xx + 1][yy - 1] == ' ') { yf1 = true; xf1 = true; xf2 = true; } } else if (predungeon[xx][yy + 1] == ' ' && predungeon[xx][yy - 1] == '.') { if (predungeon[xx - 1][yy - 1] == '.' && predungeon[xx + 1][yy - 1] == '.' && predungeon[xx - 1][yy + 1] == ' ' && predungeon[xx + 1][yy + 1] == ' ') { yf2 = true; xf1 = true; xf2 = true; } } if (xf1 || yf1 || xf2 || yf2) { FillVoid(xf1, yf1, xf2, yf2, xx, yy); } to++; } return CountEmptyTiles() <= 700; } bool CreateDungeon() { std::optional size; switch (currlevel) { case 5: if (Quests[Q_BLOOD]._qactive != QUEST_NOTAVAIL) size = { 14, 20 }; break; case 6: if (Quests[Q_SCHAMB]._qactive != QUEST_NOTAVAIL) size = { 10, 10 }; break; case 7: if (Quests[Q_BLIND]._qactive != QUEST_NOTAVAIL) size = { 15, 15 }; break; case 8: break; } CreateRoom({ 2, 2 }, { DMAXX - 1, DMAXY - 1 }, 0, HallDirection::None, size); while (!HallList.empty()) { ConnectHall(HallList.front()); HallList.pop_front(); } for (int j = 0; j < DMAXY; j++) { /// BUGFIX: change '<=' to '<' (fixed) for (int i = 0; i < DMAXX; i++) { /// BUGFIX: change '<=' to '<' (fixed) if (IsAnyOf(predungeon[i][j], 'A', 'B', 'C', 'E')) { predungeon[i][j] = '#'; } if (predungeon[i][j] == ',') { predungeon[i][j] = '.'; for (int a = -1; a <= 1; a++) { for (int b = -1; b <= 1; b++) { if (a == 0 && b == 0) continue; if (i + a < 0 || j + b < 0) continue; if (i + a >= DMAXX || j + b >= DMAXY) continue; if (predungeon[i + a][j + b] == ' ') { predungeon[i + a][j + b] = '#'; } } } } } } if (!FillVoids()) { return false; } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { DoPatternCheck(i, j); } } return true; } void FixTransparency() { int yy = 16; for (int j = 0; j < DMAXY; j++) { int xx = 16; for (int i = 0; i < DMAXX; i++) { // BUGFIX: Should check for `j > 0` first. if (dungeon[i][j] == 14 && dungeon[i][j - 1] == 10) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } // BUGFIX: Should check for `i + 1 < DMAXY` first. if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 11) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 10) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 11) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 16) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } xx += 2; } yy += 2; } } void FixDirtTiles() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 13 && dungeon[i + 1][j] != 11) { dungeon[i][j] = 146; } if (dungeon[i][j] == 11 && dungeon[i + 1][j] != 11) { dungeon[i][j] = 144; } if (dungeon[i][j] == 15 && dungeon[i + 1][j] != 11) { dungeon[i][j] = 148; } if (dungeon[i][j] == 10 && dungeon[i][j + 1] != 10) { dungeon[i][j] = 143; } if (dungeon[i][j] == 13 && dungeon[i][j + 1] != 10) { dungeon[i][j] = 146; } if (dungeon[i][j] == 14 && dungeon[i][j + 1] != 15) { dungeon[i][j] = 147; } } } } void FixLockout() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 4 && dungeon[i - 1][j] != 3) { dungeon[i][j] = 1; } if (dungeon[i][j] == 5 && dungeon[i][j - 1] != 3) { dungeon[i][j] = 2; } } } for (int j = 1; j < DMAXY - 1; j++) { for (int i = 1; i < DMAXX - 1; i++) { if (Protected.test(i, j)) { continue; } if ((dungeon[i][j] == 2 || dungeon[i][j] == 5) && dungeon[i][j - 1] == 3 && dungeon[i][j + 1] == 3) { bool doorok = false; while (true) { if (dungeon[i][j] != 2 && dungeon[i][j] != 5) { break; } if (dungeon[i][j - 1] != 3 || dungeon[i][j + 1] != 3) { break; } if (dungeon[i][j] == 5) { doorok = true; } i++; } if (!doorok && !Protected.test(i - 1, j)) { dungeon[i - 1][j] = 5; } } } } for (int j = 1; j < DMAXX - 1; j++) { /* check: might be flipped */ for (int i = 1; i < DMAXY - 1; i++) { if (Protected.test(j, i)) { continue; } if ((dungeon[j][i] == 1 || dungeon[j][i] == 4) && dungeon[j - 1][i] == 3 && dungeon[j + 1][i] == 3) { bool doorok = false; while (true) { if (dungeon[j][i] != 1 && dungeon[j][i] != 4) { break; } if (dungeon[j - 1][i] != 3 || dungeon[j + 1][i] != 3) { break; } if (dungeon[j][i] == 4) { doorok = true; } i++; } if (!doorok && !Protected.test(j, i - 1)) { dungeon[j][i - 1] = 4; } } } } } void FixDoors() { for (int j = 1; j < DMAXY; j++) { for (int i = 1; i < DMAXX; i++) { if (dungeon[i][j] == 4 && dungeon[i][j - 1] == 3) { dungeon[i][j] = 7; } if (dungeon[i][j] == 5 && dungeon[i - 1][j] == 3) { dungeon[i][j] = 9; } } } } bool PlaceStairs(lvl_entry entry) { std::optional position; // Place stairs up position = PlaceMiniSet(USTAIRS); if (!position) return false; if (entry == ENTRY_MAIN) ViewPosition = position->megaToWorld() + Displacement { 5, 4 }; // Place stairs down position = PlaceMiniSet(DSTAIRS); if (!position) return false; if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 4, 6 }; // Place town warp stairs if (currlevel == 5) { position = PlaceMiniSet(WARPSTAIRS); if (!position) return false; if (entry == ENTRY_TWARPDN) ViewPosition = position->megaToWorld() + Displacement { 5, 4 }; } return true; } void GenerateLevel(lvl_entry entry) { if (LevelSeeds[currlevel]) SetRndSeed(*LevelSeeds[currlevel]); while (true) { LevelSeeds[currlevel] = GetLCGEngineState(); nRoomCnt = 0; InitDungeonFlags(); DRLG_InitTrans(); if (!CreateDungeon()) { continue; } FixTilesPatterns(); InitSetPiece(); FloodTransparencyValues(3); FixTransparency(); if (PlaceStairs(entry)) break; } FixLockout(); FixDoors(); FixDirtTiles(); DRLG_PlaceThemeRooms(6, 10, 3, 0, false); PlaceMiniSetRandom(CTRDOOR1, 100); PlaceMiniSetRandom(CTRDOOR2, 100); PlaceMiniSetRandom(CTRDOOR3, 100); PlaceMiniSetRandom(CTRDOOR4, 100); PlaceMiniSetRandom(CTRDOOR5, 100); PlaceMiniSetRandom(CTRDOOR6, 100); PlaceMiniSetRandom(CTRDOOR7, 100); PlaceMiniSetRandom(CTRDOOR8, 100); PlaceMiniSetRandom(VARCH33, 100); PlaceMiniSetRandom(VARCH34, 100); PlaceMiniSetRandom(VARCH35, 100); PlaceMiniSetRandom(VARCH36, 100); PlaceMiniSetRandom(VARCH37, 100); PlaceMiniSetRandom(VARCH38, 100); PlaceMiniSetRandom(VARCH39, 100); PlaceMiniSetRandom(VARCH40, 100); PlaceMiniSetRandom(VARCH1, 100); PlaceMiniSetRandom(VARCH2, 100); PlaceMiniSetRandom(VARCH3, 100); PlaceMiniSetRandom(VARCH4, 100); PlaceMiniSetRandom(VARCH5, 100); PlaceMiniSetRandom(VARCH6, 100); PlaceMiniSetRandom(VARCH7, 100); PlaceMiniSetRandom(VARCH8, 100); PlaceMiniSetRandom(VARCH9, 100); PlaceMiniSetRandom(VARCH10, 100); PlaceMiniSetRandom(VARCH11, 100); PlaceMiniSetRandom(VARCH12, 100); PlaceMiniSetRandom(VARCH13, 100); PlaceMiniSetRandom(VARCH14, 100); PlaceMiniSetRandom(VARCH15, 100); PlaceMiniSetRandom(VARCH16, 100); PlaceMiniSetRandom(VARCH17, 100); PlaceMiniSetRandom(VARCH18, 100); PlaceMiniSetRandom(VARCH19, 100); PlaceMiniSetRandom(VARCH20, 100); PlaceMiniSetRandom(VARCH21, 100); PlaceMiniSetRandom(VARCH22, 100); PlaceMiniSetRandom(VARCH23, 100); PlaceMiniSetRandom(VARCH24, 100); PlaceMiniSetRandom(VARCH25, 100); PlaceMiniSetRandom(VARCH26, 100); PlaceMiniSetRandom(VARCH27, 100); PlaceMiniSetRandom(VARCH28, 100); PlaceMiniSetRandom(VARCH29, 100); PlaceMiniSetRandom(VARCH30, 100); PlaceMiniSetRandom(VARCH31, 100); PlaceMiniSetRandom(VARCH32, 100); PlaceMiniSetRandom(HARCH1, 100); PlaceMiniSetRandom(HARCH2, 100); PlaceMiniSetRandom(HARCH3, 100); PlaceMiniSetRandom(HARCH4, 100); PlaceMiniSetRandom(HARCH5, 100); PlaceMiniSetRandom(HARCH6, 100); PlaceMiniSetRandom(HARCH7, 100); PlaceMiniSetRandom(HARCH8, 100); PlaceMiniSetRandom(HARCH9, 100); PlaceMiniSetRandom(HARCH10, 100); PlaceMiniSetRandom(HARCH11, 100); PlaceMiniSetRandom(HARCH12, 100); PlaceMiniSetRandom(HARCH13, 100); PlaceMiniSetRandom(HARCH14, 100); PlaceMiniSetRandom(HARCH15, 100); PlaceMiniSetRandom(HARCH16, 100); PlaceMiniSetRandom(HARCH17, 100); PlaceMiniSetRandom(HARCH18, 100); PlaceMiniSetRandom(HARCH19, 100); PlaceMiniSetRandom(HARCH20, 100); PlaceMiniSetRandom(HARCH21, 100); PlaceMiniSetRandom(HARCH22, 100); PlaceMiniSetRandom(HARCH23, 100); PlaceMiniSetRandom(HARCH24, 100); PlaceMiniSetRandom(HARCH25, 100); PlaceMiniSetRandom(HARCH26, 100); PlaceMiniSetRandom(HARCH27, 100); PlaceMiniSetRandom(HARCH28, 100); PlaceMiniSetRandom(HARCH29, 100); PlaceMiniSetRandom(HARCH30, 100); PlaceMiniSetRandom(HARCH31, 100); PlaceMiniSetRandom(HARCH32, 100); PlaceMiniSetRandom(HARCH33, 100); PlaceMiniSetRandom(HARCH34, 100); PlaceMiniSetRandom(HARCH35, 100); PlaceMiniSetRandom(HARCH36, 100); PlaceMiniSetRandom(HARCH37, 100); PlaceMiniSetRandom(HARCH38, 100); PlaceMiniSetRandom(HARCH39, 100); PlaceMiniSetRandom(HARCH40, 100); PlaceMiniSetRandom(CRUSHCOL, 99); PlaceMiniSetRandom1x1(1, 80, 10); PlaceMiniSetRandom1x1(1, 81, 10); PlaceMiniSetRandom1x1(1, 82, 10); PlaceMiniSetRandom1x1(2, 84, 10); PlaceMiniSetRandom1x1(2, 85, 10); PlaceMiniSetRandom1x1(2, 86, 10); PlaceMiniSetRandom1x1(8, 87, 50); PlaceMiniSetRandom(PANCREAS1, 1); PlaceMiniSetRandom(PANCREAS2, 1); PlaceMiniSetRandom(BIG1, 3); PlaceMiniSetRandom(BIG2, 3); PlaceMiniSetRandom(BIG3, 3); PlaceMiniSetRandom(BIG4, 3); PlaceMiniSetRandom(BIG5, 3); PlaceMiniSetRandom(BIG6, 20); PlaceMiniSetRandom(BIG7, 20); PlaceMiniSetRandom(BIG8, 3); PlaceMiniSetRandom(BIG9, 20); PlaceMiniSetRandom(BIG10, 20); Substitution(); ApplyShadowsPatterns(); memcpy(pdungeon, dungeon, sizeof(pdungeon)); DRLG_CheckQuests(SetPieceRoom.position); } void Pass3() { DRLG_LPass3(12 - 1); InitDungeonPieces(); } } // namespace void CreateL2Dungeon(uint32_t rseed, lvl_entry entry) { SetRndSeed(rseed); GenerateLevel(entry); Pass3(); } void LoadPreL2Dungeon(const char *path) { memset(dungeon, 12, sizeof(dungeon)); auto dunData = LoadFileInMem(path); PlaceDunTiles(dunData.get(), { 0, 0 }, 3); memcpy(pdungeon, dungeon, sizeof(pdungeon)); } void LoadL2Dungeon(const char *path, Point spawn) { LoadDungeonBase(path, spawn, 3, 12); Pass3(); AddL2Objs(0, 0, MAXDUNX, MAXDUNY); } } // namespace devilution ================================================ FILE: Source/levels/drlg_l2.h ================================================ /** * @file levels/drlg_l2.h * * Interface of the catacombs level generation algorithms. */ #pragma once #include #include "levels/gendung.h" namespace devilution { void CreateL2Dungeon(uint32_t rseed, lvl_entry entry); void LoadPreL2Dungeon(const char *path); void LoadL2Dungeon(const char *path, Point spawn); } // namespace devilution ================================================ FILE: Source/levels/drlg_l3.cpp ================================================ #include "levels/drlg_l3.h" #include #include #include "engine/load_file.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "levels/gendung.h" #include "levels/setmaps.h" #include "lighting.h" #include "monster.h" #include "objects.h" #include "player.h" #include "quests.h" #include "tables/objdat.h" #include "utils/is_of.hpp" namespace devilution { namespace { int lockoutcnt; /** * A lookup table for the 16 possible patterns of a 2x2 area, * where each cell either contains a SW wall or it doesn't. */ const uint8_t L3ConvTbl[16] = { 8, 11, 3, 10, 1, 9, 12, 12, 6, 13, 4, 13, 2, 14, 5, 7 }; /** Miniset: Stairs up. */ const Miniset L3UP { { 3, 3 }, { { 8, 8, 0 }, { 10, 10, 0 }, { 7, 7, 0 }, }, { { 51, 50, 0 }, { 48, 49, 0 }, { 0, 0, 0 }, } }; const Miniset L6UP { { 3, 3 }, { { 8, 8, 0 }, { 10, 10, 0 }, { 7, 7, 0 }, }, { { 20, 19, 0 }, { 17, 18, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stairs down. */ const Miniset L3DOWN { { 3, 3 }, { { 8, 9, 7 }, { 8, 9, 7 }, { 0, 0, 0 }, }, { { 0, 47, 0 }, { 0, 46, 0 }, { 0, 0, 0 }, } }; const Miniset L6DOWN { { 3, 3 }, { { 8, 9, 7 }, { 8, 9, 7 }, { 0, 0, 0 }, }, { { 0, 16, 0 }, { 0, 15, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stairs up to town. */ const Miniset L3HOLDWARP { { 3, 3 }, { { 8, 8, 0 }, { 10, 10, 0 }, { 7, 7, 0 }, }, { { 125, 125, 0 }, { 125, 125, 0 }, { 0, 0, 0 }, } }; const Miniset L6HOLDWARP { { 3, 3 }, { { 8, 8, 0 }, { 10, 10, 0 }, { 7, 7, 0 }, }, { { 24, 23, 0 }, { 21, 22, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stalagmite white stalactite 1. */ const Miniset L3TITE1 { { 4, 4 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 0, 0, 0, 0 }, { 0, 57, 58, 0 }, { 0, 56, 55, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stalagmite white stalactite 2. */ const Miniset L3TITE2 { { 4, 4 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 0, 0, 0, 0 }, { 0, 61, 62, 0 }, { 0, 60, 59, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stalagmite white stalactite 3. */ const Miniset L3TITE3 { { 4, 4 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 0, 0, 0, 0 }, { 0, 65, 66, 0 }, { 0, 64, 63, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stalagmite white stalactite horizontal. */ const Miniset L3TITE6 { { 5, 4 }, { { 7, 7, 7, 7, 7 }, { 7, 7, 7, 0, 7 }, { 7, 7, 7, 0, 7 }, { 7, 7, 7, 7, 7 }, }, { { 0, 0, 0, 0, 0 }, { 0, 77, 78, 0, 0 }, { 0, 76, 74, 75, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Miniset: Stalagmite white stalactite vertical. */ const Miniset L3TITE7 { { 4, 5 }, { { 7, 7, 7, 7 }, { 7, 7, 0, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 0, 0, 0, 0 }, { 0, 83, 0, 0 }, { 0, 82, 80, 0 }, { 0, 81, 79, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stalagmite 1. */ const Miniset L3TITE8 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 52, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stalagmite 2. */ const Miniset L3TITE9 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 53, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stalagmite 3. */ const Miniset L3TITE10 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 54, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stalagmite 4. */ const Miniset L3TITE11 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 67, 0 }, { 0, 0, 0 }, } }; /** Miniset: Stalagmite on vertical wall. */ const Miniset L3TITE12 { { 2, 1 }, { { 9, 7 } }, { { 68, 0 } } }; /** Miniset: Stalagmite on horizontal wall. */ const Miniset L3TITE13 { { 1, 2 }, { { 10 }, { 7 }, }, { { 69 }, { 0 }, } }; /** Miniset: Cracked vertical wall 1. */ const Miniset L3CREV1 { { 2, 1 }, { { 8, 7 } }, { { 84, 85 } } }; /** Miniset: Cracked vertical wall - north corner. */ const Miniset L3CREV2 { { 2, 1 }, { { 8, 11 } }, { { 86, 87 } } }; /** Miniset: Cracked horizontal wall 1. */ const Miniset L3CREV3 { { 1, 2 }, { { 8 }, { 10 }, }, { { 89 }, { 88 }, } }; /** Miniset: Cracked vertical wall 2. */ const Miniset L3CREV4 { { 2, 1 }, { { 8, 7 } }, { { 90, 91 } } }; /** Miniset: Cracked horizontal wall - north corner. */ const Miniset L3CREV5 { { 1, 2 }, { { 8 }, { 11 }, }, { { 92 }, { 93 }, } }; /** Miniset: Cracked horizontal wall 2. */ const Miniset L3CREV6 { { 1, 2 }, { { 8 }, { 10 }, }, { { 95 }, { 94 }, } }; /** Miniset: Cracked vertical wall - west corner. */ const Miniset L3CREV7 { { 2, 1 }, { { 8, 7 } }, { { 96, 101 } } }; /** Miniset: Cracked horizontal wall - north. */ const Miniset L3CREV8 { { 1, 2 }, { { 2 }, { 8 }, }, { { 102 }, { 97 }, } }; /** Miniset: Cracked vertical wall - east corner. */ const Miniset L3CREV9 { { 2, 1 }, { { 3, 8 } }, { { 103, 98 } } }; /** Miniset: Cracked vertical wall - west. */ const Miniset L3CREV10 { { 2, 1 }, { { 4, 8 } }, { { 104, 99 } } }; /** Miniset: Cracked horizontal wall - south corner. */ const Miniset L3CREV11 { { 1, 2 }, { { 6 }, { 8 }, }, { { 105 }, { 100 }, } }; /** Miniset: Replace broken wall with floor 1. */ const Miniset L3ISLE1 { { 2, 3 }, { { 5, 14 }, { 4, 9 }, { 13, 12 }, }, { { 7, 7 }, { 7, 7 }, { 7, 7 }, } }; /** Miniset: Replace small wall with floor 2. */ const Miniset L3ISLE2 { { 3, 2 }, { { 5, 2, 14 }, { 13, 10, 12 }, }, { { 7, 7, 7 }, { 7, 7, 7 }, } }; /** Miniset: Replace small wall with lava 1. */ const Miniset L3ISLE3 { { 2, 3 }, { { 5, 14 }, { 4, 9 }, { 13, 12 }, }, { { 29, 30 }, { 25, 28 }, { 31, 32 }, } }; /** Miniset: Replace small wall with lava 2. */ const Miniset L3ISLE4 { { 3, 2 }, { { 5, 2, 14 }, { 13, 10, 12 }, }, { { 29, 26, 30 }, { 31, 27, 32 }, } }; /** Miniset: Replace small wall with floor 3. */ const Miniset L3ISLE5 { { 2, 2 }, { { 5, 14 }, { 13, 12 }, }, { { 7, 7 }, { 7, 7 }, } }; const Miniset HivePattern9 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 126, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern10 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 0, 0, 0 }, { 0, 124, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern29 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 67, 0, 0 }, { 66, 51, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern30 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 69, 0, 0 }, { 68, 52, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern31 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 70, 0, 0 }, { 71, 53, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern32 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 73, 0, 0 }, { 72, 54, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern33 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 75, 0, 0 }, { 74, 55, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern34 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 77, 0, 0 }, { 76, 56, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern35 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 79, 0, 0 }, { 78, 57, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern36 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 81, 0, 0 }, { 80, 58, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern37 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 83, 0, 0 }, { 82, 59, 0 }, { 0, 0, 0 }, } }; const Miniset HivePattern38 { { 3, 3 }, { { 7, 7, 7 }, { 7, 7, 7 }, { 7, 7, 7 }, }, { { 84, 0, 0 }, { 85, 60, 0 }, { 0, 0, 0 }, } }; const Miniset L6ISLE1 { { 2, 3 }, { { 5, 14 }, { 4, 9 }, { 13, 12 }, }, { { 7, 7 }, { 7, 7 }, { 7, 7 }, } }; const Miniset L6ISLE2 { { 3, 2 }, { { 5, 2, 14 }, { 13, 10, 12 }, }, { { 7, 7, 7 }, { 7, 7, 7 }, } }; const Miniset L6ISLE3 { { 2, 3 }, { { 5, 14 }, { 4, 9 }, { 13, 12 }, }, { { 107, 115 }, { 119, 122 }, { 131, 123 }, } }; const Miniset L6ISLE4 { { 3, 2 }, { { 5, 2, 14 }, { 13, 10, 12 }, }, { { 107, 120, 115 }, { 131, 121, 123 }, } }; const Miniset L6ISLE5 { { 2, 2 }, { { 5, 14 }, { 13, 12 }, }, { { 7, 7 }, { 7, 7 }, } }; const Miniset HivePattern39 { { 4, 4 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 7, 7, 7, 7 }, { 7, 107, 115, 7 }, { 7, 131, 123, 7 }, { 7, 7, 7, 7 }, } }; const Miniset HivePattern40 { { 4, 4 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 7, 7, 7, 7 }, { 7, 7, 108, 7 }, { 7, 109, 112, 7 }, { 7, 7, 7, 7 }, } }; const Miniset HivePattern41 { { 4, 5 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 7, 7, 7, 7 }, { 7, 107, 115, 7 }, { 7, 119, 122, 7 }, { 7, 131, 123, 7 }, { 7, 7, 7, 7 }, } }; const Miniset HivePattern42 { { 4, 5 }, { { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, { 7, 7, 7, 7 }, }, { { 7, 7, 7, 7 }, { 7, 126, 108, 7 }, { 7, 7, 117, 7 }, { 7, 109, 112, 7 }, { 7, 7, 7, 7 }, } }; void InitDungeonFlags() { memset(dungeon, 0, sizeof(dungeon)); Protected.reset(); } bool FillRoom(int x1, int y1, int x2, int y2) { if (x1 <= 1 || x2 >= 34 || y1 <= 1 || y2 >= 38) { return false; } int v = 0; for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { v += dungeon[i][j]; } } if (v != 0) { return false; } for (int j = y1 + 1; j < y2; j++) { for (int i = x1 + 1; i < x2; i++) { dungeon[i][j] = 1; } } for (int j = y1; j <= y2; j++) { if (!FlipCoin()) { dungeon[x1][j] = 1; } if (!FlipCoin()) { dungeon[x2][j] = 1; } } for (int i = x1; i <= x2; i++) { if (!FlipCoin()) { dungeon[i][y1] = 1; } if (!FlipCoin()) { dungeon[i][y2] = 1; } } return true; } void CreateBlock(Point point, int obs, int dir) { int x1; int y1; int x2; int y2; const int blksizex = RandomIntBetween(3, 4); const int blksizey = RandomIntBetween(3, 4); if (dir == 0) { y2 = point.y - 1; y1 = y2 - blksizey; if (blksizex < obs) { x1 = GenerateRnd(blksizex) + point.x; } if (blksizex == obs) { x1 = point.x; } if (blksizex > obs) { x1 = point.x - GenerateRnd(blksizex); } x2 = blksizex + x1; } if (dir == 1) { x1 = point.x + 1; x2 = x1 + blksizex; if (blksizey < obs) { y1 = GenerateRnd(blksizey) + point.y; } if (blksizey == obs) { y1 = point.y; } if (blksizey > obs) { y1 = point.y - GenerateRnd(blksizey); } y2 = y1 + blksizey; } if (dir == 2) { y1 = point.y + 1; y2 = y1 + blksizey; if (blksizex < obs) { x1 = GenerateRnd(blksizex) + point.x; } if (blksizex == obs) { x1 = point.x; } if (blksizex > obs) { x1 = point.x - GenerateRnd(blksizex); } x2 = blksizex + x1; } if (dir == 3) { x2 = point.x - 1; x1 = x2 - blksizex; if (blksizey < obs) { y1 = GenerateRnd(blksizey) + point.y; } if (blksizey == obs) { y1 = point.y; } if (blksizey > obs) { y1 = point.y - GenerateRnd(blksizey); } y2 = y1 + blksizey; } if (FillRoom(x1, y1, x2, y2)) { if (FlipCoin(4)) return; if (dir != 2) { CreateBlock({ x1, y1 }, blksizey, 0); } if (dir != 3) { CreateBlock({ x2, y1 }, blksizex, 1); } if (dir != 0) { CreateBlock({ x1, y2 }, blksizey, 2); } if (dir != 1) { CreateBlock({ x1, y1 }, blksizex, 3); } } } void FloorArea(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { dungeon[i][j] = 1; } } } void FillDiagonals() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { const int v = dungeon[i + 1][j + 1] + 2 * dungeon[i][j + 1] + 4 * dungeon[i + 1][j] + 8 * dungeon[i][j]; if (v == 6) { if (FlipCoin()) { dungeon[i][j] = 1; } else { dungeon[i + 1][j + 1] = 1; } } if (v == 9) { if (FlipCoin()) { dungeon[i + 1][j] = 1; } else { dungeon[i][j + 1] = 1; } } } } } void FillSingles() { for (int j = 1; j < DMAXY - 1; j++) { for (int i = 1; i < DMAXX - 1; i++) { if (dungeon[i][j] == 0 && dungeon[i][j - 1] + dungeon[i - 1][j - 1] + dungeon[i + 1][j - 1] == 3 && dungeon[i + 1][j] + dungeon[i - 1][j] == 2 && dungeon[i][j + 1] + dungeon[i - 1][j + 1] + dungeon[i + 1][j + 1] == 3) { dungeon[i][j] = 1; } } } } void FillStraights() { int xc; int yc; for (int j = 0; j < DMAXY - 1; j++) { int xs = 0; for (int i = 0; i < 37; i++) { if (dungeon[i][j] == 0 && dungeon[i][j + 1] == 1) { if (xs == 0) { xc = i; } xs++; } else { if (xs > 3 && !FlipCoin()) { for (int k = xc; k < i; k++) { const int rv = GenerateRnd(2); dungeon[k][j] = rv; } } xs = 0; } } } for (int j = 0; j < DMAXY - 1; j++) { int xs = 0; for (int i = 0; i < 37; i++) { if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 0) { if (xs == 0) { xc = i; } xs++; } else { if (xs > 3 && !FlipCoin()) { for (int k = xc; k < i; k++) { const int rv = GenerateRnd(2); dungeon[k][j + 1] = rv; } } xs = 0; } } } for (int i = 0; i < DMAXX - 1; i++) { int ys = 0; for (int j = 0; j < 37; j++) { if (dungeon[i][j] == 0 && dungeon[i + 1][j] == 1) { if (ys == 0) { yc = j; } ys++; } else { if (ys > 3 && !FlipCoin()) { for (int k = yc; k < j; k++) { const int rv = GenerateRnd(2); dungeon[i][k] = rv; } } ys = 0; } } } for (int i = 0; i < DMAXX - 1; i++) { int ys = 0; for (int j = 0; j < 37; j++) { if (dungeon[i][j] == 1 && dungeon[i + 1][j] == 0) { if (ys == 0) { yc = j; } ys++; } else { if (ys > 3 && !FlipCoin()) { for (int k = yc; k < j; k++) { const int rv = GenerateRnd(2); dungeon[i + 1][k] = rv; } } ys = 0; } } } } void Edges() { for (int j = 0; j < DMAXY; j++) { dungeon[DMAXX - 1][j] = 0; } for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) dungeon[i][DMAXY - 1] = 0; } } int GetFloorArea() { int gfa = 0; for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) gfa += dungeon[i][j]; } } return gfa; } void MakeMegas() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { int v = dungeon[i + 1][j + 1] + 2 * dungeon[i][j + 1] + 4 * dungeon[i + 1][j] + 8 * dungeon[i][j]; if (v == 6) { v = PickRandomlyAmong({ 12, 5 }); } if (v == 9) { v = PickRandomlyAmong({ 13, 14 }); } dungeon[i][j] = L3ConvTbl[v]; } dungeon[DMAXX - 1][j] = 8; } for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) dungeon[i][DMAXY - 1] = 8; } } void River() { int dir; int nodir; int river[3][100]; int riveramt; int rivercnt = 0; int tries = 0; /// BUGFIX: pdir is uninitialized, add code `pdir = -1;`(fixed) int pdir = -1; while (tries < 200 && rivercnt < 4) { bool bail = false; while (!bail && tries < 200) { tries++; int rx = 0; int ry = 0; int i = 0; // BUGFIX: Replace with `(ry >= DMAXY || dungeon[rx][ry] < 25 || dungeon[rx][ry] > 28) && i < 100` (fixed) while ((ry >= DMAXY || dungeon[rx][ry] < 25 || dungeon[rx][ry] > 28) && i < 100) { rx = GenerateRnd(DMAXX); ry = GenerateRnd(DMAXY); i++; // BUGFIX: Move `ry < DMAXY` check before dungeon checks (fixed) while (ry < DMAXY && (dungeon[rx][ry] < 25 || dungeon[rx][ry] > 28)) { rx++; if (rx >= DMAXX) { rx = 0; ry++; } } } // BUGFIX: Continue if `ry >= DMAXY` (fixed) if (ry >= DMAXY) continue; if (i >= 100) { return; } switch (dungeon[rx][ry]) { case 25: dir = 3; nodir = 2; river[2][0] = 40; break; case 26: dir = 0; nodir = 1; river[2][0] = 38; break; case 27: dir = 1; nodir = 0; river[2][0] = 41; break; case 28: dir = 2; nodir = 3; river[2][0] = 39; break; } river[0][0] = rx; river[1][0] = ry; riveramt = 1; int nodir2 = 4; int dircheck = 0; while (dircheck < 4 && riveramt < 100) { const int px = rx; const int py = ry; if (dircheck == 0) { dir = GenerateRnd(4); } else { dir = (dir + 1) & 3; } dircheck++; while (dir == nodir || dir == nodir2) { dir = (dir + 1) & 3; dircheck++; } if (dir == 0 && ry > 0) { ry--; } if (dir == 1 && ry < DMAXY) { ry++; } if (dir == 2 && rx < DMAXX) { rx++; } if (dir == 3 && rx > 0) { rx--; } if (dungeon[rx][ry] == 7) { dircheck = 0; if (dir < 2) { river[2][riveramt] = PickRandomlyAmong({ 17, 18 }); } if (dir > 1) { river[2][riveramt] = PickRandomlyAmong({ 15, 16 }); } river[0][riveramt] = rx; river[1][riveramt] = ry; riveramt++; if ((dir == 0 && pdir == 2) || (dir == 3 && pdir == 1)) { if (riveramt > 2) { river[2][riveramt - 2] = 22; } if (dir == 0) { nodir2 = 1; } else { nodir2 = 2; } } if ((dir == 0 && pdir == 3) || (dir == 2 && pdir == 1)) { if (riveramt > 2) { river[2][riveramt - 2] = 21; } if (dir == 0) { nodir2 = 1; } else { nodir2 = 3; } } if ((dir == 1 && pdir == 2) || (dir == 3 && pdir == 0)) { if (riveramt > 2) { river[2][riveramt - 2] = 20; } if (dir == 1) { nodir2 = 0; } else { nodir2 = 2; } } if ((dir == 1 && pdir == 3) || (dir == 2 && pdir == 0)) { if (riveramt > 2) { river[2][riveramt - 2] = 19; } if (dir == 1) { nodir2 = 0; } else { nodir2 = 3; } } pdir = dir; } else { rx = px; ry = py; } } // BUGFIX: Check `ry >= 2` (fixed) if (dir == 0 && ry >= 2 && dungeon[rx][ry - 1] == 10 && dungeon[rx][ry - 2] == 8) { river[0][riveramt] = rx; river[1][riveramt] = ry - 1; river[2][riveramt] = 24; if (pdir == 2) { river[2][riveramt - 1] = 22; } if (pdir == 3) { river[2][riveramt - 1] = 21; } bail = true; } // BUGFIX: Check `ry + 2 < DMAXY` (fixed) if (dir == 1 && ry + 2 < DMAXY && dungeon[rx][ry + 1] == 2 && dungeon[rx][ry + 2] == 8) { river[0][riveramt] = rx; river[1][riveramt] = ry + 1; river[2][riveramt] = 42; if (pdir == 2) { river[2][riveramt - 1] = 20; } if (pdir == 3) { river[2][riveramt - 1] = 19; } bail = true; } // BUGFIX: Check `rx + 2 < DMAXX` (fixed) if (dir == 2 && rx + 2 < DMAXX && dungeon[rx + 1][ry] == 4 && dungeon[rx + 2][ry] == 8) { river[0][riveramt] = rx + 1; river[1][riveramt] = ry; river[2][riveramt] = 43; if (pdir == 0) { river[2][riveramt - 1] = 19; } if (pdir == 1) { river[2][riveramt - 1] = 21; } bail = true; } // BUGFIX: Check `rx >= 2` (fixed) if (dir == 3 && rx >= 2 && dungeon[rx - 1][ry] == 9 && dungeon[rx - 2][ry] == 8) { river[0][riveramt] = rx - 1; river[1][riveramt] = ry; river[2][riveramt] = 23; if (pdir == 0) { river[2][riveramt - 1] = 20; } if (pdir == 1) { river[2][riveramt - 1] = 22; } bail = true; } } if (bail && riveramt < 7) { bail = false; } if (bail) { int found = 0; int lpcnt = 0; int bridge; while (found == 0 && lpcnt < 30) { lpcnt++; bridge = GenerateRnd(riveramt); if ((river[2][bridge] == 15 || river[2][bridge] == 16) && dungeon[river[0][bridge]][river[1][bridge] - 1] == 7 && dungeon[river[0][bridge]][river[1][bridge] + 1] == 7) { found = 1; } if ((river[2][bridge] == 17 || river[2][bridge] == 18) && dungeon[river[0][bridge] - 1][river[1][bridge]] == 7 && dungeon[river[0][bridge] + 1][river[1][bridge]] == 7) { found = 2; } for (int i = 0; i < riveramt && found != 0; i++) { if (found == 1 && (river[1][bridge] - 1 == river[1][i] || river[1][bridge] + 1 == river[1][i]) && river[0][bridge] == river[0][i]) { found = 0; } if (found == 2 && (river[0][bridge] - 1 == river[0][i] || river[0][bridge] + 1 == river[0][i]) && river[1][bridge] == river[1][i]) { found = 0; } } } if (found != 0) { if (found == 1) { river[2][bridge] = 44; } else { river[2][bridge] = 45; } rivercnt++; for (bridge = 0; bridge <= riveramt; bridge++) { dungeon[river[0][bridge]][river[1][bridge]] = river[2][bridge]; } } else { bail = false; } } } } bool Spawn(int x, int y, int *totarea); bool SpawnEdge(int x, int y, int *totarea) { constexpr uint8_t spawntable[15] = { 0x00, 0x0A, 0x43, 0x05, 0x2c, 0x06, 0x09, 0x00, 0x00, 0x1c, 0x83, 0x06, 0x09, 0x0A, 0x05 }; if (*totarea > 40) { return true; } if (x < 0 || y < 0 || x >= DMAXX || y >= DMAXY) { return true; } if ((dungeon[x][y] & 0x80) != 0) { return false; } if (dungeon[x][y] > 15) { return true; } const uint8_t i = dungeon[x][y]; dungeon[x][y] |= 0x80; *totarea += 1; if ((spawntable[i] & 8) != 0 && SpawnEdge(x, y - 1, totarea)) { return true; } if ((spawntable[i] & 4) != 0 && SpawnEdge(x, y + 1, totarea)) { return true; } if ((spawntable[i] & 2) != 0 && SpawnEdge(x + 1, y, totarea)) { return true; } if ((spawntable[i] & 1) != 0 && SpawnEdge(x - 1, y, totarea)) { return true; } if ((spawntable[i] & 0x80) != 0 && Spawn(x, y - 1, totarea)) { return true; } if ((spawntable[i] & 0x40) != 0 && Spawn(x, y + 1, totarea)) { return true; } if ((spawntable[i] & 0x20) != 0 && Spawn(x + 1, y, totarea)) { return true; } if ((spawntable[i] & 0x10) != 0 && Spawn(x - 1, y, totarea)) { return true; } return false; } bool Spawn(int x, int y, int *totarea) { constexpr uint8_t spawntable[15] = { 0x00, 0x0A, 0x03, 0x05, 0x0C, 0x06, 0x09, 0x00, 0x00, 0x0C, 0x03, 0x06, 0x09, 0x0A, 0x05 }; if (*totarea > 40) { return true; } if (x < 0 || y < 0 || x >= DMAXX || y >= DMAXY) { return true; } if ((dungeon[x][y] & 0x80) != 0) { return false; } if (dungeon[x][y] > 15) { return true; } const uint8_t i = dungeon[x][y]; dungeon[x][y] |= 0x80; *totarea += 1; if (i != 8) { if ((spawntable[i] & 8) != 0 && SpawnEdge(x, y - 1, totarea)) { return true; } if ((spawntable[i] & 4) != 0 && SpawnEdge(x, y + 1, totarea)) { return true; } if ((spawntable[i] & 2) != 0 && SpawnEdge(x + 1, y, totarea)) { return true; } if ((spawntable[i] & 1) != 0 && SpawnEdge(x - 1, y, totarea)) { return true; } } else { if (Spawn(x + 1, y, totarea)) { return true; } if (Spawn(x - 1, y, totarea)) { return true; } if (Spawn(x, y + 1, totarea)) { return true; } if (Spawn(x, y - 1, totarea)) { return true; } } return false; } bool CanReplaceTile(uint8_t replace, Point tile) { if (replace < 84 || replace > 100) { return true; } // BUGFIX: p2 is a workaround for a bug, only p1 should have been used (fixing this breaks compatibility) constexpr auto ComparisonWithBoundsCheck = [](Point p1, Point p2) { return (p1.x >= 0 && p1.x < DMAXX && p1.y >= 0 && p1.y < DMAXY) && (p2.x >= 0 && p2.x < DMAXX && p2.y >= 0 && p2.y < DMAXY) && (dungeon[p1.x][p1.y] >= 84 && dungeon[p2.x][p2.y] <= 100); }; if (ComparisonWithBoundsCheck(tile + Direction::NorthWest, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::SouthEast, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::SouthWest, tile + Direction::NorthWest) || ComparisonWithBoundsCheck(tile + Direction::NorthEast, tile + Direction::NorthWest)) { return false; } return true; } /** * @brief Randomly places the given miniset throughout the dungeon wherever it would fit * @return true if at least one instance was placed */ bool PlaceMiniSetRandom(const Miniset &miniset, int rndper) { const WorldTileCoord sw = miniset.size.width; const WorldTileCoord sh = miniset.size.height; bool placed = false; for (WorldTileCoord sy = 0; sy < DMAXY - sh; sy++) { for (WorldTileCoord sx = 0; sx < DMAXX - sw; sx++) { if (!miniset.matches({ sx, sy })) continue; // BUGFIX: This should not be applied to Nest levels if (!CanReplaceTile(miniset.replace[0][0], { sx, sy })) continue; if (GenerateRnd(100) >= rndper) continue; miniset.place({ sx, sy }); placed = true; } } return placed; } void PlaceMiniSetRandom1x1(uint8_t search, uint8_t replace, int rndper) { PlaceMiniSetRandom({ { 1, 1 }, { { search } }, { { replace } } }, rndper); } bool PlaceSlimePool() { int lavapool = 0; if (PlaceMiniSetRandom(HivePattern41, 30)) lavapool++; if (PlaceMiniSetRandom(HivePattern42, 40)) lavapool++; if (PlaceMiniSetRandom(HivePattern39, 50)) lavapool++; if (PlaceMiniSetRandom(HivePattern40, 60)) lavapool++; return lavapool >= 3; } /** * Flood fills dirt and wall tiles looking for * an area of at most 40 tiles and disconnected from the map edge. * If it finds one, converts it to lava tiles and return true. */ bool PlaceLavaPool() { constexpr uint8_t Poolsub[15] = { 0, 35, 26, 36, 25, 29, 34, 7, 33, 28, 27, 37, 32, 31, 30 }; bool lavePoolPlaced = false; for (int duny = 0; duny < DMAXY; duny++) { for (int dunx = 0; dunx < DMAXY; dunx++) { if (dungeon[dunx][duny] != 8) { continue; } dungeon[dunx][duny] |= 0x80; int totarea = 1; bool found = true; if (dunx + 1 < DMAXX) { found = Spawn(dunx + 1, duny, &totarea); } if (dunx - 1 > 0 && !found) { found = Spawn(dunx - 1, duny, &totarea); } else { found = true; } if (duny + 1 < DMAXY && !found) { found = Spawn(dunx, duny + 1, &totarea); } else { found = true; } if (duny - 1 > 0 && !found) { found = Spawn(dunx, duny - 1, &totarea); } else { found = true; } const bool placePool = GenerateRnd(100) < 25; for (int j = std::max(duny - totarea, 0); j < std::min(duny + totarea, DMAXY); j++) { for (int i = std::max(dunx - totarea, 0); i < std::min(dunx + totarea, DMAXX); i++) { // BUGFIX: In the following swap the order to first do the // index checks and only then access dungeon[i][j] (fixed) if ((dungeon[i][j] & 0x80) != 0) { dungeon[i][j] &= ~0x80; if (totarea > 4 && placePool && !found) { const uint8_t k = Poolsub[dungeon[i][j]]; if (k != 0 && k <= 37) { dungeon[i][j] = k; } lavePoolPlaced = true; } } } } } } return lavePoolPlaced; } bool PlacePool() { if (leveltype == DTYPE_NEST) { return PlaceSlimePool(); } return PlaceLavaPool(); } /** * @brief Fill lava pools correctly, because River() only generates the edges. */ void PoolFix() { for (const Point tile : PointsInRectangle(Rectangle { { 1, 1 }, { DMAXX - 2, DMAXY - 2 } })) { // Check if the tile is the default dirt ceiling tile if (dungeon[tile.x][tile.y] != 8) continue; for (const Point adjacentTiles : PointsInRectangle(Rectangle { tile - Displacement(1, 1), { 3, 3 } })) { const int tileId = dungeon[adjacentTiles.x][adjacentTiles.y]; // Check if the adjacent tile is a ground lava tile if (tileId >= 25 && tileId <= 41) { // A ground lava tile can never be directly connected to our ceiling tile. // There must always be a kind of transition tile between (from ground to ceiling). // That means our tile is part of a lava pool (and was missed in River()), so we should change our tile to a ground lava tile. dungeon[tile.x][tile.y] = 33; break; } } } } bool FenceVerticalUp(int i, int y) { if ((dungeon[i + 1][y] > 152 || dungeon[i + 1][y] < 130) && (dungeon[i - 1][y] > 152 || dungeon[i - 1][y] < 130)) { if (IsAnyOf(dungeon[i][y], 7, 10, 126, 129, 134, 136)) { return true; } } return false; } bool FenceVerticalDown(int i, int y) { if ((dungeon[i + 1][y] > 152 || dungeon[i + 1][y] < 130) && (dungeon[i - 1][y] > 152 || dungeon[i - 1][y] < 130)) { if (IsAnyOf(dungeon[i][y], 2, 7, 134, 136)) { return true; } } return false; } bool FenceHorizontalLeft(int x, int j) { if ((dungeon[x][j + 1] > 152 || dungeon[x][j + 1] < 130) && (dungeon[x][j - 1] > 152 || dungeon[x][j - 1] < 130)) { if (IsAnyOf(dungeon[x][j], 7, 9, 121, 124, 135, 137)) { return true; } } return false; } bool FenceHorizontalRight(int x, int j) { if ((dungeon[x][j + 1] > 152 || dungeon[x][j + 1] < 130) && (dungeon[x][j - 1] > 152 || dungeon[x][j - 1] < 130)) { if (IsAnyOf(dungeon[x][j], 4, 7, 135, 137)) { return true; } } return false; } void AddFenceDoors() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 7) { if (dungeon[i - 1][j] <= 152 && dungeon[i - 1][j] >= 130 && dungeon[i + 1][j] <= 152 && dungeon[i + 1][j] >= 130) { dungeon[i][j] = 146; continue; } } if (dungeon[i][j] == 7) { if (dungeon[i][j - 1] <= 152 && dungeon[i][j - 1] >= 130 && dungeon[i][j + 1] <= 152 && dungeon[i][j + 1] >= 130) { dungeon[i][j] = 147; continue; } } } } } void FenceDoorFix() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 146) { if (dungeon[i + 1][j] > 152 || dungeon[i + 1][j] < 130 || dungeon[i - 1][j] > 152 || dungeon[i - 1][j] < 130) { dungeon[i][j] = 7; continue; } } if (dungeon[i][j] == 146) { if (IsNoneOf(dungeon[i + 1][j], 130, 132, 133, 134, 136, 138, 140) && IsNoneOf(dungeon[i - 1][j], 130, 132, 133, 134, 136, 138, 140)) { dungeon[i][j] = 7; continue; } } if (dungeon[i][j] == 147) { if (dungeon[i][j + 1] > 152 || dungeon[i][j + 1] < 130 || dungeon[i][j - 1] > 152 || dungeon[i][j - 1] < 130) { dungeon[i][j] = 7; continue; } } if (dungeon[i][j] == 147) { if (IsNoneOf(dungeon[i][j + 1], 131, 132, 133, 135, 137, 138, 139) && IsNoneOf(dungeon[i][j - 1], 131, 132, 133, 135, 137, 138, 139)) { dungeon[i][j] = 7; continue; } } } } } void Fence() { for (int j = 1; j < DMAXY - 1; j++) { // BUGFIX: Change '0' to '1' (fixed) for (int i = 1; i < DMAXX - 1; i++) { // BUGFIX: Change '0' to '1' (fixed) if (dungeon[i][j] == 10 && !FlipCoin()) { int x = i; while (dungeon[x][j] == 10) { x++; } x--; if (x - i > 0) { dungeon[i][j] = 127; for (int xx = i + 1; xx < x; xx++) { dungeon[xx][j] = PickRandomlyAmong({ 129, 126 }); } dungeon[x][j] = 128; } } if (dungeon[i][j] == 9 && !FlipCoin()) { int y = j; while (dungeon[i][y] == 9) { y++; } y--; if (y - j > 0) { dungeon[i][j] = 123; for (int yy = j + 1; yy < y; yy++) { dungeon[i][yy] = PickRandomlyAmong({ 124, 121 }); } dungeon[i][y] = 122; } } if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 10 && dungeon[i][j + 1] == 9 && !FlipCoin()) { dungeon[i][j] = 125; int x = i + 1; while (dungeon[x][j] == 10) { x++; } x--; for (int xx = i + 1; xx < x; xx++) { dungeon[xx][j] = PickRandomlyAmong({ 129, 126 }); } dungeon[x][j] = 128; int y = j + 1; while (dungeon[i][y] == 9) { y++; } y--; for (int yy = j + 1; yy < y; yy++) { dungeon[i][yy] = PickRandomlyAmong({ 124, 121 }); } dungeon[i][y] = 122; } } } for (WorldTileCoord j = 1; j < DMAXY; j++) { // BUGFIX: Change '0' to '1' (fixed) for (WorldTileCoord i = 1; i < DMAXX; i++) { // BUGFIX: Change '0' to '1' (fixed) // note the comma operator is used here to advance the RNG state if (dungeon[i][j] == 7 && (DiscardRandomValues(1), !IsNearThemeRoom({ i, j }))) { if (FlipCoin()) { int y1 = j; // BUGFIX: Check `y1 >= 0` first (fixed) while (y1 >= 0 && FenceVerticalUp(i, y1)) { y1--; } y1++; int y2 = j; // BUGFIX: Check `y2 < DMAXY` first (fixed) while (y2 < DMAXY && FenceVerticalDown(i, y2)) { y2++; } y2--; bool skip = true; if (dungeon[i][y1] == 7) { skip = false; } if (dungeon[i][y2] == 7) { skip = false; } if (y2 - y1 > 1 && skip) { const int rp = GenerateRnd(y2 - y1 - 1) + y1 + 1; for (int y = y1; y <= y2; y++) { if (y == rp) { continue; } if (dungeon[i][y] == 7) { dungeon[i][y] = PickRandomlyAmong({ 137, 135 }); } if (dungeon[i][y] == 10) { dungeon[i][y] = 131; } if (dungeon[i][y] == 126) { dungeon[i][y] = 133; } if (dungeon[i][y] == 129) { dungeon[i][y] = 133; } if (dungeon[i][y] == 2) { dungeon[i][y] = 139; } if (dungeon[i][y] == 134) { dungeon[i][y] = 138; } if (dungeon[i][y] == 136) { dungeon[i][y] = 138; } } } } else { int x1 = i; // BUGFIX: Check `x1 >= 0` first (fixed) while (x1 >= 0 && FenceHorizontalLeft(x1, j)) { x1--; } x1++; int x2 = i; // BUGFIX: Check `x2 < DMAXX` first (fixed) while (x2 < DMAXX && FenceHorizontalRight(x2, j)) { x2++; } x2--; bool skip = true; if (dungeon[x1][j] == 7) { skip = false; } if (dungeon[x2][j] == 7) { skip = false; } if (x2 - x1 > 1 && skip) { const int rp = GenerateRnd(x2 - x1 - 1) + x1 + 1; for (int x = x1; x <= x2; x++) { if (x == rp) { continue; } if (dungeon[x][j] == 7) { dungeon[x][j] = PickRandomlyAmong({ 136, 134 }); } if (dungeon[x][j] == 9) { dungeon[x][j] = 130; } if (dungeon[x][j] == 121) { dungeon[x][j] = 132; } if (dungeon[x][j] == 124) { dungeon[x][j] = 132; } if (dungeon[x][j] == 4) { dungeon[x][j] = 140; } if (dungeon[x][j] == 135) { dungeon[x][j] = 138; } if (dungeon[x][j] == 137) { dungeon[x][j] = 138; } } } } } } } AddFenceDoors(); FenceDoorFix(); } bool PlaceAnvil() { const std::unique_ptr setPieceData = LoadFileInMem("levels\\l3data\\anvil.dun"); // growing the size by 2 to allow a 1 tile border on all sides const WorldTileSize areaSize = GetDunSize(setPieceData.get()) + 2; WorldTileCoord sx = GenerateRnd(DMAXX - areaSize.width); WorldTileCoord sy = GenerateRnd(DMAXY - areaSize.height); for (int tries = 0;; tries++, sx++) { if (tries > 198) return false; if (sx == DMAXX - areaSize.width) { sx = 0; sy++; if (sy == DMAXY - areaSize.height) { sy = 0; } } bool found = true; for (const WorldTilePosition tile : PointsInRectangle(WorldTileRectangle { { sx, sy }, areaSize })) { if (Protected.test(tile.x, tile.y) || dungeon[tile.x][tile.y] != 7) { found = false; break; } } if (found) break; } PlaceDunTiles(setPieceData.get(), { sx + 1, sy + 1 }, 7); SetPiece = { { sx, sy }, areaSize }; for (const WorldTilePosition tile : PointsInRectangle(SetPiece)) { Protected.set(tile.x, tile.y); } // Hack to avoid rivers entering the island, reversed later dungeon[SetPiece.position.x + 7][SetPiece.position.y + 5] = 2; dungeon[SetPiece.position.x + 8][SetPiece.position.y + 5] = 2; dungeon[SetPiece.position.x + 9][SetPiece.position.y + 5] = 2; return true; } void Warp() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 125 && dungeon[i + 1][j] == 125 && dungeon[i][j + 1] == 125 && dungeon[i + 1][j + 1] == 125) { dungeon[i][j] = 156; dungeon[i + 1][j] = 155; dungeon[i][j + 1] = 153; dungeon[i + 1][j + 1] = 154; return; } if (dungeon[i][j] == 5 && dungeon[i + 1][j + 1] == 7) { dungeon[i][j] = 7; } } } } void HallOfHeroes() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 5 && dungeon[i + 1][j + 1] == 7) { dungeon[i][j] = 7; } } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 5 && dungeon[i + 1][j + 1] == 12 && dungeon[i + 1][j] == 7) { dungeon[i][j] = 7; dungeon[i][j + 1] = 7; dungeon[i + 1][j + 1] = 7; } if (dungeon[i][j] == 5 && dungeon[i + 1][j + 1] == 12 && dungeon[i][j + 1] == 7) { dungeon[i][j] = 7; dungeon[i + 1][j] = 7; dungeon[i + 1][j + 1] = 7; } } } } void LockRectangle(int x, int y) { if (!DungeonMask.test(x, y)) { return; } DungeonMask.reset(x, y); lockoutcnt++; LockRectangle(x, y - 1); LockRectangle(x, y + 1); LockRectangle(x - 1, y); LockRectangle(x + 1, y); } bool Lockout() { DungeonMask.reset(); int fx; int fy; int t = 0; for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] != 0) { DungeonMask.set(i, j); fx = i; fy = j; t++; } } } lockoutcnt = 0; LockRectangle(fx, fy); return t == lockoutcnt; } bool PlaceCaveStairs(lvl_entry entry) { std::optional position; // Place stairs up position = PlaceMiniSet(L3UP); if (!position) return false; if (entry == ENTRY_MAIN) ViewPosition = position->megaToWorld() + Displacement { 1, 3 }; // Place stairs down position = PlaceMiniSet(L3DOWN); if (!position) return false; if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 3, 1 }; // Place town warp stairs if (currlevel == 9) { position = PlaceMiniSet(L3HOLDWARP); if (!position) return false; if (entry == ENTRY_TWARPDN) ViewPosition = position->megaToWorld() + Displacement { 1, 3 }; } return true; } bool PlaceNestStairs(lvl_entry entry) { std::optional position; // Place stairs up position = PlaceMiniSet(currlevel != 17 ? L6UP : L6HOLDWARP); if (!position) return false; if (entry == ENTRY_MAIN || entry == ENTRY_TWARPDN) ViewPosition = position->megaToWorld() + Displacement { 1, 3 }; // Place stairs down if (currlevel != 20) { position = PlaceMiniSet(L6DOWN); if (!position) return false; if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 3, 1 }; } return true; } bool PlaceStairs(lvl_entry entry) { if (leveltype == DTYPE_NEST) { return PlaceNestStairs(entry); } return PlaceCaveStairs(entry); } void GenerateLevel(lvl_entry entry) { if (LevelSeeds[currlevel]) SetRndSeed(*LevelSeeds[currlevel]); while (true) { LevelSeeds[currlevel] = GetLCGEngineState(); InitDungeonFlags(); int x1 = GenerateRnd(20) + 10; int y1 = GenerateRnd(20) + 10; int x2 = x1 + 2; int y2 = y1 + 2; FillRoom(x1, y1, x2, y2); CreateBlock({ x1, y1 }, 2, 0); CreateBlock({ x2, y1 }, 2, 1); CreateBlock({ x1, y2 }, 2, 2); CreateBlock({ x1, y1 }, 2, 3); if (Quests[Q_ANVIL].IsAvailable()) { x1 = GenerateRnd(10) + 10; y1 = GenerateRnd(10) + 10; x2 = x1 + 12; y2 = y1 + 12; FloorArea(x1, y1, x2, y2); } FillDiagonals(); FillSingles(); FillStraights(); FillDiagonals(); Edges(); if (GetFloorArea() < 600 || !Lockout()) continue; MakeMegas(); if (!PlaceStairs(entry)) continue; if (Quests[Q_ANVIL].IsAvailable() && !PlaceAnvil()) continue; if (PlacePool()) break; } if (leveltype == DTYPE_NEST) { PlaceMiniSetRandom(L6ISLE1, 70); PlaceMiniSetRandom(L6ISLE2, 70); PlaceMiniSetRandom(L6ISLE3, 30); PlaceMiniSetRandom(L6ISLE4, 30); PlaceMiniSetRandom(L6ISLE1, 100); PlaceMiniSetRandom(L6ISLE2, 100); PlaceMiniSetRandom(L6ISLE5, 90); PlaceMiniSetRandom1x1(8, 25, 20); PlaceMiniSetRandom1x1(8, 26, 20); PlaceMiniSetRandom1x1(8, 27, 20); PlaceMiniSetRandom1x1(8, 28, 20); PlaceMiniSetRandom(HivePattern29, 10); PlaceMiniSetRandom(HivePattern30, 15); PlaceMiniSetRandom(HivePattern31, 20); PlaceMiniSetRandom(HivePattern32, 25); PlaceMiniSetRandom(HivePattern33, 30); PlaceMiniSetRandom(HivePattern34, 35); PlaceMiniSetRandom(HivePattern35, 40); PlaceMiniSetRandom(HivePattern36, 45); PlaceMiniSetRandom(HivePattern37, 50); PlaceMiniSetRandom(HivePattern38, 55); PlaceMiniSetRandom(HivePattern38, 10); PlaceMiniSetRandom(HivePattern37, 15); PlaceMiniSetRandom(HivePattern36, 20); PlaceMiniSetRandom(HivePattern35, 25); PlaceMiniSetRandom(HivePattern34, 30); PlaceMiniSetRandom(HivePattern33, 35); PlaceMiniSetRandom(HivePattern32, 40); PlaceMiniSetRandom(HivePattern31, 45); PlaceMiniSetRandom(HivePattern30, 50); PlaceMiniSetRandom(HivePattern29, 55); PlaceMiniSetRandom(HivePattern9, 40); PlaceMiniSetRandom(HivePattern10, 45); PlaceMiniSetRandom1x1(7, 29, 25); PlaceMiniSetRandom1x1(7, 30, 25); PlaceMiniSetRandom1x1(7, 31, 25); PlaceMiniSetRandom1x1(7, 32, 25); PlaceMiniSetRandom1x1(9, 33, 25); PlaceMiniSetRandom1x1(9, 34, 25); PlaceMiniSetRandom1x1(9, 35, 25); PlaceMiniSetRandom1x1(9, 36, 25); PlaceMiniSetRandom1x1(9, 37, 25); PlaceMiniSetRandom1x1(10, 39, 25); PlaceMiniSetRandom1x1(10, 40, 25); PlaceMiniSetRandom1x1(10, 41, 25); PlaceMiniSetRandom1x1(10, 42, 25); PlaceMiniSetRandom1x1(10, 43, 25); PlaceMiniSetRandom1x1(9, 45, 25); PlaceMiniSetRandom1x1(9, 46, 25); PlaceMiniSetRandom1x1(10, 47, 25); PlaceMiniSetRandom1x1(10, 48, 25); PlaceMiniSetRandom1x1(11, 38, 25); PlaceMiniSetRandom1x1(11, 44, 25); PlaceMiniSetRandom1x1(11, 49, 25); PlaceMiniSetRandom1x1(11, 50, 25); } else { PoolFix(); Warp(); PlaceMiniSetRandom(L3ISLE1, 70); PlaceMiniSetRandom(L3ISLE2, 70); PlaceMiniSetRandom(L3ISLE3, 30); PlaceMiniSetRandom(L3ISLE4, 30); PlaceMiniSetRandom(L3ISLE1, 100); PlaceMiniSetRandom(L3ISLE2, 100); PlaceMiniSetRandom(L3ISLE5, 90); HallOfHeroes(); River(); if (Quests[Q_ANVIL].IsAvailable()) { dungeon[SetPiece.position.x + 7][SetPiece.position.y + 5] = 7; dungeon[SetPiece.position.x + 8][SetPiece.position.y + 5] = 7; dungeon[SetPiece.position.x + 9][SetPiece.position.y + 5] = 7; if (dungeon[SetPiece.position.x + 10][SetPiece.position.y + 5] == 17 || dungeon[SetPiece.position.x + 10][SetPiece.position.y + 5] == 18) { dungeon[SetPiece.position.x + 10][SetPiece.position.y + 5] = 45; } } DRLG_PlaceThemeRooms(5, 10, 7, 0, false); Fence(); PlaceMiniSetRandom(L3TITE1, 10); PlaceMiniSetRandom(L3TITE2, 10); PlaceMiniSetRandom(L3TITE3, 10); PlaceMiniSetRandom(L3TITE6, 20); PlaceMiniSetRandom(L3TITE7, 20); PlaceMiniSetRandom(L3TITE8, 20); PlaceMiniSetRandom(L3TITE9, 20); PlaceMiniSetRandom(L3TITE10, 20); PlaceMiniSetRandom(L3TITE11, 30); PlaceMiniSetRandom(L3TITE12, 20); PlaceMiniSetRandom(L3TITE13, 20); PlaceMiniSetRandom(L3CREV1, 30); PlaceMiniSetRandom(L3CREV2, 30); PlaceMiniSetRandom(L3CREV3, 30); PlaceMiniSetRandom(L3CREV4, 30); PlaceMiniSetRandom(L3CREV5, 30); PlaceMiniSetRandom(L3CREV6, 30); PlaceMiniSetRandom(L3CREV7, 30); PlaceMiniSetRandom(L3CREV8, 30); PlaceMiniSetRandom(L3CREV9, 30); PlaceMiniSetRandom(L3CREV10, 30); PlaceMiniSetRandom(L3CREV11, 30); PlaceMiniSetRandom1x1(7, 106, 25); PlaceMiniSetRandom1x1(7, 107, 25); PlaceMiniSetRandom1x1(7, 108, 25); PlaceMiniSetRandom1x1(9, 109, 25); PlaceMiniSetRandom1x1(10, 110, 25); } memcpy(pdungeon, dungeon, sizeof(pdungeon)); } void Pass3() { DRLG_LPass3(8 - 1); } void PlaceCaveLights() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] >= 55 && dPiece[i][j] <= 146) { DoLighting({ i, j }, 7, {}); } else if (dPiece[i][j] >= 153 && dPiece[i][j] <= 160) { DoLighting({ i, j }, 7, {}); } else if (IsAnyOf(dPiece[i][j], 149, 151)) { DoLighting({ i, j }, 7, {}); } } } } void PlaceHiveLights() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] >= 381 && dPiece[i][j] <= 456) { DoLighting({ i, j }, 9, {}); } } } } void PlaceLights() { if (leveltype == DTYPE_NEST) { PlaceHiveLights(); return; } PlaceCaveLights(); } } // namespace void CreateL3Dungeon(uint32_t rseed, lvl_entry entry) { SetRndSeed(rseed); GenerateLevel(entry); Pass3(); PlaceLights(); } void LoadPreL3Dungeon(const char *path) { memset(dungeon, 8, sizeof(dungeon)); auto dunData = LoadFileInMem(path); PlaceDunTiles(dunData.get(), { 0, 0 }, 7); memcpy(pdungeon, dungeon, sizeof(pdungeon)); } void LoadL3Dungeon(const char *path, Point spawn) { LoadDungeonBase(path, spawn, 7, 8); Pass3(); PlaceLights(); if (leveltype == DTYPE_CAVES) AddL3Objs(0, 0, MAXDUNX, MAXDUNY); } } // namespace devilution ================================================ FILE: Source/levels/drlg_l3.h ================================================ /** * @file levels/drlg_l3.h * * Interface of the caves level generation algorithms. */ #pragma once #include #include "levels/gendung.h" namespace devilution { void CreateL3Dungeon(uint32_t rseed, lvl_entry entry); void LoadPreL3Dungeon(const char *sFileName); void LoadL3Dungeon(const char *sFileName, Point spawn); } // namespace devilution ================================================ FILE: Source/levels/drlg_l4.cpp ================================================ /** * @file levels/drlg_l4.cpp * * Implementation of the hell level generation algorithms. */ #include "levels/drlg_l4.h" #include #include "engine/load_file.hpp" #include "engine/random.hpp" #include "levels/gendung.h" #include "monster.h" #include "multi.h" #include "player.h" #include "tables/objdat.h" #include "utils/is_of.hpp" namespace devilution { WorldTilePosition DiabloQuad1; WorldTilePosition DiabloQuad2; WorldTilePosition DiabloQuad3; WorldTilePosition DiabloQuad4; namespace { bool hallok[20]; WorldTilePosition L4Hold; /** * A lookup table for the 16 possible patterns of a 2x2 area, * where each cell either contains a SW wall or it doesn't. */ const uint8_t L4ConvTbl[16] = { 30, 6, 1, 6, 2, 6, 6, 6, 9, 6, 1, 6, 2, 6, 3, 6 }; /** Miniset: Stairs up. */ const Miniset L4USTAIRS { { 4, 5 }, { { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, }, { { 0, 0, 0, 0 }, { 36, 38, 35, 0 }, { 37, 34, 33, 32 }, { 0, 0, 31, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stairs up to town. */ const Miniset L4TWARP { { 4, 5 }, { { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, { 6, 6, 6, 6 }, }, { { 0, 0, 0, 0 }, { 134, 136, 133, 0 }, { 135, 132, 131, 130 }, { 0, 0, 129, 0 }, { 0, 0, 0, 0 }, } }; /** Miniset: Stairs down. */ const Miniset L4DSTAIRS { { 5, 5 }, { { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, }, { { 0, 0, 0, 0, 0 }, { 0, 0, 45, 41, 0 }, { 0, 44, 43, 40, 0 }, { 0, 46, 42, 39, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Miniset: Pentagram. */ const Miniset L4PENTA { { 5, 5 }, { { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, }, { { 0, 0, 0, 0, 0 }, { 0, 98, 100, 103, 0 }, { 0, 99, 102, 105, 0 }, { 0, 101, 104, 106, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Miniset: Pentagram portal. */ const Miniset L4PENTA2 { { 5, 5 }, { { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, { 6, 6, 6, 6, 6 }, }, { { 0, 0, 0, 0, 0 }, { 0, 107, 109, 112, 0 }, { 0, 108, 111, 114, 0 }, { 0, 110, 113, 115, 0 }, { 0, 0, 0, 0, 0 }, } }; /** Maps tile IDs to their corresponding undecorated tile ID. */ const uint8_t L4BTYPES[140] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 1, 2, 1, 1, 2, 2, 0, 0, 0, 0, 0, 0, 15, 16, 9, 12, 4, 5, 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, 0, 0, 0 }; void ApplyShadowsPatterns() { for (int y = 1; y < DMAXY; y++) { for (int x = 1; x < DMAXY; x++) { if (IsNoneOf(dungeon[x][y], 3, 4, 8, 15)) { continue; } if (dungeon[x - 1][y] == 6) { dungeon[x - 1][y] = 47; } if (dungeon[x - 1][y - 1] == 6) { dungeon[x - 1][y - 1] = 48; } } } } void InitSetPiece() { std::unique_ptr setPieceData; if (Quests[Q_WARLORD].IsAvailable()) { setPieceData = LoadFileInMem("levels\\l4data\\warlord.dun"); } else if (currlevel == 15 && UseMultiplayerQuests()) { setPieceData = LoadFileInMem("levels\\l4data\\vile1.dun"); } else { return; // no setpiece needed for this level } const WorldTilePosition setPiecePosition = SetPieceRoom.position; PlaceDunTiles(setPieceData.get(), setPiecePosition, 6); SetPiece = { setPiecePosition, GetDunSize(setPieceData.get()) }; } void InitDungeonFlags() { DungeonMask.reset(); Protected.reset(); memset(dungeon, 30, sizeof(dungeon)); } void MapRoom(WorldTileRectangle room) { for (WorldTileCoord y = 0; y < room.size.height && y + room.position.y < DMAXY / 2; y++) { for (WorldTileCoord x = 0; x < room.size.width && x + room.position.x < DMAXX / 2; x++) { DungeonMask.set(room.position.x + x, room.position.y + y); } } } bool CheckRoom(WorldTileRectangle room) { if (room.position.x <= 0 || room.position.y <= 0) { return false; } for (int y = 0; y < room.size.height; y++) { for (int x = 0; x < room.size.width; x++) { if (x + room.position.x < 0 || x + room.position.x >= DMAXX / 2 || y + room.position.y < 0 || y + room.position.y >= DMAXY / 2) { return false; } if (DungeonMask.test(room.position.x + x, room.position.y + y)) { return false; } } } return true; } void GenerateRoom(WorldTileRectangle area, bool verticalLayout) { const bool rotate = !FlipCoin(4); verticalLayout = (!verticalLayout && rotate) || (verticalLayout && !rotate); bool placeRoom1; WorldTileRectangle room1; for (int num = 0; num < 20; num++) { const int32_t randomWidth = (GenerateRnd(5) + 2) & ~1; const int32_t randomHeight = (GenerateRnd(5) + 2) & ~1; room1.size = WorldTileSize(randomWidth, randomHeight); room1.position = area.position; if (verticalLayout) { room1.position += WorldTileDisplacement(-room1.size.width, area.size.height / 2 - room1.size.height / 2); placeRoom1 = CheckRoom({ room1.position + WorldTileDisplacement { -1, -1 }, WorldTileSize(room1.size.height + 2, room1.size.width + 1) }); /// BUGFIX: swap height and width ({ room1.size.width + 1, room1.size.height + 2 }) (workaround applied below) } else { room1.position += WorldTileDisplacement(area.size.width / 2 - room1.size.width / 2, -room1.size.height); placeRoom1 = CheckRoom({ room1.position + WorldTileDisplacement { -1, -1 }, WorldTileSize(room1.size.width + 2, room1.size.height + 1) }); } if (placeRoom1) break; } if (placeRoom1) MapRoom({ room1.position, WorldTileSize(std::min(DMAXX - room1.position.x, room1.size.width), std::min(DMAXX - room1.position.y, room1.size.height)) }); bool placeRoom2; WorldTileRectangle room2 = room1; if (verticalLayout) { room2.position.x = area.position.x + area.size.width; placeRoom2 = CheckRoom({ room2.position + WorldTileDisplacement { 0, -1 }, WorldTileSize(room2.size.width + 1, room2.size.height + 2) }); } else { room2.position.y = area.position.y + area.size.height; placeRoom2 = CheckRoom({ room2.position + WorldTileDisplacement { -1, 0 }, WorldTileSize(room2.size.width + 2, room2.size.height + 1) }); } if (placeRoom2) MapRoom(room2); if (placeRoom1) GenerateRoom(room1, verticalLayout); if (placeRoom2) GenerateRoom(room2, verticalLayout); } void FirstRoom() { WorldTileRectangle room { { 0, 0 }, { 14, 14 } }; if (currlevel != 16) { if (currlevel == Quests[Q_WARLORD]._qlevel && Quests[Q_WARLORD]._qactive != QUEST_NOTAVAIL) { room.size = { 11, 11 }; } else if (currlevel == Quests[Q_BETRAYER]._qlevel && UseMultiplayerQuests()) { room.size = { 11, 11 }; } else { const int32_t randomWidth = GenerateRnd(5) + 2; const int32_t randomHeight = GenerateRnd(5) + 2; room.size = WorldTileSize(randomWidth, randomHeight); } } const int xmin = (DMAXX / 2 - room.size.width) / 2; const int xmax = DMAXX / 2 - 1 - room.size.width; const int ymin = (DMAXY / 2 - room.size.height) / 2; const int ymax = DMAXY / 2 - 1 - room.size.height; const int32_t randomX = GenerateRnd(xmax - xmin + 1) + xmin; const int32_t randomY = GenerateRnd(ymax - ymin + 1) + ymin; room.position = WorldTilePosition(randomX, randomY); if (currlevel == 16) { L4Hold = room.position; } if (Quests[Q_WARLORD].IsAvailable() || (currlevel == Quests[Q_BETRAYER]._qlevel && UseMultiplayerQuests())) { SetPieceRoom = { room.position + WorldTileDisplacement { 1, 1 }, WorldTileSize(room.size.width + 1, room.size.height + 1) }; } else { SetPieceRoom = {}; } MapRoom(room); GenerateRoom(room, !FlipCoin()); } /** * @brief Mirrors the first quadrant to the rest of the map */ void MirrorDungeonLayout() { for (int y = 0; y < DMAXY / 2; y++) { for (int x = 0; x < DMAXX / 2; x++) { if (DungeonMask.test(x, y)) { DungeonMask.set(x, DMAXY - 1 - y); DungeonMask.set(DMAXX - 1 - x, y); DungeonMask.set(DMAXX - 1 - x, DMAXY - 1 - y); } } } } void MakeDmt() { for (int y = 0; y < DMAXY - 1; y++) { for (int x = 0; x < DMAXX - 1; x++) { const int val = (DungeonMask.test(x + 1, y + 1) << 3) | (DungeonMask.test(x, y + 1) << 2) | (DungeonMask.test(x + 1, y) << 1) | (DungeonMask.test(x, y) << 0); dungeon[x][y] = L4ConvTbl[val]; } } } int HorizontalWallOk(int i, int j) { int x; for (x = 1; dungeon[i + x][j] == 6; x++) { if (Protected.test(i + x, j)) { break; } if (dungeon[i + x][j - 1] != 6) { break; } if (dungeon[i + x][j + 1] != 6) { break; } } if (IsAnyOf(dungeon[i + x][j], 10, 12, 13, 15, 16, 21, 22) && x > 3) return x; return -1; } int VerticalWallOk(int i, int j) { int y; for (y = 1; dungeon[i][j + y] == 6; y++) { if (Protected.test(i, j + y)) { break; } if (dungeon[i - 1][j + y] != 6) { break; } if (dungeon[i + 1][j + y] != 6) { break; } } if (IsAnyOf(dungeon[i][j + y], 8, 9, 11, 14, 15, 16, 21, 23) && y > 3) return y; return -1; } void HorizontalWall(int i, int j, int dx) { if (dungeon[i][j] == 13) { dungeon[i][j] = 17; } if (dungeon[i][j] == 16) { dungeon[i][j] = 11; } if (dungeon[i][j] == 12) { dungeon[i][j] = 14; } for (int xx = 1; xx < dx; xx++) { dungeon[i + xx][j] = 2; } if (dungeon[i + dx][j] == 15) { dungeon[i + dx][j] = 14; } if (dungeon[i + dx][j] == 10) { dungeon[i + dx][j] = 17; } if (dungeon[i + dx][j] == 21) { dungeon[i + dx][j] = 23; } if (dungeon[i + dx][j] == 22) { dungeon[i + dx][j] = 29; } const int xx = GenerateRnd(dx - 3) + 1; dungeon[i + xx][j] = 57; dungeon[i + xx + 2][j] = 56; dungeon[i + xx + 1][j] = 60; if (dungeon[i + xx][j - 1] == 6) { dungeon[i + xx][j - 1] = 58; } if (dungeon[i + xx + 1][j - 1] == 6) { dungeon[i + xx + 1][j - 1] = 59; } } void VerticalWall(int i, int j, int dy) { if (dungeon[i][j] == 14) { dungeon[i][j] = 17; } if (dungeon[i][j] == 8) { dungeon[i][j] = 9; } if (dungeon[i][j] == 15) { dungeon[i][j] = 10; } for (int yy = 1; yy < dy; yy++) { dungeon[i][j + yy] = 1; } if (dungeon[i][j + dy] == 11) { dungeon[i][j + dy] = 17; } if (dungeon[i][j + dy] == 9) { dungeon[i][j + dy] = 10; } if (dungeon[i][j + dy] == 16) { dungeon[i][j + dy] = 13; } if (dungeon[i][j + dy] == 21) { dungeon[i][j + dy] = 22; } if (dungeon[i][j + dy] == 23) { dungeon[i][j + dy] = 29; } const int yy = GenerateRnd(dy - 3) + 1; dungeon[i][j + yy] = 53; dungeon[i][j + yy + 2] = 52; dungeon[i][j + yy + 1] = 6; if (dungeon[i - 1][j + yy] == 6) { dungeon[i - 1][j + yy] = 54; } if (dungeon[i - 1][j + yy - 1] == 6) { dungeon[i - 1][j + yy - 1] = 55; } } void AddWall() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (Protected.test(i, j)) { continue; } for (auto d : { 10, 12, 13, 15, 16, 21, 22 }) { if (d == dungeon[i][j]) { DiscardRandomValues(1); const int x = HorizontalWallOk(i, j); if (x != -1) { HorizontalWall(i, j, x); } } } for (auto d : { 8, 9, 11, 14, 15, 16, 21, 23 }) { if (d == dungeon[i][j]) { DiscardRandomValues(1); const int y = VerticalWallOk(i, j); if (y != -1) { VerticalWall(i, j, y); } } } } } } void FixTilesPatterns() { for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 6) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 13; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 14; } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 6) dungeon[i + 1][j] = 2; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 9 && dungeon[i + 1][j] == 6) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 13; if (dungeon[i][j] == 6 && dungeon[i + 1][j] == 14) dungeon[i + 1][j] = 15; if (dungeon[i][j] == 6 && dungeon[i][j + 1] == 13) dungeon[i][j + 1] = 16; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 6 && dungeon[i][j - 1] == 1) dungeon[i][j - 1] = 1; } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 27; if (dungeon[i][j] == 27 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 27; if (dungeon[i][j] == 27 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 27) dungeon[i + 1][j] = 26; if (dungeon[i][j] == 27 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 15) dungeon[i + 1][j] = 14; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 15) dungeon[i + 1][j] = 14; if (dungeon[i][j] == 22 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 27 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 6 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] != 0) /* check */ dungeon[i + 1][j] = 22; if (dungeon[i][j] == 22 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 1 && dungeon[i + 1][j - 1] == 1) dungeon[i + 1][j] = 13; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 30 && dungeon[i][j + 1] == 6) dungeon[i + 1][j] = 28; if (dungeon[i][j] == 16 && dungeon[i + 1][j] == 6 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 27; if (dungeon[i][j] == 16 && dungeon[i][j + 1] == 30 && dungeon[i + 1][j + 1] == 30) dungeon[i][j + 1] = 27; if (dungeon[i][j] == 6 && dungeon[i + 1][j] == 30 && dungeon[i + 1][j - 1] == 6) dungeon[i + 1][j] = 21; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] == 9) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 9 && dungeon[i + 1][j] == 15) dungeon[i + 1][j] = 14; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] == 2) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 18) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 9 && dungeon[i + 1][j] == 15) dungeon[i + 1][j] = 14; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 19 && dungeon[i + 1][j - 1] == 30) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 24 && dungeon[i][j - 1] == 30 && dungeon[i][j - 2] == 6) dungeon[i][j - 1] = 21; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 28; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 28; if (dungeon[i][j] == 28 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 28 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 19 && dungeon[i + 2][j] == 2 && dungeon[i + 1][j - 1] == 18 && dungeon[i + 1][j + 1] == 1) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 19 && dungeon[i + 2][j] == 2 && dungeon[i + 1][j - 1] == 22 && dungeon[i + 1][j + 1] == 1) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 19 && dungeon[i + 2][j] == 2 && dungeon[i + 1][j - 1] == 18 && dungeon[i + 1][j + 1] == 13) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 21 && dungeon[i + 2][j] == 2 && dungeon[i + 1][j - 1] == 18 && dungeon[i + 1][j + 1] == 1) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 21 && dungeon[i + 1][j + 1] == 1 && dungeon[i + 1][j - 1] == 22 && dungeon[i + 2][j] == 3) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 28 && dungeon[i + 2][j] == 30 && dungeon[i + 1][j - 1] == 6) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 28 && dungeon[i + 2][j] == 1) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] == 30) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 28 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 21 && dungeon[i + 1][j - 1] == 21) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] == 30) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 18) dungeon[i + 1][j] = 25; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 9 && dungeon[i + 2][j] == 2) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 10) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 15 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 22 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 18 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 24 && dungeon[i - 1][j] == 30) dungeon[i - 1][j] = 19; if (dungeon[i][j] == 21 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 21 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 22 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 21 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 16 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 22 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 18 && dungeon[i + 2][j] == 30) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 9 && dungeon[i + 1][j + 1] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 27 && dungeon[i + 1][j + 1] == 2) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 23 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 23 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 25 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 22 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 23 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 15) dungeon[i + 1][j] = 14; if (dungeon[i][j] == 23 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 27) dungeon[i + 1][j] = 26; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 18) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 26 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 29 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 29 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 1 && dungeon[i][j - 1] == 15) dungeon[i][j - 1] = 10; if (dungeon[i][j] == 18 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 23 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 18 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 30 && dungeon[i + 1][j + 1] == 30) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 28 && dungeon[i + 1][j - 1] == 6) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 23 && dungeon[i + 1][j] == 18 && dungeon[i][j - 1] == 6) dungeon[i + 1][j] = 24; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 23 && dungeon[i + 2][j] == 30) dungeon[i + 1][j] = 28; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 28 && dungeon[i + 2][j] == 30 && dungeon[i + 1][j - 1] == 6) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 23 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 29 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 29 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 26 && dungeon[i + 1][j] == 30) dungeon[i + 1][j] = 19; if (dungeon[i][j] == 16 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 25 && dungeon[i][j + 1] == 30) dungeon[i][j + 1] = 18; if (dungeon[i][j] == 18 && dungeon[i][j + 1] == 2) dungeon[i][j + 1] = 15; if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 13; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 13 && dungeon[i + 1][j - 1] == 6) dungeon[i + 1][j] = 16; } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 21 && dungeon[i][j + 1] == 24 && dungeon[i][j + 2] == 1) dungeon[i][j + 1] = 17; if (dungeon[i][j] == 15 && dungeon[i + 1][j + 1] == 9 && dungeon[i + 1][j - 1] == 1 && dungeon[i + 2][j] == 16) dungeon[i + 1][j] = 29; if (dungeon[i][j] == 2 && dungeon[i - 1][j] == 6) dungeon[i - 1][j] = 8; if (dungeon[i][j] == 1 && dungeon[i][j - 1] == 6) dungeon[i][j - 1] = 7; if (dungeon[i][j] == 6 && dungeon[i + 1][j] == 15 && dungeon[i + 1][j + 1] == 4) dungeon[i + 1][j] = 10; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 6) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 9 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 10 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 16) dungeon[i][j + 1] = 13; if (dungeon[i][j] == 6 && dungeon[i][j + 1] == 13) dungeon[i][j + 1] = 16; if (dungeon[i][j] == 25 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 28 && dungeon[i][j - 1] == 6 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 10) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 10 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 27 && dungeon[i + 1][j] == 9) dungeon[i + 1][j] = 11; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 1) dungeon[i + 1][j] = 16; if (dungeon[i][j] == 11 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 9 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 14 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 3) dungeon[i + 1][j] = 5; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 5 && dungeon[i + 1][j - 1] == 16) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 2 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 9 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 1 && dungeon[i][j - 1] == 8) dungeon[i][j - 1] = 9; if (dungeon[i][j] == 28 && dungeon[i + 1][j] == 23 && dungeon[i + 1][j + 1] == 3) dungeon[i + 1][j] = 16; } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 10) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 17 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 10 && dungeon[i + 1][j] == 4) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 17 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 29 && dungeon[i][j + 1] == 9) dungeon[i][j + 1] = 10; if (dungeon[i][j] == 13 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 9 && dungeon[i][j + 1] == 16) dungeon[i][j + 1] = 13; if (dungeon[i][j] == 10 && dungeon[i][j + 1] == 16) dungeon[i][j + 1] = 13; if (dungeon[i][j] == 16 && dungeon[i][j + 1] == 3) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 11 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 10 && dungeon[i + 1][j] == 3 && dungeon[i + 1][j - 1] == 16) dungeon[i + 1][j] = 12; if (dungeon[i][j] == 16 && dungeon[i][j + 1] == 5) dungeon[i][j + 1] = 12; if (dungeon[i][j] == 1 && dungeon[i][j + 1] == 6) dungeon[i][j + 1] = 4; if (dungeon[i][j] == 21 && dungeon[i + 1][j] == 13 && dungeon[i][j + 1] == 10) dungeon[i + 1][j + 1] = 12; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 10) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 22 && dungeon[i][j + 1] == 11) dungeon[i][j + 1] = 17; if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 28 && dungeon[i + 2][j] == 16) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 28 && dungeon[i + 1][j] == 23 && dungeon[i + 1][j + 1] == 1 && dungeon[i + 2][j] == 6) dungeon[i + 1][j] = 16; } } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == 15 && dungeon[i + 1][j] == 28 && dungeon[i + 2][j] == 16) dungeon[i + 1][j] = 23; if (dungeon[i][j] == 21 && dungeon[i + 1][j - 1] == 21 && dungeon[i + 1][j + 1] == 13 && dungeon[i + 2][j] == 2) dungeon[i + 1][j] = 17; if (dungeon[i][j] == 19 && dungeon[i + 1][j] == 15 && dungeon[i + 1][j + 1] == 12) dungeon[i + 1][j] = 17; } } } void Substitution() { for (int y = 0; y < DMAXY; y++) { for (int x = 0; x < DMAXX; x++) { if (FlipCoin(3)) { const uint8_t c = L4BTYPES[dungeon[x][y]]; if (c != 0 && !Protected.test(x, y)) { int rv = GenerateRnd(16); int i = -1; while (rv >= 0) { i++; if (i == sizeof(L4BTYPES)) { i = 0; } if (c == L4BTYPES[i]) { rv--; } } dungeon[x][y] = i; } } } } for (int y = 0; y < DMAXY; y++) { for (int x = 0; x < DMAXX; x++) { if (FlipCoin(10)) { const uint8_t c = dungeon[x][y]; if (L4BTYPES[c] == 6 && !Protected.test(x, y)) { dungeon[x][y] = PickRandomlyAmong({ 95, 96, 97 }); } } } } } /** * @brief Sets up the inside borders of the first quadrant so there are valid paths after mirroring the layout */ void PrepareInnerBorders() { for (int y = DMAXY / 2 - 1; y >= 0; y--) { for (int x = DMAXX / 2 - 1; x >= 0; x--) { if (!DungeonMask.test(x, y)) { hallok[y] = false; } else { const bool hasSouthWestRoom = y + 1 < DMAXY / 2 && DungeonMask.test(x, y + 1); const bool hasSouthRoom = x + 1 < DMAXX / 2 && y + 1 < DMAXY / 2 && DungeonMask.test(x + 1, y + 1); hallok[y] = hasSouthWestRoom && !hasSouthRoom; x = 0; } } } int ry = GenerateRnd(DMAXY / 2 - 1) + 1; do { if (hallok[ry]) { for (int x = DMAXX / 2 - 1; x >= 0; x--) { if (DungeonMask.test(x, ry)) { x = -1; ry = 0; } else { DungeonMask.set(x, ry); DungeonMask.set(x, ry + 1); } } } else { ry++; if (ry == DMAXY / 2) { ry = 1; } } } while (ry != 0); for (int x = DMAXX / 2 - 1; x >= 0; x--) { for (int y = DMAXY / 2 - 1; y >= 0; y--) { if (!DungeonMask.test(x, y)) { hallok[x] = false; } else { const bool hasSouthEastRoom = x + 1 < DMAXX / 2 && DungeonMask.test(x + 1, y); const bool hasSouthRoom = x + 1 < DMAXX / 2 && y + 1 < DMAXY / 2 && DungeonMask.test(x + 1, y + 1); hallok[x] = hasSouthEastRoom && !hasSouthRoom; y = 0; } } } int rx = GenerateRnd(DMAXX / 2 - 1) + 1; do { if (hallok[rx]) { for (int y = DMAXY / 2 - 1; y >= 0; y--) { if (DungeonMask.test(rx, y)) { y = -1; rx = 0; } else { DungeonMask.set(rx, y); DungeonMask.set(rx + 1, y); } } } else { rx++; if (rx == DMAXX / 2) { rx = 1; } } } while (rx != 0); } /** * @brief Find the number of mega tiles used by layout */ inline size_t FindArea() { // Hell layouts are mirrored based on a single quadrant, this function is called after the quadrant has been // generated but before mirroring the layout. We need to multiply by 4 to get the expected number of tiles return DungeonMask.count() * 4; } void ProtectQuads() { for (int y = 0; y < 14; y++) { for (int x = 0; x < 14; x++) { Protected.set(L4Hold.x + x, L4Hold.y + y); Protected.set(DMAXX - 1 - x - L4Hold.x, L4Hold.y + y); Protected.set(L4Hold.x + x, DMAXY - 1 - y - L4Hold.y); Protected.set(DMAXX - 1 - x - L4Hold.x, DMAXY - 1 - y - L4Hold.y); } } } void LoadDiabQuads(bool preflag) { { auto dunData = LoadFileInMem("levels\\l4data\\diab1.dun"); DiabloQuad1 = L4Hold + WorldTileDisplacement { 4, 4 }; PlaceDunTiles(dunData.get(), DiabloQuad1, 6); } { auto dunData = LoadFileInMem(preflag ? "levels\\l4data\\diab2b.dun" : "levels\\l4data\\diab2a.dun"); DiabloQuad2 = WorldTilePosition(27 - L4Hold.x, 1 + L4Hold.y); PlaceDunTiles(dunData.get(), DiabloQuad2, 6); } { auto dunData = LoadFileInMem(preflag ? "levels\\l4data\\diab3b.dun" : "levels\\l4data\\diab3a.dun"); DiabloQuad3 = WorldTilePosition(1 + L4Hold.x, 27 - L4Hold.y); PlaceDunTiles(dunData.get(), DiabloQuad3, 6); } { auto dunData = LoadFileInMem(preflag ? "levels\\l4data\\diab4b.dun" : "levels\\l4data\\diab4a.dun"); DiabloQuad4 = WorldTilePosition(28 - L4Hold.x, 28 - L4Hold.y); PlaceDunTiles(dunData.get(), DiabloQuad4, 6); } } bool IsDURightWall(char d) { if (d == 25) { return true; } if (d == 28) { return true; } if (d == 23) { return true; } return false; } bool IsDLLeftWall(char dd) { if (dd == 27) { return true; } if (dd == 26) { return true; } if (dd == 22) { return true; } return false; } void FixTransparency() { int yy = 16; for (int j = 0; j < DMAXY; j++) { int xx = 16; for (int i = 0; i < DMAXX; i++) { // BUGFIX: Should check for `j > 0` first. if (IsDURightWall(dungeon[i][j]) && dungeon[i][j - 1] == 18) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } // BUGFIX: Should check for `i + 1 < DMAXY` first. if (IsDLLeftWall(dungeon[i][j]) && dungeon[i + 1][j] == 19) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 18) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 19) { dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 24) { dTransVal[xx + 1][yy] = dTransVal[xx][yy]; dTransVal[xx][yy + 1] = dTransVal[xx][yy]; dTransVal[xx + 1][yy + 1] = dTransVal[xx][yy]; } if (dungeon[i][j] == 57) { dTransVal[xx - 1][yy] = dTransVal[xx][yy + 1]; dTransVal[xx][yy] = dTransVal[xx][yy + 1]; } if (dungeon[i][j] == 53) { dTransVal[xx][yy - 1] = dTransVal[xx + 1][yy]; dTransVal[xx][yy] = dTransVal[xx + 1][yy]; } xx += 2; } yy += 2; } } void FixCornerTiles() { for (int j = 1; j < DMAXY - 1; j++) { for (int i = 1; i < DMAXX - 1; i++) { if (dungeon[i][j] >= 18 && dungeon[i][j] <= 30) { if (dungeon[i + 1][j] < 18 || dungeon[i][j + 1] < 18) { dungeon[i][j] += 98; } } } } } /** * @brief Marks the edge of the map as solid/not part of the dungeon layout */ void CloseOuterBorders() { for (int x = 0; x < DMAXX / 2; x++) { // NOLINT(modernize-loop-convert) DungeonMask.reset(x, 0); } for (int y = 0; y < DMAXY / 2; y++) { DungeonMask.reset(0, y); } } void GeneralFix() { for (int j = 0; j < DMAXY - 1; j++) { for (int i = 0; i < DMAXX - 1; i++) { if ((dungeon[i][j] == 24 || dungeon[i][j] == 122) && dungeon[i + 1][j] == 2 && dungeon[i][j + 1] == 5) { dungeon[i][j] = 17; } } } } bool PlaceStairs(lvl_entry entry) { std::optional position; // Place stairs up position = PlaceMiniSet(L4USTAIRS); if (!position) return false; if (entry == ENTRY_MAIN) ViewPosition = position->megaToWorld() + Displacement { 6, 6 }; if (currlevel != 15) { // Place stairs down if (currlevel != 16) { if (Quests[Q_WARLORD].IsAvailable()) { if (entry == ENTRY_PREV) ViewPosition = SetPiece.position.megaToWorld() + Displacement { 7, 7 }; } else { position = PlaceMiniSet(L4DSTAIRS); if (!position) return false; if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 7, 5 }; } } // Place town warp stairs if (currlevel == 13) { position = PlaceMiniSet(L4TWARP); if (!position) return false; if (entry == ENTRY_TWARPDN) ViewPosition = position->megaToWorld() + Displacement { 6, 6 }; } } else { // Place hell gate position = PlaceMiniSet(L4PENTA2); if (!position) return false; Quests[Q_DIABLO].position = *position; if (entry == ENTRY_PREV) ViewPosition = position->megaToWorld() + Displacement { 6, 5 }; } return true; } void GenerateLevel(lvl_entry entry) { if (LevelSeeds[currlevel]) SetRndSeed(*LevelSeeds[currlevel]); while (true) { DRLG_InitTrans(); constexpr size_t Minarea = 692; do { LevelSeeds[currlevel] = GetLCGEngineState(); InitDungeonFlags(); FirstRoom(); CloseOuterBorders(); } while (FindArea() < Minarea); PrepareInnerBorders(); MirrorDungeonLayout(); MakeDmt(); FixTilesPatterns(); if (currlevel == 16) { ProtectQuads(); } if (Quests[Q_WARLORD].IsAvailable() || (currlevel == Quests[Q_BETRAYER]._qlevel && UseMultiplayerQuests())) { for (int spi = SetPieceRoom.position.x; spi < SetPieceRoom.position.x + SetPieceRoom.size.width - 1; spi++) { for (int spj = SetPieceRoom.position.y; spj < SetPieceRoom.position.y + SetPieceRoom.size.height - 1; spj++) { Protected.set(spi, spj); } } } AddWall(); FloodTransparencyValues(6); FixTransparency(); InitSetPiece(); if (currlevel == 16) { LoadDiabQuads(true); } if (PlaceStairs(entry)) break; } GeneralFix(); if (currlevel != 16) { DRLG_PlaceThemeRooms(7, 10, 6, 8, true); } ApplyShadowsPatterns(); FixCornerTiles(); Substitution(); memcpy(pdungeon, dungeon, sizeof(pdungeon)); DRLG_CheckQuests(SetPieceRoom.position); if (currlevel == 15) { const bool isGateOpen = UseMultiplayerQuests() || IsAnyOf(Quests[Q_DIABLO]._qactive, QUEST_ACTIVE, QUEST_DONE); if (!isGateOpen) L4PENTA.place(Quests[Q_DIABLO].position); for (WorldTileCoord j = 1; j < DMAXY; j++) { for (WorldTileCoord i = 1; i < DMAXX; i++) { if (IsAnyOf(dungeon[i][j], 98, 107)) { Make_SetPC({ WorldTilePosition(i - 1, j - 1), { 5, 5 } }); // Set the portal position to the location of the northmost pentagram tile. Quests[Q_BETRAYER].position = Point(i, j).megaToWorld(); } } } } if (currlevel == 16) { LoadDiabQuads(false); } } void Pass3() { DRLG_LPass3(30 - 1); } } // namespace void CreateL4Dungeon(uint32_t rseed, lvl_entry entry) { SetRndSeed(rseed); GenerateLevel(entry); Pass3(); } void LoadPreL4Dungeon(const char *path) { memset(dungeon, 30, sizeof(dungeon)); auto dunData = LoadFileInMem(path); PlaceDunTiles(dunData.get(), { 0, 0 }, 6); memcpy(pdungeon, dungeon, sizeof(pdungeon)); } void LoadL4Dungeon(const char *path, Point spawn) { LoadDungeonBase(path, spawn, 6, 30); Pass3(); } } // namespace devilution ================================================ FILE: Source/levels/drlg_l4.h ================================================ /** * @file levels/drlg_l4.h * * Interface of the hell level generation algorithms. */ #pragma once #include #include "engine/world_tile.hpp" #include "levels/gendung.h" namespace devilution { extern WorldTilePosition DiabloQuad1; extern WorldTilePosition DiabloQuad2; extern WorldTilePosition DiabloQuad3; extern WorldTilePosition DiabloQuad4; void CreateL4Dungeon(uint32_t rseed, lvl_entry entry); void LoadPreL4Dungeon(const char *path); void LoadL4Dungeon(const char *path, Point spawn); } // namespace devilution ================================================ FILE: Source/levels/dun_tile.hpp ================================================ #pragma once #include #include "utils/enum_traits.h" #define TILE_WIDTH 64 #define TILE_HEIGHT 32 namespace devilution { /** * Level tile type. * * The tile type determines data encoding and the shape. * * Each tile type has its own encoding but they all encode data in the order * of bottom-to-top (bottom row first). */ enum class TileType : uint8_t { /** * 🮆 A 32x32 square. Stored as an array of pixels. */ Square, /** * 🮆 A 32x32 square with transparency. RLE encoded. * * Each run starts with an int8_t value. * If positive, it is followed by this many pixels. * If negative, it indicates `-value` fully transparent pixels, which are omitted. * * Runs do not cross row boundaries. */ TransparentSquare, /** *🭮 Left-pointing 32x31 triangle. Encoded as 31 varying-width rows with 2 padding bytes before every even row. * We remove the padding bytes in `ReencodeDungeonCels`. * * The smallest rows (bottom and top) are 2px wide, the largest row is 32px wide (middle row). * * Encoding: * for i in [0, 30]: * - 2 unused bytes if i is even * - row (only the pixels within the triangle) */ LeftTriangle, /** * 🭬Right-pointing 32x31 triangle. Encoded as 31 varying-width rows with 2 padding bytes after every even row. * We remove the padding bytes in `ReencodeDungeonCels`. * * The smallest rows (bottom and top) are 2px wide, the largest row is 32px wide (middle row). * * Encoding: * for i in [0, 30]: * - row (only the pixels within the triangle) * - 2 unused bytes if i is even */ RightTriangle, /** * 🭓 Left-pointing 32x32 trapezoid: a 32x16 rectangle and the 16x16 bottom part of `LeftTriangle`. * * Begins with triangle part, which uses the `LeftTriangle` encoding, * and is followed by a flat array of pixels for the top rectangle part. */ LeftTrapezoid, /** * 🭞 Right-pointing 32x32 trapezoid: 32x16 rectangle and the 16x16 bottom part of `RightTriangle`. * * Begins with the triangle part, which uses the `RightTriangle` encoding, * and is followed by a flat array of pixels for the top rectangle part. */ RightTrapezoid, }; /** * Specifies the current MIN block of the level CEL file, as used during rendering of the level tiles. */ struct LevelCelBlock { uint16_t data; [[nodiscard]] bool hasValue() const { return data != 0; } [[nodiscard]] TileType type() const { return static_cast((data & 0x7000) >> 12); } /** * @brief Returns the 1-based index of the frame in `pDungeonCels`. */ [[nodiscard]] uint16_t frame() const { return data & 0xFFF; } }; enum class TileProperties : uint8_t { // clang-format off None = 0, Solid = 1 << 0, BlockLight = 1 << 1, BlockMissile = 1 << 2, Transparent = 1 << 3, TransparentLeft = 1 << 4, TransparentRight = 1 << 5, Trap = 1 << 7, // clang-format on }; use_enum_as_flags(TileProperties); struct MICROS { LevelCelBlock mt[16]; }; /** Width of a tile rendering primitive. */ constexpr int_fast16_t DunFrameWidth = TILE_WIDTH / 2; /** Height of a tile rendering primitive (except triangles). */ constexpr int_fast16_t DunFrameHeight = TILE_HEIGHT; constexpr int_fast16_t DunFrameTriangleHeight = 31; constexpr size_t ReencodedTriangleFrameSize = 544 - 32; constexpr size_t ReencodedTrapezoidFrameSize = 800 - 16; /** * @return Returns the center of the sprite relative to the center of the tile. */ constexpr int CalculateSpriteTileCenterX(int width) { return (width - TILE_WIDTH) / 2; } } // namespace devilution ================================================ FILE: Source/levels/gendung.cpp ================================================ #include "levels/gendung.h" #include #include #include #include #include #include #include #include #include #include #include "engine/clx_sprite.hpp" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "items.h" #include "levels/drlg_l1.h" #include "levels/drlg_l2.h" #include "levels/drlg_l3.h" #include "levels/drlg_l4.h" #include "levels/reencode_dun_cels.hpp" #include "levels/town.h" #include "lighting.h" #include "monster.h" #include "objects.h" #include "utils/algorithm/container.hpp" #include "utils/bitset2d.hpp" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/status_macros.hpp" namespace devilution { Bitset2d DungeonMask; uint8_t dungeon[DMAXX][DMAXY]; uint8_t pdungeon[DMAXX][DMAXY]; Bitset2d Protected; WorldTileRectangle SetPieceRoom; WorldTileRectangle SetPiece; OptionalOwnedClxSpriteList pSpecialCels; std::unique_ptr pMegaTiles; std::unique_ptr pDungeonCels; TileProperties SOLData[MAXTILES]; WorldTilePosition dminPosition; WorldTilePosition dmaxPosition; dungeon_type leveltype; uint8_t currlevel; bool setlevel; _setlevels setlvlnum; dungeon_type setlvltype; Point ViewPosition; uint_fast8_t MicroTileLen; int8_t TransVal; std::array TransList; uint16_t dPiece[MAXDUNX][MAXDUNY]; MICROS DPieceMicros[MAXTILES]; int8_t dTransVal[MAXDUNX][MAXDUNY]; uint8_t dLight[MAXDUNX][MAXDUNY]; uint8_t dPreLight[MAXDUNX][MAXDUNY]; DungeonFlag dFlags[MAXDUNX][MAXDUNY]; int8_t dPlayer[MAXDUNX][MAXDUNY]; int16_t dMonster[MAXDUNX][MAXDUNY]; int8_t dCorpse[MAXDUNX][MAXDUNY]; int8_t dObject[MAXDUNX][MAXDUNY]; int8_t dSpecial[MAXDUNX][MAXDUNY]; int themeCount; THEME_LOC themeLoc[MAXTHEMES]; namespace { std::unique_ptr LoadMinData(size_t &tileCount) { switch (leveltype) { case DTYPE_TOWN: { auto min = LoadFileInMemWithStatus("nlevels\\towndata\\town.min", &tileCount); if (!min.has_value()) { return LoadFileInMem("levels\\towndata\\town.min", &tileCount); } else { return std::move(*min); } } case DTYPE_CATHEDRAL: return LoadFileInMem("levels\\l1data\\l1.min", &tileCount); case DTYPE_CATACOMBS: return LoadFileInMem("levels\\l2data\\l2.min", &tileCount); case DTYPE_CAVES: return LoadFileInMem("levels\\l3data\\l3.min", &tileCount); case DTYPE_HELL: return LoadFileInMem("levels\\l4data\\l4.min", &tileCount); case DTYPE_NEST: return LoadFileInMem("nlevels\\l6data\\l6.min", &tileCount); case DTYPE_CRYPT: return LoadFileInMem("nlevels\\l5data\\l5.min", &tileCount); default: app_fatal("LoadMinData"); } } /** * @brief Starting from the origin point determine how much floor space is available with the given bounds * * Essentially looks for the widest/tallest rectangular area of at least the minimum size, but due to a weird/buggy * bounds check can return an area smaller than the available width/height. * * @param floor what value defines floor tiles within a dungeon * @param origin starting point for the search * @param minSize minimum allowable value for both dimensions * @param maxSize maximum allowable value for both dimensions * @return how much width/height is available for a theme room or an empty optional if there's not enough space */ std::optional GetSizeForThemeRoom(uint8_t floor, WorldTilePosition origin, WorldTileCoord minSize, WorldTileCoord maxSize) { if (origin.x + maxSize > DMAXX && origin.y + maxSize > DMAXY) { return {}; // Original broken bounds check, avoids lower right corner } if (IsNearThemeRoom(origin)) { return {}; } const WorldTileCoord maxWidth = std::min(maxSize, DMAXX - origin.x); const WorldTileCoord maxHeight = std::min(maxSize, DMAXY - origin.y); WorldTileSize room { maxWidth, maxHeight }; for (WorldTileCoord i = 0; i < maxSize; i++) { WorldTileCoord width = i < room.height ? i : 0; if (i < maxHeight) { while (width < room.width) { if (dungeon[origin.x + width][origin.y + i] != floor) break; width++; } } WorldTileCoord height = i < room.width ? i : 0; if (i < maxWidth) { while (height < room.height) { if (dungeon[origin.x + i][origin.y + height] != floor) break; height++; } } if (width < minSize || height < minSize) { if (i < minSize) return {}; break; } room = { std::min(room.width, width), std::min(room.height, height) }; } return room - 2; } void CreateThemeRoom(int themeIndex) { const int lx = themeLoc[themeIndex].room.position.x; const int ly = themeLoc[themeIndex].room.position.y; const int hx = lx + themeLoc[themeIndex].room.size.width; const int hy = ly + themeLoc[themeIndex].room.size.height; for (int yy = ly; yy < hy; yy++) { for (int xx = lx; xx < hx; xx++) { if (leveltype == DTYPE_CATACOMBS) { if (yy == ly || yy == hy - 1) { dungeon[xx][yy] = 2; } else if (xx == lx || xx == hx - 1) { dungeon[xx][yy] = 1; } else { dungeon[xx][yy] = 3; } } if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST)) { if (yy == ly || yy == hy - 1) { dungeon[xx][yy] = 134; } else if (xx == lx || xx == hx - 1) { dungeon[xx][yy] = 137; } else { dungeon[xx][yy] = 7; } } if (leveltype == DTYPE_HELL) { if (yy == ly || yy == hy - 1) { dungeon[xx][yy] = 2; } else if (xx == lx || xx == hx - 1) { dungeon[xx][yy] = 1; } else { dungeon[xx][yy] = 6; } } } } if (leveltype == DTYPE_CATACOMBS) { dungeon[lx][ly] = 8; dungeon[hx - 1][ly] = 7; dungeon[lx][hy - 1] = 9; dungeon[hx - 1][hy - 1] = 6; } if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST)) { dungeon[lx][ly] = 150; dungeon[hx - 1][ly] = 151; dungeon[lx][hy - 1] = 152; dungeon[hx - 1][hy - 1] = 138; } if (leveltype == DTYPE_HELL) { dungeon[lx][ly] = 9; dungeon[hx - 1][ly] = 16; dungeon[lx][hy - 1] = 15; dungeon[hx - 1][hy - 1] = 12; } if (leveltype == DTYPE_CATACOMBS) { if (FlipCoin()) dungeon[hx - 1][(ly + hy) / 2] = 4; else dungeon[(lx + hx) / 2][hy - 1] = 5; } if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST)) { if (FlipCoin()) dungeon[hx - 1][(ly + hy) / 2] = 147; else dungeon[(lx + hx) / 2][hy - 1] = 146; } if (leveltype == DTYPE_HELL) { if (FlipCoin()) { const int yy = (ly + hy) / 2; dungeon[hx - 1][yy - 1] = 53; dungeon[hx - 1][yy] = 6; dungeon[hx - 1][yy + 1] = 52; dungeon[hx - 2][yy - 1] = 54; } else { const int xx = (lx + hx) / 2; dungeon[xx - 1][hy - 1] = 57; dungeon[xx][hy - 1] = 6; dungeon[xx + 1][hy - 1] = 56; dungeon[xx][hy - 2] = 59; dungeon[xx - 1][hy - 2] = 58; } } } bool IsFloor(Point p, uint8_t floorID) { const int i = (p.x - 16) / 2; const int j = (p.y - 16) / 2; if (i < 0 || i >= DMAXX) return false; if (j < 0 || j >= DMAXY) return false; return dungeon[i][j] == floorID; } void FillTransparencyValues(Point floor, uint8_t floorID) { const Direction allDirections[] = { Direction::North, Direction::South, Direction::East, Direction::West, Direction::NorthEast, Direction::NorthWest, Direction::SouthEast, Direction::SouthWest, }; // We only fill in the surrounding tiles if they are not floor tiles // because they would otherwise not be visited by the span filling algorithm for (const Direction dir : allDirections) { const Point adjacent = floor + dir; if (!IsFloor(adjacent, floorID)) dTransVal[adjacent.x][adjacent.y] = TransVal; } dTransVal[floor.x][floor.y] = TransVal; } void FindTransparencyValues(Point floor, uint8_t floorID) { // Algorithm adapted from https://en.wikipedia.org/wiki/Flood_fill#Span_Filling // Modified to include diagonally adjacent tiles that would otherwise not be visited // Also, Wikipedia's selection for the initial seed is incorrect struct Seed { int scanStart; int scanEnd; int y; int dy; }; std::stack> seedStack; seedStack.push({ floor.x, floor.x + 1, floor.y, 1 }); const auto isInside = [floorID](int x, int y) { if (dTransVal[x][y] != 0) return false; return IsFloor({ x, y }, floorID); }; const auto set = [floorID](int x, int y) { FillTransparencyValues({ x, y }, floorID); }; const Displacement left = { -1, 0 }; const Displacement right = { 1, 0 }; const auto checkDiagonals = [&](Point p, Displacement direction) { const Point up = p + Displacement { 0, -1 }; const Point upOver = up + direction; if (!isInside(up.x, up.y) && isInside(upOver.x, upOver.y)) seedStack.push({ upOver.x, upOver.x + 1, upOver.y, -1 }); const Point down = p + Displacement { 0, 1 }; const Point downOver = down + direction; if (!isInside(down.x, down.y) && isInside(downOver.x, downOver.y)) seedStack.push(Seed { downOver.x, downOver.x + 1, downOver.y, 1 }); }; while (!seedStack.empty()) { const auto [scanStart, scanEnd, y, dy] = seedStack.top(); seedStack.pop(); int scanLeft = scanStart; if (isInside(scanLeft, y)) { while (isInside(scanLeft - 1, y)) { set(scanLeft - 1, y); scanLeft--; } checkDiagonals({ scanLeft, y }, left); } if (scanLeft < scanStart) seedStack.push(Seed { scanLeft, scanStart - 1, y - dy, -dy }); int scanRight = scanStart; while (scanRight < scanEnd) { while (isInside(scanRight, y)) { set(scanRight, y); scanRight++; } seedStack.push(Seed { scanLeft, scanRight - 1, y + dy, dy }); if (scanRight - 1 > scanEnd) seedStack.push(Seed { scanEnd + 1, scanRight - 1, y - dy, -dy }); if (scanLeft < scanRight) checkDiagonals({ scanRight - 1, y }, right); while (scanRight < scanEnd && !isInside(scanRight, y)) scanRight++; scanLeft = scanRight; if (scanLeft < scanEnd) checkDiagonals({ scanLeft, y }, left); } } } void InitGlobals() { memset(dFlags, 0, sizeof(dFlags)); memset(dPlayer, 0, sizeof(dPlayer)); memset(dMonster, 0, sizeof(dMonster)); memset(dCorpse, 0, sizeof(dCorpse)); memset(dItem, 0, sizeof(dItem)); memset(dObject, 0, sizeof(dObject)); memset(dSpecial, 0, sizeof(dSpecial)); uint8_t defaultLight = leveltype == DTYPE_TOWN ? 0 : 15; #ifdef _DEBUG if (DisableLighting) defaultLight = 0; #endif memset(dLight, defaultLight, sizeof(dLight)); DRLG_InitTrans(); dminPosition = WorldTilePosition(0, 0).megaToWorld(); dmaxPosition = WorldTilePosition(40, 40).megaToWorld(); SetPieceRoom = { { 0, 0 }, { 0, 0 } }; SetPiece = { { 0, 0 }, { 0, 0 } }; } } // namespace #ifdef BUILD_TESTING std::optional GetSizeForThemeRoom() { return GetSizeForThemeRoom(0, { 0, 0 }, 5, 10); } #endif dungeon_type GetLevelType(int level) { if (level == 0) return DTYPE_TOWN; if (level <= 4) return DTYPE_CATHEDRAL; if (level <= 8) return DTYPE_CATACOMBS; if (level <= 12) return DTYPE_CAVES; if (level <= 16) return DTYPE_HELL; if (level <= 20) return DTYPE_NEST; if (level <= 24) return DTYPE_CRYPT; return DTYPE_NONE; } void CreateDungeon(uint32_t rseed, lvl_entry entry) { InitGlobals(); switch (leveltype) { case DTYPE_TOWN: CreateTown(entry); break; case DTYPE_CATHEDRAL: case DTYPE_CRYPT: CreateL5Dungeon(rseed, entry); break; case DTYPE_CATACOMBS: CreateL2Dungeon(rseed, entry); break; case DTYPE_CAVES: case DTYPE_NEST: CreateL3Dungeon(rseed, entry); break; case DTYPE_HELL: CreateL4Dungeon(rseed, entry); break; default: app_fatal("Invalid level type"); } Make_SetPC(SetPiece); } tl::expected LoadLevelSOLData() { switch (leveltype) { case DTYPE_TOWN: if (!LoadFileInMemWithStatus("nlevels\\towndata\\town.sol", SOLData).has_value()) { RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\towndata\\town.sol", SOLData)); } break; case DTYPE_CATHEDRAL: RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l1data\\l1.sol", SOLData)); // Fix incorrectly marked arched tiles SOLData[9] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[15] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[16] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[20] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[21] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[27] |= TileProperties::BlockMissile; SOLData[28] |= TileProperties::BlockMissile; SOLData[51] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[56] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[58] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[61] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[63] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[65] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[72] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[208] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[247] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[253] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[257] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[323] |= TileProperties::BlockLight | TileProperties::BlockMissile; SOLData[403] |= TileProperties::BlockLight; // Fix incorrectly marked pillar tile SOLData[24] |= TileProperties::BlockLight; // Fix incorrectly marked wall tile SOLData[450] |= TileProperties::BlockLight | TileProperties::BlockMissile; break; case DTYPE_CATACOMBS: RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l2data\\l2.sol", SOLData)); break; case DTYPE_CAVES: RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l3data\\l3.sol", SOLData)); // The graphics for tile 48 sub-tile 171 frame 461 are partly incorrect, as they // have a few pixels that should belong to the solid tile 49 instead. // Marks the sub-tile as "BlockMissile" to avoid treating it as a floor during rendering. SOLData[170] |= TileProperties::BlockMissile; // Fence sub-tiles 481 and 487 are substitutes for solid sub-tiles 473 and 479 // but are not marked as solid. SOLData[481] |= TileProperties::Solid; SOLData[487] |= TileProperties::Solid; break; case DTYPE_HELL: RETURN_IF_ERROR(LoadFileInMemWithStatus("levels\\l4data\\l4.sol", SOLData)); SOLData[210] = TileProperties::None; // Tile is incorrectly marked as being solid break; case DTYPE_NEST: RETURN_IF_ERROR(LoadFileInMemWithStatus("nlevels\\l6data\\l6.sol", SOLData)); break; case DTYPE_CRYPT: RETURN_IF_ERROR(LoadFileInMemWithStatus("nlevels\\l5data\\l5.sol", SOLData)); SOLData[142] = TileProperties::None; // Tile is incorrectly marked as being solid break; default: return tl::make_unexpected("LoadLevelSOLData"); } return {}; } void SetDungeonMicros(std::unique_ptr &dungeonCels, uint_fast8_t µTileLen) { microTileLen = 10; size_t blocks = 10; if (leveltype == DTYPE_TOWN) { microTileLen = 16; blocks = 16; } else if (leveltype == DTYPE_HELL) { microTileLen = 12; blocks = 16; } size_t tileCount; const std::unique_ptr levelPieces = LoadMinData(tileCount); ankerl::unordered_dense::map frameToTypeMap; frameToTypeMap.reserve(4096); for (size_t levelPieceId = 0; levelPieceId < tileCount / blocks; levelPieceId++) { uint16_t *pieces = &levelPieces[blocks * levelPieceId]; for (uint32_t block = 0; block < blocks; block++) { const LevelCelBlock levelCelBlock { Swap16LE(pieces[blocks - 2 + (block & 1) - (block & 0xE)]) }; DPieceMicros[levelPieceId].mt[block] = levelCelBlock; if (levelCelBlock.hasValue()) { if (const auto it = frameToTypeMap.find(levelCelBlock.frame()); it == frameToTypeMap.end()) { frameToTypeMap.emplace_hint(it, levelCelBlock.frame(), DunFrameInfo { static_cast(block), levelCelBlock.type(), SOLData[levelPieceId] }); } } } } std::vector> frameToTypeList = std::move(frameToTypeMap).extract(); c_sort(frameToTypeList, [](const std::pair &a, const std::pair &b) { return a.first < b.first; }); ReencodeDungeonCels(dungeonCels, frameToTypeList); std::vector> celBlockAdjustments = ComputeCelBlockAdjustments(frameToTypeList); if (celBlockAdjustments.size() == 0) return; for (size_t levelPieceId = 0; levelPieceId < tileCount / blocks; levelPieceId++) { for (uint32_t block = 0; block < blocks; block++) { LevelCelBlock &levelCelBlock = DPieceMicros[levelPieceId].mt[block]; const uint16_t frame = levelCelBlock.frame(); const auto pair = std::make_pair(frame, frame); const auto it = std::upper_bound(celBlockAdjustments.begin(), celBlockAdjustments.end(), pair, [](std::pair p1, std::pair p2) { return p1.first < p2.first; }); if (it != celBlockAdjustments.end()) { levelCelBlock.data -= it->second; } } } } void DRLG_InitTrans() { memset(dTransVal, 0, sizeof(dTransVal)); TransList = {}; // TODO duplicate reset in InitLighting() TransVal = 1; } void DRLG_RectTrans(WorldTileRectangle area) { const WorldTilePosition position = area.position; const WorldTileSize size = area.size; for (int j = position.y; j <= position.y + size.height; j++) { for (int i = position.x; i <= position.x + size.width; i++) { dTransVal[i][j] = TransVal; } } TransVal++; } void DRLG_MRectTrans(WorldTileRectangle area) { DRLG_RectTrans({ area.position.megaToWorld() + WorldTileDisplacement { 1, 1 }, area.size * 2 - 1 }); } void DRLG_MRectTrans(WorldTilePosition origin, WorldTilePosition extent) { DRLG_MRectTrans({ origin, WorldTileSize(extent.x - origin.x, extent.y - origin.y) }); } void DRLG_CopyTrans(int sx, int sy, int dx, int dy) { dTransVal[dx][dy] = dTransVal[sx][sy]; } void LoadTransparency(const uint16_t *dunData) { WorldTileSize size = GetDunSize(dunData); const int layer2Offset = 2 + size.width * size.height; // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *transparentLayer = &dunData[layer2Offset + size.width * size.height * 3]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { dTransVal[16 + i][16 + j] = static_cast(Swap16LE(*transparentLayer)); transparentLayer++; } } } void LoadDungeonBase(const char *path, Point spawn, int floorId, int dirtId) { ViewPosition = spawn; InitGlobals(); memset(dungeon, dirtId, sizeof(dungeon)); auto dunData = LoadFileInMem(path); PlaceDunTiles(dunData.get(), { 0, 0 }, floorId); LoadTransparency(dunData.get()); SetMapMonsters(dunData.get(), Point(0, 0).megaToWorld()); InitAllMonsterGFX(); SetMapObjects(dunData.get(), 0, 0); } void Make_SetPC(WorldTileRectangle area) { const WorldTilePosition position = area.position.megaToWorld(); const WorldTileSize size = area.size * 2; for (unsigned j = 0; j < size.height; j++) { for (unsigned i = 0; i < size.width; i++) { dFlags[position.x + i][position.y + j] |= DungeonFlag::Populated; } } } std::optional PlaceMiniSet(const Miniset &miniset, int tries, bool drlg1Quirk) { const int sw = miniset.size.width; const int sh = miniset.size.height; Point position { GenerateRnd(DMAXX - sw), GenerateRnd(DMAXY - sh) }; for (int i = 0; i < tries; i++, position.x++) { if (position.x == DMAXX - sw) { position.x = 0; position.y++; if (position.y == DMAXY - sh) { position.y = 0; } } // Limit the position of SetPieces for compatibility with Diablo bug if (drlg1Quirk) { bool valid = true; if (position.x <= 12) { position.x++; valid = false; } if (position.y <= 12) { position.y++; valid = false; } if (!valid) { continue; } } if (SetPieceRoom.contains(position)) continue; if (!miniset.matches(position)) continue; miniset.place(position); return position; } return {}; } void PlaceDunTiles(const uint16_t *dunData, Point position, int floorId) { const WorldTileSize size = GetDunSize(dunData); const uint16_t *tileLayer = &dunData[2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto tileId = static_cast(Swap16LE(tileLayer[j * size.width + i])); if (tileId != 0) { dungeon[position.x + i][position.y + j] = tileId; Protected.set(position.x + i, position.y + j); } else if (floorId != 0) { dungeon[position.x + i][position.y + j] = floorId; } } } } void DRLG_PlaceThemeRooms(int minSize, int maxSize, int floor, int freq, bool rndSize) { themeCount = 0; memset(themeLoc, 0, sizeof(*themeLoc)); for (WorldTileCoord j = 0; j < DMAXY; j++) { for (WorldTileCoord i = 0; i < DMAXX; i++) { if (dungeon[i][j] == floor && FlipCoin(freq)) { std::optional themeSize = GetSizeForThemeRoom(floor, { i, j }, minSize, maxSize); if (!themeSize) continue; if (rndSize) { const int min = minSize - 2; const int max = maxSize - 2; themeSize->width = min + GenerateRnd(GenerateRnd(themeSize->width - min + 1)); if (themeSize->width < min || themeSize->width > max) themeSize->width = min; themeSize->height = min + GenerateRnd(GenerateRnd(themeSize->height - min + 1)); if (themeSize->height < min || themeSize->height > max) themeSize->height = min; } THEME_LOC &theme = themeLoc[themeCount]; theme.room = { WorldTilePosition { i, j } + Direction::South, *themeSize }; if (IsAnyOf(leveltype, DTYPE_CAVES, DTYPE_NEST)) { DRLG_RectTrans({ (theme.room.position + Direction::South).megaToWorld(), theme.room.size * 2 - 5 }); } else { DRLG_MRectTrans({ theme.room.position, theme.room.size - 1 }); } theme.ttval = TransVal - 1; CreateThemeRoom(themeCount); themeCount++; } } } } // namespace void DRLG_HoldThemeRooms() { for (int i = 0; i < themeCount; i++) { for (int y = themeLoc[i].room.position.y; y < themeLoc[i].room.position.y + themeLoc[i].room.size.height - 1; y++) { for (int x = themeLoc[i].room.position.x; x < themeLoc[i].room.position.x + themeLoc[i].room.size.width - 1; x++) { const int xx = 2 * x + 16; const int yy = 2 * y + 16; dFlags[xx][yy] |= DungeonFlag::Populated; dFlags[xx + 1][yy] |= DungeonFlag::Populated; dFlags[xx][yy + 1] |= DungeonFlag::Populated; dFlags[xx + 1][yy + 1] |= DungeonFlag::Populated; } } } } WorldTileSize GetDunSize(const uint16_t *dunData) { return WorldTileSize(static_cast(Swap16LE(dunData[0])), static_cast(Swap16LE(dunData[1]))); } void DRLG_LPass3(int lv) { { const MegaTile mega = pMegaTiles[lv]; const int v1 = Swap16LE(mega.micro1); const int v2 = Swap16LE(mega.micro2); const int v3 = Swap16LE(mega.micro3); const int v4 = Swap16LE(mega.micro4); for (int j = 0; j < MAXDUNY; j += 2) { for (int i = 0; i < MAXDUNX; i += 2) { dPiece[i + 0][j + 0] = v1; dPiece[i + 1][j + 0] = v2; dPiece[i + 0][j + 1] = v3; dPiece[i + 1][j + 1] = v4; } } } int yy = 16; for (int j = 0; j < DMAXY; j++) { int xx = 16; for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) const int tileId = dungeon[i][j] - 1; const MegaTile mega = pMegaTiles[tileId]; dPiece[xx + 0][yy + 0] = Swap16LE(mega.micro1); dPiece[xx + 1][yy + 0] = Swap16LE(mega.micro2); dPiece[xx + 0][yy + 1] = Swap16LE(mega.micro3); dPiece[xx + 1][yy + 1] = Swap16LE(mega.micro4); xx += 2; } yy += 2; } } bool IsNearThemeRoom(WorldTilePosition testPosition) { for (int i = 0; i < themeCount; i++) { if (WorldTileRectangle(themeLoc[i].room.position - WorldTileDisplacement { 2 }, themeLoc[i].room.size + 5).contains(testPosition)) return true; } return false; } void InitLevels() { currlevel = 0; leveltype = DTYPE_TOWN; setlevel = false; } void FloodTransparencyValues(uint8_t floorID) { int yy = 16; for (int j = 0; j < DMAXY; j++) { int xx = 16; for (int i = 0; i < DMAXX; i++) { if (dungeon[i][j] == floorID && dTransVal[xx][yy] == 0) { FindTransparencyValues({ xx, yy }, floorID); TransVal++; } xx += 2; } yy += 2; } } tl::expected ParseDungeonType(std::string_view value) { if (value.empty()) return DTYPE_NONE; if (value == "DTYPE_TOWN") return DTYPE_TOWN; if (value == "DTYPE_CATHEDRAL") return DTYPE_CATHEDRAL; if (value == "DTYPE_CATACOMBS") return DTYPE_CATACOMBS; if (value == "DTYPE_CAVES") return DTYPE_CAVES; if (value == "DTYPE_HELL") return DTYPE_HELL; if (value == "DTYPE_NEST") return DTYPE_NEST; if (value == "DTYPE_CRYPT") return DTYPE_CRYPT; return tl::make_unexpected("Unknown enum value"); } tl::expected<_setlevels, std::string> ParseSetLevel(std::string_view value) { const std::optional<_setlevels> enumValueOpt = magic_enum::enum_cast<_setlevels>(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } } // namespace devilution ================================================ FILE: Source/levels/gendung.h ================================================ /** * @file gendung.h * * Interface of general dungeon generation code. */ #pragma once #include #include #include #include #include #include #include #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/render/scrollrt.h" #include "engine/world_tile.hpp" #include "levels/dun_tile.hpp" #include "levels/gendung_defs.hpp" #include "utils/attributes.h" #include "utils/bitset2d.hpp" #include "utils/enum_traits.h" namespace devilution { #define MAXTHEMES 50 #define MAXTILES 1379 enum _setlevels : int8_t { SL_NONE, SL_SKELKING, SL_BONECHAMB, SL_MAZE, SL_POISONWATER, SL_VILEBETRAYER, SL_ARENA_CHURCH, SL_ARENA_HELL, SL_ARENA_CIRCLE_OF_LIFE, SL_FIRST_ARENA = SL_ARENA_CHURCH, SL_LAST = SL_ARENA_CIRCLE_OF_LIFE, }; inline bool IsArenaLevel(_setlevels setLevel) { switch (setLevel) { case SL_ARENA_CHURCH: case SL_ARENA_HELL: case SL_ARENA_CIRCLE_OF_LIFE: return true; default: return false; } } tl::expected ParseDungeonType(std::string_view value); tl::expected<_setlevels, std::string> ParseSetLevel(std::string_view value); enum class DungeonFlag : uint8_t { // clang-format off None = 0, // Only used by lighting/automap Missile = 1 << 0, Visible = 1 << 1, DeadPlayer = 1 << 2, Populated = 1 << 3, MissileFireWall = 1 << 4, MissileLightningWall = 1 << 5, Lit = 1 << 6, Explored = 1 << 7, SavedFlags = (Populated | Lit | Explored), // ~(Missile | Visible | DeadPlayer) LoadedFlags = (Missile | Visible | DeadPlayer | Populated | Lit | Explored) // clang-format on }; use_enum_as_flags(DungeonFlag); enum _difficulty : uint8_t { DIFF_NORMAL, DIFF_NIGHTMARE, DIFF_HELL, DIFF_LAST = DIFF_HELL, }; struct THEME_LOC { RectangleOf room; int8_t ttval; }; struct MegaTile { uint16_t micro1; uint16_t micro2; uint16_t micro3; uint16_t micro4; }; struct ShadowStruct { uint8_t strig; uint8_t s1; uint8_t s2; uint8_t s3; uint8_t nv1; uint8_t nv2; uint8_t nv3; }; /** Reprecents what tiles are being utilized in the generated map. */ extern Bitset2d DungeonMask; /** Contains the tile IDs of the map. */ extern DVL_API_FOR_TEST uint8_t dungeon[DMAXX][DMAXY]; /** Contains a backup of the tile IDs of the map. */ extern uint8_t pdungeon[DMAXX][DMAXY]; /** Tile that may not be overwritten by the level generator */ extern Bitset2d Protected; extern WorldTileRectangle SetPieceRoom; /** Specifies the active set quest piece in coordinate. */ extern WorldTileRectangle SetPiece; extern OptionalOwnedClxSpriteList pSpecialCels; /** Specifies the tile definitions of the active dungeon type; (e.g. levels/l1data/l1.til). */ extern DVL_API_FOR_TEST std::unique_ptr pMegaTiles; extern DVL_API_FOR_TEST std::unique_ptr pDungeonCels; /** * List tile properties */ extern DVL_API_FOR_TEST TileProperties SOLData[MAXTILES]; /** Specifies the minimum X,Y-coordinates of the map. */ extern WorldTilePosition dminPosition; /** Specifies the maximum X,Y-coordinates of the map. */ extern WorldTilePosition dmaxPosition; /** Specifies the active dungeon type of the current game. */ extern DVL_API_FOR_TEST dungeon_type leveltype; /** Specifies the active dungeon level of the current game. */ extern DVL_API_FOR_TEST uint8_t currlevel; extern bool setlevel; /** Specifies the active quest level of the current game. */ extern _setlevels setlvlnum; /** Specifies the dungeon type of the active quest level of the current game. */ extern dungeon_type setlvltype; /** Specifies the player viewpoint X,Y-coordinates of the map. */ extern DVL_API_FOR_TEST Point ViewPosition; extern uint_fast8_t MicroTileLen; extern int8_t TransVal; /** Specifies the active transparency indices. */ extern std::array TransList; /** Contains the piece IDs of each tile on the map. */ extern DVL_API_FOR_TEST uint16_t dPiece[MAXDUNX][MAXDUNY]; /** Map of micros that comprises a full tile for any given dungeon piece. */ extern DVL_API_FOR_TEST MICROS DPieceMicros[MAXTILES]; /** Specifies the transparency at each coordinate of the map. */ extern DVL_API_FOR_TEST int8_t dTransVal[MAXDUNX][MAXDUNY]; /** Current realtime lighting. Per tile. */ extern DVL_API_FOR_TEST uint8_t dLight[MAXDUNX][MAXDUNY]; /** Precalculated static lights. dLight uses this as a base before applying lights. Per tile. */ extern uint8_t dPreLight[MAXDUNX][MAXDUNY]; /** Holds various information about dungeon tiles, @see DungeonFlag */ extern DungeonFlag dFlags[MAXDUNX][MAXDUNY]; /** Contains the player numbers (players array indices) of the map. negative id indicates player moving. */ extern int8_t dPlayer[MAXDUNX][MAXDUNY]; /** * Contains the NPC numbers of the map. The NPC number represents a * towner number (towners array index) in Tristram and a monster number * (monsters array index) in the dungeon. * Negative id indicates monsters moving. */ extern int16_t dMonster[MAXDUNX][MAXDUNY]; /** * Contains the dead numbers (deads array indices) and dead direction of * the map, encoded as specified by the pseudo-code below. * dDead[x][y] & 0x1F - index of dead * dDead[x][y] >> 0x5 - direction */ extern DVL_API_FOR_TEST int8_t dCorpse[MAXDUNX][MAXDUNY]; /** * Contains the object numbers (objects array indices) of the map. * Large objects have negative id for their extended area. */ extern DVL_API_FOR_TEST int8_t dObject[MAXDUNX][MAXDUNY]; /** * Contains the arch frame numbers of the map from the special tileset * (e.g. "levels/l1data/l1s"). Note, the special tileset of Tristram (i.e. * "levels/towndata/towns") contains trees rather than arches. */ extern int8_t dSpecial[MAXDUNX][MAXDUNY]; extern int themeCount; extern THEME_LOC themeLoc[MAXTHEMES]; #ifdef BUILD_TESTING std::optional GetSizeForThemeRoom(); #endif dungeon_type GetLevelType(int level); void CreateDungeon(uint32_t rseed, lvl_entry entry); DVL_ALWAYS_INLINE constexpr bool InDungeonBounds(Point position) { return position.x >= 0 && position.x < MAXDUNX && position.y >= 0 && position.y < MAXDUNY; } /** * @brief Checks if a given tile contains at least one missile * @param position Coordinates of the dungeon tile to check * @return true if a missile exists at this position */ constexpr bool TileContainsMissile(Point position) { return InDungeonBounds(position) && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Missile); } /** * @brief Checks if a given tile contains a player corpse * @param position Coordinates of the dungeon tile to check * @return true if a dead player exists at this position */ constexpr bool TileContainsDeadPlayer(Point position) { return InDungeonBounds(position) && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::DeadPlayer); } /** * @brief Check if a given tile contains a decorative object (or similar non-pathable set piece) * * This appears to include stairs so that monsters do not spawn or path onto them, but players can path to them to navigate between layers * * @param position Coordinates of the dungeon tile to check * @return true if a set piece was spawned at this position */ constexpr bool TileContainsSetPiece(Point position) { return InDungeonBounds(position) && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Populated); } /** * @brief Checks if any player can currently see this tile * * Currently only used by monster AI routines so basic monsters out of sight can be ignored until they're likely to interact with the player * * @param position Coordinates of the dungeon tile to check * @return true if the tile is within at least one players vision */ constexpr bool IsTileVisible(Point position) { return InDungeonBounds(position) && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Visible); } /** * @brief Checks if a light source is illuminating this tile * @param position Coordinates of the dungeon tile to check * @return true if the tile is within the radius of at least one light source */ constexpr bool IsTileLit(Point position) { return InDungeonBounds(position) && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Lit); } struct Miniset { WorldTileSize size; /* these are indexed as [y][x] */ uint8_t search[6][6]; uint8_t replace[6][6]; /** * @param position Coordinates of the dungeon tile to check * @param respectProtected Match bug from Crypt levels if false */ bool matches(WorldTilePosition position, bool respectProtected = true) const { for (WorldTileCoord yy = 0; yy < size.height; yy++) { for (WorldTileCoord xx = 0; xx < size.width; xx++) { if (search[yy][xx] != 0 && dungeon[xx + position.x][yy + position.y] != search[yy][xx]) return false; if (respectProtected && Protected.test(xx + position.x, yy + position.y)) return false; } } return true; } void place(WorldTilePosition position, bool protect = false) const { for (WorldTileCoord y = 0; y < size.height; y++) { for (WorldTileCoord x = 0; x < size.width; x++) { if (replace[y][x] == 0) continue; dungeon[x + position.x][y + position.y] = replace[y][x]; if (protect) Protected.set(x + position.x, y + position.y); } } } }; [[nodiscard]] DVL_ALWAYS_INLINE bool TileHasAny(Point coords, TileProperties property) { return HasAnyOf(SOLData[dPiece[coords.x][coords.y]], property); } tl::expected LoadLevelSOLData(); void SetDungeonMicros(std::unique_ptr &dungeonCels, uint_fast8_t µTileLen); void DRLG_InitTrans(); void DRLG_MRectTrans(WorldTilePosition origin, WorldTilePosition extent); void DRLG_MRectTrans(WorldTileRectangle area); void DRLG_RectTrans(WorldTileRectangle area); void DRLG_CopyTrans(int sx, int sy, int dx, int dy); void LoadTransparency(const uint16_t *dunData); void LoadDungeonBase(const char *path, Point spawn, int floorId, int dirtId); void Make_SetPC(WorldTileRectangle area); /** * @param miniset The miniset to place * @param tries Tiles to try, 1600 will scan the full map * @param drlg1Quirk Match buggy behaviour of Diablo's Cathedral */ std::optional PlaceMiniSet(const Miniset &miniset, int tries = 199, bool drlg1Quirk = false); void PlaceDunTiles(const uint16_t *dunData, Point position, int floorId = 0); void DRLG_PlaceThemeRooms(int minSize, int maxSize, int floor, int freq, bool rndSize); void DRLG_HoldThemeRooms(); /** * @brief Returns the size in tiles of the specified ".dun" Data */ WorldTileSize GetDunSize(const uint16_t *dunData); void DRLG_LPass3(int lv); /** * @brief Checks if a theme room is located near the target point * @param position Target location in dungeon coordinates * @return True if a theme room is near (within 2 tiles of) this point, false if it is free. */ bool IsNearThemeRoom(WorldTilePosition position); void InitLevels(); void FloodTransparencyValues(uint8_t floorID); } // namespace devilution ================================================ FILE: Source/levels/gendung_defs.hpp ================================================ #pragma once #include #define DMAXX 40 #define DMAXY 40 #define MAXDUNX (16 + DMAXX * 2 + 16) #define MAXDUNY (16 + DMAXY * 2 + 16) namespace devilution { enum dungeon_type : int8_t { DTYPE_TOWN, DTYPE_CATHEDRAL, DTYPE_CATACOMBS, DTYPE_CAVES, DTYPE_HELL, DTYPE_NEST, DTYPE_CRYPT, DTYPE_LAST = DTYPE_CRYPT, DTYPE_NONE = -1, }; enum lvl_entry : uint8_t { ENTRY_MAIN, ENTRY_PREV, ENTRY_SETLVL, ENTRY_RTNLVL, ENTRY_LOAD, ENTRY_WARPLVL, ENTRY_TWARPDN, ENTRY_TWARPUP, }; } // namespace devilution ================================================ FILE: Source/levels/reencode_dun_cels.cpp ================================================ #include "levels/reencode_dun_cels.hpp" #include #include #include #include #include #include #include #include #include "levels/dun_tile.hpp" #include "utils/attributes.h" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/endian_write.hpp" #include "utils/format_int.hpp" #include "utils/log.hpp" namespace devilution { namespace { DVL_ALWAYS_INLINE void ReencodeDungeonCelsLeftTriangleLower(uint8_t *&dst, const uint8_t *&src) { unsigned width = 0; for (unsigned i = 0; i < 8; ++i) { src += 2; // Skips the two zero bytes (aka bloat). width += 2; std::memcpy(dst, src, width); src += width; dst += width; width += 2; std::memcpy(dst, src, width); src += width; dst += width; } } DVL_ALWAYS_INLINE void ReencodeDungeonCelsLeftTriangle(uint8_t *&dst, const uint8_t *&src) { ReencodeDungeonCelsLeftTriangleLower(dst, src); unsigned width = DunFrameWidth; for (unsigned i = 0; i < 7; ++i) { src += 2; // Skips the two zero bytes (aka bloat). width -= 2; std::memcpy(dst, src, width); src += width; dst += width; width -= 2; std::memcpy(dst, src, width); src += width; dst += width; } src += 2; // Skips the two zero bytes (aka bloat). width -= 2; std::memcpy(dst, src, width); dst += width; } DVL_ALWAYS_INLINE void ReencodeDungeonCelsRightTriangleLower(uint8_t *&dst, const uint8_t *&src) { unsigned width = 0; for (unsigned i = 0; i < 8; ++i) { width += 2; std::memcpy(dst, src, width); src += width + 2; // Skips the two zero bytes (aka bloat). dst += width; width += 2; std::memcpy(dst, src, width); src += width; dst += width; } } DVL_ALWAYS_INLINE void ReencodeDungeonCelsRightTriangle(uint8_t *&dst, const uint8_t *&src) { ReencodeDungeonCelsRightTriangleLower(dst, src); unsigned width = DunFrameWidth; for (unsigned i = 0; i < 7; ++i) { width -= 2; std::memcpy(dst, src, width); src += width + 2; // Skips the two zero bytes (aka bloat). dst += width; width -= 2; std::memcpy(dst, src, width); src += width; dst += width; } width -= 2; std::memcpy(dst, src, width); dst += width; } DVL_ALWAYS_INLINE void ReencodeDungeonCelsLeftTrapezoid(uint8_t *&dst, const uint8_t *&src) { ReencodeDungeonCelsLeftTriangleLower(dst, src); std::memcpy(dst, src, DunFrameWidth * 16); dst += DunFrameWidth * 16; } DVL_ALWAYS_INLINE void ReencodeDungeonCelsRightTrapezoid(uint8_t *&dst, const uint8_t *&src) { ReencodeDungeonCelsRightTriangleLower(dst, src); std::memcpy(dst, src, DunFrameWidth * 16); dst += DunFrameWidth * 16; } DVL_ALWAYS_INLINE void RenderTransparentSquare(uint8_t *dst, const uint8_t *src) { for (unsigned i = 0; i < DunFrameHeight; ++i, dst -= 2 * DunFrameWidth) { uint_fast8_t drawWidth = DunFrameWidth; while (drawWidth > 0) { auto v = static_cast(*src++); if (v > 0) { std::memcpy(dst, src, v); src += v; } else { v = static_cast(-v); } dst += v; drawWidth -= v; } } } DVL_ALWAYS_INLINE void ExtractFoliageLeftTriangle(uint8_t *&dst, uint8_t *src) { for (int w = 2, y = 31; y >= 16; --y, w += 2, src -= DunFrameWidth) { std::memcpy(dst, src + (DunFrameWidth - w), w); std::memset(src + (DunFrameWidth - w), 0, w); dst += w; } for (int w = 30, y = 15; y > 0; --y, w -= 2, src -= DunFrameWidth) { std::memcpy(dst, src + (DunFrameWidth - w), w); std::memset(src + (DunFrameWidth - w), 0, w); dst += w; } } DVL_ALWAYS_INLINE void ExtractFoliageRightTriangle(uint8_t *&dst, uint8_t *src) { for (int w = 2, y = 31; y >= 16; --y, w += 2, src -= DunFrameWidth) { std::memcpy(dst, src, w); std::memset(src, 0, w); dst += w; } for (int w = 30, y = 15; y > 0; --y, w -= 2, src -= DunFrameWidth) { std::memcpy(dst, src, w); std::memset(src, 0, w); dst += w; } } DVL_ALWAYS_INLINE void ExtractFoliageTransparentSquare(uint8_t *&dst, const uint8_t *src) { // The bottom 16 lines are always transparent, foliage only // applies to the upper half of the tile. src -= DunFrameHeight * 16; for (int y = 16; y > 0; --y, src -= 2 * DunFrameWidth) { unsigned transparentRun = 0; unsigned solidRun = 0; for (int x = 0; x < DunFrameWidth; ++x) { if (*src++ != 0) { if (transparentRun != 0) { *dst++ = static_cast(-static_cast(transparentRun)); transparentRun = 0; } ++solidRun; } else { if (solidRun != 0) { *dst++ = solidRun; std::memcpy(dst, src - solidRun, solidRun); dst += solidRun; solidRun = 0; } ++transparentRun; } } if (transparentRun != 0) { *dst++ = static_cast(-static_cast(transparentRun)); } else if (solidRun != 0) { *dst++ = solidRun; std::memcpy(dst, src - solidRun, solidRun); dst += solidRun; } } } DVL_ALWAYS_INLINE void ReencodeFloorWithFoliage(uint8_t *&dst, const uint8_t *&src, TileType tileType) { uint8_t surface[DunFrameWidth * DunFrameHeight] {}; uint8_t *surfaceLastLine = &surface[DunFrameWidth * (DunFrameHeight - 1)]; RenderTransparentSquare(surfaceLastLine, src); if (tileType == TileType::LeftTriangle) { ExtractFoliageLeftTriangle(dst, surfaceLastLine); } else { ExtractFoliageRightTriangle(dst, surfaceLastLine); } ExtractFoliageTransparentSquare(dst, surfaceLastLine); } size_t GetReencodedSize(const uint8_t *dungeonCels, std::span> frames) { size_t result = (2 + frames.size()) * 4; const auto *srcOffsets = reinterpret_cast(dungeonCels); for (const auto &[frame, info] : frames) { size_t frameSize; switch (info.type) { case TileType::TransparentSquare: { const uint32_t srcFrameBegin = Swap32LE(srcOffsets[frame]); if (info.isFloor()) { uint8_t out[1024]; uint8_t *outIt = out; const uint8_t *src = &dungeonCels[srcFrameBegin]; const TileType newType = info.isFloorLeft() ? TileType::LeftTriangle : TileType::RightTriangle; ReencodeFloorWithFoliage(outIt, src, newType); frameSize = outIt - out; } else { const uint32_t srcFrameEnd = Swap32LE(srcOffsets[frame + 1]); frameSize = srcFrameEnd - srcFrameBegin; } } break; case TileType::Square: { frameSize = DunFrameWidth * DunFrameHeight; } break; case TileType::LeftTriangle: case TileType::RightTriangle: frameSize = ReencodedTriangleFrameSize; break; case TileType::LeftTrapezoid: case TileType::RightTrapezoid: frameSize = ReencodedTrapezoidFrameSize; break; } result += frameSize; } return result; } } // namespace void ReencodeDungeonCels(std::unique_ptr &dungeonCels, std::span> frames) { const auto *srcData = reinterpret_cast(dungeonCels.get()); const auto *srcOffsets = reinterpret_cast(srcData); int numFoliage = 0; LogVerbose("Re-encoding dungeon CELs: {} frames, {} bytes", FormatInteger(Swap32LE(srcOffsets[0])), FormatInteger(Swap32LE(srcOffsets[Swap32LE(srcOffsets[0]) + 1]))); const size_t outSize = GetReencodedSize(srcData, frames); std::unique_ptr result { new std::byte[outSize] }; auto *const resultPtr = reinterpret_cast(result.get()); WriteLE32(resultPtr, static_cast(frames.size())); uint8_t *lookup = resultPtr + 4; uint8_t *out = resultPtr + (2 + frames.size()) * 4; // number of frames, frame offsets, file size for (const auto &[frame, info] : frames) { WriteLE32(lookup, static_cast(out - resultPtr)); lookup += 4; const uint32_t srcFrameBegin = Swap32LE(srcOffsets[frame]); const uint8_t *src = &srcData[srcFrameBegin]; switch (info.type) { case TileType::TransparentSquare: { if (info.isFloor()) { const TileType newType = info.isFloorLeft() ? TileType::LeftTriangle : TileType::RightTriangle; ReencodeFloorWithFoliage(out, src, newType); ++numFoliage; } else { const uint32_t srcFrameEnd = Swap32LE(srcOffsets[frame + 1]); const uint32_t size = srcFrameEnd - srcFrameBegin; std::memcpy(out, src, size); out += size; } } break; case TileType::Square: std::memcpy(out, src, DunFrameWidth * DunFrameHeight); out += DunFrameWidth * DunFrameHeight; break; case TileType::LeftTriangle: ReencodeDungeonCelsLeftTriangle(out, src); break; case TileType::RightTriangle: ReencodeDungeonCelsRightTriangle(out, src); break; case TileType::LeftTrapezoid: ReencodeDungeonCelsLeftTrapezoid(out, src); break; case TileType::RightTrapezoid: ReencodeDungeonCelsRightTrapezoid(out, src); break; } } WriteLE32(lookup, static_cast(outSize)); const auto *dstOffsets = reinterpret_cast(resultPtr); LogVerbose(" Re-encoded dungeon CELs: {} frames, {} bytes. Extracted {} foliage tiles.", FormatInteger(Swap32LE(dstOffsets[0])), FormatInteger(Swap32LE(dstOffsets[Swap32LE(dstOffsets[0]) + 1])), FormatInteger(numFoliage)); dungeonCels = std::move(result); } std::vector> ComputeCelBlockAdjustments(std::span> frames) { std::vector> celBlockAdjustments; uint16_t lastFrameIndex = 0; uint16_t adjustment = 0; for (auto &[frame, info] : frames) { const uint16_t diff = frame - lastFrameIndex - 1; if (diff > 0) celBlockAdjustments.emplace_back(frame, adjustment); adjustment += diff; lastFrameIndex = frame; } if (adjustment > 0) { celBlockAdjustments.emplace_back(std::numeric_limits::max(), adjustment); } return celBlockAdjustments; } } // namespace devilution ================================================ FILE: Source/levels/reencode_dun_cels.hpp ================================================ #pragma once #include #include #include #include #include #include #include "levels/dun_tile.hpp" namespace devilution { struct DunFrameInfo { // Only floor tiles have this. uint8_t microTileIndex; TileType type; TileProperties properties; [[nodiscard]] bool isFloor() const { // The BlockMissile check is for stairs in L3 and L4, e.g. tile 46 sub-tile 141 frame 386 in L4. return !HasAnyOf(properties, TileProperties::Solid | TileProperties::BlockMissile) && (microTileIndex == 0 || microTileIndex == 1); } [[nodiscard]] bool isFloorLeft() const { return microTileIndex == 0; } }; /** * @brief Re-encodes dungeon cels. * * 1. Removing redundant padding bytes from triangles and trapezoids. * 2. Extracts floor tile foliage into a triangle with the floor frame and a separate 16-px tall `TransparentSquare`. * * This reduces memory usage and simplifies the rendering. */ void ReencodeDungeonCels(std::unique_ptr &dungeonCels, std::span> frames); /** * @brief Computes adjustments to apply to frame indexes in cel block data. * * Re-encoding the dungeon cels removes frames that are not referenced. * Indexes must also be adjusted to avoid errors when doing lookups. */ std::vector> ComputeCelBlockAdjustments(std::span> frames); } // namespace devilution ================================================ FILE: Source/levels/setmaps.cpp ================================================ #include "levels/setmaps.h" #include #ifdef _DEBUG #include "debug.h" #endif #include "engine/load_file.hpp" #include "engine/palette.h" #include "levels/drlg_l1.h" #include "levels/drlg_l2.h" #include "levels/drlg_l3.h" #include "levels/drlg_l4.h" #include "levels/gendung.h" #include "levels/trigs.h" #include "msg.h" #include "objects.h" #include "quests.h" #include "tables/objdat.h" #include "utils/language.h" namespace devilution { /** Maps from quest level to quest level names. */ const char *const QuestLevelNames[] = { "", N_("Skeleton King's Lair"), N_("Chamber of Bone"), N_("Maze"), N_("Poisoned Water Supply"), N_("Archbishop Lazarus' Lair"), N_("Church Arena"), N_("Hell Arena"), N_("Circle of Life Arena"), }; namespace { void AddSKingObjs() { constexpr WorldTileRectangle SmallSecretRoom { { 20, 7 }, { 3, 3 } }; ObjectAtPosition({ 64, 34 }).InitializeLoadedObject(SmallSecretRoom, 1); constexpr WorldTileRectangle Gate { { 20, 14 }, { 1, 2 } }; ObjectAtPosition({ 64, 59 }).InitializeLoadedObject(Gate, 2); constexpr WorldTileRectangle LargeSecretRoom { { 8, 1 }, { 7, 10 } }; ObjectAtPosition({ 27, 37 }).InitializeLoadedObject(LargeSecretRoom, 3); ObjectAtPosition({ 46, 35 }).InitializeLoadedObject(LargeSecretRoom, 3); ObjectAtPosition({ 49, 53 }).InitializeLoadedObject(LargeSecretRoom, 3); ObjectAtPosition({ 27, 53 }).InitializeLoadedObject(LargeSecretRoom, 3); } void AddSChamObjs() { ObjectAtPosition({ 37, 30 }).InitializeLoadedObject({ { 17, 0 }, { 4, 5 } }, 1); ObjectAtPosition({ 37, 46 }).InitializeLoadedObject({ { 13, 0 }, { 3, 5 } }, 2); } void AddVileObjs() { ObjectAtPosition({ 26, 45 }).InitializeLoadedObject({ { 1, 1 }, { 8, 9 } }, 1); ObjectAtPosition({ 45, 46 }).InitializeLoadedObject({ { 11, 1 }, { 9, 9 } }, 2); ObjectAtPosition({ 35, 36 }).InitializeLoadedObject({ { 7, 11 }, { 6, 7 } }, 3); } void SetMapTransparency(const char *path) { auto dunData = LoadFileInMem(path); LoadTransparency(dunData.get()); } void LoadCustomMap(const char *path, Point viewPosition) { switch (setlvltype) { case DTYPE_CATHEDRAL: case DTYPE_CRYPT: LoadL1Dungeon(path, viewPosition); break; case DTYPE_CATACOMBS: LoadL2Dungeon(path, viewPosition); break; case DTYPE_CAVES: case DTYPE_NEST: LoadL3Dungeon(path, viewPosition); break; case DTYPE_HELL: LoadL4Dungeon(path, viewPosition); break; case DTYPE_TOWN: case DTYPE_NONE: break; } LoadRndLvlPal(setlvltype); } void LoadArenaMap(const char *path, Point viewPosition, Point exitTrigger) { LoadCustomMap(path, viewPosition); trigflag = false; numtrigs = 1; trigs[0].position = exitTrigger; trigs[0]._tmsg = WM_DIABRTNLVL; } } // namespace void LoadSetMap() { switch (setlvlnum) { case SL_SKELKING: if (Quests[Q_SKELKING]._qactive == QUEST_INIT) { Quests[Q_SKELKING]._qactive = QUEST_ACTIVE; Quests[Q_SKELKING]._qvar1 = 1; NetSendCmdQuest(true, Quests[Q_SKELKING]); } LoadPreL1Dungeon("levels\\l1data\\sklkng1.dun"); LoadL1Dungeon("levels\\l1data\\sklkng2.dun", { 83, 44 }); SetMapTransparency("levels\\l1data\\sklkngt.dun"); LoadPaletteAndInitBlending("levels\\l1data\\l1_2.pal"); AddSKingObjs(); InitSKingTriggers(); break; case SL_BONECHAMB: LoadPreL2Dungeon("levels\\l2data\\bonecha2.dun"); LoadL2Dungeon("levels\\l2data\\bonecha1.dun", { 70, 40 }); SetMapTransparency("levels\\l2data\\bonechat.dun"); LoadPaletteAndInitBlending("levels\\l2data\\l2_2.pal"); AddSChamObjs(); InitSChambTriggers(); break; case SL_MAZE: break; case SL_POISONWATER: if (Quests[Q_PWATER]._qactive == QUEST_INIT) Quests[Q_PWATER]._qactive = QUEST_ACTIVE; LoadL3Dungeon("levels\\l3data\\foulwatr.dun", { 31, 83 }); LoadPaletteAndInitBlending("levels\\l3data\\l3pfoul.pal"); InitPWaterTriggers(); break; case SL_VILEBETRAYER: if (Quests[Q_BETRAYER]._qactive == QUEST_DONE) { Quests[Q_BETRAYER]._qvar2 = 4; } else if (Quests[Q_BETRAYER]._qactive == QUEST_ACTIVE) { Quests[Q_BETRAYER]._qvar2 = 3; } LoadPreL1Dungeon("levels\\l1data\\vile1.dun"); LoadL1Dungeon("levels\\l1data\\vile2.dun", { 35, 36 }); SetMapTransparency("levels\\l1data\\vile1.dun"); LoadPaletteAndInitBlending("levels\\l1data\\l1_2.pal"); AddVileObjs(); InitNoTriggers(); break; case SL_ARENA_CHURCH: LoadArenaMap("arena\\church.dun", { 29, 22 }, { 28, 20 }); break; case SL_ARENA_HELL: LoadArenaMap("arena\\hell.dun", { 34, 26 }, { 33, 26 }); break; case SL_ARENA_CIRCLE_OF_LIFE: LoadArenaMap("arena\\circle_of_death.dun", { 30, 26 }, { 29, 26 }); break; case SL_NONE: #ifdef _DEBUG LoadCustomMap(TestMapPath.c_str(), ViewPosition); InitNoTriggers(); #endif break; } } } // namespace devilution ================================================ FILE: Source/levels/setmaps.h ================================================ /** * @file setmaps.cpp * * Interface of functionality for the special quest dungeons. */ #pragma once #include "levels/gendung.h" namespace devilution { /** * @brief Get the tile type used to render the given arena level */ inline dungeon_type GetArenaLevelType(_setlevels arenaLevel) { constexpr dungeon_type DungeonTypeForArena[] = { dungeon_type::DTYPE_CATHEDRAL, // SL_ARENA_CHURCH dungeon_type::DTYPE_HELL, // SL_ARENA_HELL dungeon_type::DTYPE_HELL, // SL_ARENA_CIRCLE_OF_LIFE }; constexpr size_t arenaCount = sizeof(DungeonTypeForArena) / sizeof(dungeon_type); const size_t index = arenaLevel - SL_FIRST_ARENA; return index < arenaCount ? DungeonTypeForArena[index] : DTYPE_NONE; } /** * @brief Load a quest map, the given map is specified via the global setlvlnum */ void LoadSetMap(); /* rdata */ extern const char *const QuestLevelNames[]; } // namespace devilution ================================================ FILE: Source/levels/themes.cpp ================================================ /** * @file themes.cpp * * Implementation of the theme room placing algorithms. */ #include "levels/themes.h" #include #include #include "engine/path.h" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "items.h" #include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "monster.h" #include "objects.h" #include "quests.h" #include "utils/algorithm/container.hpp" #include "utils/is_of.hpp" #include "utils/str_cat.hpp" namespace devilution { int numthemes; bool armorFlag; bool weaponFlag; int zharlib; ThemeStruct themes[MAXTHEMES]; namespace { bool cauldronFlag; bool bFountainFlag; bool mFountainFlag; bool pFountainFlag; bool tFountainFlag; bool treasureFlag; int themex; int themey; size_t themeVar1; bool TFit_Shrine(int i) { Point position { 0, 0 }; size_t found = 0; while (found == 0) { const Point testPosition = position; if (dTransVal[position.x][position.y] == themes[i].ttval) { if (TileHasAny(position + Direction::NorthEast, TileProperties::Trap) && IsTileNotSolid(testPosition + Direction::NorthWest) && IsTileNotSolid(testPosition + Direction::SouthEast) && dTransVal[position.x - 1][position.y] == themes[i].ttval && dTransVal[position.x + 1][position.y] == themes[i].ttval && !IsObjectAtPosition(testPosition + Direction::North) && !IsObjectAtPosition(testPosition + Direction::East)) { found = 1; } if (found == 0 && TileHasAny(position + Direction::NorthWest, TileProperties::Trap) && IsTileNotSolid(testPosition + Direction::NorthEast) && IsTileNotSolid(testPosition + Direction::SouthWest) && dTransVal[position.x][position.y - 1] == themes[i].ttval && dTransVal[position.x][position.y + 1] == themes[i].ttval && !IsObjectAtPosition(testPosition + Direction::North) && !IsObjectAtPosition(testPosition + Direction::West)) { found = 2; } } if (found == 0) { position.x++; if (position.x == MAXDUNX) { position.x = 0; position.y++; if (position.y == MAXDUNY) return false; } } } themex = position.x; themey = position.y; themeVar1 = found; return true; } bool CheckThemeObj5(Point origin, int8_t regionId) { return c_all_of(PointsInRectangle(Rectangle { origin, 2 }), [regionId](Point testPosition) { // note out-of-bounds tiles are not solid, this function relies on the guard in TFit_Obj5 and dungeon border if (IsTileSolid(testPosition)) { return false; } // If the theme object would extend into a different region then it doesn't fit. if (dTransVal[testPosition.x][testPosition.y] != regionId) { return false; } return true; }); } bool TFit_Obj5(int t) { const int targetCandidates = GenerateRnd(5); if (targetCandidates < 0) { // vanilla rng can return -3 for GenerateRnd(5), default behaviour is to set the output to 0,0 and return true in this case... themex = 0; themey = 0; return true; } int candidatesFound = 0; for (const Point tile : PointsInRectangle(Rectangle { { 0, 0 }, { MAXDUNX, MAXDUNY } })) { if (dTransVal[tile.x][tile.y] == themes[t].ttval && IsTileNotSolid(tile) && CheckThemeObj5(tile, themes[t].ttval)) { // Use themex/y to keep track of the last candidate area found, in case we end up with fewer candidates than the target themex = tile.x; themey = tile.y; candidatesFound++; if (candidatesFound > targetCandidates) return true; } } return candidatesFound > 0; } bool TFit_SkelRoom(int t) { if (IsNoneOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS)) { return false; } for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsSkel(LevelMonsterTypes[i].type)) { themeVar1 = i; return TFit_Obj5(t); } } return false; } bool TFit_GoatShrine(int t) { for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsGoat(LevelMonsterTypes[i].type)) { themeVar1 = i; return TFit_Obj5(t); } } return false; } bool CheckThemeObj3(Point origin, int8_t regionId, unsigned frequency = 0) { return c_all_of(PointsInRectangle(Rectangle { origin, 1 }), [regionId, frequency](Point testPosition) { if (!InDungeonBounds(testPosition)) { return false; } if (IsTileSolid(testPosition)) { return false; } // If the theme object would extend into a different region then it doesn't fit. if (dTransVal[testPosition.x][testPosition.y] != regionId) { return false; } if (IsObjectAtPosition(testPosition)) { return false; } if (frequency > 0 && FlipCoin(frequency)) { return false; } return true; }); } bool TFit_Obj3(int8_t regionId) { constexpr unsigned objrnd[4] = { 4, 4, 3, 5 }; for (int yp = 1; yp < MAXDUNY - 1; yp++) { for (int xp = 1; xp < MAXDUNX - 1; xp++) { if (CheckThemeObj3({ xp, yp }, regionId, objrnd[leveltype - 1])) { themex = xp; themey = yp; return true; } } } return false; } bool CheckThemeReqs(theme_id t) { switch (t) { case THEME_SHRINE: case THEME_SKELROOM: case THEME_LIBRARY: return IsNoneOf(leveltype, DTYPE_CAVES, DTYPE_HELL); case THEME_ARMORSTAND: case THEME_WEAPONRACK: return IsNoneOf(leveltype, DTYPE_CATHEDRAL); case THEME_CAULDRON: return leveltype == DTYPE_HELL && cauldronFlag; case THEME_BLOODFOUNTAIN: return bFountainFlag; case THEME_PURIFYINGFOUNTAIN: return pFountainFlag; case THEME_MURKYFOUNTAIN: return mFountainFlag; case THEME_TEARFOUNTAIN: return tFountainFlag; case THEME_TREASURE: return treasureFlag; default: return true; } } bool SpecialThemeFit(int i, theme_id t) { bool rv = CheckThemeReqs(t); switch (t) { case THEME_SHRINE: case THEME_LIBRARY: if (rv) { rv = TFit_Shrine(i); } break; case THEME_SKELROOM: if (rv) { rv = TFit_SkelRoom(i); } break; case THEME_BLOODFOUNTAIN: if (rv) { rv = TFit_Obj5(i); } if (rv) { bFountainFlag = false; } break; case THEME_PURIFYINGFOUNTAIN: if (rv) { rv = TFit_Obj5(i); } if (rv) { pFountainFlag = false; } break; case THEME_MURKYFOUNTAIN: if (rv) { rv = TFit_Obj5(i); } if (rv) { mFountainFlag = false; } break; case THEME_TEARFOUNTAIN: if (rv) { rv = TFit_Obj5(i); } if (rv) { tFountainFlag = false; } break; case THEME_CAULDRON: if (rv) { rv = TFit_Obj5(i); } if (rv) { cauldronFlag = false; } break; case THEME_GOATSHRINE: if (rv) { rv = TFit_GoatShrine(i); } break; case THEME_TORTURE: case THEME_DECAPITATED: case THEME_ARMORSTAND: case THEME_BRNCROSS: case THEME_WEAPONRACK: if (rv) { rv = TFit_Obj3(themes[i].ttval); } break; case THEME_TREASURE: if (rv) { treasureFlag = false; } break; default: break; } return rv; } bool CheckThemeRoom(int8_t tv) { for (int i = 0; i < numtrigs; i++) { if (dTransVal[trigs[i].position.x][trigs[i].position.y] == tv) return false; } int tarea = 0; for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dTransVal[i][j] != tv) continue; if (TileContainsSetPiece({ i, j })) return false; tarea++; } } if (leveltype == DTYPE_CATHEDRAL && (tarea < 9 || tarea > 100)) return false; for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { if (dTransVal[i][j] != tv || TileHasAny({ i, j }, TileProperties::Solid)) continue; if (dTransVal[i - 1][j] != tv && IsTileNotSolid({ i - 1, j })) return false; if (dTransVal[i + 1][j] != tv && IsTileNotSolid({ i + 1, j })) return false; if (dTransVal[i][j - 1] != tv && IsTileNotSolid({ i, j - 1 })) return false; if (dTransVal[i][j + 1] != tv && IsTileNotSolid({ i, j + 1 })) return false; } } return true; } /** * PlaceThemeMonsts places theme monsters with the specified frequency. * * @param t theme number (index into themes array). * @param f frequency (1/f likelihood of adding monster). */ void PlaceThemeMonsts(int t, int f) { size_t scattertypes[138]; int numscattypes = 0; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if ((LevelMonsterTypes[i].placeFlags & PLACE_SCATTER) != 0) { scattertypes[numscattypes] = i; numscattypes++; } } const size_t mtype = scattertypes[GenerateRnd(numscattypes)]; for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp }) && dItem[xp][yp] == 0 && !IsObjectAtPosition({ xp, yp })) { if (FlipCoin(f)) { AddMonster({ xp, yp }, static_cast(GenerateRnd(8)), mtype, true); } } } } } /** * Theme_Barrel initializes the barrel theme. * * @param t theme number (index into themes array). */ void Theme_Barrel(int t) { const int barrnd[4] = { 2, 6, 4, 8 }; const int monstrnd[4] = { 5, 7, 3, 9 }; for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp })) { if (FlipCoin(barrnd[leveltype - 1])) { const _object_id r = FlipCoin(barrnd[leveltype - 1]) ? OBJ_BARREL : OBJ_BARRELEX; AddObject(r, { xp, yp }); } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_Shrine initializes the shrine theme. * * @param t theme number (index into themes array). */ void Theme_Shrine(int t) { const int monstrnd[4] = { 6, 6, 3, 9 }; TFit_Shrine(t); if (themeVar1 == 1) { AddObject(OBJ_CANDLE2, { themex - 1, themey }); AddObject(OBJ_SHRINER, { themex, themey }); AddObject(OBJ_CANDLE2, { themex + 1, themey }); } else { AddObject(OBJ_CANDLE2, { themex, themey - 1 }); AddObject(OBJ_SHRINEL, { themex, themey }); AddObject(OBJ_CANDLE2, { themex, themey + 1 }); } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_MonstPit initializes the monster pit theme. * * @param t theme number (index into themes array). */ void Theme_MonstPit(int t) { const int monstrnd[4] = { 6, 7, 3, 9 }; int r = GenerateRnd(100) + 1; int ixp = 0; int iyp = 0; while (r > 0) { if (dTransVal[ixp][iyp] == themes[t].ttval && IsTileNotSolid({ ixp, iyp })) { --r; } if (r <= 0) continue; ixp++; if (ixp == MAXDUNX) { ixp = 0; iyp++; if (iyp == MAXDUNY) { iyp = 0; } } } CreateRndItem({ ixp, iyp }, true, false, true); ItemNoFlippy(); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } void SpawnObjectOrSkeleton(unsigned frequency, _object_id objectType, Point tile) { if (FlipCoin(frequency)) { AddObject(objectType, tile); } else { Monster *skeleton = PreSpawnSkeleton(); if (skeleton != nullptr) ActivateSkeleton(*skeleton, tile); } } /** * Theme_SkelRoom initializes the skeleton room theme. * * @param t theme number (index into themes array). */ void Theme_SkelRoom(int t) { constexpr unsigned monstrnd[4] = { 6, 7, 3, 9 }; TFit_SkelRoom(t); const int xp = themex; const int yp = themey; AddObject(OBJ_SKFIRE, { xp, yp }); SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERL, { xp - 1, yp - 1 }); { Monster *skeleton = PreSpawnSkeleton(); if (skeleton != nullptr) ActivateSkeleton(*skeleton, { xp, yp - 1 }); } SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERR, { xp + 1, yp - 1 }); SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERM, { xp - 1, yp }); SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERM, { xp + 1, yp }); SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERR, { xp - 1, yp + 1 }); { Monster *skeleton = PreSpawnSkeleton(); if (skeleton != nullptr) ActivateSkeleton(*skeleton, { xp, yp + 1 }); } SpawnObjectOrSkeleton(monstrnd[leveltype - 1], OBJ_BANNERL, { xp + 1, yp + 1 }); if (!IsObjectAtPosition({ xp, yp - 3 })) { AddObject(OBJ_SKELBOOK, { xp, yp - 2 }); } if (!IsObjectAtPosition({ xp, yp + 3 })) { AddObject(OBJ_SKELBOOK, { xp, yp + 2 }); } } /** * Theme_Treasure initializes the treasure theme. * * @param t theme number (index into themes array). */ void Theme_Treasure(int t) { const int treasrnd[4] = { 4, 9, 7, 10 }; const int monstrnd[4] = { 6, 8, 3, 7 }; DiscardRandomValues(1); for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp })) { const int8_t treasureType = treasrnd[leveltype - 1]; const int rv = GenerateRnd(treasureType); // BUGFIX: this used to be `2*GenerateRnd(treasureType) == 0` however 2*0 has no effect, should probably be `FlipCoin(2*treasureType)` if (FlipCoin(treasureType)) { CreateTypeItem({ xp, yp }, false, ItemType::Gold, IMISC_NONE, false, true); ItemNoFlippy(); } if (rv == 0) { CreateRndItem({ xp, yp }, false, false, true); ItemNoFlippy(); } // BUGFIX: the following code is likely not working as intended. // `rv >= treasureType - 2` is not connected to either // of the item creation branches above, thus the last (unrelated) // item spawned/dropped on ground would be halved in value. if (rv >= treasureType - 2 && leveltype != DTYPE_CATHEDRAL) { Item &item = Items[ActiveItems[ActiveItemCount - 1]]; if (item.IDidx == IDI_GOLD) { item._ivalue = std::max(item._ivalue / 2, 1); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_Library initializes the library theme. * * @param t theme number (index into themes array). */ void Theme_Library(int t) { constexpr unsigned librnd[4] = { 1, 2, 2, 5 }; const int monstrnd[4] = { 5, 7, 3, 9 }; TFit_Shrine(t); if (themeVar1 == 1) { AddObject(OBJ_BOOKCANDLE, { themex - 1, themey }); AddObject(OBJ_BOOKCASER, { themex, themey }); AddObject(OBJ_BOOKCANDLE, { themex + 1, themey }); } else { AddObject(OBJ_BOOKCANDLE, { themex, themey - 1 }); AddObject(OBJ_BOOKCASEL, { themex, themey }); AddObject(OBJ_BOOKCANDLE, { themex, themey + 1 }); } for (int yp = 1; yp < MAXDUNY - 1; yp++) { for (int xp = 1; xp < MAXDUNX - 1; xp++) { if (CheckThemeObj3({ xp, yp }, themes[t].ttval) && dMonster[xp][yp] == 0 && FlipCoin(librnd[leveltype - 1])) { Object *bookstand = AddObject(OBJ_BOOKSTAND, { xp, yp }); if (!FlipCoin(2 * librnd[leveltype - 1])) { if (bookstand != nullptr) { bookstand->selectionRegion = SelectionRegion::None; bookstand->_oAnimFrame += 2; } } } } } if (Quests[Q_ZHAR].IsAvailable() && t == zharlib) { return; } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_Torture initializes the torture theme. * * @param t theme number (index into themes array). */ void Theme_Torture(int t) { constexpr unsigned tortrnd[4] = { 6, 8, 3, 8 }; const int monstrnd[4] = { 6, 8, 3, 9 }; for (int yp = 1; yp < MAXDUNY - 1; yp++) { for (int xp = 1; xp < MAXDUNX - 1; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp })) { if (CheckThemeObj3({ xp, yp }, themes[t].ttval)) { if (FlipCoin(tortrnd[leveltype - 1])) { AddObject(OBJ_TNUDEM2, { xp, yp }); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_BloodFountain initializes the blood fountain theme. * @param t Theme number (index into themes array). */ void Theme_BloodFountain(int t) { const int monstrnd[4] = { 6, 8, 3, 9 }; TFit_Obj5(t); AddObject(OBJ_BLOODFTN, { themex, themey }); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_Decap initializes the decapitated theme. * * @param t theme number (index into themes array). */ void Theme_Decap(int t) { constexpr unsigned decaprnd[4] = { 6, 8, 3, 8 }; const int monstrnd[4] = { 6, 8, 3, 9 }; for (int yp = 1; yp < MAXDUNY - 1; yp++) { for (int xp = 1; xp < MAXDUNX - 1; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp })) { if (CheckThemeObj3({ xp, yp }, themes[t].ttval)) { if (FlipCoin(decaprnd[leveltype - 1])) { AddObject(OBJ_DECAP, { xp, yp }); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_PurifyingFountain initializes the purifying fountain theme. * * @param t theme number (index into themes array). */ void Theme_PurifyingFountain(int t) { const int monstrnd[4] = { 6, 7, 3, 9 }; TFit_Obj5(t); AddObject(OBJ_PURIFYINGFTN, { themex, themey }); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_ArmorStand initializes the armor stand theme. * * @param t theme number (index into themes array). */ void Theme_ArmorStand(int t) { constexpr unsigned armorrnd[4] = { 6, 8, 3, 8 }; const int monstrnd[4] = { 6, 7, 3, 9 }; if (armorFlag) { TFit_Obj3(themes[t].ttval); AddObject(OBJ_ARMORSTAND, { themex, themey }); } for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == themes[t].ttval && IsTileNotSolid({ xp, yp })) { if (CheckThemeObj3({ xp, yp }, themes[t].ttval)) { if (FlipCoin(armorrnd[leveltype - 1])) { AddObject(OBJ_ARMORSTANDN, { xp, yp }); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); armorFlag = false; } /** * Theme_GoatShrine initializes the goat shrine theme. * * @param t theme number (index into themes array). */ void Theme_GoatShrine(int t) { TFit_GoatShrine(t); AddObject(OBJ_GOATSHRINE, { themex, themey }); for (int yy = themey - 1; yy <= themey + 1; yy++) { for (int xx = themex - 1; xx <= themex + 1; xx++) { if (dTransVal[xx][yy] == themes[t].ttval && IsTileNotSolid({ xx, yy }) && (xx != themex || yy != themey)) { AddMonster({ xx, yy }, Direction::SouthWest, themeVar1, true); } } } } /** * Theme_Cauldron initializes the cauldron theme. * * @param t theme number (index into themes array). */ void Theme_Cauldron(int t) { const int monstrnd[4] = { 6, 7, 3, 9 }; TFit_Obj5(t); AddObject(OBJ_CAULDRON, { themex, themey }); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_MurkyFountain initializes the murky fountain theme. * * @param t theme number (index into themes array). */ void Theme_MurkyFountain(int t) { const int monstrnd[4] = { 6, 7, 3, 9 }; TFit_Obj5(t); AddObject(OBJ_MURKYFTN, { themex, themey }); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_TearFountain initializes the tear fountain theme. * * @param t theme number (index into themes array). */ void Theme_TearFountain(int t) { const int monstrnd[4] = { 6, 7, 3, 9 }; TFit_Obj5(t); AddObject(OBJ_TEARFTN, { themex, themey }); PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_BrnCross initializes the burning cross theme. * * @param t theme number (index into themes array). */ void Theme_BrnCross(int t) { const int8_t regionId = themes[t].ttval; const int monstrnd[4] = { 6, 8, 3, 9 }; constexpr unsigned bcrossrnd[4] = { 5, 7, 3, 8 }; for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == regionId && IsTileNotSolid({ xp, yp })) { if (CheckThemeObj3({ xp, yp }, regionId)) { if (FlipCoin(bcrossrnd[leveltype - 1])) { AddObject(OBJ_TBCROSS, { xp, yp }); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); } /** * Theme_WeaponRack initializes the weapon rack theme. * * @param t theme number (index into themes array). */ void Theme_WeaponRack(int t) { const int8_t regionId = themes[t].ttval; constexpr unsigned weaponrnd[4] = { 6, 8, 5, 8 }; const int monstrnd[4] = { 6, 7, 3, 9 }; if (weaponFlag) { TFit_Obj3(regionId); AddObject(OBJ_WEAPONRACK, { themex, themey }); } for (int yp = 0; yp < MAXDUNY; yp++) { for (int xp = 0; xp < MAXDUNX; xp++) { if (dTransVal[xp][yp] == regionId && IsTileNotSolid({ xp, yp })) { if (CheckThemeObj3({ xp, yp }, regionId)) { if (FlipCoin(weaponrnd[leveltype - 1])) { AddObject(OBJ_WEAPONRACKN, { xp, yp }); } } } } } PlaceThemeMonsts(t, monstrnd[leveltype - 1]); weaponFlag = false; } /** * UpdateL4Trans sets each value of the transparency map to 1. */ void UpdateL4Trans() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) if (dTransVal[i][j] != 0) { dTransVal[i][j] = 1; } } } } } // namespace void InitThemes() { zharlib = -1; numthemes = 0; armorFlag = true; bFountainFlag = true; cauldronFlag = true; mFountainFlag = true; pFountainFlag = true; tFountainFlag = true; treasureFlag = true; weaponFlag = true; if (currlevel == 16 || IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { return; } /** Specifies the set of special theme IDs from which one will be selected at random. */ constexpr theme_id ThemeGood[4] = { THEME_GOATSHRINE, THEME_SHRINE, THEME_SKELROOM, THEME_LIBRARY }; if (leveltype == DTYPE_CATHEDRAL) { for (int8_t i = 0; numthemes < MAXTHEMES; i++) { if (CheckThemeRoom(i)) { themes[numthemes].ttval = i; theme_id j = ThemeGood[GenerateRnd(4)]; while (!SpecialThemeFit(numthemes, j)) { j = (theme_id)GenerateRnd(17); } themes[numthemes].ttype = j; numthemes++; } if (i > TransVal) break; } return; } for (int i = 0; i < themeCount; i++) { themes[i].ttype = THEME_NONE; } if (Quests[Q_ZHAR].IsAvailable()) { for (int j = 0; j < themeCount; j++) { themes[j].ttval = themeLoc[j].ttval; if (SpecialThemeFit(j, THEME_LIBRARY)) { themes[j].ttype = THEME_LIBRARY; zharlib = j; break; } } } for (int i = 0; i < themeCount; i++) { if (themes[i].ttype == THEME_NONE) { themes[i].ttval = themeLoc[i].ttval; theme_id j = ThemeGood[GenerateRnd(4)]; while (!SpecialThemeFit(i, j)) { j = (theme_id)GenerateRnd(17); } themes[i].ttype = j; } } numthemes += themeCount; } void HoldThemeRooms() { if (currlevel == 16 || IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { return; } if (leveltype != DTYPE_CATHEDRAL) { DRLG_HoldThemeRooms(); return; } for (int i = 0; i < numthemes; i++) { const int8_t v = themes[i].ttval; for (int y = 0; y < MAXDUNY; y++) { for (int x = 0; x < MAXDUNX; x++) { if (dTransVal[x][y] == v) { dFlags[x][y] |= DungeonFlag::Populated; } } } } } void CreateThemeRooms() { if (currlevel == 16 || IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { return; } for (int i = 0; i < numthemes; i++) { themex = 0; themey = 0; switch (themes[i].ttype) { case THEME_BARREL: Theme_Barrel(i); break; case THEME_SHRINE: Theme_Shrine(i); break; case THEME_MONSTPIT: Theme_MonstPit(i); break; case THEME_SKELROOM: Theme_SkelRoom(i); break; case THEME_TREASURE: Theme_Treasure(i); break; case THEME_LIBRARY: Theme_Library(i); break; case THEME_TORTURE: Theme_Torture(i); break; case THEME_BLOODFOUNTAIN: Theme_BloodFountain(i); break; case THEME_DECAPITATED: Theme_Decap(i); break; case THEME_PURIFYINGFOUNTAIN: Theme_PurifyingFountain(i); break; case THEME_ARMORSTAND: Theme_ArmorStand(i); break; case THEME_GOATSHRINE: Theme_GoatShrine(i); break; case THEME_CAULDRON: Theme_Cauldron(i); break; case THEME_MURKYFOUNTAIN: Theme_MurkyFountain(i); break; case THEME_TEARFOUNTAIN: Theme_TearFountain(i); break; case THEME_BRNCROSS: Theme_BrnCross(i); break; case THEME_WEAPONRACK: Theme_WeaponRack(i); break; case THEME_NONE: app_fatal(StrCat("Unknown theme type: ", static_cast(themes[i].ttype))); } } if (leveltype == DTYPE_HELL && themeCount > 0) { UpdateL4Trans(); } } } // namespace devilution ================================================ FILE: Source/levels/themes.h ================================================ /** * @file themes.h * * Interface of the theme room placing algorithms. */ #pragma once #include #include "levels/gendung.h" #include "tables/objdat.h" namespace devilution { struct ThemeStruct { theme_id ttype; int8_t ttval; }; extern int numthemes; extern bool armorFlag; extern bool weaponFlag; extern int zharlib; extern ThemeStruct themes[MAXTHEMES]; void InitThemes(); /** * @brief HoldThemeRooms marks theme rooms as populated. */ void HoldThemeRooms(); /** * CreateThemeRooms adds thematic elements to rooms. */ void CreateThemeRooms(); } // namespace devilution ================================================ FILE: Source/levels/tile_properties.cpp ================================================ #include "levels/tile_properties.hpp" #include "engine/direction.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "gendung.h" #include "objects.h" namespace devilution { bool IsTileNotSolid(Point position) { if (!InDungeonBounds(position)) { return false; } return !TileHasAny(position, TileProperties::Solid); } bool IsTileSolid(Point position) { if (!InDungeonBounds(position)) { return false; } return TileHasAny(position, TileProperties::Solid); } bool IsTileWalkable(Point position, bool ignoreDoors) { Object *object = FindObjectAtPosition(position); if (object != nullptr) { if (ignoreDoors && object->isDoor()) { return true; } if (object->_oSolidFlag) { return false; } } return IsTileNotSolid(position); } bool IsTileOccupied(Point position) { if (!InDungeonBounds(position)) { return true; // OOB positions are considered occupied. } if (IsTileSolid(position)) { return true; } if (dMonster[position.x][position.y] != 0) { return true; } if (dPlayer[position.x][position.y] != 0) { return true; } if (IsObjectAtPosition(position)) { return true; } return false; } bool CanStep(Point startPosition, Point destinationPosition) { // These checks are written as if working backwards from the destination to the source, given // both tiles are expected to be adjacent this doesn't matter beyond being a bit confusing bool rv = true; switch (GetPathDirection(startPosition, destinationPosition)) { case 5: // Stepping north rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::SouthEast); break; case 6: // Stepping east rv = IsTileNotSolid(destinationPosition + Direction::SouthWest) && IsTileNotSolid(destinationPosition + Direction::NorthWest); break; case 7: // Stepping south rv = IsTileNotSolid(destinationPosition + Direction::NorthEast) && IsTileNotSolid(destinationPosition + Direction::NorthWest); break; case 8: // Stepping west rv = IsTileNotSolid(destinationPosition + Direction::SouthEast) && IsTileNotSolid(destinationPosition + Direction::NorthEast); break; } return rv; } } // namespace devilution ================================================ FILE: Source/levels/tile_properties.hpp ================================================ #pragma once #include "engine/point.hpp" namespace devilution { [[nodiscard]] bool IsTileNotSolid(Point position); [[nodiscard]] bool IsTileSolid(Point position); /** * @brief Checks the position is solid or blocked by an object */ [[nodiscard]] bool IsTileWalkable(Point position, bool ignoreDoors = false); /** * @brief Checks if the position contains an object, player, monster, or solid dungeon piece */ [[nodiscard]] bool IsTileOccupied(Point position); /** * @brief check if stepping from a given position to a neighbouring tile cuts a corner. * * If you step from A to B, both Xs need to be clear: * * AX * XB * * @return true if step is allowed */ [[nodiscard]] bool CanStep(Point startPosition, Point destinationPosition); } // namespace devilution ================================================ FILE: Source/levels/town.cpp ================================================ #include "levels/town.h" #include #include "engine/load_file.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "levels/drlg_l1.h" #include "levels/trigs.h" #include "multi.h" #include "player.h" #include "quests.h" #include "utils/endian_swap.hpp" namespace devilution { namespace { /** * @brief Load level data into dPiece * @param path Path of dun file * @param xi upper left destination * @param yy upper left destination */ void FillSector(const char *path, int xi, int yy) { auto dunData = LoadFileInMem(path); const WorldTileSize size = GetDunSize(dunData.get()); const uint16_t *tileLayer = &dunData[2]; for (WorldTileCoord j = 0; j < size.height; j++) { int xx = xi; for (WorldTileCoord i = 0; i < size.width; i++) { int v1 = 218; int v2 = 218; int v3 = 218; int v4 = 218; const int tileId = Swap16LE(tileLayer[j * size.width + i]) - 1; if (tileId >= 0) { const MegaTile mega = pMegaTiles[tileId]; v1 = Swap16LE(mega.micro1); v2 = Swap16LE(mega.micro2); v3 = Swap16LE(mega.micro3); v4 = Swap16LE(mega.micro4); } dPiece[xx + 0][yy + 0] = v1; dPiece[xx + 1][yy + 0] = v2; dPiece[xx + 0][yy + 1] = v3; dPiece[xx + 1][yy + 1] = v4; xx += 2; } yy += 2; } } /** * @brief Load a tile in to dPiece * @param xx upper left destination * @param yy upper left destination * @param t tile id */ void FillTile(int xx, int yy, int t) { const MegaTile mega = pMegaTiles[t - 1]; dPiece[xx + 0][yy + 0] = Swap16LE(mega.micro1); dPiece[xx + 1][yy + 0] = Swap16LE(mega.micro2); dPiece[xx + 0][yy + 1] = Swap16LE(mega.micro3); dPiece[xx + 1][yy + 1] = Swap16LE(mega.micro4); } /** * @brief Update the map to show the closed hive */ void TownCloseHive() { dungeon[35][27] = 18; dungeon[36][27] = 63; dPiece[78][60] = 0x489; dPiece[79][60] = 0x4ea; dPiece[78][61] = 0x4eb; dPiece[79][61] = 0x4ec; dPiece[78][62] = 0x4ed; dPiece[79][62] = 0x4ee; dPiece[78][63] = 0x4ef; dPiece[79][63] = 0x4f0; dPiece[78][64] = 0x4f1; dPiece[79][64] = 0x4f2; dPiece[78][65] = 0x4f3; dPiece[80][60] = 0x4f4; dPiece[81][60] = 0x4f5; dPiece[80][61] = 0x4f6; dPiece[81][61] = 0x4f7; dPiece[82][60] = 0x4f8; dPiece[83][60] = 0x4f9; dPiece[82][61] = 0x4fa; dPiece[83][61] = 0x4fb; dPiece[80][62] = 0x4fc; dPiece[81][62] = 0x4fd; dPiece[80][63] = 0x4fe; dPiece[81][63] = 0x4ff; dPiece[80][64] = 0x500; dPiece[81][64] = 0x501; dPiece[80][65] = 0x502; dPiece[81][65] = 0x503; dPiece[82][64] = 0x508; dPiece[83][64] = 0x509; dPiece[82][65] = 0x50a; dPiece[83][65] = 0x50b; dPiece[82][62] = 0x504; dPiece[83][62] = 0x505; dPiece[82][63] = 0x506; dPiece[83][63] = 0x507; dPiece[84][61] = 279; dPiece[84][62] = 280; dPiece[84][63] = 279; dPiece[84][64] = 10; dPiece[85][60] = 11; dPiece[85][61] = 12; dPiece[85][62] = 13; dPiece[85][63] = 14; dPiece[85][64] = 15; dPiece[86][60] = 16; dPiece[86][61] = 17; } /** * @brief Update the map to show the closed grave */ void TownCloseGrave() { dPiece[36][21] = 0x52a; dPiece[37][21] = 0x52b; dPiece[36][22] = 0x52c; dPiece[37][22] = 0x52d; dPiece[36][23] = 0x52e; dPiece[37][23] = 0x52f; dPiece[36][24] = 0x530; dPiece[37][24] = 0x531; dPiece[35][21] = 0x53a; dPiece[34][21] = 0x53b; } void InitTownPieces() { for (int y = 0; y < MAXDUNY; y++) { for (int x = 0; x < MAXDUNX; x++) { if (dPiece[x][y] == 359) { dSpecial[x][y] = 1; } else if (dPiece[x][y] == 357) { dSpecial[x][y] = 2; } else if (dPiece[x][y] == 128) { dSpecial[x][y] = 6; } else if (dPiece[x][y] == 129) { dSpecial[x][y] = 7; } else if (dPiece[x][y] == 127) { dSpecial[x][y] = 8; } else if (dPiece[x][y] == 116) { dSpecial[x][y] = 9; } else if (dPiece[x][y] == 156) { dSpecial[x][y] = 10; } else if (dPiece[x][y] == 157) { dSpecial[x][y] = 11; } else if (dPiece[x][y] == 155) { dSpecial[x][y] = 12; } else if (dPiece[x][y] == 161) { dSpecial[x][y] = 13; } else if (dPiece[x][y] == 159) { dSpecial[x][y] = 14; } else if (dPiece[x][y] == 213) { dSpecial[x][y] = 15; } else if (dPiece[x][y] == 211) { dSpecial[x][y] = 16; } else if (dPiece[x][y] == 216) { dSpecial[x][y] = 17; } else if (dPiece[x][y] == 215) { dSpecial[x][y] = 18; } } } } /** * @brief Initialize all of the levels data */ void DrlgTPass3() { for (int yy = 0; yy < MAXDUNY; yy += 2) { for (int xx = 0; xx < MAXDUNX; xx += 2) { dPiece[xx][yy] = 426; dPiece[xx + 1][yy] = 426; dPiece[xx][yy + 1] = 426; dPiece[xx + 1][yy + 1] = 426; } } FillSector("levels\\towndata\\sector1s.dun", 46, 46); FillSector("levels\\towndata\\sector2s.dun", 46, 0); FillSector("levels\\towndata\\sector3s.dun", 0, 46); FillSector("levels\\towndata\\sector4s.dun", 0, 0); auto dunData = LoadFileInMem("levels\\towndata\\automap.dun"); PlaceDunTiles(dunData.get(), { 0, 0 }); if (!IsWarpOpen(DTYPE_CATACOMBS)) { dungeon[20][7] = 10; dungeon[20][6] = 8; FillTile(48, 20, 320); } if (!IsWarpOpen(DTYPE_CAVES)) { dungeon[4][30] = 8; FillTile(16, 68, 332); FillTile(16, 70, 331); } if (!IsWarpOpen(DTYPE_HELL)) { dungeon[15][35] = 7; dungeon[16][35] = 7; dungeon[17][35] = 7; for (int x = 36; x < 46; x++) { FillTile(x, 78, PickRandomlyAmong({ 1, 2, 3, 4 })); } } if (gbIsHellfire) { if (IsWarpOpen(DTYPE_NEST)) { TownOpenHive(); } else { TownCloseHive(); } if (IsWarpOpen(DTYPE_CRYPT)) TownOpenGrave(); else TownCloseGrave(); } if (Quests[Q_PWATER]._qactive != QUEST_DONE && Quests[Q_PWATER]._qactive != QUEST_NOTAVAIL) { FillTile(60, 70, 342); } else { FillTile(60, 70, 71); } InitTownPieces(); } } // namespace bool OpensHive(Point position) { const int yp = position.y; const int xp = position.x; return xp >= 79 && xp <= 82 && yp >= 61 && yp <= 64; } bool OpensGrave(Point position) { const int yp = position.y; const int xp = position.x; return xp >= 35 && xp <= 38 && yp >= 20 && yp <= 24; } void OpenHive() { NetSendCmd(false, CMD_OPENHIVE); auto &quest = Quests[Q_FARMER]; quest._qactive = QUEST_DONE; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); } void OpenGrave() { NetSendCmd(false, CMD_OPENGRAVE); auto &quest = Quests[Q_GRAVE]; quest._qactive = QUEST_DONE; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); } void TownOpenHive() { dungeon[36][27] = 47; dPiece[78][60] = 0x489; dPiece[79][60] = 0x48a; dPiece[78][61] = 0x48b; dPiece[79][61] = 0x50d; dPiece[78][62] = 0x4ed; dPiece[78][63] = 0x4ef; dPiece[79][62] = 0x50f; dPiece[79][63] = 0x510; dPiece[79][64] = 0x511; dPiece[78][64] = 0x119; dPiece[78][65] = 0x11b; dPiece[79][65] = 0x11c; dPiece[80][60] = 0x512; dPiece[80][61] = 0x514; dPiece[81][61] = 0x515; dPiece[82][60] = 0x516; dPiece[83][60] = 0x517; dPiece[82][61] = 0x518; dPiece[83][61] = 0x519; dPiece[80][62] = 0x51a; dPiece[81][62] = 0x51b; dPiece[80][63] = 0x51c; dPiece[81][63] = 0x51d; dPiece[80][64] = 0x51e; dPiece[81][64] = 0x51f; dPiece[80][65] = 0x520; dPiece[81][65] = 0x521; dPiece[82][64] = 0x526; dPiece[83][64] = 0x527; dPiece[82][65] = 0x528; dPiece[83][65] = 0x529; dPiece[82][62] = 0x522; dPiece[83][62] = 0x523; dPiece[82][63] = 0x524; dPiece[83][63] = 0x525; dPiece[84][61] = 279; dPiece[84][62] = 280; dPiece[84][63] = 279; dPiece[84][64] = 10; dPiece[85][60] = 11; dPiece[85][61] = 12; dPiece[85][62] = 13; dPiece[85][63] = 14; dPiece[85][64] = 15; dPiece[86][60] = 16; dPiece[86][61] = 17; } void TownOpenGrave() { dungeon[14][8] = 47; dungeon[14][7] = 47; dPiece[36][21] = 0x532; dPiece[37][21] = 0x533; dPiece[36][22] = 0x534; dPiece[37][22] = 0x535; dPiece[36][23] = 0x536; dPiece[37][23] = 0x537; dPiece[36][24] = 0x538; dPiece[37][24] = 0x539; dPiece[35][21] = 0x53a; dPiece[34][21] = 0x53b; } void CleanTownFountain() { if (!pMegaTiles) return; FillTile(60, 70, 71); } void CreateTown(lvl_entry entry) { dminPosition = { 10, 10 }; dmaxPosition = { 84, 84 }; if (entry == ENTRY_MAIN) { // New game ViewPosition = { 75, 68 }; } else if (entry == ENTRY_PREV) { // Cathedral ViewPosition = { 25, 31 }; } else if (entry == ENTRY_TWARPUP) { if (TWarpFrom == 5) { ViewPosition = { 49, 22 }; } if (TWarpFrom == 9) { ViewPosition = { 18, 69 }; } if (TWarpFrom == 13) { ViewPosition = { 41, 81 }; } if (TWarpFrom == 21) { ViewPosition = { 36, 25 }; } if (TWarpFrom == 17) { ViewPosition = { 79, 62 }; } } DrlgTPass3(); } } // namespace devilution ================================================ FILE: Source/levels/town.h ================================================ /** * @file town.h * * Interface of functionality for rendering the town, towners and calling other render routines. */ #pragma once #include "interfac.h" #include "levels/gendung.h" namespace devilution { /** * @brief Check if hive can be opened by dropping rune bomb on a tile * @param position The position of the tile * @return True if the bomb would open hive */ bool OpensHive(Point position); /** * @brief Check if grave can be opened by dropping cathedral map on a tile * @param position The position of the tile * @return True if the map would open the grave */ bool OpensGrave(Point position); /** * @brief Initiate opening of hive by sending network messages and updating quest state */ void OpenHive(); /** * @brief Initiate opening of grave by sending network messages and updating quest state */ void OpenGrave(); /** * @brief Update the map to show the open hive */ void TownOpenHive(); /** * @brief Update the map to show the open grave */ void TownOpenGrave(); /** * @brief Update town to show clean/not poisoned water fountain */ void CleanTownFountain(); /** * @brief Initialize town level * @param entry Method of entry */ void CreateTown(lvl_entry entry); } // namespace devilution ================================================ FILE: Source/levels/trigs.cpp ================================================ /** * @file trigs.cpp * * Implementation of functionality for triggering events when the player enters an area. */ #include "levels/trigs.h" #include #include #include #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "diablo_msg.hpp" #include "game_mode.hpp" #include "multi.h" #include "utils/algorithm/container.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/utf8.hpp" namespace devilution { bool trigflag; int numtrigs; TriggerStruct trigs[MAXTRIGGERS]; int TWarpFrom; namespace { /** Specifies the dungeon piece IDs which constitute stairways leading down to the cathedral from town. */ const uint16_t TownDownList[] = { 715, 714, 718, 719, 720, 722, 723, 724, 725, 726 }; /** Specifies the dungeon piece IDs which constitute stairways leading down to the catacombs from town. */ const uint16_t TownWarp1List[] = { 1170, 1171, 1172, 1173, 1174, 1175, 1176, 1177, 1178, 1180, 1182, 1184 }; const uint16_t TownCryptList[] = { 1330, 1331, 1332, 1333, 1334, 1335, 1336, 1337 }; const uint16_t TownHiveList[] = { 1306, 1307, 1308, 1309 }; /** Specifies the dungeon piece IDs which constitute stairways leading up from the cathedral. */ const uint16_t L1UpList[] = { 126, 128, 129, 130, 131, 132, 134, 136, 137, 138, 139 }; /** Specifies the dungeon piece IDs which constitute stairways leading down from the cathedral. */ const uint16_t L1DownList[] = { 105, 106, 107, 108, 109, 111, 113, 114, 117 }; /** Specifies the dungeon piece IDs which constitute stairways leading up from the catacombs. */ const uint16_t L2UpList[] = { 265, 266 }; /** Specifies the dungeon piece IDs which constitute stairways leading down from the catacombs. */ const uint16_t L2DownList[] = { 268, 269, 270, 271 }; /** Specifies the dungeon piece IDs which constitute stairways leading up to town from the catacombs. */ const uint16_t L2TWarpUpList[] = { 557, 558 }; /** Specifies the dungeon piece IDs which constitute stairways leading up from the caves. */ const uint16_t L3UpList[] = { 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182 }; /** Specifies the dungeon piece IDs which constitute stairways leading down from the caves. */ const uint16_t L3DownList[] = { 161, 162, 163, 164, 165, 166, 167, 168 }; /** Specifies the dungeon piece IDs which constitute stairways leading up to town from the caves. */ const uint16_t L3TWarpUpList[] = { 181, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559 }; /** Specifies the dungeon piece IDs which constitute stairways leading up from hell. */ const uint16_t L4UpList[] = { 81, 82, 89 }; /** Specifies the dungeon piece IDs which constitute stairways leading down from hell. */ const uint16_t L4DownList[] = { 119, 129, 130, 131, 132 }; /** Specifies the dungeon piece IDs which constitute stairways leading up to town from hell. */ const uint16_t L4TWarpUpList[] = { 420, 421, 428 }; /** Specifies the dungeon piece IDs which constitute stairways leading down to Diablo from hell. */ const uint16_t L4PentaList[] = { 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383 }; const uint16_t L5TWarpUpList[] = { 171, 172, 173, 174, 175, 176, 177, 178, 183 }; const uint16_t L5UpList[] = { 148, 149, 150, 151, 152, 153, 154, 156, 157, 158 }; const uint16_t L5DownList[] = { 124, 125, 128, 130, 131, 134, 135, 139, 141 }; const uint16_t L6TWarpUpList[] = { 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91 }; const uint16_t L6UpList[] = { 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77 }; const uint16_t L6DownList[] = { 56, 57, 58, 59, 60, 61, 62, 63 }; } // namespace void InitNoTriggers() { numtrigs = 0; trigflag = false; } bool IsWarpOpen(dungeon_type type) { if (gbIsSpawn) return false; if (gbIsMultiplayer && type != DTYPE_NEST) // Opening the nest is part of in town quest return true; const Player &myPlayer = *MyPlayer; if (type == DTYPE_CATACOMBS && (myPlayer.pTownWarps & 1) != 0) return true; if (type == DTYPE_CAVES && (myPlayer.pTownWarps & 2) != 0) return true; if (type == DTYPE_HELL && (myPlayer.pTownWarps & 4) != 0) return true; if (gbIsHellfire) { if (type == DTYPE_CATACOMBS && myPlayer.getCharacterLevel() >= 10) return true; if (type == DTYPE_CAVES && myPlayer.getCharacterLevel() >= 15) return true; if (type == DTYPE_HELL && myPlayer.getCharacterLevel() >= 20) return true; if (type == DTYPE_NEST && IsAnyOf(Quests[Q_FARMER]._qactive, QUEST_DONE, QUEST_HIVE_DONE)) return true; if (type == DTYPE_CRYPT && Quests[Q_GRAVE]._qactive == QUEST_DONE) return true; } return false; } void InitTownTriggers() { numtrigs = 0; // Cathedral trigs[numtrigs].position = { 25, 29 }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; if (IsWarpOpen(DTYPE_CATACOMBS)) { trigs[numtrigs].position = { 49, 21 }; trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; trigs[numtrigs]._tlvl = 5; numtrigs++; } if (IsWarpOpen(DTYPE_CAVES)) { trigs[numtrigs].position = { 17, 69 }; trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; trigs[numtrigs]._tlvl = 9; numtrigs++; } if (IsWarpOpen(DTYPE_HELL)) { trigs[numtrigs].position = { 41, 80 }; trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; trigs[numtrigs]._tlvl = 13; numtrigs++; } if (IsWarpOpen(DTYPE_NEST)) { trigs[numtrigs].position = { 80, 62 }; trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; trigs[numtrigs]._tlvl = 17; numtrigs++; } if (IsWarpOpen(DTYPE_CRYPT)) { trigs[numtrigs].position = { 36, 24 }; trigs[numtrigs]._tmsg = WM_DIABTOWNWARP; trigs[numtrigs]._tlvl = 21; numtrigs++; } trigflag = false; } void InitL1Triggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 128) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 114) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } trigflag = false; } void InitL2Triggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 266 && (!Quests[Q_SCHAMB].IsAvailable() || i != Quests[Q_SCHAMB].position.x || j != Quests[Q_SCHAMB].position.y)) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 558) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABTWARPUP; trigs[numtrigs]._tlvl = 0; numtrigs++; } if (dPiece[i][j] == 270) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } trigflag = false; } void InitL3Triggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 170) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 167) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } if (dPiece[i][j] == 548) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABTWARPUP; numtrigs++; } } } trigflag = false; } void InitL4Triggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 82) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 421) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABTWARPUP; trigs[numtrigs]._tlvl = 0; numtrigs++; } if (dPiece[i][j] == 119) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 369 && Quests[Q_BETRAYER]._qactive == QUEST_DONE) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } trigflag = false; } void InitHiveTriggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 65) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 62) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } if (dPiece[i][j] == 79) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABTWARPUP; numtrigs++; } } } trigflag = false; } void InitCryptTriggers() { numtrigs = 0; for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 183) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABTWARPUP; trigs[numtrigs]._tlvl = 0; numtrigs++; } if (dPiece[i][j] == 157) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABPREVLVL; numtrigs++; } if (dPiece[i][j] == 125) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } trigflag = false; } void InitSKingTriggers() { trigflag = false; numtrigs = 1; trigs[0].position = { 82, 42 }; trigs[0]._tmsg = WM_DIABRTNLVL; } void InitSChambTriggers() { trigflag = false; numtrigs = 1; trigs[0].position = { 70, 39 }; trigs[0]._tmsg = WM_DIABRTNLVL; } void InitPWaterTriggers() { trigflag = false; numtrigs = 1; trigs[0].position = { 30, 83 }; trigs[0]._tmsg = WM_DIABRTNLVL; } void InitVPTriggers() { trigflag = false; numtrigs = 1; trigs[0].position = { 35, 32 }; trigs[0]._tmsg = WM_DIABRTNLVL; } bool ForceTownTrig() { for (const uint16_t tileId : TownDownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = _("Down to dungeon"); cursPosition = { 25, 29 }; return true; } } if (IsWarpOpen(DTYPE_CATACOMBS)) { for (const uint16_t tileId : TownWarp1List) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = _("Down to catacombs"); cursPosition = { 49, 21 }; return true; } } } if (IsWarpOpen(DTYPE_CAVES)) { for (uint16_t i = 1198; i <= 1219; ++i) { if (dPiece[cursPosition.x][cursPosition.y] == i) { InfoString = _("Down to caves"); cursPosition = { 17, 69 }; return true; } } } if (IsWarpOpen(DTYPE_HELL)) { for (uint16_t i = 1239; i <= 1254; ++i) { if (dPiece[cursPosition.x][cursPosition.y] == i) { InfoString = _("Down to hell"); cursPosition = { 41, 80 }; return true; } } } if (IsWarpOpen(DTYPE_NEST)) { for (const uint16_t tileId : TownHiveList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = _("Down to Hive"); cursPosition = { 80, 62 }; return true; } } } if (IsWarpOpen(DTYPE_CRYPT)) { for (const uint16_t tileId : TownCryptList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = _("Down to Crypt"); cursPosition = { 36, 24 }; return true; } } } return false; } bool ForceL1Trig() { for (const uint16_t tileId : L1UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { if (currlevel > 1) InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); else InfoString = _("Up to town"); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { cursPosition = trigs[j].position; return true; } } } } for (const uint16_t tileId : L1DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } return false; } bool ForceL2Trig() { for (const uint16_t tileId : L2UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); cursPosition = trigs[j].position; return true; } } } } } for (const uint16_t tileId : L2DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } if (currlevel == 5) { for (const uint16_t tileId : L2TWarpUpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABTWARPUP) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = _("Up to town"); cursPosition = trigs[j].position; return true; } } } } } } return false; } bool ForceL3Trig() { for (const uint16_t tileId : L3UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { cursPosition = trigs[j].position; return true; } } } } } for (const uint16_t tileId : L3DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId || dPiece[cursPosition.x + 1][cursPosition.y] == tileId || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } if (currlevel == 9) { for (const uint16_t tileId : L3TWarpUpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABTWARPUP) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = _("Up to town"); cursPosition = trigs[j].position; return true; } } } } } } return false; } bool ForceL4Trig() { for (const uint16_t tileId : L4UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Up to level {:d}")), currlevel - 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { cursPosition = trigs[j].position; return true; } } } } for (const uint16_t tileId : L4DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel + 1); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } if (currlevel == 13) { for (const uint16_t tileId : L4TWarpUpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABTWARPUP) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = _("Up to town"); cursPosition = trigs[j].position; return true; } } } } } } if (currlevel == 15) { for (const uint16_t tileId : L4PentaList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = _("Down to Diablo"); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } } return false; } bool ForceHiveTrig() { for (const uint16_t tileId : L6UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Up to Nest level {:d}")), currlevel - 17); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { cursPosition = trigs[j].position; return true; } } } } for (const uint16_t tileId : L6DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId || dPiece[cursPosition.x + 1][cursPosition.y] == tileId || dPiece[cursPosition.x + 2][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to level {:d}")), currlevel - 15); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } if (currlevel == 17) { for (const uint16_t tileId : L6TWarpUpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABTWARPUP) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = _("Up to town"); cursPosition = trigs[j].position; return true; } } } } } } return false; } bool ForceCryptTrig() { for (const uint16_t tileId : L5UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Up to Crypt level {:d}")), currlevel - 21); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABPREVLVL) { cursPosition = trigs[j].position; return true; } } } } if (dPiece[cursPosition.x][cursPosition.y] == 316) { InfoString = _("Cornerstone of the World"); return true; } for (const uint16_t tileId : L5DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Down to Crypt level {:d}")), currlevel - 19); for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABNEXTLVL) { cursPosition = trigs[j].position; return true; } } } } if (currlevel == 21) { for (const uint16_t tileId : L5TWarpUpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { for (int j = 0; j < numtrigs; j++) { if (trigs[j]._tmsg == WM_DIABTWARPUP) { const int dx = std::abs(trigs[j].position.x - cursPosition.x); const int dy = std::abs(trigs[j].position.y - cursPosition.y); if (dx < 4 && dy < 4) { InfoString = _("Up to town"); cursPosition = trigs[j].position; return true; } } } } } } return false; } void Freeupstairs() { for (int i = 0; i < numtrigs; i++) { const int tx = trigs[i].position.x; const int ty = trigs[i].position.y; for (int yy = -2; yy <= 2; yy++) { for (int xx = -2; xx <= 2; xx++) { dFlags[tx + xx][ty + yy] |= DungeonFlag::Populated; } } } } bool ForceSKingTrig() { for (const uint16_t tileId : L1UpList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SKELKING]._qlevel); cursPosition = trigs[0].position; return true; } } return false; } bool ForceSChambTrig() { for (const uint16_t tileId : L2DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_SCHAMB]._qlevel); cursPosition = trigs[0].position; return true; } } return false; } bool ForcePWaterTrig() { for (const uint16_t tileId : L3DownList) { if (dPiece[cursPosition.x][cursPosition.y] == tileId) { InfoString = fmt::format(fmt::runtime(_("Back to Level {:d}")), Quests[Q_PWATER]._qlevel); cursPosition = trigs[0].position; return true; } } return false; } bool ForceArenaTrig() { const uint16_t *checkList; size_t len; switch (setlvltype) { case DTYPE_TOWN: checkList = TownWarp1List; len = sizeof(TownWarp1List) / sizeof(TownWarp1List[0]); break; case DTYPE_CATHEDRAL: checkList = L1UpList; len = sizeof(L1UpList) / sizeof(L1UpList[0]); break; case DTYPE_CATACOMBS: checkList = L2TWarpUpList; len = sizeof(L2TWarpUpList) / sizeof(L2TWarpUpList[0]); break; case DTYPE_CAVES: checkList = L3TWarpUpList; len = sizeof(L3TWarpUpList) / sizeof(L3TWarpUpList[0]); break; case DTYPE_HELL: checkList = L4TWarpUpList; len = sizeof(L4TWarpUpList) / sizeof(L4TWarpUpList[0]); break; case DTYPE_NEST: checkList = L5TWarpUpList; len = sizeof(L5TWarpUpList) / sizeof(L5TWarpUpList[0]); break; case DTYPE_CRYPT: checkList = L6TWarpUpList; len = sizeof(L6TWarpUpList) / sizeof(L6TWarpUpList[0]); break; default: return false; } for (size_t i = 0; i < len; ++i) { if (dPiece[cursPosition.x][cursPosition.y] == checkList[i]) { InfoString = _("Up to town"); cursPosition = trigs[0].position; return true; } } return false; } void CheckTrigForce() { trigflag = false; if (ControlMode == ControlTypes::KeyboardAndMouse && GetMainPanel().contains(MousePosition)) { return; } if (!setlevel) { switch (leveltype) { case DTYPE_TOWN: trigflag = ForceTownTrig(); break; case DTYPE_CATHEDRAL: trigflag = ForceL1Trig(); break; case DTYPE_CATACOMBS: trigflag = ForceL2Trig(); break; case DTYPE_CAVES: trigflag = ForceL3Trig(); break; case DTYPE_HELL: trigflag = ForceL4Trig(); break; case DTYPE_NEST: trigflag = ForceHiveTrig(); break; case DTYPE_CRYPT: trigflag = ForceCryptTrig(); break; default: break; } if (leveltype != DTYPE_TOWN && !trigflag) { trigflag = ForceQuests(); } } else { switch (setlvlnum) { case SL_SKELKING: trigflag = ForceSKingTrig(); break; case SL_BONECHAMB: trigflag = ForceSChambTrig(); break; case SL_POISONWATER: trigflag = ForcePWaterTrig(); break; default: if (IsArenaLevel(setlvlnum)) trigflag = ForceArenaTrig(); break; } } } void CheckTriggers() { Player &myPlayer = *MyPlayer; if (myPlayer._pmode != PM_STAND) return; for (int i = 0; i < numtrigs; i++) { if (myPlayer.position.tile != trigs[i].position) { continue; } switch (trigs[i]._tmsg) { case WM_DIABNEXTLVL: if (gbIsSpawn && currlevel >= 2) { NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, { myPlayer.position.tile.x, myPlayer.position.tile.y + 1 }); myPlayer.Say(HeroSpeech::NotAChance); InitDiabloMsg(EMSG_NOT_IN_SHAREWARE); } else { StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel + 1); } break; case WM_DIABPREVLVL: StartNewLvl(myPlayer, trigs[i]._tmsg, currlevel - 1); break; case WM_DIABRTNLVL: StartNewLvl(myPlayer, trigs[i]._tmsg, GetMapReturnLevel()); break; case WM_DIABTOWNWARP: if (gbIsMultiplayer) { bool abort = false; diablo_message abortflag; auto position = myPlayer.position.tile; if (trigs[i]._tlvl == 5 && myPlayer.getCharacterLevel() < 8) { abort = true; position.y += 1; abortflag = EMSG_REQUIRES_LVL_8; } if (IsAnyOf(trigs[i]._tlvl, 9, 17) && myPlayer.getCharacterLevel() < 13) { abort = true; position.x += 1; abortflag = EMSG_REQUIRES_LVL_13; } if (IsAnyOf(trigs[i]._tlvl, 13, 21) && myPlayer.getCharacterLevel() < 17) { abort = true; position.y += 1; abortflag = EMSG_REQUIRES_LVL_17; } if (abort) { myPlayer.Say(HeroSpeech::ICantGetThereFromHere); InitDiabloMsg(abortflag); NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, position); return; } } StartNewLvl(myPlayer, trigs[i]._tmsg, trigs[i]._tlvl); break; case WM_DIABTWARPUP: TWarpFrom = currlevel; StartNewLvl(myPlayer, trigs[i]._tmsg, 0); break; default: app_fatal("Unknown trigger msg"); } } } bool EntranceBoundaryContains(Point entrance, Point position) { constexpr Displacement entranceOffsets[7] = { { 0, 0 }, { -1, 0 }, { 0, -1 }, { -1, -1 }, { -2, -1 }, { -1, -2 }, { -2, -2 } }; return c_any_of( entranceOffsets, [=](Displacement offset) { return entrance + offset == position; }); } } // namespace devilution ================================================ FILE: Source/levels/trigs.h ================================================ /** * @file trigs.h * * Interface of functionality for triggering events when the player enters an area. */ #pragma once #include "engine/point.hpp" #include "interfac.h" #include "levels/gendung.h" namespace devilution { #define MAXTRIGGERS 7 struct TriggerStruct { WorldTilePosition position; interface_mode _tmsg; int _tlvl; }; extern bool trigflag; extern int numtrigs; extern TriggerStruct trigs[MAXTRIGGERS]; extern int TWarpFrom; void InitNoTriggers(); bool IsWarpOpen(dungeon_type type); void InitTownTriggers(); void InitL1Triggers(); void InitL2Triggers(); void InitL3Triggers(); void InitL4Triggers(); void InitHiveTriggers(); void InitCryptTriggers(); void InitSKingTriggers(); void InitSChambTriggers(); void InitPWaterTriggers(); void InitVPTriggers(); void Freeupstairs(); void CheckTrigForce(); void CheckTriggers(); /** * @brief Check if the provided position is in the entrance boundary of the entrance. * @param entrance The entrance to check. * @param position The position to check against the entrance boundary. */ bool EntranceBoundaryContains(Point entrance, Point position); } // namespace devilution ================================================ FILE: Source/lighting.cpp ================================================ /** * @file lighting.cpp * * Implementation of light and vision. */ #include "lighting.h" #include #include #include #include #include #include #include "automap.h" #include "engine/displacement.hpp" #include "engine/lighting_defs.hpp" #include "engine/load_file.hpp" #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/world_tile.hpp" #include "levels/tile_properties.hpp" #include "objects.h" #include "player.h" #include "utils/attributes.h" #include "utils/is_of.hpp" #include "utils/status_macros.hpp" #include "vision.hpp" namespace devilution { std::array VisionActive; Light VisionList[MAXVISION]; Light Lights[MAXLIGHTS]; std::array ActiveLights; int ActiveLightCount; std::array, NumLightingLevels> LightTables; uint8_t *FullyLitLightTable = nullptr; uint8_t *FullyDarkLightTable = nullptr; std::array InfravisionTable; std::array StoneTable; std::array PauseTable; #ifdef _DEBUG bool DisableLighting; #endif bool UpdateLighting; namespace { /** @brief Number of supported light radiuses (first radius starts with 0) */ constexpr size_t NumLightRadiuses = 16; /** Falloff tables for the light cone */ uint8_t LightFalloffs[NumLightRadiuses][128]; bool UpdateVision; /** interpolations of a 32x32 (16x16 mirrored) light circle moving between tiles in steps of 1/8 of a tile */ uint8_t LightConeInterpolations[8][8][16][16]; void RotateRadius(DisplacementOf &offset, DisplacementOf &dist, DisplacementOf &light, DisplacementOf &block) { dist = { static_cast(7 - dist.deltaY), dist.deltaX }; light = { static_cast(7 - light.deltaY), light.deltaX }; offset = { static_cast(dist.deltaX - light.deltaX), static_cast(dist.deltaY - light.deltaY) }; block.deltaX = 0; if (offset.deltaX < 0) { offset.deltaX += 8; block.deltaX = 1; } block.deltaY = 0; if (offset.deltaY < 0) { offset.deltaY += 8; block.deltaY = 1; } } DVL_ALWAYS_INLINE void SetLight(Point position, uint8_t v) { if (LoadingMapObjects) dPreLight[position.x][position.y] = v; else dLight[position.x][position.y] = v; } DVL_ALWAYS_INLINE uint8_t GetLight(Point position) { if (LoadingMapObjects) return dPreLight[position.x][position.y]; return dLight[position.x][position.y]; } bool TileAllowsLight(Point position) { if (!InDungeonBounds(position)) return false; return !TileHasAny(position, TileProperties::BlockLight); } void DoVisionFlags(Point position, MapExplorationType doAutomap, bool visible) { if (doAutomap != MAP_EXP_NONE) { if (dFlags[position.x][position.y] != DungeonFlag::None) SetAutomapView(position, doAutomap); dFlags[position.x][position.y] |= DungeonFlag::Explored; } if (visible) dFlags[position.x][position.y] |= DungeonFlag::Lit; dFlags[position.x][position.y] |= DungeonFlag::Visible; } } // namespace void DoUnLight(Point position, uint8_t radius) { radius++; radius++; // If lights moved at a diagonal it can result in some extra tiles being lit auto searchArea = PointsInRectangle(WorldTileRectangle { position, radius }); for (const WorldTilePosition targetPosition : searchArea) { if (InDungeonBounds(targetPosition)) dLight[targetPosition.x][targetPosition.y] = dPreLight[targetPosition.x][targetPosition.y]; } } void DoLighting(Point position, uint8_t radius, DisplacementOf offset) { assert(radius >= 0 && radius <= NumLightRadiuses); assert(InDungeonBounds(position)); DisplacementOf light = {}; DisplacementOf block = {}; if (offset.deltaX < 0) { offset.deltaX += 8; position -= { 1, 0 }; } if (offset.deltaY < 0) { offset.deltaY += 8; position -= { 0, 1 }; } DisplacementOf dist = offset; int minX = 15; if (position.x - 15 < 0) { minX = position.x + 1; } int maxX = 15; if (position.x + 15 > MAXDUNX) { maxX = MAXDUNX - position.x; } int minY = 15; if (position.y - 15 < 0) { minY = position.y + 1; } int maxY = 15; if (position.y + 15 > MAXDUNY) { maxY = MAXDUNY - position.y; } // Allow for dim lights in crypt and nest if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { if (GetLight(position) > LightFalloffs[radius][0]) SetLight(position, LightFalloffs[radius][0]); } else { SetLight(position, 0); } for (int i = 0; i < 4; i++) { const int yBound = i > 0 && i < 3 ? maxY : minY; const int xBound = i < 2 ? maxX : minX; for (int y = 0; y < yBound; y++) { for (int x = 1; x < xBound; x++) { const int linearDistance = LightConeInterpolations[offset.deltaX][offset.deltaY][x + block.deltaX][y + block.deltaY]; if (linearDistance >= 128) continue; const Point temp = position + (Displacement { x, y }).Rotate(-i); const uint8_t v = LightFalloffs[radius][linearDistance]; if (!InDungeonBounds(temp)) continue; if (v < GetLight(temp)) SetLight(temp, v); } } RotateRadius(offset, dist, light, block); } } void DoUnVision(Point position, uint8_t radius) { radius++; radius++; // increasing the radius even further here prevents leaving stray vision tiles behind and doesn't seem to affect monster AI - applying new vision happens in the same tick auto searchArea = PointsInRectangle(WorldTileRectangle { position, radius }); for (const WorldTilePosition targetPosition : searchArea) { if (InDungeonBounds(targetPosition)) dFlags[targetPosition.x][targetPosition.y] &= ~(DungeonFlag::Visible | DungeonFlag::Lit); } } void DoVision(Point position, uint8_t radius, MapExplorationType doAutomap, bool visible) { auto markVisibleFn = [doAutomap, visible](Point rayPoint) { DoVisionFlags(rayPoint, doAutomap, visible); }; auto markTransparentFn = [](Point rayPoint) { const int8_t trans = dTransVal[rayPoint.x][rayPoint.y]; if (trans != 0) TransList[trans] = true; }; auto passesLightFn = [](Point rayPoint) { return TileAllowsLight(rayPoint); }; auto inBoundsFn = [](Point rayPoint) { return InDungeonBounds(rayPoint); }; DoVision(position, radius, markVisibleFn, markTransparentFn, passesLightFn, inBoundsFn); } tl::expected LoadTrns() { RETURN_IF_ERROR(LoadFileInMemWithStatus("plrgfx\\infra.trn", InfravisionTable)); RETURN_IF_ERROR(LoadFileInMemWithStatus("plrgfx\\stone.trn", StoneTable)); return LoadFileInMemWithStatus("gendata\\pause.trn", PauseTable); } void MakeLightTable() { // Generate 16 gradually darker translation tables for doing lighting uint8_t shade = 0; constexpr uint8_t Black = 0; constexpr uint8_t White = 255; for (auto &lightTable : LightTables) { uint8_t colorIndex = 0; for (const uint8_t steps : { 16, 16, 16, 16, 16, 16, 16, 16, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16 }) { const uint8_t shading = shade * steps / 16; const uint8_t shadeStart = colorIndex; const uint8_t shadeEnd = shadeStart + steps - 1; for (uint8_t step = 0; step < steps; step++) { if (colorIndex == Black) { lightTable[colorIndex++] = Black; continue; } int color = shadeStart + step + shading; if (color > shadeEnd || color == White) color = Black; lightTable[colorIndex++] = color; } } shade++; } LightTables[15] = {}; // Make last shade pitch black FullyLitLightTable = LightTables[0].data(); FullyDarkLightTable = LightTables[LightsMax].data(); if (leveltype == DTYPE_HELL) { // Blood wall lighting const auto shades = static_cast(LightTables.size() - 1); for (int i = 0; i < shades; i++) { auto &lightTable = LightTables[i]; constexpr int Range = 16; for (int j = 0; j < Range; j++) { uint8_t color = ((Range - 1) << 4) / shades * (shades - i) / Range * (j + 1); color = 1 + (color >> 4); int idx = j + 1; lightTable[idx] = color; idx = 31 - j; lightTable[idx] = color; } } FullyLitLightTable = nullptr; // A color map is used for the ceiling animation, so even fully lit tiles have a color map } else if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { // Make the lava fully bright for (auto &lightTable : LightTables) std::iota(lightTable.begin(), lightTable.begin() + 16, uint8_t { 0 }); LightTables[15][0] = 0; std::fill_n(LightTables[15].begin() + 1, 15, 1); FullyDarkLightTable = nullptr; // Tiles in Hellfire levels are never completely black } // Verify that fully lit and fully dark light table optimizations are correctly enabled/disabled (nullptr = disabled) assert((FullyLitLightTable != nullptr) == (LightTables[0][0] == 0 && std::adjacent_find(LightTables[0].begin(), LightTables[0].end() - 1, [](auto x, auto y) { return (x + 1) != y; }) == LightTables[0].end() - 1)); assert((FullyDarkLightTable != nullptr) == (std::all_of(LightTables[LightsMax].begin(), LightTables[LightsMax].end(), [](auto x) { return x == 0; }))); // Generate light falloffs ranges const float maxDarkness = 15; const float maxBrightness = 0; for (unsigned radius = 0; radius < NumLightRadiuses; radius++) { const unsigned maxDistance = (radius + 1) * 8; for (unsigned distance = 0; distance < 128; distance++) { if (distance > maxDistance) { LightFalloffs[radius][distance] = 15; } else { const float factor = static_cast(distance) / static_cast(maxDistance); float scaled; if (IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { // quardratic falloff with over exposure const float brightness = static_cast(radius) * 1.25F; scaled = factor * factor * brightness + (maxDarkness - brightness); scaled = std::max(maxBrightness, scaled); } else { // Leaner falloff scaled = factor * maxDarkness; } scaled += 0.5F; // Round up LightFalloffs[radius][distance] = static_cast(scaled); } } } // Generate the light cone interpolations for (int offsetY = 0; offsetY < 8; offsetY++) { for (int offsetX = 0; offsetX < 8; offsetX++) { for (int y = 0; y < 16; y++) { for (int x = 0; x < 16; x++) { const int a = (8 * x - offsetX); const int b = (8 * y - offsetY); LightConeInterpolations[offsetX][offsetY][x][y] = static_cast(sqrt(a * a + b * b)); } } } } } #ifdef _DEBUG void ToggleLighting() { DisableLighting = !DisableLighting; if (DisableLighting) { memset(dLight, 0, sizeof(dLight)); return; } memcpy(dLight, dPreLight, sizeof(dLight)); for (const Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) { DoLighting(player.position.tile, player._pLightRad, {}); } } } #endif void InitLighting() { ActiveLightCount = 0; UpdateLighting = false; UpdateVision = false; #ifdef _DEBUG DisableLighting = false; #endif std::iota(ActiveLights.begin(), ActiveLights.end(), uint8_t { 0 }); VisionActive = {}; TransList = {}; } int AddLight(Point position, uint8_t radius) { #ifdef _DEBUG if (DisableLighting) return NO_LIGHT; #endif if (ActiveLightCount >= MAXLIGHTS) return NO_LIGHT; const int lid = ActiveLights[ActiveLightCount++]; Light &light = Lights[lid]; light.position.tile = position; light.radius = radius; light.position.offset = { 0, 0 }; light.isInvalid = false; light.hasChanged = false; UpdateLighting = true; return lid; } void AddUnLight(int i) { #ifdef _DEBUG if (DisableLighting) return; #endif if (i == NO_LIGHT) return; Lights[i].isInvalid = true; UpdateLighting = true; } void ChangeLightRadius(int i, uint8_t radius) { #ifdef _DEBUG if (DisableLighting) return; #endif if (i == NO_LIGHT) return; Light &light = Lights[i]; light.hasChanged = true; light.position.old = light.position.tile; light.oldRadius = light.radius; light.radius = radius; UpdateLighting = true; } void ChangeLightXY(int i, Point position) { #ifdef _DEBUG if (DisableLighting) return; #endif if (i == NO_LIGHT) return; Light &light = Lights[i]; light.hasChanged = true; light.position.old = light.position.tile; light.oldRadius = light.radius; light.position.tile = position; UpdateLighting = true; } void ChangeLightOffset(int i, DisplacementOf offset) { #ifdef _DEBUG if (DisableLighting) return; #endif if (i == NO_LIGHT) return; Light &light = Lights[i]; if (light.position.offset == offset) return; light.hasChanged = true; light.position.old = light.position.tile; light.oldRadius = light.radius; light.position.offset = offset; UpdateLighting = true; } void ChangeLight(int i, Point position, uint8_t radius) { #ifdef _DEBUG if (DisableLighting) return; #endif if (i == NO_LIGHT) return; Light &light = Lights[i]; light.hasChanged = true; light.position.old = light.position.tile; light.oldRadius = light.radius; light.position.tile = position; light.radius = radius; UpdateLighting = true; } void ProcessLightList() { #ifdef _DEBUG if (DisableLighting) return; #endif if (!UpdateLighting) return; for (int i = 0; i < ActiveLightCount; i++) { Light &light = Lights[ActiveLights[i]]; if (light.isInvalid) { DoUnLight(light.position.tile, light.radius); } if (light.hasChanged) { DoUnLight(light.position.old, light.oldRadius); light.hasChanged = false; } } for (int i = 0; i < ActiveLightCount; i++) { const Light &light = Lights[ActiveLights[i]]; if (light.isInvalid) { ActiveLightCount--; std::swap(ActiveLights[ActiveLightCount], ActiveLights[i]); i--; continue; } if (TileHasAny(light.position.tile, TileProperties::Solid)) continue; // Monster hidden in a wall, don't spoil the surprise DoLighting(light.position.tile, light.radius, light.position.offset); } UpdateLighting = false; } void SavePreLighting() { memcpy(dPreLight, dLight, sizeof(dPreLight)); } void ActivateVision(Point position, int r, size_t id) { auto &vision = VisionList[id]; vision.position.tile = position; vision.radius = r; vision.isInvalid = false; vision.hasChanged = false; VisionActive[id] = true; UpdateVision = true; } void ChangeVisionRadius(size_t id, int r) { auto &vision = VisionList[id]; vision.hasChanged = true; vision.position.old = vision.position.tile; vision.oldRadius = vision.radius; vision.radius = r; UpdateVision = true; } void ChangeVisionXY(size_t id, Point position) { auto &vision = VisionList[id]; vision.hasChanged = true; vision.position.old = vision.position.tile; vision.oldRadius = vision.radius; vision.position.tile = position; UpdateVision = true; } void ProcessVisionList() { if (!UpdateVision) return; TransList = {}; for (const Player &player : Players) { const size_t id = player.getId(); if (!VisionActive[id]) continue; Light &vision = VisionList[id]; if (!player.plractive || !player.isOnActiveLevel() || (player._pLvlChanging && &player != MyPlayer)) { DoUnVision(vision.position.tile, vision.radius); VisionActive[id] = false; continue; } if (vision.hasChanged) { DoUnVision(vision.position.old, vision.oldRadius); vision.hasChanged = false; } } for (const Player &player : Players) { const size_t id = player.getId(); if (!VisionActive[id]) continue; const Light &vision = VisionList[id]; MapExplorationType doautomap = MAP_EXP_SELF; if (&player != MyPlayer) doautomap = player.friendlyMode ? MAP_EXP_OTHERS : MAP_EXP_NONE; DoVision( vision.position.tile, vision.radius, doautomap, &player == MyPlayer); } UpdateVision = false; } void lighting_color_cycling() { for (auto &lightTable : LightTables) { // shift elements between indexes 1-31 to left std::rotate(lightTable.begin() + 1, lightTable.begin() + 2, lightTable.begin() + 32); } } } // namespace devilution ================================================ FILE: Source/lighting.h ================================================ /** * @file lighting.h * * Interface of light and vision. */ #pragma once #include #include #include #include "automap.h" #include "engine/displacement.hpp" #include "engine/lighting_defs.hpp" #include "engine/point.hpp" #include "engine/world_tile.hpp" #include "utils/attributes.h" namespace devilution { struct LightPosition { WorldTilePosition tile; /** Pixel offset from tile. */ DisplacementOf offset; /** Previous position. */ WorldTilePosition old; }; struct Light { LightPosition position; uint8_t radius; uint8_t oldRadius; bool isInvalid; bool hasChanged; }; extern Light VisionList[MAXVISION]; extern std::array VisionActive; extern Light Lights[MAXLIGHTS]; extern std::array ActiveLights; extern int ActiveLightCount; extern DVL_API_FOR_TEST std::array, NumLightingLevels> LightTables; /** @brief Contains a pointer to a light table that is fully lit (no color mapping is required). Can be null in hell. */ extern DVL_API_FOR_TEST uint8_t *FullyLitLightTable; /** @brief Contains a pointer to a light table that is fully dark (every color result to 0/black). Can be null in hellfire levels. */ extern DVL_API_FOR_TEST uint8_t *FullyDarkLightTable; extern std::array InfravisionTable; extern std::array StoneTable; extern std::array PauseTable; #ifdef _DEBUG extern bool DisableLighting; #endif extern bool UpdateLighting; void DoUnLight(Point position, uint8_t radius); void DoLighting(Point position, uint8_t radius, DisplacementOf offset); void DoUnVision(Point position, uint8_t radius); void DoVision(Point position, uint8_t radius, MapExplorationType doAutomap, bool visible); tl::expected LoadTrns(); void MakeLightTable(); #ifdef _DEBUG void ToggleLighting(); #endif void InitLighting(); int AddLight(Point position, uint8_t radius); void AddUnLight(int i); void ChangeLightRadius(int i, uint8_t radius); void ChangeLightXY(int i, Point position); void ChangeLightOffset(int i, DisplacementOf offset); void ChangeLight(int i, Point position, uint8_t radius); void ProcessLightList(); void SavePreLighting(); void ActivateVision(Point position, int r, size_t id); void ChangeVisionRadius(size_t id, int r); void ChangeVisionXY(size_t id, Point position); void ProcessVisionList(); void lighting_color_cycling(); constexpr int MaxCrawlRadius = 18; } // namespace devilution ================================================ FILE: Source/loadsave.cpp ================================================ /** * @file loadsave.cpp * * Implementation of save game functionality. */ #include "loadsave.h" #include #include #include #include #include #include #include #include #include "automap.h" #include "codec.h" #include "control/control.hpp" #include "cursor.h" #include "dead.h" #include "doom.h" #include "engine/point.hpp" #include "engine/random.hpp" #include "game_mode.hpp" #include "inv.h" #include "levels/dun_tile.hpp" #include "lighting.h" #include "menu.h" #include "missiles.h" #include "monster.h" #include "monsters/validation.hpp" #include "mpq/mpq_common.hpp" #include "pfile.h" #include "plrmsg.h" #include "qol/stash.h" #include "stores.h" #include "tables/playerdat.hpp" #include "utils/algorithm/container.hpp" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/status_macros.hpp" namespace devilution { bool gbIsHellfireSaveGame; uint8_t giNumberOfLevels; namespace { constexpr size_t MaxMissilesForSaveGame = 125; constexpr size_t PlayerWalkPathSizeForSaveGame = 25; uint8_t giNumberQuests; uint8_t giNumberOfSmithPremiumItems; template T SwapLE(T in) { switch (sizeof(T)) { case 2: return static_cast(Swap16LE(static_cast(in))); case 4: return static_cast(Swap32LE(static_cast(in))); case 8: return static_cast(Swap64LE(in)); default: return in; } } template T SwapBE(T in) { switch (sizeof(T)) { case 2: return Swap16BE(in); case 4: return Swap32BE(in); case 8: return static_cast(Swap64BE(in)); default: return in; } } void TerminateUtf8(char *str, size_t maxLength) { const std::string_view inStr { str, maxLength }; const std::string_view truncStr = TruncateUtf8(inStr, maxLength - 1); const size_t utf8Length = truncStr.size(); str[utf8Length] = '\0'; } class LoadHelper { std::unique_ptr m_buffer_; size_t m_cur_ = 0; size_t m_size_; template T Next() { const auto size = sizeof(T); if (!IsValid(size)) return 0; T value; memcpy(&value, &m_buffer_[m_cur_], size); m_cur_ += size; return value; } public: LoadHelper(std::optional archive, const char *szFileName) { if (archive) m_buffer_ = ReadArchive(*archive, szFileName, &m_size_); else m_buffer_ = nullptr; } bool IsValid(size_t size = 1) { return m_buffer_ != nullptr && m_size_ >= (m_cur_ + size); } size_t Size() const { return m_size_; } template constexpr void Skip(size_t count = 1) { Skip(sizeof(T) * count); } void Skip(size_t size) { m_cur_ += size; } void NextBytes(void *bytes, size_t size) { if (!IsValid(size)) return; memcpy(bytes, &m_buffer_[m_cur_], size); m_cur_ += size; } template T NextLE() { return SwapLE(Next()); } template T NextBE() { return SwapBE(Next()); } template TDesired NextLENarrow(TSource modifier = 0) { static_assert(sizeof(TSource) > sizeof(TDesired), "Can only narrow to a smaller type"); TSource value = SwapLE(Next()) + modifier; return static_cast(std::clamp(value, std::numeric_limits::min(), std::numeric_limits::max())); } bool NextBool8() { return Next() != 0; } bool NextBool32() { return Next() != 0; } }; class SaveHelper { SaveWriter &m_mpqWriter; const char *m_szFileName_; std::unique_ptr m_buffer_; size_t m_cur_ = 0; size_t m_capacity_; public: SaveHelper(SaveWriter &mpqWriter, const char *szFileName, size_t bufferLen) : m_mpqWriter(mpqWriter) , m_szFileName_(szFileName) , m_buffer_(new std::byte[codec_get_encoded_len(bufferLen)]) , m_capacity_(bufferLen) { } bool IsValid(size_t len = 1) { return m_buffer_ != nullptr && m_capacity_ >= (m_cur_ + len); } template constexpr void Skip(size_t count = 1) { Skip(sizeof(T) * count); } void Skip(size_t len) { std::memset(&m_buffer_[m_cur_], 0, len); m_cur_ += len; } void WriteBytes(const void *bytes, size_t len) { if (!IsValid(len)) return; const auto *src = static_cast(bytes); for (size_t i = 0; i < len; ++i) { m_buffer_[m_cur_ + i] = src[i]; } m_cur_ += len; } template void WriteLE(T value) { value = SwapLE(value); WriteBytes(&value, sizeof(value)); } template void WriteBE(T value) { value = SwapBE(value); WriteBytes(&value, sizeof(value)); } ~SaveHelper() { const auto encodedLen = codec_get_encoded_len(m_cur_); const char *const password = pfile_get_password(); codec_encode(m_buffer_.get(), m_cur_, encodedLen, password); m_mpqWriter.WriteFile(m_szFileName_, m_buffer_.get(), encodedLen); } }; struct MonsterConversionData { int8_t monsterLevel; uint16_t experience; uint8_t toHit; uint8_t toHitSpecial; }; struct LevelConversionData { MonsterConversionData monsterConversionData[MaxMonsters]; }; [[nodiscard]] bool LoadItemData(LoadHelper &file, Item &item) { item._iSeed = file.NextLE(); item._iCreateInfo = file.NextLE(); file.Skip(2); // Alignment item._itype = static_cast(file.NextLE()); item.position.x = file.NextLE(); item.position.y = file.NextLE(); item._iAnimFlag = file.NextBool32(); file.Skip(4); // Skip pointer _iAnimData item.AnimInfo = {}; item.AnimInfo.numberOfFrames = file.NextLENarrow(); item.AnimInfo.currentFrame = file.NextLENarrow(-1); file.Skip(8); // Skip _iAnimWidth and _iAnimWidth2 file.Skip(4); // Unused since 1.02 item.selectionRegion = static_cast(file.NextLE()); file.Skip(3); // Alignment item._iPostDraw = file.NextBool32(); item._iIdentified = file.NextBool32(); item._iMagical = static_cast(file.NextLE()); file.NextBytes(item._iName, ItemNameLength); TerminateUtf8(item._iName, ItemNameLength); file.NextBytes(item._iIName, ItemNameLength); TerminateUtf8(item._iIName, ItemNameLength); item._iLoc = static_cast(file.NextLE()); item._iClass = static_cast(file.NextLE()); file.Skip(1); // Alignment item._iCurs = file.NextLE(); item._ivalue = file.NextLE(); item._iIvalue = file.NextLE(); item._iMinDam = file.NextLE(); item._iMaxDam = file.NextLE(); item._iAC = file.NextLE(); item._iFlags = static_cast(file.NextLE()); item._iMiscId = static_cast(file.NextLE()); item._iSpell = static_cast(file.NextLE()); item._iCharges = file.NextLE(); item._iMaxCharges = file.NextLE(); item._iDurability = file.NextLE(); item._iMaxDur = file.NextLE(); item._iPLDam = file.NextLE(); item._iPLToHit = file.NextLE(); item._iPLAC = file.NextLE(); item._iPLStr = file.NextLE(); item._iPLMag = file.NextLE(); item._iPLDex = file.NextLE(); item._iPLVit = file.NextLE(); item._iPLFR = file.NextLE(); item._iPLLR = file.NextLE(); item._iPLMR = file.NextLE(); item._iPLMana = file.NextLE(); item._iPLHP = file.NextLE(); item._iPLDamMod = file.NextLE(); item._iPLGetHit = file.NextLE(); item._iPLLight = file.NextLE(); item._iSplLvlAdd = file.NextLE(); item._iRequest = file.NextBool8(); file.Skip(2); // Alignment const int32_t uniqueMappingId = file.NextLE(); if (item._iMagical == ITEM_QUALITY_UNIQUE) { const auto findIt = UniqueItemMappingIdsToIndices.find(uniqueMappingId); if (findIt == UniqueItemMappingIdsToIndices.end()) { return false; } const int uniqueIndex = findIt->second; item._iUid = uniqueIndex; } else { item._iUid = 0; } item._iFMinDam = file.NextLE(); item._iFMaxDam = file.NextLE(); item._iLMinDam = file.NextLE(); item._iLMaxDam = file.NextLE(); item._iPLEnAc = file.NextLE(); item._iPrePower = static_cast(file.NextLE()); item._iSufPower = static_cast(file.NextLE()); file.Skip(2); // Alignment item._iVAdd1 = file.NextLE(); item._iVMult1 = file.NextLE(); item._iVAdd2 = file.NextLE(); item._iVMult2 = file.NextLE(); item._iMinStr = file.NextLE(); item._iMinMag = file.NextLE(); item._iMinDex = file.NextLE(); file.Skip(1); // Alignment item._iStatFlag = file.NextBool32(); int32_t itemMappingId = file.NextLE(); if (gbIsSpawn && itemMappingId < IDI_NUM_DEFAULT_ITEMS) { itemMappingId = RemapItemIdxFromSpawn(static_cast<_item_indexes>(itemMappingId)); } if (!gbIsHellfireSaveGame && itemMappingId < IDI_NUM_DEFAULT_ITEMS) { itemMappingId = RemapItemIdxFromDiablo(static_cast<_item_indexes>(itemMappingId)); } const auto findIt = ItemMappingIdsToIndices.find(itemMappingId); if (findIt == ItemMappingIdsToIndices.end()) { return false; } const _item_indexes itemIndex = static_cast<_item_indexes>(findIt->second); item.IDidx = itemIndex; item.dwBuff = file.NextLE(); if (gbIsHellfireSaveGame) item._iDamAcFlags = static_cast(file.NextLE()); else item._iDamAcFlags = ItemSpecialEffectHf::None; UpdateHellfireFlag(item, item._iIName); return true; } void LoadAndValidateItemData(LoadHelper &file, Item &item) { const bool success = LoadItemData(file, item); if (!success) { item.clear(); return; } RemoveInvalidItem(item); } void LoadPlayer(LoadHelper &file, Player &player) { player._pmode = static_cast(file.NextLE()); for (size_t i = 0; i < PlayerWalkPathSizeForSaveGame; ++i) { player.walkpath[i] = file.NextLE(); } player.walkpath[PlayerWalkPathSizeForSaveGame] = WALK_NONE; player.plractive = file.NextBool8(); file.Skip(2); // Alignment player.destAction = static_cast(file.NextLE()); player.destParam1 = file.NextLE(); player.destParam2 = file.NextLE(); player.destParam3 = file.NextLE(); player.destParam4 = file.NextLE(); player.setLevel(file.NextLE()); player.position.tile.x = file.NextLE(); player.position.tile.y = file.NextLE(); player.position.future.x = file.NextLE(); player.position.future.y = file.NextLE(); file.Skip(2); // Skip _ptargx and _ptargy player.position.last.x = file.NextLE(); player.position.last.y = file.NextLE(); player.position.old.x = file.NextLE(); player.position.old.y = file.NextLE(); file.Skip(4); // Skip offset and velocity player._pdir = static_cast(file.NextLE()); file.Skip(4); // Unused player._pgfxnum = file.NextLENarrow(); file.Skip(); // Skip pointer pData player.AnimInfo = {}; player.AnimInfo.ticksPerFrame = file.NextLENarrow(1); player.AnimInfo.tickCounterOfCurrentFrame = file.NextLENarrow(); player.AnimInfo.numberOfFrames = file.NextLENarrow(); player.AnimInfo.currentFrame = file.NextLENarrow(-1); file.Skip(3); // Skip _pAnimWidth, _pAnimWidth2, _peflag player.lightId = file.NextLE(); file.Skip(); // _pvid player.queuedSpell.spellId = static_cast(file.NextLE()); player.queuedSpell.spellType = static_cast(file.NextLE()); auto spellFrom = file.NextLE(); if (!IsValidSpellFrom(spellFrom)) spellFrom = 0; player.spellFrom = spellFrom; player.queuedSpell.spellFrom = spellFrom; file.Skip(2); // Alignment player.inventorySpell = static_cast(file.NextLE()); file.Skip(); // Skip _pTSplType file.Skip(3); // Alignment player._pRSpell = static_cast(file.NextLE()); player._pRSplType = static_cast(file.NextLE()); file.Skip(3); // Alignment player._pSBkSpell = static_cast(file.NextLE()); file.Skip(); // Skip _pSBkSplType // Only read spell levels for learnable spells for (int i = 0; i < static_cast(SpellID::LAST); i++) { auto spl = static_cast(i); if (GetSpellBookLevel(spl) != -1) player._pSplLvl[i] = file.NextLE(); else file.Skip(); } // Skip indices that are unused for (int i = static_cast(SpellID::LAST); i < 64; i++) file.Skip(); // These spells are unavailable in Diablo as learnable spells if (!gbIsHellfire) { player._pSplLvl[static_cast(SpellID::Apocalypse)] = 0; player._pSplLvl[static_cast(SpellID::Nova)] = 0; } file.Skip(7); // Alignment player._pMemSpells = file.NextLE(); player._pAblSpells = file.NextLE(); player._pScrlSpells = file.NextLE(); player._pSpellFlags = static_cast(file.NextLE()); file.Skip(3); // Alignment // Extra hotkeys: to keep single player save compatibility, read only 4 hotkeys here, rely on LoadHotkeys for the rest for (size_t i = 0; i < 4; i++) { player._pSplHotKey[i] = static_cast(file.NextLE()); } for (size_t i = 0; i < 4; i++) { player._pSplTHotKey[i] = static_cast(file.NextLE()); } file.Skip(); // Skip _pwtype player._pBlockFlag = file.NextBool8(); player._pInvincible = file.NextBool8(); player._pLightRad = file.NextLE(); player._pLvlChanging = file.NextBool8(); file.NextBytes(player._pName, PlayerNameLength); TerminateUtf8(player._pName, PlayerNameLength); player._pClass = static_cast(file.NextLE()); file.Skip(3); // Alignment player._pStrength = file.NextLE(); player._pBaseStr = file.NextLE(); player._pMagic = file.NextLE(); player._pBaseMag = file.NextLE(); player._pDexterity = file.NextLE(); player._pBaseDex = file.NextLE(); player._pVitality = file.NextLE(); player._pBaseVit = file.NextLE(); player._pStatPts = file.NextLE(); player._pDamageMod = file.NextLE(); file.Skip(); // Skip _pBaseToBlk - always a copy of PlayerData.blockBonus player._pHPBase = file.NextLE(); player._pMaxHPBase = file.NextLE(); player._pHitPoints = file.NextLE(); player._pMaxHP = file.NextLE(); file.Skip(); // Skip _pHPPer - always derived from hp and maxHP. player._pManaBase = file.NextLE(); player._pMaxManaBase = file.NextLE(); player._pMana = file.NextLE(); player._pMaxMana = file.NextLE(); file.Skip(); // Skip _pManaPer - always derived from mana and maxMana player.setCharacterLevel(file.NextLE()); file.Skip(); // Skip _pMaxLevel - unused file.Skip(2); // Alignment player._pExperience = file.NextLE(); file.Skip(); // Skip _pMaxExp - unused file.Skip(); // Skip _pNextExper, we retrieve it when needed based on _pLevel player._pArmorClass = file.NextLE(); player._pMagResist = file.NextLE(); player._pFireResist = file.NextLE(); player._pLghtResist = file.NextLE(); player._pGold = file.NextLE(); player._pInfraFlag = file.NextBool32(); int32_t tempPositionX = file.NextLE(); int32_t tempPositionY = file.NextLE(); if (player._pmode == PM_WALK_NORTHWARDS) { // These values are saved as offsets to remain consistent with old savefiles tempPositionX += player.position.tile.x; tempPositionY += player.position.tile.y; } player.position.temp.x = static_cast(tempPositionX); player.position.temp.y = static_cast(tempPositionY); player.tempDirection = static_cast(file.NextLE()); player.queuedSpell.spellLevel = file.NextLE(); file.Skip(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking file.Skip(2); // skip offset2; file.Skip(); // Skip actionFrame for (uint8_t i = 0; i < giNumberOfLevels; i++) player._pLvlVisited[i] = file.NextBool8(); for (uint8_t i = 0; i < giNumberOfLevels; i++) player._pSLvlVisited[i] = file.NextBool8(); file.Skip(2); // Alignment file.Skip(); // skip _pGFXLoad file.Skip(8); // Skip pointers _pNAnim player._pNFrames = file.NextLENarrow(); file.Skip(); // skip _pNWidth file.Skip(8); // Skip pointers _pWAnim player._pWFrames = file.NextLENarrow(); file.Skip(); // skip _pWWidth file.Skip(8); // Skip pointers _pAAnim player._pAFrames = file.NextLENarrow(); file.Skip(); // skip _pAWidth player._pAFNum = file.NextLENarrow(); file.Skip(8); // Skip pointers _pLAnim file.Skip(8); // Skip pointers _pFAnim file.Skip(8); // Skip pointers _pTAnim player._pSFrames = file.NextLENarrow(); file.Skip(); // skip _pSWidth player._pSFNum = file.NextLENarrow(); file.Skip(8); // Skip pointers _pHAnim player._pHFrames = file.NextLENarrow(); file.Skip(); // skip _pHWidth file.Skip(8); // Skip pointers _pDAnim player._pDFrames = file.NextLENarrow(); file.Skip(); // skip _pDWidth file.Skip(8); // Skip pointers _pBAnim player._pBFrames = file.NextLENarrow(); file.Skip(); // skip _pBWidth for (Item &item : player.InvBody) LoadAndValidateItemData(file, item); for (Item &item : player.InvList) LoadAndValidateItemData(file, item); player._pNumInv = file.NextLE(); for (int8_t &cell : player.InvGrid) cell = file.NextLE(); for (Item &item : player.SpdList) LoadAndValidateItemData(file, item); LoadAndValidateItemData(file, player.HoldItem); player._pIMinDam = file.NextLE(); player._pIMaxDam = file.NextLE(); player._pIAC = file.NextLE(); player._pIBonusDam = file.NextLE(); player._pIBonusToHit = file.NextLE(); player._pIBonusAC = file.NextLE(); player._pIBonusDamMod = file.NextLE(); file.Skip(4); // Alignment player._pISpells = file.NextLE(); player._pIFlags = static_cast(file.NextLE()); player._pIGetHit = file.NextLE(); player._pISplLvlAdd = file.NextLE(); file.Skip(1); // Unused file.Skip(2); // Alignment file.Skip(); // _pISplDur player._pIEnAc = file.NextLE(); player._pIFMinDam = file.NextLE(); player._pIFMaxDam = file.NextLE(); player._pILMinDam = file.NextLE(); player._pILMaxDam = file.NextLE(); player._pOilType = static_cast(file.NextLE()); player.pTownWarps = file.NextLE(); player.pDungMsgs = file.NextLE(); player.pLvlLoad = file.NextLE(); if (gbIsHellfireSaveGame) { player.pDungMsgs2 = file.NextLE(); } else { player.pDungMsgs2 = 0; file.Skip(1); // pBattleNet } player.pManaShield = file.NextBool8(); if (gbIsHellfireSaveGame) { player.pOriginalCathedral = file.NextBool8(); } else { file.Skip(1); player.pOriginalCathedral = true; } file.Skip(2); // Available bytes player.wReflections = file.NextLE(); file.Skip(14); // Available bytes player.pDiabloKillLevel = file.NextLE(); sgGameInitInfo.nDifficulty = static_cast<_difficulty>(file.NextLE()); player.pDamAcFlags = static_cast(file.NextLE()); file.Skip(20); // Available bytes CalcPlrInv(player, false); player.executedSpell = player.queuedSpell; // Ensures backwards compatibility // Omit pointer _pNData // Omit pointer _pWData // Omit pointer _pAData // Omit pointer _pLData // Omit pointer _pFData // Omit pointer _pTData // Omit pointer _pHData // Omit pointer _pDData // Omit pointer _pBData // Omit pointer pReserved // Ensure plrIsOnSetLevel and plrlevel is correctly initialized, because in vanilla sometimes plrlevel is not updated to setlvlnum if (setlevel) player.setLevel(setlvlnum); else player.setLevel(currlevel); } bool gbSkipSync = false; [[nodiscard]] bool LoadMonster(LoadHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr) { monster.levelType = file->NextLE(); monster.mode = static_cast(file->NextLE()); monster.goal = static_cast(file->NextLE()); file->Skip(3); // Alignment monster.goalVar1 = file->NextLENarrow(); monster.goalVar2 = file->NextLENarrow(); monster.goalVar3 = file->NextLENarrow(); file->Skip(4); // Unused monster.pathCount = file->NextLE(); file->Skip(3); // Alignment monster.position.tile.x = file->NextLE(); monster.position.tile.y = file->NextLE(); monster.position.future.x = file->NextLE(); monster.position.future.y = file->NextLE(); monster.position.old.x = file->NextLE(); monster.position.old.y = file->NextLE(); file->Skip(4); // Skip offset and velocity monster.direction = static_cast(file->NextLE()); monster.enemy = file->NextLE(); monster.enemyPosition.x = file->NextLE(); monster.enemyPosition.y = file->NextLE(); file->Skip(2); // Unused file->Skip(4); // Skip pointer _mAnimData monster.animInfo = {}; monster.animInfo.ticksPerFrame = file->NextLENarrow(); // Ensure that we can increase the tickCounterOfCurrentFrame at least once without overflow (needed for backwards compatibility for sitting gargoyles) monster.animInfo.tickCounterOfCurrentFrame = file->NextLENarrow(1) - 1; monster.animInfo.numberOfFrames = file->NextLENarrow(); monster.animInfo.currentFrame = file->NextLENarrow(-1); file->Skip(4); // Skip _meflag monster.isInvalid = file->NextBool32(); monster.var1 = file->NextLENarrow(); monster.var2 = file->NextLENarrow(); monster.var3 = file->NextLENarrow(); monster.position.temp.x = file->NextLENarrow(); monster.position.temp.y = file->NextLENarrow(); file->Skip(2); // skip offset2; file->Skip(4); // Skip actionFrame monster.maxHitPoints = file->NextLE(); monster.hitPoints = file->NextLE(); monster.ai = static_cast(file->NextLE()); monster.intelligence = file->NextLE(); file->Skip(2); // Alignment monster.flags = file->NextLE(); monster.activeForTicks = file->NextLE(); file->Skip(3); // Alignment file->Skip(4); // Unused monster.position.last.x = file->NextLE(); monster.position.last.y = file->NextLE(); monster.rndItemSeed = file->NextLE(); monster.aiSeed = file->NextLE(); file->Skip(4); // Unused monster.uniqueType = static_cast(file->NextLE() - 1); monster.uniqTrans = file->NextLE(); monster.corpseId = file->NextLE(); monster.whoHit = file->NextLE(); if (monsterConversionData != nullptr) monsterConversionData->monsterLevel = file->NextLE(); else file->Skip(1); // Skip level - now calculated on the fly file->Skip(1); // Alignment if (monsterConversionData != nullptr) monsterConversionData->experience = file->NextLE(); else file->Skip(2); // Skip exp - now calculated from monstdat when the monster dies if (monsterConversionData != nullptr) monsterConversionData->toHit = file->NextLE(); else if (monster.isPlayerMinion()) // Don't skip for golems monster.golemToHit = file->NextLE(); else file->Skip(1); // Skip toHit - now calculated on the fly monster.minDamage = file->NextLE(); monster.maxDamage = file->NextLE(); if (monsterConversionData != nullptr) monsterConversionData->toHitSpecial = file->NextLE(); else file->Skip(1); // Skip toHitSpecial - now calculated on the fly monster.minDamageSpecial = file->NextLE(); monster.maxDamageSpecial = file->NextLE(); monster.armorClass = file->NextLE(); file->Skip(1); // Alignment monster.resistance = file->NextLE(); file->Skip(2); // Alignment monster.talkMsg = static_cast<_speech_id>(file->NextLE()); if (monster.talkMsg == TEXT_KING1) // Fix original bad mapping of NONE for monsters monster.talkMsg = TEXT_NONE; monster.leader = file->NextLE(); if (monster.leader == 0) monster.leader = Monster::NoLeader; // Golems shouldn't be leaders of other monsters monster.leaderRelation = static_cast(file->NextLE()); monster.packSize = file->NextLE(); monster.lightId = file->NextLE(); if (monster.lightId == 0) monster.lightId = NO_LIGHT; // Correct incorrect values in old saves // Omit pointer name; if (monster.mode == MonsterMode::Petrified) monster.animInfo.isPetrified = true; if (monster.isUnique()) { // check if the unique monster is still valid (it could no longer be valid e.g. because the loaded mods changed and the unique monsters changed as a consequence) const bool valid = IsMonsterValid(monster); if (!valid) { LogWarn("Monster no longer valid, skipping it."); return false; } } return true; } void LoadMonsters(LoadHelper &file, ankerl::unordered_dense::set &removedMonsterIds, const bool applyLight, LevelConversionData *levelConversionData) { for (unsigned &monsterId : ActiveMonsters) monsterId = file.NextBE(); for (size_t i = 0; i < ActiveMonsterCount;) { Monster &monster = Monsters[ActiveMonsters[i]]; MonsterConversionData *monsterConversionData = nullptr; if (levelConversionData != nullptr) monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]]; const bool valid = LoadMonster(&file, monster, monsterConversionData); if (!valid) { Monsters[ActiveMonsters[i]] = {}; removedMonsterIds.insert(ActiveMonsters[i]); for (size_t j = i + 1; j < ActiveMonsterCount; j++) { ActiveMonsters[j - 1] = ActiveMonsters[j]; } --ActiveMonsterCount; continue; } if (applyLight && monster.isUnique() && monster.lightId != NO_LIGHT) Lights[monster.lightId].isInvalid = false; i++; } for (const unsigned removedMonsterId : removedMonsterIds) { for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &activeMonster = Monsters[ActiveMonsters[i]]; if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) != 0 && activeMonster.enemy == removedMonsterId) { activeMonster.flags |= MFLAG_NO_ENEMY; } } } } /** * @brief Recalculate the pack size of monster group that may have underflown */ void SyncPackSize(Monster &leader) { if (!leader.isUnique()) return; if (leader.ai != MonsterAIID::Scavenger) return; leader.packSize = 0; for (size_t i = 0; i < ActiveMonsterCount; i++) { const Monster &minion = Monsters[ActiveMonsters[i]]; if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader) leader.packSize++; } } void LoadMissile(LoadHelper *file) { Missile missile = {}; missile._mitype = static_cast(file->NextLE()); missile.position.tile.x = file->NextLE(); missile.position.tile.y = file->NextLE(); missile.position.offset.deltaX = file->NextLE(); missile.position.offset.deltaY = file->NextLE(); missile.position.velocity.deltaX = file->NextLE(); missile.position.velocity.deltaY = file->NextLE(); missile.position.start.x = file->NextLE(); missile.position.start.y = file->NextLE(); missile.position.traveled.deltaX = file->NextLE(); missile.position.traveled.deltaY = file->NextLE(); missile.setFrameGroupRaw(file->NextLE()); missile._mispllvl = file->NextLE(); missile._miDelFlag = file->NextBool32(); missile._miAnimType = static_cast(file->NextLE()); file->Skip(3); // Alignment missile._miAnimFlags = static_cast(file->NextLE()); file->Skip(4); // Skip pointer _miAnimData missile._miAnimDelay = file->NextLE(); missile._miAnimLen = file->NextLE(); missile._miAnimWidth = file->NextLE(); missile._miAnimWidth2 = file->NextLE(); missile._miAnimCnt = file->NextLE(); missile._miAnimAdd = file->NextLE(); missile._miAnimFrame = file->NextLE(); missile._miDrawFlag = file->NextBool32(); missile._miLightFlag = file->NextBool32(); missile._miPreFlag = file->NextBool32(); missile._miUniqTrans = file->NextLE(); missile.duration = file->NextLE(); missile._misource = file->NextLE(); missile._micaster = static_cast(file->NextLE()); missile._midam = file->NextLE(); missile._miHitFlag = file->NextBool32(); missile._midist = file->NextLE(); missile._mlid = file->NextLE(); missile._mirnd = file->NextLE(); missile.var1 = file->NextLE(); missile.var2 = file->NextLE(); missile.var3 = file->NextLE(); missile.var4 = file->NextLE(); missile.var5 = file->NextLE(); missile.var6 = file->NextLE(); missile.var7 = file->NextLE(); missile.limitReached = file->NextBool32(); missile.lastCollisionTargetHash = 0; if (Missiles.size() < Missiles.max_size()) { Missiles.push_back(missile); } } _object_id ConvertFromHellfireObject(_object_id type) { if (leveltype == DTYPE_NEST) { switch (type) { case OBJ_BARREL: return OBJ_POD; case OBJ_BARRELEX: return OBJ_PODEX; default: break; } } if (leveltype == DTYPE_CRYPT) { switch (type) { case OBJ_BARREL: return OBJ_URN; case OBJ_BARRELEX: return OBJ_URNEX; case OBJ_STORYBOOK: return OBJ_L5BOOKS; case OBJ_STORYCANDLE: return OBJ_L5CANDLE; case OBJ_L1LDOOR: return OBJ_L5LDOOR; case OBJ_L1RDOOR: return OBJ_L5RDOOR; case OBJ_LEVER: return OBJ_L5LEVER; case OBJ_SARC: return OBJ_L5SARC; default: break; } } return type; } void LoadObject(LoadHelper &file, Object &object) { object._otype = ConvertFromHellfireObject(static_cast<_object_id>(file.NextLE())); object.position.x = file.NextLE(); object.position.y = file.NextLE(); object.applyLighting = file.NextBool32(); object._oAnimFlag = file.NextBool32(); file.Skip(4); // Skip pointer _oAnimData object._oAnimDelay = file.NextLE(); object._oAnimCnt = file.NextLE(); object._oAnimLen = file.NextLE(); object._oAnimFrame = file.NextLE(); object._oAnimWidth = static_cast(file.NextLE()); file.Skip(4); // Skip _oAnimWidth2 object._oDelFlag = file.NextBool32(); object._oBreak = file.NextLE(); file.Skip(3); // Alignment object._oSolidFlag = file.NextBool32(); object._oMissFlag = file.NextBool32(); object.selectionRegion = static_cast(file.NextLE()); file.Skip(3); // Alignment object._oPreFlag = file.NextBool32(); object._oTrapFlag = file.NextBool32(); object._oDoorFlag = file.NextBool32(); object._olid = file.NextLE(); object._oRndSeed = file.NextLE(); object._oVar1 = file.NextLE(); object._oVar2 = file.NextLE(); object._oVar3 = file.NextLE(); object._oVar4 = file.NextLE(); object._oVar5 = file.NextLE(); object._oVar6 = file.NextLE(); object.bookMessage = static_cast<_speech_id>(file.NextLE()); object._oVar8 = file.NextLE(); } void LoadItem(LoadHelper &file, Item &item) { LoadAndValidateItemData(file, item); GetItemFrm(item); } void LoadPremium(LoadHelper &file, int i) { LoadAndValidateItemData(file, PremiumItems[i]); } void LoadQuest(LoadHelper *file, int i) { auto &quest = Quests[i]; quest._qlevel = file->NextLE(); file->Skip(); // _qtype, identical to _qidx quest._qactive = static_cast(file->NextLE()); quest._qlvltype = static_cast(file->NextLE()); quest.position.x = file->NextLE(); quest.position.y = file->NextLE(); quest._qslvl = static_cast<_setlevels>(file->NextLE()); quest._qidx = static_cast(file->NextLE()); if (gbIsHellfireSaveGame) { file->Skip(2); // Alignment quest._qmsg = static_cast<_speech_id>(file->NextLE()); } else { quest._qmsg = static_cast<_speech_id>(file->NextLE()); } quest._qvar1 = file->NextLE(); quest._qvar2 = file->NextLE(); file->Skip(2); // Alignment if (!gbIsHellfireSaveGame) file->Skip(1); // Alignment quest._qlog = file->NextBool32(); ReturnLvlPosition.x = file->NextBE(); ReturnLvlPosition.y = file->NextBE(); ReturnLevel = file->NextBE(); ReturnLevelType = static_cast(file->NextBE()); file->Skip(sizeof(int32_t)); // Skip DoomQuestState } void LoadLighting(LoadHelper *file, Light *pLight) { pLight->position.tile.x = file->NextLE(); pLight->position.tile.y = file->NextLE(); pLight->radius = file->NextLE(); file->Skip(); // _lid pLight->isInvalid = file->NextBool32(); pLight->hasChanged = file->NextBool32(); file->Skip(4); // Unused pLight->position.old.x = file->NextLE(); pLight->position.old.y = file->NextLE(); pLight->oldRadius = file->NextLE(); pLight->position.offset.deltaX = file->NextLE(); pLight->position.offset.deltaY = file->NextLE(); file->Skip(); // _lflags } void LoadPortal(LoadHelper *file, int i) { Portal *pPortal = &Portals[i]; pPortal->open = file->NextBool32(); pPortal->position.x = file->NextLE(); pPortal->position.y = file->NextLE(); pPortal->level = file->NextLE(); pPortal->ltype = static_cast(file->NextLE()); pPortal->setlvl = file->NextBool32(); if (!pPortal->setlvl) pPortal->ltype = GetLevelType(pPortal->level); } void GetLevelNames(std::string_view prefix, char *out) { char suf; uint8_t num; if (setlevel) { suf = 's'; num = static_cast(setlvlnum); } else { suf = 'l'; num = currlevel; } *BufCopy(out, prefix, std::string_view(&suf, 1), LeftPad(num, 2, '0')) = '\0'; } void GetTempLevelNames(char *szTemp) { return GetLevelNames("temp", szTemp); } void GetPermLevelNames(char *szPerm) { return GetLevelNames("perm", szPerm); } bool LevelFileExists(SaveWriter &archive) { char szName[MaxMpqPathSize]; GetTempLevelNames(szName); if (archive.HasFile(szName)) return true; GetPermLevelNames(szName); return archive.HasFile(szName); } void LoadMatchingItems(LoadHelper &file, const Player &player, const int n, Item *pItem) { Item heroItem; for (int i = 0; i < n; i++) { Item &unpackedItem = pItem[i]; const bool success = LoadItemData(file, heroItem); if (!success) { heroItem.clear(); unpackedItem = Item(); } if (unpackedItem.isEmpty() || heroItem.isEmpty()) continue; if (unpackedItem._iSeed != heroItem._iSeed) continue; if (heroItem.IDidx == IDI_EAR) continue; if (gbIsMultiplayer) { // Ensure that the unpacked item was regenerated using the appropriate // game's item generation logic before attempting to use it for validation if ((heroItem.dwBuff & CF_HELLFIRE) != (unpackedItem.dwBuff & CF_HELLFIRE)) { unpackedItem = {}; RecreateItem(player, unpackedItem, heroItem.IDidx, heroItem._iCreateInfo, heroItem._iSeed, heroItem._ivalue, heroItem.dwBuff); unpackedItem._iIdentified = heroItem._iIdentified; unpackedItem._iMaxDur = heroItem._iMaxDur; unpackedItem._iDurability = ClampDurability(unpackedItem, heroItem._iDurability); unpackedItem._iMaxCharges = std::clamp(heroItem._iMaxCharges, 0, unpackedItem._iMaxCharges); unpackedItem._iCharges = std::clamp(heroItem._iCharges, 0, unpackedItem._iMaxCharges); } if (gbIsHellfire) { unpackedItem._iPLToHit = ClampToHit(unpackedItem, heroItem._iPLToHit); // Oil of Accuracy unpackedItem._iMaxDam = ClampMaxDam(unpackedItem, heroItem._iMaxDam); // Oil of Sharpness } } else { unpackedItem = heroItem; } } } /** * @brief Loads items on the current dungeon floor * @param file interface to the save file * @param savedItemCount how many items to read from the save file */ void LoadDroppedItems(LoadHelper &file, size_t savedItemCount) { // Skip loading ActiveItems and AvailableItems, the indices are initialised below based on the number of valid items file.Skip(MAXITEMS * 2); // Reset ActiveItems, the Items array will be populated from the start std::iota(ActiveItems, ActiveItems + MAXITEMS, uint8_t { 0 }); ActiveItemCount = 0; // Clear dItem so we can populate valid drop locations memset(dItem, 0, sizeof(dItem)); for (size_t i = 0; i < savedItemCount; i++) { Item &item = Items[ActiveItemCount]; LoadItem(file, item); if (!item.isEmpty()) { // Loaded a valid item ActiveItemCount++; // populate its location in the lookup table with the offset in the Items array + 1 (so 0 can be used for "no item") dItem[item.position.x][item.position.y] = ActiveItemCount; } } } int getHellfireLevelType(int type) { if (type == DTYPE_CRYPT) return DTYPE_CATHEDRAL; if (type == DTYPE_NEST) return DTYPE_CAVES; return type; } void SaveItem(SaveHelper &file, const Item &item) { int32_t idx = item.IDidx != IDI_NONE ? AllItemsList[item.IDidx].iMappingId : -1; if (!gbIsHellfire && idx < IDI_NUM_DEFAULT_ITEMS) idx = RemapItemIdxToDiablo(static_cast<_item_indexes>(idx)); if (gbIsSpawn && idx < IDI_NUM_DEFAULT_ITEMS) idx = RemapItemIdxToSpawn(static_cast<_item_indexes>(idx)); ItemType iType = item._itype; if (idx == -1) { idx = _item_indexes::IDI_GOLD; iType = ItemType::None; } file.WriteLE(item._iSeed); file.WriteLE(item._iCreateInfo); file.Skip(2); // Alignment file.WriteLE(static_cast(iType)); file.WriteLE(item.position.x); file.WriteLE(item.position.y); file.WriteLE(item._iAnimFlag ? 1 : 0); file.Skip(4); // Skip pointer _iAnimData file.WriteLE(item.AnimInfo.numberOfFrames); file.WriteLE(item.AnimInfo.currentFrame + 1); // write _iAnimWidth for vanilla compatibility file.WriteLE(ItemAnimWidth); // write _iAnimWidth2 for vanilla compatibility file.WriteLE(CalculateSpriteTileCenterX(ItemAnimWidth)); file.Skip(); // _delFlag, unused since 1.02 file.WriteLE(static_cast(item.selectionRegion)); file.Skip(3); // Alignment file.WriteLE(item._iPostDraw ? 1 : 0); file.WriteLE(item._iIdentified ? 1 : 0); file.WriteLE(item._iMagical); file.WriteBytes(item._iName, ItemNameLength); file.WriteBytes(item._iIName, ItemNameLength); file.WriteLE(item._iLoc); file.WriteLE(item._iClass); file.Skip(1); // Alignment file.WriteLE(item._iCurs); file.WriteLE(item._ivalue); file.WriteLE(item._iIvalue); file.WriteLE(item._iMinDam); file.WriteLE(item._iMaxDam); file.WriteLE(item._iAC); file.WriteLE(static_cast(item._iFlags)); file.WriteLE(item._iMiscId); file.WriteLE(static_cast(item._iSpell)); file.WriteLE(item._iCharges); file.WriteLE(item._iMaxCharges); file.WriteLE(item._iDurability); file.WriteLE(item._iMaxDur); file.WriteLE(item._iPLDam); file.WriteLE(item._iPLToHit); file.WriteLE(item._iPLAC); file.WriteLE(item._iPLStr); file.WriteLE(item._iPLMag); file.WriteLE(item._iPLDex); file.WriteLE(item._iPLVit); file.WriteLE(item._iPLFR); file.WriteLE(item._iPLLR); file.WriteLE(item._iPLMR); file.WriteLE(item._iPLMana); file.WriteLE(item._iPLHP); file.WriteLE(item._iPLDamMod); file.WriteLE(item._iPLGetHit); file.WriteLE(item._iPLLight); file.WriteLE(item._iSplLvlAdd); file.WriteLE(item._iRequest ? 1 : 0); file.Skip(2); // Alignment file.WriteLE(UniqueItems[item._iUid].mappingId); file.WriteLE(item._iFMinDam); file.WriteLE(item._iFMaxDam); file.WriteLE(item._iLMinDam); file.WriteLE(item._iLMaxDam); file.WriteLE(item._iPLEnAc); file.WriteLE(item._iPrePower); file.WriteLE(item._iSufPower); file.Skip(2); // Alignment file.WriteLE(item._iVAdd1); file.WriteLE(item._iVMult1); file.WriteLE(item._iVAdd2); file.WriteLE(item._iVMult2); file.WriteLE(item._iMinStr); file.WriteLE(item._iMinMag); file.WriteLE(item._iMinDex); file.Skip(1); // Alignment file.WriteLE(item._iStatFlag ? 1 : 0); file.WriteLE(idx); file.WriteLE(item.dwBuff); if (gbIsHellfire) file.WriteLE(static_cast(item._iDamAcFlags)); } void SavePlayer(SaveHelper &file, const Player &player) { file.WriteLE(player._pmode); for (size_t i = 0; i < PlayerWalkPathSizeForSaveGame; ++i) { file.WriteLE(player.walkpath[i]); } file.WriteLE(player.plractive ? 1 : 0); file.Skip(2); // Alignment file.WriteLE(player.destAction); file.WriteLE(player.destParam1); file.WriteLE(player.destParam2); file.WriteLE(static_cast(player.destParam3)); file.WriteLE(player.destParam4); file.WriteLE(player.plrlevel); file.WriteLE(player.position.tile.x); file.WriteLE(player.position.tile.y); file.WriteLE(player.position.future.x); file.WriteLE(player.position.future.y); // For backwards compatibility const Point target = player.GetTargetPosition(); file.WriteLE(target.x); file.WriteLE(target.y); file.WriteLE(player.position.last.x); file.WriteLE(player.position.last.y); file.WriteLE(player.position.old.x); file.WriteLE(player.position.old.y); DisplacementOf offset = {}; DisplacementOf offset2 = {}; DisplacementOf velocity = {}; if (player.isWalking()) { offset = player.position.CalculateWalkingOffset(player._pdir, player.AnimInfo); offset2 = player.position.CalculateWalkingOffsetShifted8(player._pdir, player.AnimInfo); velocity = player.position.GetWalkingVelocityShifted8(player._pdir, player.AnimInfo); } file.WriteLE(offset.deltaX); file.WriteLE(offset.deltaY); file.WriteLE(velocity.deltaX); file.WriteLE(velocity.deltaY); file.WriteLE(static_cast(player._pdir)); file.Skip(4); // Unused file.WriteLE(player._pgfxnum); file.Skip(4); // Skip pointer _pAnimData file.WriteLE(std::max(0, player.AnimInfo.ticksPerFrame - 1)); file.WriteLE(player.AnimInfo.tickCounterOfCurrentFrame); file.WriteLE(player.AnimInfo.numberOfFrames); file.WriteLE(player.AnimInfo.currentFrame + 1); // write _pAnimWidth for vanilla compatibility const int animWidth = player.getSpriteWidth(); file.WriteLE(animWidth); // write _pAnimWidth2 for vanilla compatibility file.WriteLE(CalculateSpriteTileCenterX(animWidth)); file.Skip(); // Skip _peflag file.WriteLE(player.lightId); file.WriteLE(1); // _pvid file.WriteLE(static_cast(player.queuedSpell.spellId)); file.WriteLE(static_cast(player.queuedSpell.spellType)); file.WriteLE(player.queuedSpell.spellFrom); file.Skip(2); // Alignment file.WriteLE(static_cast(player.inventorySpell)); file.Skip(); // Skip _pTSplType file.Skip(3); // Alignment file.WriteLE(static_cast(player._pRSpell)); file.WriteLE(static_cast(player._pRSplType)); file.Skip(3); // Alignment file.WriteLE(static_cast(player._pSBkSpell)); file.Skip(); // Skip _pSBkSplType for (const uint8_t spellLevel : player._pSplLvl) file.WriteLE(spellLevel); file.Skip(7); // Alignment file.WriteLE(player._pMemSpells); file.WriteLE(player._pAblSpells); file.WriteLE(player._pScrlSpells); file.WriteLE(static_cast(player._pSpellFlags)); file.Skip(3); // Alignment // Extra hotkeys: to keep single player save compatibility, write only 4 hotkeys here, rely on SaveHotkeys for the rest for (size_t i = 0; i < 4; i++) { file.WriteLE(static_cast(player._pSplHotKey[i])); } for (size_t i = 0; i < 4; i++) { file.WriteLE(static_cast(player._pSplTHotKey[i])); } file.WriteLE(player.UsesRangedWeapon() ? 1 : 0); file.WriteLE(player._pBlockFlag ? 1 : 0); file.WriteLE(player._pInvincible ? 1 : 0); file.WriteLE(player._pLightRad); file.WriteLE(player._pLvlChanging ? 1 : 0); file.WriteBytes(player._pName, PlayerNameLength); file.WriteLE(static_cast(player._pClass)); file.Skip(3); // Alignment file.WriteLE(player._pStrength); file.WriteLE(player._pBaseStr); file.WriteLE(player._pMagic); file.WriteLE(player._pBaseMag); file.WriteLE(player._pDexterity); file.WriteLE(player._pBaseDex); file.WriteLE(player._pVitality); file.WriteLE(player._pBaseVit); file.WriteLE(player._pStatPts); file.WriteLE(player._pDamageMod); file.WriteLE(player.getBaseToBlock()); // set _pBaseToBlk for backwards compatibility file.WriteLE(player._pHPBase); file.WriteLE(player._pMaxHPBase); file.WriteLE(player._pHitPoints); file.WriteLE(player._pMaxHP); file.Skip(); // Skip _pHPPer file.WriteLE(player._pManaBase); file.WriteLE(player._pMaxManaBase); file.WriteLE(player._pMana); file.WriteLE(player._pMaxMana); file.Skip(); // Skip _pManaPer file.WriteLE(player.getCharacterLevel()); file.Skip(); // skip _pMaxLevel, this value is uninitialised in most cases in Diablo/Hellfire so there's no point setting it. file.Skip(2); // Alignment file.WriteLE(player._pExperience); file.Skip(); // Skip _pMaxExp file.WriteLE(player.getNextExperienceThreshold()); // set _pNextExper for backwards compatibility file.WriteLE(player._pArmorClass); file.WriteLE(player._pMagResist); file.WriteLE(player._pFireResist); file.WriteLE(player._pLghtResist); file.WriteLE(player._pGold); file.WriteLE(player._pInfraFlag ? 1 : 0); int32_t tempPositionX = player.position.temp.x; int32_t tempPositionY = player.position.temp.y; if (player._pmode == PM_WALK_NORTHWARDS) { // For backwards compatibility, save this as an offset tempPositionX -= player.position.tile.x; tempPositionY -= player.position.tile.y; } file.WriteLE(tempPositionX); file.WriteLE(tempPositionY); file.WriteLE(static_cast(player.tempDirection)); file.WriteLE(player.queuedSpell.spellLevel); file.Skip(); // skip _pVar5, was used for storing position of a tile which should have its HorizontalMovingPlayer flag removed after walking file.WriteLE(offset2.deltaX); file.WriteLE(offset2.deltaY); file.Skip(); // Skip _pVar8 for (uint8_t i = 0; i < giNumberOfLevels; i++) file.WriteLE(player._pLvlVisited[i] ? 1 : 0); for (uint8_t i = 0; i < giNumberOfLevels; i++) file.WriteLE(player._pSLvlVisited[i] ? 1 : 0); // only 10 used file.Skip(2); // Alignment file.Skip(); // Skip _pGFXLoad file.Skip(8); // Skip pointers _pNAnim file.WriteLE(player._pNFrames); file.Skip(); // Skip _pNWidth file.Skip(8); // Skip pointers _pWAnim file.WriteLE(player._pWFrames); file.Skip(); // Skip _pWWidth file.Skip(8); // Skip pointers _pAAnim file.WriteLE(player._pAFrames); file.Skip(); // Skip _pAWidth file.WriteLE(player._pAFNum); file.Skip(8); // Skip pointers _pLAnim file.Skip(8); // Skip pointers _pFAnim file.Skip(8); // Skip pointers _pTAnim file.WriteLE(player._pSFrames); file.Skip(); // Skip _pSWidth file.WriteLE(player._pSFNum); file.Skip(8); // Skip pointers _pHAnim file.WriteLE(player._pHFrames); file.Skip(); // Skip _pHWidth file.Skip(8); // Skip pointers _pDAnim file.WriteLE(player._pDFrames); file.Skip(); // Skip _pDWidth file.Skip(8); // Skip pointers _pBAnim file.WriteLE(player._pBFrames); file.Skip(); // Skip _pBWidth for (const Item &item : player.InvBody) SaveItem(file, item); for (const Item &item : player.InvList) SaveItem(file, item); file.WriteLE(player._pNumInv); for (const int8_t cell : player.InvGrid) file.WriteLE(cell); for (const Item &item : player.SpdList) SaveItem(file, item); SaveItem(file, player.HoldItem); file.WriteLE(player._pIMinDam); file.WriteLE(player._pIMaxDam); file.WriteLE(player._pIAC); file.WriteLE(player._pIBonusDam); file.WriteLE(player._pIBonusToHit); file.WriteLE(player._pIBonusAC); file.WriteLE(player._pIBonusDamMod); file.Skip(4); // Alignment file.WriteLE(player._pISpells); file.WriteLE(static_cast(player._pIFlags)); file.WriteLE(player._pIGetHit); file.WriteLE(player._pISplLvlAdd); file.Skip(); // Skip _pISplCost file.Skip(2); // Alignment file.Skip(); // _pISplDur file.WriteLE(player._pIEnAc); file.WriteLE(player._pIFMinDam); file.WriteLE(player._pIFMaxDam); file.WriteLE(player._pILMinDam); file.WriteLE(player._pILMaxDam); file.WriteLE(player._pOilType); file.WriteLE(player.pTownWarps); file.WriteLE(player.pDungMsgs); file.WriteLE(player.pLvlLoad); if (gbIsHellfire) file.WriteLE(player.pDungMsgs2); else file.WriteLE(0); file.WriteLE(player.pManaShield ? 1 : 0); file.WriteLE(player.pOriginalCathedral ? 1 : 0); file.Skip(2); // Available bytes file.WriteLE(player.wReflections); file.Skip(14); // Available bytes file.WriteLE(player.pDiabloKillLevel); file.WriteLE(sgGameInitInfo.nDifficulty); file.WriteLE(static_cast(player.pDamAcFlags)); file.Skip(20); // Available bytes // Omit pointer _pNData // Omit pointer _pWData // Omit pointer _pAData // Omit pointer _pLData // Omit pointer _pFData // Omit pointer _pTData // Omit pointer _pHData // Omit pointer _pDData // Omit pointer _pBData // Omit pointer pReserved } void SaveMonster(SaveHelper *file, Monster &monster, MonsterConversionData *monsterConversionData = nullptr) { file->WriteLE(monster.levelType); file->WriteLE(static_cast(monster.mode)); file->WriteLE(static_cast(monster.goal)); file->Skip(3); // Alignment file->WriteLE(monster.goalVar1); file->WriteLE(monster.goalVar2); file->WriteLE(monster.goalVar3); file->Skip(4); // Unused file->WriteLE(monster.pathCount); file->Skip(3); // Alignment file->WriteLE(monster.position.tile.x); file->WriteLE(monster.position.tile.y); file->WriteLE(monster.position.future.x); file->WriteLE(monster.position.future.y); file->WriteLE(monster.position.old.x); file->WriteLE(monster.position.old.y); DisplacementOf offset = {}; DisplacementOf offset2 = {}; DisplacementOf velocity = {}; if (monster.isWalking()) { offset = monster.position.CalculateWalkingOffset(monster.direction, monster.animInfo); offset2 = monster.position.CalculateWalkingOffsetShifted4(monster.direction, monster.animInfo); velocity = monster.position.GetWalkingVelocityShifted4(monster.direction, monster.animInfo); } file->WriteLE(offset.deltaX); file->WriteLE(offset.deltaY); file->WriteLE(velocity.deltaX); file->WriteLE(velocity.deltaY); file->WriteLE(static_cast(monster.direction)); file->WriteLE(monster.enemy); file->WriteLE(monster.enemyPosition.x); file->WriteLE(monster.enemyPosition.y); file->Skip(2); // Unused file->Skip(4); // Skip pointer _mAnimData file->WriteLE(monster.animInfo.ticksPerFrame); file->WriteLE(monster.animInfo.tickCounterOfCurrentFrame); file->WriteLE(monster.animInfo.numberOfFrames); file->WriteLE(monster.animInfo.currentFrame + 1); file->Skip(); // Skip _meflag file->WriteLE(monster.isInvalid ? 1 : 0); file->WriteLE(monster.var1); file->WriteLE(monster.var2); file->WriteLE(monster.var3); file->WriteLE(monster.position.temp.x); file->WriteLE(monster.position.temp.y); file->WriteLE(offset2.deltaX); file->WriteLE(offset2.deltaY); file->Skip(); // Skip _mVar8 file->WriteLE(monster.maxHitPoints); file->WriteLE(monster.hitPoints); file->WriteLE(static_cast(monster.ai)); file->WriteLE(monster.intelligence); file->Skip(2); // Alignment file->WriteLE(monster.flags); file->WriteLE(monster.activeForTicks); file->Skip(3); // Alignment file->Skip(4); // Unused file->WriteLE(monster.position.last.x); file->WriteLE(monster.position.last.y); file->WriteLE(monster.rndItemSeed); file->WriteLE(monster.aiSeed); file->Skip(4); // Unused file->WriteLE(static_cast(monster.uniqueType) + 1); file->WriteLE(monster.uniqTrans); file->WriteLE(monster.corpseId); file->WriteLE(monster.whoHit); if (monsterConversionData != nullptr) file->WriteLE(monsterConversionData->monsterLevel); else file->WriteLE(static_cast(monster.level(sgGameInitInfo.nDifficulty))); file->Skip(1); // Alignment if (monsterConversionData != nullptr) file->WriteLE(monsterConversionData->experience); else file->WriteLE(static_cast(std::min(std::numeric_limits::max(), monster.exp(sgGameInitInfo.nDifficulty)))); if (monsterConversionData != nullptr) file->WriteLE(monsterConversionData->toHit); else file->WriteLE(static_cast(std::min(monster.toHit(sgGameInitInfo.nDifficulty), std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamage); file->WriteLE(monster.maxDamage); if (monsterConversionData != nullptr) file->WriteLE(monsterConversionData->toHitSpecial); else file->WriteLE(static_cast(std::min(monster.toHitSpecial(sgGameInitInfo.nDifficulty), std::numeric_limits::max()))); // For backwards compatibility file->WriteLE(monster.minDamageSpecial); file->WriteLE(monster.maxDamageSpecial); file->WriteLE(monster.armorClass); file->Skip(1); // Alignment file->WriteLE(monster.resistance); file->Skip(2); // Alignment file->WriteLE(monster.talkMsg == TEXT_NONE ? 0 : monster.talkMsg); // Replicate original bad mapping of none for monsters file->WriteLE(monster.leader == Monster::NoLeader ? 0 : monster.leader); // Vanilla uses 0 as the default leader which corresponds to player 0s golem file->WriteLE(static_cast(monster.leaderRelation)); file->WriteLE(monster.packSize); // vanilla compatibility if (monster.lightId == NO_LIGHT) file->WriteLE(0); else file->WriteLE(monster.lightId); // Omit pointer name; } void SaveMissile(SaveHelper *file, const Missile &missile) { file->WriteLE(static_cast(missile._mitype)); file->WriteLE(missile.position.tile.x); file->WriteLE(missile.position.tile.y); file->WriteLE(missile.position.offset.deltaX); file->WriteLE(missile.position.offset.deltaY); file->WriteLE(missile.position.velocity.deltaX); file->WriteLE(missile.position.velocity.deltaY); file->WriteLE(missile.position.start.x); file->WriteLE(missile.position.start.y); file->WriteLE(missile.position.traveled.deltaX); file->WriteLE(missile.position.traveled.deltaY); file->WriteLE(missile.getFrameGroupRaw()); file->WriteLE(missile._mispllvl); file->WriteLE(missile._miDelFlag ? 1 : 0); file->WriteLE(static_cast(missile._miAnimType)); file->Skip(3); // Alignment file->WriteLE(static_cast(missile._miAnimFlags)); file->Skip(4); // Skip pointer _miAnimData file->WriteLE(missile._miAnimDelay); file->WriteLE(missile._miAnimLen); file->WriteLE(missile._miAnimWidth); file->WriteLE(missile._miAnimWidth2); file->WriteLE(missile._miAnimCnt); file->WriteLE(missile._miAnimAdd); file->WriteLE(missile._miAnimFrame); file->WriteLE(missile._miDrawFlag ? 1 : 0); file->WriteLE(missile._miLightFlag ? 1 : 0); file->WriteLE(missile._miPreFlag ? 1 : 0); file->WriteLE(missile._miUniqTrans); file->WriteLE(missile.duration); file->WriteLE(missile._misource); file->WriteLE(missile._micaster); file->WriteLE(missile._midam); file->WriteLE(missile._miHitFlag ? 1 : 0); file->WriteLE(missile._midist); file->WriteLE(missile._mlid); file->WriteLE(missile._mirnd); file->WriteLE(missile.var1); file->WriteLE(missile.var2); file->WriteLE(missile.var3); file->WriteLE(missile.var4); file->WriteLE(missile.var5); file->WriteLE(missile.var6); file->WriteLE(missile.var7); file->WriteLE(missile.limitReached ? 1 : 0); } _object_id ConvertToHellfireObject(_object_id type) { if (leveltype == DTYPE_NEST) { switch (type) { case OBJ_POD: return OBJ_BARREL; case OBJ_PODEX: return OBJ_BARRELEX; default: break; } } if (leveltype == DTYPE_CRYPT) { switch (type) { case OBJ_URN: return OBJ_BARREL; case OBJ_URNEX: return OBJ_BARRELEX; case OBJ_L5BOOKS: return OBJ_STORYBOOK; case OBJ_L5CANDLE: return OBJ_STORYCANDLE; case OBJ_L5LDOOR: return OBJ_L1LDOOR; case OBJ_L5RDOOR: return OBJ_L1RDOOR; case OBJ_L5LEVER: return OBJ_LEVER; case OBJ_L5SARC: return OBJ_SARC; default: break; } } return type; } void SaveObject(SaveHelper &file, const Object &object) { file.WriteLE(ConvertToHellfireObject(object._otype)); file.WriteLE(object.position.x); file.WriteLE(object.position.y); file.WriteLE(object.applyLighting ? 1 : 0); file.WriteLE(object._oAnimFlag ? 1 : 0); file.Skip(4); // Skip pointer _oAnimData file.WriteLE(object._oAnimDelay); file.WriteLE(object._oAnimCnt); file.WriteLE(object._oAnimLen); file.WriteLE(object._oAnimFrame); file.WriteLE(object._oAnimWidth); file.WriteLE(CalculateSpriteTileCenterX(static_cast(object._oAnimWidth))); // Write _oAnimWidth2 for vanilla compatibility file.WriteLE(object._oDelFlag ? 1 : 0); file.WriteLE(object._oBreak); file.Skip(3); // Alignment file.WriteLE(object._oSolidFlag ? 1 : 0); file.WriteLE(object._oMissFlag ? 1 : 0); file.WriteLE(static_cast(object.selectionRegion)); file.Skip(3); // Alignment file.WriteLE(object._oPreFlag ? 1 : 0); file.WriteLE(object._oTrapFlag ? 1 : 0); file.WriteLE(object._oDoorFlag ? 1 : 0); file.WriteLE(object._olid); file.WriteLE(object._oRndSeed); /* Make dynamic light sources unseen when saving level data for level change */ int32_t var1 = object._oVar1; switch (object._otype) { case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: case OBJ_STORYCANDLE: case OBJ_L5CANDLE: case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: case OBJ_BCROSS: case OBJ_TBCROSS: if (var1 != -1) var1 = 0; break; default: break; } file.WriteLE(var1); file.WriteLE(object._oVar2); file.WriteLE(object._oVar3); file.WriteLE(object._oVar4); file.WriteLE(object._oVar5); file.WriteLE(object._oVar6); file.WriteLE(object.bookMessage); file.WriteLE(object._oVar8); } void SaveQuest(SaveHelper *file, int i) { auto &quest = Quests[i]; file->WriteLE(quest._qlevel); file->WriteLE(quest._qidx); // _qtype for compatibility, used in DRLG_CheckQuests file->WriteLE(quest._qactive); file->WriteLE(quest._qlvltype); file->WriteLE(quest.position.x); file->WriteLE(quest.position.y); file->WriteLE(quest._qslvl); file->WriteLE(quest._qidx); if (gbIsHellfire) { file->Skip(2); // Alignment file->WriteLE(quest._qmsg); } else { file->WriteLE(quest._qmsg); } file->WriteLE(quest._qvar1); file->WriteLE(quest._qvar2); file->Skip(2); // Alignment if (!gbIsHellfire) file->Skip(1); // Alignment file->WriteLE(quest._qlog ? 1 : 0); file->WriteBE(ReturnLvlPosition.x); file->WriteBE(ReturnLvlPosition.y); file->WriteBE(ReturnLevel); file->WriteBE(ReturnLevelType); file->Skip(sizeof(int32_t)); // Skip DoomQuestState } void SaveLighting(SaveHelper *file, Light *pLight, bool vision = false) { file->WriteLE(pLight->position.tile.x); file->WriteLE(pLight->position.tile.y); file->WriteLE(pLight->radius); file->WriteLE(vision ? 1 : 0); // _lid file->WriteLE(pLight->isInvalid ? 1 : 0); file->WriteLE(pLight->hasChanged ? 1 : 0); file->Skip(4); // Unused file->WriteLE(pLight->position.old.x); file->WriteLE(pLight->position.old.y); file->WriteLE(pLight->oldRadius); file->WriteLE(pLight->position.offset.deltaX); file->WriteLE(pLight->position.offset.deltaY); file->WriteLE(vision ? 1 : 0); } void SavePortal(SaveHelper *file, int i) { Portal *pPortal = &Portals[i]; file->WriteLE(pPortal->open ? 1 : 0); file->WriteLE(pPortal->position.x); file->WriteLE(pPortal->position.y); file->WriteLE(pPortal->level); file->WriteLE(pPortal->setlvl ? pPortal->ltype : getHellfireLevelType(pPortal->ltype)); file->WriteLE(pPortal->setlvl ? 1 : 0); } /** * @brief Saves items on the current dungeon floor * @param file interface to the save file * @return a map converting from runtime item indexes to the relative position in the save file, used by SaveDroppedItemLocations * @see SaveDroppedItemLocations */ ankerl::unordered_dense::map SaveDroppedItems(SaveHelper &file) { // Vanilla Diablo/Hellfire initialise the ActiveItems and AvailableItems arrays based on saved data, so write valid values for compatibility for (uint8_t i = 0; i < MAXITEMS; i++) file.WriteLE(i); // Strictly speaking everything from ActiveItemCount onwards is unused but no harm writing non-zero values here. for (uint8_t i = 0; i < MAXITEMS; i++) file.WriteLE((i + ActiveItemCount) % MAXITEMS); ankerl::unordered_dense::map itemIndexes; itemIndexes.reserve(ActiveItemCount + 1); itemIndexes.emplace(0, 0); for (uint8_t i = 0; i < ActiveItemCount; i++) { itemIndexes[ActiveItems[i] + 1] = i + 1; SaveItem(file, Items[ActiveItems[i]]); } return itemIndexes; } /** * @brief Saves the position of dropped items (in dItem) * @param file interface to the save file * @param itemIndexes a map converting from runtime item indexes to the relative position in the save file */ void SaveDroppedItemLocations(SaveHelper &file, const ankerl::unordered_dense::map &itemIndexes) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(itemIndexes.at(dItem[i][j])); } } constexpr uint32_t VersionAdditionalMissiles = 0; void SaveAdditionalMissiles(SaveWriter &saveWriter) { constexpr size_t BytesWrittenBySaveMissile = 180; const uint32_t missileCountAdditional = (Missiles.size() > MaxMissilesForSaveGame) ? static_cast(Missiles.size() - MaxMissilesForSaveGame) : 0; SaveHelper file(saveWriter, "additionalMissiles", sizeof(uint32_t) + sizeof(uint32_t) + (missileCountAdditional * BytesWrittenBySaveMissile)); file.WriteLE(VersionAdditionalMissiles); file.WriteLE(missileCountAdditional); if (missileCountAdditional > 0) { auto it = Missiles.cbegin(); // std::list::const_iterator doesn't provide operator+() :/ using std::advance to get past the missiles we've already saved std::advance(it, MaxMissilesForSaveGame); for (; it != Missiles.cend(); it++) { SaveMissile(&file, *it); } } } void LoadAdditionalMissiles() { LoadHelper file(OpenSaveArchive(gSaveNumber), "additionalMissiles"); if (!file.IsValid()) { // no additional Missiles saved return; } auto loadedVersion = file.NextLE(); if (loadedVersion > VersionAdditionalMissiles) { // unknown version return; } auto missileCountAdditional = file.NextLE(); for (uint32_t i = 0U; i < missileCountAdditional; i++) { LoadMissile(&file); } } void SaveLevelSeeds(SaveWriter &saveWriter) { SaveHelper file(saveWriter, "levelseeds", giNumberOfLevels * (sizeof(uint8_t) + sizeof(uint32_t))); for (int i = 0; i < giNumberOfLevels; i++) { file.WriteLE(LevelSeeds[i] ? 1 : 0); if (LevelSeeds[i]) { file.WriteLE(*LevelSeeds[i]); } } } void LoadLevelSeeds() { LoadHelper file(OpenSaveArchive(gSaveNumber), "levelseeds"); if (!file.IsValid()) return; for (int i = 0; i < giNumberOfLevels; i++) { if (file.NextLE() != 0) { LevelSeeds[i] = file.NextLE(); } else { LevelSeeds[i] = std::nullopt; } } } void SaveLevel(SaveWriter &saveWriter, LevelConversionData *levelConversionData) { Player &myPlayer = *MyPlayer; DoUnVision(myPlayer.position.tile, myPlayer._pLightRad); // fix for vision staying on the level if (leveltype == DTYPE_TOWN) DungeonSeeds[0] = GenerateSeed(); char szName[MaxMpqPathSize]; GetTempLevelNames(szName); SaveHelper file(saveWriter, szName, 256 * 1024); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dCorpse[i][j]); } } file.WriteBE(static_cast(ActiveMonsterCount)); file.WriteBE(ActiveItemCount); file.WriteBE(ActiveObjectCount); if (leveltype != DTYPE_TOWN) { for (const unsigned monsterId : ActiveMonsters) file.WriteBE(monsterId); for (size_t i = 0; i < ActiveMonsterCount; i++) { MonsterConversionData *monsterConversionData = nullptr; if (levelConversionData != nullptr) monsterConversionData = &levelConversionData->monsterConversionData[ActiveMonsters[i]]; SaveMonster(&file, Monsters[ActiveMonsters[i]], monsterConversionData); } for (const int objectId : ActiveObjects) file.WriteLE(objectId); for (const int objectId : AvailableObjects) file.WriteLE(objectId); for (int i = 0; i < ActiveObjectCount; i++) { SaveObject(file, Objects[ActiveObjects[i]]); } } auto itemIndexes = SaveDroppedItems(file); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(static_cast(dFlags[i][j] & DungeonFlag::SavedFlags)); } SaveDroppedItemLocations(file, itemIndexes); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteBE(dMonster[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dObject[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPreLight[i][j]); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(AutomapView[i][j]); } } if (!setlevel) myPlayer._pLvlVisited[currlevel] = true; else myPlayer._pSLvlVisited[setlvlnum] = true; } tl::expected LoadLevel(LevelConversionData *levelConversionData) { char szName[MaxMpqPathSize]; std::optional archive = OpenSaveArchive(gSaveNumber); GetTempLevelNames(szName); if (!archive || !archive->HasFile(szName)) GetPermLevelNames(szName); LoadHelper file(std::move(archive), szName); if (!file.IsValid()) return tl::make_unexpected(std::string(_("Unable to open save file archive"))); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dCorpse[i][j] = file.NextLE(); } MoveLightsToCorpses(); } ActiveMonsterCount = file.NextBE(); auto savedItemCount = file.NextBE(); ActiveObjectCount = file.NextBE(); ankerl::unordered_dense::set removedMonsterIds; if (leveltype != DTYPE_TOWN) { LoadMonsters(file, removedMonsterIds, true, levelConversionData); if (!gbSkipSync) { for (size_t i = 0; i < ActiveMonsterCount; i++) RETURN_IF_ERROR(SyncMonsterAnim(Monsters[ActiveMonsters[i]])); } for (int &objectId : ActiveObjects) objectId = file.NextLE(); for (int &objectId : AvailableObjects) objectId = file.NextLE(); for (int i = 0; i < ActiveObjectCount; i++) LoadObject(file, Objects[ActiveObjects[i]]); if (!gbSkipSync) { for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); } } LoadDroppedItems(file, savedItemCount); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dFlags[i][j] = static_cast(file.NextLE()) & DungeonFlag::LoadedFlags; } // skip dItem indexes, this gets populated in LoadDroppedItems file.Skip(MAXDUNX * MAXDUNY); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) { dMonster[i][j] = file.NextBE(); if (dMonster[i][j] > 0 && removedMonsterIds.contains(std::abs(dMonster[i][j]) - 1)) { dMonster[i][j] = 0; } } } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dObject[i][j] = file.NextLE(); } file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPreLight[i][j] = file.NextLE(); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) const auto automapView = static_cast(file.NextLE()); AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView; } } // No need to load dLight, we can recreate it accurately from LightList memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light ChangeLightXY(Players[MyPlayerId].lightId, Players[MyPlayerId].position.tile); // forces player light refresh } else { memset(dLight, 0, sizeof(dLight)); } if (!gbSkipSync) { AutomapZoomReset(); ResyncQuests(); RedoMissileFlags(); UpdateLighting = true; } for (const Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) Lights[player.lightId].hasChanged = true; } return {}; } const int DiabloItemSaveSize = 368; const int HellfireItemSaveSize = 372; bool IsStashSizeValid(size_t stashSize, uint32_t pages, uint32_t itemCount) { const size_t itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize); const size_t expectedSize = sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + (sizeof(uint32_t) + 10 * 10 * sizeof(uint16_t)) * pages + sizeof(uint32_t) + itemSize * itemCount + sizeof(uint32_t); return stashSize == expectedSize; } } // namespace tl::expected ConvertLevels(SaveWriter &saveWriter) { // Backup current level state const bool tmpSetlevel = setlevel; const _setlevels tmpSetlvlnum = setlvlnum; const int tmpCurrlevel = currlevel; const dungeon_type tmpLeveltype = leveltype; gbSkipSync = true; setlevel = false; // Convert regular levels for (int i = 0; i < giNumberOfLevels; i++) { currlevel = i; if (!LevelFileExists(saveWriter)) continue; leveltype = GetLevelType(currlevel); LevelConversionData levelConversionData; RETURN_IF_ERROR(LoadLevel(&levelConversionData)); SaveLevel(saveWriter, &levelConversionData); } setlevel = true; // Convert quest levels for (auto &quest : Quests) { if (quest._qactive == QUEST_NOTAVAIL) { continue; } leveltype = quest._qlvltype; if (leveltype == DTYPE_NONE) { continue; } setlvlnum = quest._qslvl; if (!LevelFileExists(saveWriter)) continue; LevelConversionData levelConversionData; RETURN_IF_ERROR(LoadLevel(&levelConversionData)); SaveLevel(saveWriter, &levelConversionData); } gbSkipSync = false; // Restore current level state setlevel = tmpSetlevel; setlvlnum = tmpSetlvlnum; currlevel = tmpCurrlevel; leveltype = tmpLeveltype; return {}; } void RemoveInvalidItem(Item &item) { bool isInvalid = !IsItemAvailable(item.IDidx) || item._iUid >= static_cast(UniqueItems.size()); if (!gbIsHellfire) { isInvalid = isInvalid || (item._itype == ItemType::Staff && GetSpellStaffLevel(item._iSpell) == -1); isInvalid = isInvalid || (item._iMiscId == IMISC_BOOK && GetSpellBookLevel(item._iSpell) == -1); isInvalid = isInvalid || item._iDamAcFlags != ItemSpecialEffectHf::None; isInvalid = isInvalid || item._iPrePower > IPL_LASTDIABLO; isInvalid = isInvalid || item._iSufPower > IPL_LASTDIABLO; } if (isInvalid) { item.clear(); } } _item_indexes RemapItemIdxFromDiablo(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) -> int { if (i == IDI_SORCERER) { return IDI_SORCERER_DIABLO; } if (i >= 156) { i += 5; // Hellfire exclusive items } if (i >= 88) { i += 1; // Scroll of Search } if (i >= 83) { i += 4; // Oils } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxToDiablo(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) -> int { if (i == IDI_SORCERER_DIABLO) { return IDI_SORCERER; } if ((i >= 83 && i <= 86) || i == 92 || i >= 161) { return -1; // Hellfire exclusive items } if (i >= 93) { i -= 1; // Scroll of Search } if (i >= 87) { i -= 4; // Oils } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxFromSpawn(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) { if (i >= 62) { i += 9; // Medium and heavy armors } if (i >= 96) { i += 1; // Scroll of Stone Curse } if (i >= 98) { i += 1; // Scroll of Guardian } if (i >= 99) { i += 1; // Scroll of ... } if (i >= 101) { i += 1; // Scroll of Golem } if (i >= 102) { i += 1; // Scroll of None } if (i >= 104) { i += 1; // Scroll of Apocalypse } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } _item_indexes RemapItemIdxToSpawn(_item_indexes i) { constexpr auto GetItemIdValue = [](int i) { if (i >= 104) { i -= 1; // Scroll of Apocalypse } if (i >= 102) { i -= 1; // Scroll of None } if (i >= 101) { i -= 1; // Scroll of Golem } if (i >= 99) { i -= 1; // Scroll of ... } if (i >= 98) { i -= 1; // Scroll of Guardian } if (i >= 96) { i -= 1; // Scroll of Stone Curse } if (i >= 71) { i -= 9; // Medium and heavy armors } return i; }; return static_cast<_item_indexes>(GetItemIdValue(i)); } bool IsHeaderValid(uint32_t magicNumber) { gbIsHellfireSaveGame = false; if (magicNumber == LoadLE32("SHAR")) { return true; } if (magicNumber == LoadLE32("SHLF")) { gbIsHellfireSaveGame = true; return true; } if (!gbIsSpawn && magicNumber == LoadLE32("RETL")) { return true; } if (!gbIsSpawn && magicNumber == LoadLE32("HELF")) { gbIsHellfireSaveGame = true; return true; } return false; } // Returns the size of the hotkeys file with the number of hotkeys passed and if a header with the number of hotkeys is present in the file size_t HotkeysSize(size_t nHotkeys = NumHotkeys) { // header spells spell types active spell active spell type return sizeof(uint8_t) + (nHotkeys * sizeof(int32_t)) + (nHotkeys * sizeof(uint8_t)) + sizeof(int32_t) + sizeof(uint8_t); } void LoadHotkeys() { LoadHelper file(OpenSaveArchive(gSaveNumber), "hotkeys"); if (!file.IsValid()) return; Player &myPlayer = *MyPlayer; size_t nHotkeys = 4; // Defaults to old save format number // Refill the spell arrays with no selection std::fill(myPlayer._pSplHotKey, myPlayer._pSplHotKey + NumHotkeys, SpellID::Invalid); std::fill(myPlayer._pSplTHotKey, myPlayer._pSplTHotKey + NumHotkeys, SpellType::Invalid); // Checking if the save file has the old format with only 4 hotkeys and no header if (file.IsValid(HotkeysSize(nHotkeys))) { // The file contains a header byte and at least 4 entries, so we can assume it's a new format save nHotkeys = file.NextLE(); } // Read all hotkeys in the file for (size_t i = 0; i < nHotkeys; i++) { // Do not load hotkeys past the size of the spell types array, discard the rest if (i < NumHotkeys) { myPlayer._pSplHotKey[i] = static_cast(file.NextLE()); } else { file.Skip(); } } for (size_t i = 0; i < nHotkeys; i++) { // Do not load hotkeys past the size of the spells array, discard the rest if (i < NumHotkeys) { myPlayer._pSplTHotKey[i] = static_cast(file.NextLE()); } else { file.Skip(); } } // Load the selected spell last myPlayer._pRSpell = static_cast(file.NextLE()); myPlayer._pRSplType = static_cast(file.NextLE()); } void SaveHotkeys(SaveWriter &saveWriter, const Player &player) { SaveHelper file(saveWriter, "hotkeys", HotkeysSize()); // Write the number of spell hotkeys file.WriteLE(static_cast(NumHotkeys)); // Write the spell hotkeys for (const auto &spellId : player._pSplHotKey) { file.WriteLE(static_cast(spellId)); } for (const auto &spellType : player._pSplTHotKey) { file.WriteLE(static_cast(spellType)); } // Write the selected spell last file.WriteLE(static_cast(player._pRSpell)); file.WriteLE(static_cast(player._pRSplType)); } void LoadHeroItems(Player &player) { LoadHelper file(OpenSaveArchive(gSaveNumber), "heroitems"); if (!file.IsValid()) return; gbIsHellfireSaveGame = file.NextBool8(); LoadMatchingItems(file, player, NUM_INVLOC, player.InvBody); LoadMatchingItems(file, player, InventoryGridCells, player.InvList); LoadMatchingItems(file, player, MaxBeltItems, player.SpdList); gbIsHellfireSaveGame = gbIsHellfire; } constexpr uint8_t StashVersion = 0; void LoadStash() { const char *filename; if (!gbIsMultiplayer) filename = "spstashitems"; else filename = "mpstashitems"; Stash = {}; LoadHelper file(OpenStashArchive(), filename); if (!file.IsValid()) return; auto version = file.NextLE(); if (version > StashVersion) { EventPlrMsg(_("Stash version invalid. If you attempt to access your stash, data will be overwritten!!"), UiFlags::ColorRed); return; } Stash.gold = file.NextLE(); auto pages = file.NextLE(); for (unsigned i = 0; i < pages; i++) { auto page = file.NextLE(); for (auto &row : Stash.stashGrids[page]) { for (uint16_t &cell : row) { cell = file.NextLE(); } } } auto itemCount = file.NextLE(); if (!IsStashSizeValid(file.Size(), pages, itemCount)) { Stash = {}; EventPlrMsg(_("Stash size invalid. If you attempt to access your stash, data will be overwritten!!"), UiFlags::ColorRed); return; } Stash.stashList.resize(itemCount); for (unsigned i = 0; i < itemCount; i++) { LoadAndValidateItemData(file, Stash.stashList[i]); } Stash.SetPage(file.NextLE()); } void RemoveEmptyInventory(Player &player) { for (int i = InventoryGridCells; i > 0; i--) { const int8_t idx = player.InvGrid[i - 1]; if (idx > 0 && player.InvList[idx - 1].isEmpty()) { player.RemoveInvItem(idx - 1); } } } tl::expected LoadGame(bool firstflag) { FreeGameMem(); LoadHelper file(OpenSaveArchive(gSaveNumber), "game"); if (!file.IsValid()) { return tl::make_unexpected(std::string(_("Unable to open save file archive"))); } if (!IsHeaderValid(file.NextLE())) { return tl::make_unexpected(std::string(_("Invalid save file"))); } if (gbIsHellfireSaveGame) { giNumberOfLevels = 25; giNumberQuests = 24; giNumberOfSmithPremiumItems = 15; } else { // Todo initialize additional levels and quests if we are running Hellfire giNumberOfLevels = 17; giNumberQuests = 16; giNumberOfSmithPremiumItems = 6; } pfile_remove_temp_files(); setlevel = file.NextBool8(); setlvlnum = static_cast<_setlevels>(file.NextBE()); currlevel = file.NextBE(); leveltype = static_cast(file.NextBE()); if (!setlevel) leveltype = GetLevelType(currlevel); const int viewX = file.NextBE(); const int viewY = file.NextBE(); invflag = file.NextBool8(); CharFlag = file.NextBool8(); const int tmpNummonsters = file.NextBE(); auto savedItemCount = file.NextBE(); const int tmpNummissiles = file.NextBE(); const int tmpNobjects = file.NextBE(); if (!gbIsHellfire && IsAnyOf(leveltype, DTYPE_NEST, DTYPE_CRYPT)) { return tl::make_unexpected(std::string(_("Player is on a Hellfire only level"))); } for (uint8_t i = 0; i < giNumberOfLevels; i++) { DungeonSeeds[i] = file.NextBE(); LevelSeeds[i] = std::nullopt; file.Skip(4); // Skip loading gnLevelTypeTbl } LoadLevelSeeds(); Player &myPlayer = *MyPlayer; LoadPlayer(file, myPlayer); if (sgGameInitInfo.nDifficulty < DIFF_NORMAL || sgGameInitInfo.nDifficulty > DIFF_HELL) sgGameInitInfo.nDifficulty = DIFF_NORMAL; for (int i = 0; i < giNumberQuests; i++) LoadQuest(&file, i); for (int i = 0; i < MAXPORTAL; i++) LoadPortal(&file, i); if (gbIsHellfireSaveGame != gbIsHellfire) { RETURN_IF_ERROR(pfile_convert_levels()); RemoveEmptyInventory(myPlayer); } RETURN_IF_ERROR(LoadGameLevel(firstflag, ENTRY_LOAD)); SetPlrAnims(myPlayer); SyncPlrAnim(myPlayer); ViewPosition = { viewX, viewY }; ActiveMonsterCount = tmpNummonsters; ActiveObjectCount = tmpNobjects; for (size_t i = 0; i < MonstersData.size(); ++i) { int &monstkill = MonsterKillCounts[i]; monstkill = file.NextBE(); } ankerl::unordered_dense::set removedMonsterIds; // skip ahead for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] file.Skip(4 * (MaxMonsters - MonstersData.size())); if (leveltype != DTYPE_TOWN) { LoadMonsters(file, removedMonsterIds, false, nullptr); for (size_t i = 0; i < ActiveMonsterCount; i++) SyncPackSize(Monsters[ActiveMonsters[i]]); // Skip ActiveMissiles file.Skip(MaxMissilesForSaveGame); // Skip AvailableMissiles file.Skip(MaxMissilesForSaveGame); for (int i = 0; i < tmpNummissiles; i++) LoadMissile(&file); // For petrified monsters, the data in missile.var1 must be used to // load the appropriate animation data for the monster in missile.var2 for (size_t i = 0; i < ActiveMonsterCount; i++) RETURN_IF_ERROR(SyncMonsterAnim(Monsters[ActiveMonsters[i]])); for (int &objectId : ActiveObjects) objectId = file.NextLE(); for (int &objectId : AvailableObjects) objectId = file.NextLE(); for (int i = 0; i < ActiveObjectCount; i++) LoadObject(file, Objects[ActiveObjects[i]]); for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); ActiveLightCount = file.NextBE(); for (uint8_t &lightId : ActiveLights) lightId = file.NextLE(); for (int i = 0; i < ActiveLightCount; i++) LoadLighting(&file, &Lights[ActiveLights[i]]); file.Skip(); // VisionId const int visionCount = file.NextBE(); for (int i = 0; i < visionCount; i++) { LoadLighting(&file, &VisionList[i]); VisionActive[i] = true; } } LoadDroppedItems(file, savedItemCount); LoadAdditionalMissiles(); for (bool &uniqueItemFlag : UniqueItemFlags) uniqueItemFlag = file.NextBool8(); file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dFlags[i][j] = static_cast(file.NextLE()) & DungeonFlag::LoadedFlags; } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPlayer[i][j] = file.NextLE(); } // skip dItem indexes, this gets populated in LoadDroppedItems file.Skip(MAXDUNX * MAXDUNY); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) { dMonster[i][j] = file.NextBE(); if (dMonster[i][j] > 0 && removedMonsterIds.contains(std::abs(dMonster[i][j]) - 1)) { dMonster[i][j] = 0; } } } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dCorpse[i][j] = file.NextLE(); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dObject[i][j] = file.NextLE(); } file.Skip(MAXDUNY * MAXDUNX); // dLight for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) dPreLight[i][j] = file.NextLE(); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) { // NOLINT(modernize-loop-convert) const auto automapView = static_cast(file.NextLE()); AutomapView[i][j] = automapView == MAP_EXP_OLD ? MAP_EXP_SELF : automapView; } } file.Skip(MAXDUNX * MAXDUNY); // dMissile // No need to load dLight, we can recreate it accurately from LightList memcpy(dLight, dPreLight, sizeof(dLight)); // resets the light on entering a level to get rid of incorrect light ChangeLightXY(myPlayer.lightId, myPlayer.position.tile); // forces player light refresh } else { memset(dLight, 0, sizeof(dLight)); } PremiumItemCount = file.NextBE(); PremiumItemLevel = file.NextBE(); for (int i = 0; i < giNumberOfSmithPremiumItems; i++) LoadPremium(file, i); if (gbIsHellfire && !gbIsHellfireSaveGame) SpawnPremium(myPlayer); AutomapActive = file.NextBool8(); AutoMapScale = file.NextBE(); AutomapZoomReset(); ResyncQuests(); if (leveltype != DTYPE_TOWN) { RedoPlayerVision(); ProcessVisionList(); ProcessLightList(); } // convert stray manashield missiles into pManaShield flag for (auto &missile : Missiles) { if (missile._mitype == MissileID::ManaShield && !missile._miDelFlag) { Players[missile._misource].pManaShield = true; missile._miDelFlag = true; } } SetUpMissileAnimationData(); RedoMissileFlags(); gbProcessPlayers = IsDiabloAlive(!firstflag); if (gbIsHellfireSaveGame != gbIsHellfire) { SaveGame(); } gbIsHellfireSaveGame = gbIsHellfire; return {}; } void SaveHeroItems(SaveWriter &saveWriter, Player &player) { const size_t itemCount = static_cast(NUM_INVLOC) + InventoryGridCells + MaxBeltItems; SaveHelper file(saveWriter, "heroitems", itemCount * (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize) + sizeof(uint8_t)); file.WriteLE(gbIsHellfire ? 1 : 0); for (const Item &item : player.InvBody) SaveItem(file, item); for (const Item &item : player.InvList) SaveItem(file, item); for (const Item &item : player.SpdList) SaveItem(file, item); } void SaveStash(SaveWriter &stashWriter) { const char *filename; if (!gbIsMultiplayer) filename = "spstashitems"; else filename = "mpstashitems"; const int itemSize = (gbIsHellfire ? HellfireItemSaveSize : DiabloItemSaveSize); SaveHelper file( stashWriter, filename, sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t) + (sizeof(uint32_t) + 10 * 10 * sizeof(uint16_t)) * Stash.stashGrids.size() + sizeof(uint32_t) + itemSize * Stash.stashList.size() + sizeof(uint32_t)); file.WriteLE(StashVersion); file.WriteLE(Stash.gold); std::vector pagesToSave; for (const auto &[page, grid] : Stash.stashGrids) { if (c_any_of(grid, [](const auto &row) { return c_any_of(row, [](StashStruct::StashCell cell) { return cell > 0; }); })) { // found a page that contains at least one item pagesToSave.push_back(page); } }; // Current stash size is 100 pages. Will definitely fit in a 32 bit value. file.WriteLE(static_cast(pagesToSave.size())); for (const auto &page : pagesToSave) { file.WriteLE(page); for (const auto &row : Stash.stashGrids[page]) { for (const uint16_t cell : row) { file.WriteLE(cell); } } } // 100 pages of 100 items is still only 10 000, as with the page count will definitely fit in 32 bits even in the worst case. file.WriteLE(static_cast(Stash.stashList.size())); for (const Item &item : Stash.stashList) { SaveItem(file, item); } file.WriteLE(static_cast(Stash.GetPage())); } void SaveGameData(SaveWriter &saveWriter) { SaveHelper file(saveWriter, "game", 320 * 1024); if (gbIsSpawn && !gbIsHellfire) file.WriteLE(LoadLE32("SHAR")); else if (gbIsSpawn && gbIsHellfire) file.WriteLE(LoadLE32("SHLF")); else if (!gbIsSpawn && gbIsHellfire) file.WriteLE(LoadLE32("HELF")); else if (!gbIsSpawn && !gbIsHellfire) file.WriteLE(LoadLE32("RETL")); else app_fatal(_("Invalid game state")); if (gbIsHellfire) { giNumberOfLevels = 25; giNumberQuests = 24; giNumberOfSmithPremiumItems = 15; } else { giNumberOfLevels = 17; giNumberQuests = 16; giNumberOfSmithPremiumItems = 6; } file.WriteLE(setlevel ? 1 : 0); file.WriteBE(setlvlnum); file.WriteBE(currlevel); file.WriteBE(getHellfireLevelType(leveltype)); file.WriteBE(ViewPosition.x); file.WriteBE(ViewPosition.y); file.WriteLE(invflag ? 1 : 0); file.WriteLE(CharFlag ? 1 : 0); file.WriteBE(static_cast(ActiveMonsterCount)); file.WriteBE(ActiveItemCount); // ActiveMissileCount will be a value from 0-125 (for vanilla compatibility). Writing an unsigned value here to avoid // warnings about casting from unsigned to signed, but there's no sign extension issues when reading this as a signed // value later so it doesn't have to match in LoadGameData(). file.WriteBE(static_cast(std::min(Missiles.size(), MaxMissilesForSaveGame))); file.WriteBE(ActiveObjectCount); for (uint8_t i = 0; i < giNumberOfLevels; i++) { file.WriteBE(DungeonSeeds[i]); file.WriteBE(getHellfireLevelType(GetLevelType(i))); } const Player &myPlayer = *MyPlayer; SavePlayer(file, myPlayer); for (int i = 0; i < giNumberQuests; i++) SaveQuest(&file, i); for (int i = 0; i < MAXPORTAL; i++) SavePortal(&file, i); for (size_t i = 0; i < MonstersData.size(); ++i) { const int monstkill = MonsterKillCounts[i]; file.WriteBE(monstkill); } // add padding for vanilla save compatibility (Related to bugfix where MonsterKillCounts[MaxMonsters] was changed to MonsterKillCounts[NUM_MTYPES] file.Skip(4 * (MaxMonsters - MonstersData.size())); if (leveltype != DTYPE_TOWN) { for (const unsigned monsterId : ActiveMonsters) file.WriteBE(monsterId); for (size_t i = 0; i < ActiveMonsterCount; i++) SaveMonster(&file, Monsters[ActiveMonsters[i]]); // Write ActiveMissiles for (uint8_t activeMissile = 0; activeMissile < MaxMissilesForSaveGame; activeMissile++) file.WriteLE(activeMissile); // Write AvailableMissiles for (size_t availableMissiles = Missiles.size(); availableMissiles < MaxMissilesForSaveGame; availableMissiles++) file.WriteLE(static_cast(availableMissiles)); const size_t savedMissiles = std::min(Missiles.size(), MaxMissilesForSaveGame); file.Skip(savedMissiles); // Write Missile Data { auto missilesEnd = Missiles.cbegin(); std::advance(missilesEnd, savedMissiles); for (auto it = Missiles.cbegin(); it != missilesEnd; it++) { SaveMissile(&file, *it); } } for (const int objectId : ActiveObjects) file.WriteLE(static_cast(objectId)); for (const int objectId : AvailableObjects) file.WriteLE(static_cast(objectId)); for (int i = 0; i < ActiveObjectCount; i++) SaveObject(file, Objects[ActiveObjects[i]]); file.WriteBE(ActiveLightCount); for (const uint8_t lightId : ActiveLights) file.WriteLE(lightId); for (int i = 0; i < ActiveLightCount; i++) SaveLighting(&file, &Lights[ActiveLights[i]]); const auto visionCount = static_cast(Players.size()); file.WriteBE(visionCount + 1); // VisionId file.WriteBE(visionCount); for (const Player &player : Players) SaveLighting(&file, &VisionList[player.getId()], true); } auto itemIndexes = SaveDroppedItems(file); for (const bool uniqueItemFlag : UniqueItemFlags) file.WriteLE(uniqueItemFlag ? 1 : 0); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(static_cast(dFlags[i][j] & DungeonFlag::SavedFlags)); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPlayer[i][j]); } SaveDroppedItemLocations(file, itemIndexes); if (leveltype != DTYPE_TOWN) { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteBE(dMonster[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dCorpse[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dObject[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dLight[i][j]); // BUGFIX: dLight got saved already } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(dPreLight[i][j]); } for (int j = 0; j < DMAXY; j++) { for (int i = 0; i < DMAXX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(AutomapView[i][j]); } for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) // NOLINT(modernize-loop-convert) file.WriteLE(TileContainsMissile({ i, j }) ? -1 : 0); // For backwards compatibility } } file.WriteBE(PremiumItemCount); file.WriteBE(PremiumItemLevel); for (int i = 0; i < giNumberOfSmithPremiumItems; i++) SaveItem(file, PremiumItems[i]); file.WriteLE(AutomapActive ? 1 : 0); file.WriteBE(AutoMapScale); SaveAdditionalMissiles(saveWriter); SaveLevelSeeds(saveWriter); } void SaveGame() { gbValidSaveFile = true; pfile_write_hero(/*writeGameData=*/true); sfile_write_stash(); } void SaveLevel(SaveWriter &saveWriter) { SaveLevel(saveWriter, nullptr); } tl::expected LoadLevel() { return LoadLevel(nullptr); } } // namespace devilution ================================================ FILE: Source/loadsave.h ================================================ /** * @file loadsave.h * * Interface of save game functionality. */ #pragma once #include #include #include "pfile.h" #include "player.h" #include "utils/attributes.h" namespace devilution { extern DVL_API_FOR_TEST bool gbIsHellfireSaveGame; extern DVL_API_FOR_TEST uint8_t giNumberOfLevels; void RemoveInvalidItem(Item &pItem); _item_indexes RemapItemIdxFromDiablo(_item_indexes i); _item_indexes RemapItemIdxToDiablo(_item_indexes i); _item_indexes RemapItemIdxFromSpawn(_item_indexes i); _item_indexes RemapItemIdxToSpawn(_item_indexes i); bool IsHeaderValid(uint32_t magicNumber); void LoadHotkeys(); void LoadHeroItems(Player &player); /** * @brief Remove invalid inventory items from the inventory grid * @param player The player to remove invalid items from */ void RemoveEmptyInventory(Player &player); /** * @brief Load game state * @param firstflag Can be set to false if we are simply reloading the current game */ tl::expected LoadGame(bool firstflag); void SaveHotkeys(SaveWriter &saveWriter, const Player &player); void SaveHeroItems(SaveWriter &saveWriter, Player &player); void SaveGameData(SaveWriter &saveWriter); void SaveGame(); void SaveLevel(SaveWriter &saveWriter); tl::expected LoadLevel(); tl::expected ConvertLevels(SaveWriter &saveWriter); void LoadStash(); void SaveStash(SaveWriter &stashWriter); } // namespace devilution ================================================ FILE: Source/lua/autocomplete.cpp ================================================ #ifdef _DEBUG #include "lua/autocomplete.hpp" #include #include #include #include #include #include #include #include #include #include "appfat.h" #include "engine/assets.hpp" #include "lua/lua_global.hpp" #include "lua/metadoc.hpp" #include "utils/algorithm/container.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution { namespace { std::string_view GetLastToken(std::string_view text) { if (text.empty()) return {}; size_t i = text.size(); while (i > 0 && text[i - 1] != ' ' && text[i - 1] != '(' && text[i - 1] != ',') --i; return text.substr(i); } struct ValueInfo { bool callable = false; std::string signature; std::string docstring; }; sol::protected_function LoadLuaFunctionSignatureGetter(sol::state &lua) { tl::expected src = LoadAsset("lua_internal\\get_lua_function_signature.lua"); if (!src.has_value()) { app_fatal(src.error()); } const sol::object obj = SafeCallResult(lua.safe_script(std::string_view(src.value())), /*optional=*/false); if (obj.get_type() != sol::type::function) { app_fatal("Lua: expected a function"); } return obj.as(); } std::string GetNativeLuaFunctionSignature(const sol::object &fn) { sol::state &lua = GetLuaState(); constexpr std::string_view LuaFunctionSignatureGetterKey = "__DEVILUTIONX_GET_LUA_SIGNATURE"; sol::object getter = lua[LuaFunctionSignatureGetterKey]; if (getter.get_type() == sol::type::lua_nil) { getter = lua[LuaFunctionSignatureGetterKey] = LoadLuaFunctionSignatureGetter(lua); } const sol::object obj = SafeCallResult(getter.as()(fn), /*optional=*/false); if (obj.get_type() != sol::type::string) { app_fatal(StrCat("Lua: Expected a string, got ", sol::utility::to_string(obj))); } return obj.as(); } std::string GetFunctionSignature(const sol::object &value) { value.push(value.lua_state()); const bool isC = lua_iscfunction(value.lua_state(), -1) != 0; lua_pop(value.lua_state(), 1); return isC ? "(...)" : GetNativeLuaFunctionSignature(value); } void RemoveFirstArgumentFromFunctionSignature(std::string &signature) { if (signature == "(...)") return; size_t firstArgEnd = signature.find_first_of(",)"); if (firstArgEnd == std::string::npos) return; ++firstArgEnd; if (firstArgEnd == signature.size()) { signature = "()"; return; } if (signature[firstArgEnd] == ' ') ++firstArgEnd; signature.replace(0, firstArgEnd, "("); } ValueInfo GetValueInfo(const sol::table &table, std::string_view key, const sol::object &value) { ValueInfo info; if (std::optional signature = GetSignature(table, key); signature.has_value()) { info.signature = *std::move(signature); } if (std::optional docstring = GetDocstring(table, key); docstring.has_value()) { info.docstring = *std::move(docstring); } if (value.get_type() == sol::type::function) { info.callable = true; if (info.signature.empty()) info.signature = GetFunctionSignature(value); return info; } if (!value.is()) return info; const auto valueAsTable = value.as(); const auto metatable = valueAsTable.get>(sol::metatable_key); if (!metatable || !metatable->is()) return info; const auto metatableTbl = metatable->as(); const auto callFn = metatableTbl.get>(sol::meta_function::call); info.callable = callFn.has_value(); if (info.callable && info.signature.empty()) { if (info.signature.empty()) { info.signature = GetFunctionSignature(*callFn); // Remove the first argument (the table passed to `__call`): RemoveFirstArgumentFromFunctionSignature(info.signature); } } return info; } ValueInfo GetValueInfoForUserdata(const sol::userdata &obj, std::string_view key, const sol::object &value, std::optional memberType) { ValueInfo info; if (value.get_type() == sol::type::userdata) { info.callable = false; return info; } if (std::optional signature = GetLuaUserdataSignature(obj, key); signature.has_value()) { info.signature = *std::move(signature); } if (std::optional docstring = GetLuaUserdataDocstring(obj, key); docstring.has_value()) { info.docstring = *std::move(docstring); } if (memberType.has_value()) { info.callable = *memberType == LuaUserdataMemberType::MemberFunction; } else { info.callable = value.get_type() == sol::type::function; } return info; } struct UserdataQuery { const sol::userdata *obj; bool colonAccess; }; void SuggestionsFromTable(const sol::table &table, std::string_view prefix, size_t maxSuggestions, ankerl::unordered_dense::set &out, std::optional userdataQuery = std::nullopt) { for (const auto &[key, value] : table) { if (key.get_type() == sol::type::string) { std::string keyStr = key.as(); if (!keyStr.starts_with(prefix) || keyStr.size() == prefix.size()) continue; if (keyStr.starts_with("__") && !prefix.starts_with("__")) continue; // sol-internal keys -- we don't have fonts for these so skip them. if (keyStr.find("♻") != std::string::npos || keyStr.find("☢") != std::string::npos || keyStr.find("🔩") != std::string::npos) continue; ValueInfo info; std::optional memberType; if (userdataQuery.has_value()) { memberType = GetLuaUserdataMemberType(*userdataQuery->obj, keyStr, value); const bool requiresColonAccess = memberType.has_value() ? *memberType == LuaUserdataMemberType::MemberFunction : value.get_type() == sol::type::function; if (userdataQuery->colonAccess != requiresColonAccess) { continue; } info = GetValueInfoForUserdata(*userdataQuery->obj, keyStr, value, memberType); } else { info = GetValueInfo(table, keyStr, value); } std::string completionText = keyStr.substr(prefix.size()); LuaAutocompleteSuggestion suggestion { std::move(keyStr), std::move(completionText) }; if (info.callable) { suggestion.completionText.append("()"); suggestion.cursorAdjust = -1; } if (!info.signature.empty()) { if (memberType.has_value() && memberType != LuaUserdataMemberType::MemberFunction) { StrAppend(suggestion.displayText, ": "); } StrAppend(suggestion.displayText, info.signature); } if (!info.docstring.empty()) { std::string_view firstLine = info.docstring; if (const size_t newlinePos = firstLine.find('\n'); newlinePos != std::string_view::npos) { firstLine = firstLine.substr(0, newlinePos); } StrAppend(suggestion.displayText, " - ", firstLine); } out.insert(std::move(suggestion)); if (out.size() == maxSuggestions) break; } } const auto fallback = table.get>(sol::metatable_key); if (fallback.has_value() && fallback->get_type() == sol::type::table) { SuggestionsFromTable(fallback->as(), prefix, maxSuggestions, out); } } void SuggestionsFromUserdata(UserdataQuery query, std::string_view prefix, size_t maxSuggestions, ankerl::unordered_dense::set &out) { const auto &meta = query.obj->get>(sol::metatable_key); if (meta.has_value() && meta->get_type() == sol::type::table) { SuggestionsFromTable(meta->as(), prefix, maxSuggestions, out, query); } } bool IsAlnum(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); } bool IsIdentifierChar(char c) { return IsAlnum(c) || c == '_'; } bool IsIdentifierOrExprChar(char c) { return IsIdentifierChar(c) || c == '-' || c == '+' || c == '*' || c == '/' || c == '='; } } // namespace void GetLuaAutocompleteSuggestions(std::string_view text, size_t cursorPos, const sol::environment &lua, size_t maxSuggestions, std::vector &out) { out.clear(); const std::string_view textPrefix = text.substr(0, cursorPos); if (textPrefix.empty()) return; const std::string_view textSuffix = text.substr(cursorPos); if (!textSuffix.empty()) { const char c = textSuffix[0]; if (IsIdentifierOrExprChar(c) || (c == ' ' && textSuffix.size() > 1)) return; } if (textPrefix.size() >= 2 && textPrefix.back() == ' ' && IsIdentifierChar(textPrefix[textPrefix.size() - 2])) { return; } std::string_view token = GetLastToken(textPrefix); const char prevChar = token.data() == textPrefix.data() ? '\0' : *(token.data() - 1); if (prevChar == '(' || prevChar == ',') return; const size_t dotPos = token.find_last_of(".:"); const std::string_view prefix = token.substr(dotPos + 1); const char completionChar = dotPos != std::string_view::npos ? token[dotPos] : '\0'; token.remove_suffix(token.size() - (dotPos == std::string_view::npos ? 0 : dotPos)); ankerl::unordered_dense::set suggestions; const auto addSuggestions = [&](const sol::table &table) { SuggestionsFromTable(table, prefix, maxSuggestions, suggestions); }; if (token.empty()) { if (prevChar == '.') return; addSuggestions(lua); const auto fallback = lua.get>("_G"); if (fallback.has_value() && fallback->get_type() == sol::type::table) { addSuggestions(fallback->as()); } } else { std::optional obj = lua; for (const std::string_view partDot : SplitByChar(token, '.')) { for (const std::string_view part : SplitByChar(partDot, ':')) { obj = obj->as().get>(part); if (!obj.has_value() || !(obj->get_type() == sol::type::table || obj->get_type() == sol::type::userdata)) { return; } } } if (obj->get_type() == sol::type::table) { addSuggestions(obj->as()); } else if (obj->get_type() == sol::type::userdata) { const sol::userdata &data = obj->as(); SuggestionsFromUserdata(UserdataQuery { .obj = &data, .colonAccess = completionChar == ':' }, prefix, maxSuggestions, suggestions); } } out.insert(out.end(), suggestions.begin(), suggestions.end()); c_sort(out); } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/autocomplete.hpp ================================================ #pragma once #ifdef _DEBUG #include #include #include #include #include namespace devilution { struct LuaAutocompleteSuggestion { std::string displayText; std::string completionText; int cursorAdjust = 0; bool operator==(const LuaAutocompleteSuggestion &other) const { return displayText == other.displayText; } bool operator<(const LuaAutocompleteSuggestion &other) const { return displayText < other.displayText; } }; void GetLuaAutocompleteSuggestions( std::string_view text, size_t cursorPos, const sol::environment &lua, size_t maxSuggestions, std::vector &out); } // namespace devilution namespace std { template <> struct hash { size_t operator()(const devilution::LuaAutocompleteSuggestion &suggestion) const { return hash()(suggestion.displayText); } }; } // namespace std #endif // _DEBUG ================================================ FILE: Source/lua/lua_event.cpp ================================================ #include "lua/lua_event.hpp" #include #include #include #include #include "lua/lua_global.hpp" #include "monster.h" #include "player.h" #include "utils/log.hpp" namespace devilution { namespace lua { template void CallLuaEvent(std::string_view name, Args &&...args) { sol::table *events = GetLuaEvents(); if (events == nullptr) { return; } const auto trigger = events->traverse_get>(name, "trigger"); if (!trigger.has_value() || !trigger->is()) { LogError("events.{}.trigger is not a function", name); return; } const sol::protected_function fn = trigger->as(); SafeCallResult(fn(std::forward(args)...), /*optional=*/true); } template T CallLuaEventReturn(T defaultValue, std::string_view name, Args &&...args) { sol::table *events = GetLuaEvents(); if (events == nullptr) { return defaultValue; } const auto trigger = events->traverse_get>(name, "trigger"); if (!trigger.has_value() || !trigger->is()) { return defaultValue; } const sol::protected_function fn = trigger->as(); sol::object result = SafeCallResult(fn(std::forward(args)...), /*optional=*/true); if (result.is()) { return result.as(); } return defaultValue; } void MonsterDataLoaded() { CallLuaEvent("MonsterDataLoaded"); } void UniqueMonsterDataLoaded() { CallLuaEvent("UniqueMonsterDataLoaded"); } void ItemDataLoaded() { CallLuaEvent("ItemDataLoaded"); } void UniqueItemDataLoaded() { CallLuaEvent("UniqueItemDataLoaded"); } void StoreOpened(std::string_view name) { CallLuaEvent("StoreOpened", name); } void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType) { CallLuaEvent("OnMonsterTakeDamage", monster, damage, damageType); } void OnPlayerGainExperience(const Player *player, uint32_t exp) { CallLuaEvent("OnPlayerGainExperience", player, exp); } void OnPlayerTakeDamage(const Player *player, int damage, int damageType) { CallLuaEvent("OnPlayerTakeDamage", player, damage, damageType); } void LoadModsComplete() { CallLuaEvent("LoadModsComplete"); } void GameDrawComplete() { CallLuaEvent("GameDrawComplete"); } void GameStart() { CallLuaEvent("GameStart"); } } // namespace lua } // namespace devilution ================================================ FILE: Source/lua/lua_event.hpp ================================================ #pragma once #include #include namespace devilution { struct Player; struct Monster; namespace lua { void MonsterDataLoaded(); void UniqueMonsterDataLoaded(); void ItemDataLoaded(); void UniqueItemDataLoaded(); void StoreOpened(std::string_view name); void OnMonsterTakeDamage(const Monster *monster, int damage, int damageType); void OnPlayerGainExperience(const Player *player, uint32_t exp); void OnPlayerTakeDamage(const Player *player, int damage, int damageType); void LoadModsComplete(); void GameDrawComplete(); void GameStart(); } // namespace lua } // namespace devilution ================================================ FILE: Source/lua/lua_global.cpp ================================================ #include "lua/lua_global.hpp" #include #include #include #include #include #include #include "appfat.h" #include "effects.h" #include "engine/assets.hpp" #include "lua/lua_event.hpp" #include "lua/modules/audio.hpp" #include "lua/modules/floatingnumbers.hpp" #include "lua/modules/hellfire.hpp" #include "lua/modules/i18n.hpp" #include "lua/modules/items.hpp" #include "lua/modules/log.hpp" #include "lua/modules/monsters.hpp" #include "lua/modules/player.hpp" #include "lua/modules/render.hpp" #include "lua/modules/system.hpp" #include "lua/modules/towners.hpp" #include "monster.h" #include "options.h" #include "player.h" #include "plrmsg.h" #include "utils/console.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" #ifdef _DEBUG #include "lua/modules/dev.hpp" #include "lua/repl.hpp" #endif namespace devilution { namespace { struct LuaState { sol::state sol = {}; sol::table commonPackages = {}; ankerl::unordered_dense::segmented_map compiledScripts = {}; sol::environment sandbox = {}; sol::table events = {}; }; std::optional CurrentLuaState; std::vector> IsModChangeHandlers; // A Lua function that we use to generate a `require` implementation. constexpr std::string_view RequireGenSrc = R"lua( function requireGen(env, loaded, loadFn) return function(packageName) local p = loaded[packageName] if p == nil then local loader = loadFn(packageName) if type(loader) == "string" then error(loader) end setEnvironment(env, loader) p = loader(packageName) loaded[packageName] = p end return p end end )lua"; sol::object LuaLoadScriptFromAssets(std::string_view packageName) { LuaState &luaState = *CurrentLuaState; constexpr std::string_view PathPrefix = "lua\\"; constexpr std::string_view PathSuffix = ".lua"; std::string path; path.reserve(PathPrefix.size() + packageName.size() + PathSuffix.size()); StrAppend(path, PathPrefix, packageName, PathSuffix); std::replace(path.begin() + PathPrefix.size(), path.end() - PathSuffix.size(), '.', '\\'); auto iter = luaState.compiledScripts.find(path); if (iter != luaState.compiledScripts.end()) { return luaState.sol.load(iter->second.as_string_view(), path, sol::load_mode::binary); } tl::expected assetData = LoadAsset(path); if (!assetData.has_value()) { sol::stack::push(luaState.sol.lua_state(), assetData.error()); return sol::stack_object(luaState.sol.lua_state(), -1); } const sol::load_result result = luaState.sol.load(std::string_view(*assetData), path, sol::load_mode::text); if (!result.valid()) { sol::stack::push(luaState.sol.lua_state(), StrCat("Lua error when loading ", path, ": ", result.get())); return sol::stack_object(luaState.sol.lua_state(), -1); } const sol::function fn = result; luaState.compiledScripts[path] = fn.dump(); return result; } int LuaPrint(lua_State *state) { const int n = lua_gettop(state); for (int i = 1; i <= n; i++) { size_t l; const char *s = luaL_tolstring(state, i, &l); if (i > 1) printInConsole("\t"); printInConsole(std::string_view(s, l)); lua_pop(state, 1); } printNewlineInConsole(); return 0; } void LuaWarn(void *userData, const char *message, int continued) { static std::string warnBuffer; warnBuffer.append(message); if (continued != 0) return; LogWarn("{}", warnBuffer); warnBuffer.clear(); } sol::object RunScript(std::optional env, std::string_view packageName, bool optional) { const sol::object result = LuaLoadScriptFromAssets(packageName); // We return a string on error: if (result.get_type() == sol::type::string) { if (!optional) app_fatal(result.as()); LogInfo("{}", result.as()); return sol::lua_nil; } auto fn = result.as(); if (env.has_value()) { sol::set_environment(*env, fn); } return SafeCallResult(fn(), optional); } void LuaPanic(sol::optional message) { LogError("Lua is in a panic state and will now abort() the application:\n{}", message.value_or("unknown error")); } } // namespace void Sol2DebugPrintStack(lua_State *state) { LogDebug("{}", sol::detail::debug::dump_types(state)); } void Sol2DebugPrintSection(const std::string &message, lua_State *state) { LogDebug("-- {} -- [ {} ]", message, sol::detail::debug::dump_types(state)); } sol::environment CreateLuaSandbox() { sol::state &lua = CurrentLuaState->sol; sol::environment sandbox(CurrentLuaState->sol, sol::create); // Registering globals sandbox.set( "print", LuaPrint, "_DEBUG", #ifdef _DEBUG true, #else false, #endif "_VERSION", LUA_VERSION); // Register safe built-in globals. for (const std::string_view global : { // Built-ins: "assert", "warn", "error", "ipairs", "next", "pairs", "pcall", "select", "tonumber", "tostring", "type", "xpcall", "rawequal", "rawget", "rawset", "setmetatable", // Built-in packages: #ifdef _DEBUG "debug", #endif "base", "coroutine", "table", "string", "math", "utf8" }) { const sol::object obj = lua[global]; if (obj.get_type() == sol::type::lua_nil) { app_fatal(StrCat("Missing Lua global [", global, "]")); } sandbox[global] = obj; } // We only allow datetime-related functions from `os`: const sol::table os = lua["os"]; sandbox.create_named("os", "date", os["date"], "difftime", os["difftime"], "time", os["time"]); sandbox["require"] = lua["requireGen"](sandbox, CurrentLuaState->commonPackages, LuaLoadScriptFromAssets); // Expose commonly used enums globally for mods sandbox["SfxID"] = lua["SfxID"]; return sandbox; } void AddModsChangedHandler(tl::function_ref callback) { IsModChangeHandlers.push_back(callback); } void LuaReloadActiveMods() { // Loaded without a sandbox. CurrentLuaState->events = RunScript(/*env=*/std::nullopt, "devilutionx.events", /*optional=*/false); CurrentLuaState->commonPackages["devilutionx.events"] = CurrentLuaState->events; gbIsHellfire = false; UnloadModArchives(); std::vector modnames = GetOptions().Mods.GetActiveModList(); LoadModArchives(modnames); for (const std::string_view modname : modnames) { const std::string packageName = StrCat("mods.", modname, ".init"); RunScript(CreateLuaSandbox(), packageName, /*optional=*/true); } for (const tl::function_ref handler : IsModChangeHandlers) { handler(); } // Reload sound effects in case a mod archive overrides effects.tsv effects_cleanup_sfx(); if (gbRunGame) sound_init(); else ui_sound_init(); // Reload game data (this can probably be done later in the process to avoid having to reload it) LoadTextData(); LoadPlayerDataFiles(); LoadSpellData(); LoadMissileData(); LoadMonsterData(); LoadItemData(); LoadObjectData(); LoadQuestData(); lua::LoadModsComplete(); } void LuaInitialize() { CurrentLuaState.emplace(LuaState { .sol = { sol::c_call } }); sol::state &lua = CurrentLuaState->sol; lua_setwarnf(lua.lua_state(), LuaWarn, /*ud=*/nullptr); lua.open_libraries( sol::lib::base, sol::lib::coroutine, sol::lib::debug, sol::lib::math, sol::lib::os, sol::lib::package, sol::lib::string, sol::lib::table, sol::lib::utf8); // Registering devilutionx object table SafeCallResult(lua.safe_script(RequireGenSrc), /*optional=*/false); CurrentLuaState->commonPackages = lua.create_table_with( #ifdef _DEBUG "devilutionx.dev", LuaDevModule(lua), #endif "devilutionx.version", PROJECT_VERSION, "devilutionx.i18n", LuaI18nModule(lua), "devilutionx.items", LuaItemModule(lua), "devilutionx.log", LuaLogModule(lua), "devilutionx.audio", LuaAudioModule(lua), "devilutionx.monsters", LuaMonstersModule(lua), "devilutionx.player", LuaPlayerModule(lua), "devilutionx.render", LuaRenderModule(lua), "devilutionx.towners", LuaTownersModule(lua), "devilutionx.hellfire", LuaHellfireModule(lua), "devilutionx.system", LuaSystemModule(lua), "devilutionx.floatingnumbers", LuaFloatingNumbersModule(lua), "devilutionx.message", [](std::string_view text) { EventPlrMsg(text, UiFlags::ColorRed); }, // This package is loaded without a sandbox: "inspect", RunScript(/*env=*/std::nullopt, "inspect", /*optional=*/false)); // Used by the custom require implementation. lua["setEnvironment"] = [](const sol::environment &env, const sol::function &fn) { sol::set_environment(env, fn); }; for (OptionEntryBase *mod : GetOptions().Mods.GetEntries()) { mod->SetValueChangedCallback(LuaReloadActiveMods); } LuaReloadActiveMods(); } void LuaShutdown() { #ifdef _DEBUG LuaReplShutdown(); #endif CurrentLuaState = std::nullopt; } sol::table *GetLuaEvents() { return CurrentLuaState ? &CurrentLuaState->events : nullptr; } sol::state &GetLuaState() { return CurrentLuaState->sol; } sol::object SafeCallResult(sol::protected_function_result result, bool optional) { const bool valid = result.valid(); if (!valid) { const std::string error = result.get_type() == sol::type::string ? StrCat("Lua error: ", result.get()) : "Unknown Lua error"; if (!optional) app_fatal(error); LogError(error); } return result; } } // namespace devilution ================================================ FILE: Source/lua/lua_global.hpp ================================================ #pragma once #include #include #include #include namespace devilution { void LuaInitialize(); void LuaReloadActiveMods(); void LuaShutdown(); sol::state &GetLuaState(); sol::environment CreateLuaSandbox(); sol::object SafeCallResult(sol::protected_function_result result, bool optional); sol::table *GetLuaEvents(); /** Adds a handler to be called when mods status changes after the initial startup. */ void AddModsChangedHandler(tl::function_ref callback); } // namespace devilution ================================================ FILE: Source/lua/metadoc.hpp ================================================ #pragma once #include #include #include #include #include #include "utils/str_cat.hpp" namespace devilution { enum class LuaUserdataMemberType : uint8_t { ReadonlyProperty, Property, MemberFunction, Constructor, }; inline std::string LuaSignatureKey(std::string_view key) { return StrCat("__sig_", key); } inline std::string LuaDocstringKey(std::string_view key) { return StrCat("__doc_", key); } inline std::string LuaUserdataMemberTypeKey(std::string_view key) { return StrCat("__udt_", key); } namespace lua_metadoc_internal { template void SetUsertypeSignatureAndDocstring(sol::usertype &table, std::string_view key, const char *signature, const char *doc, LuaUserdataMemberType memberType) { table.set(LuaSignatureKey(key), sol::var(signature)); table.set(LuaDocstringKey(key), sol::var(doc)); table.set(LuaUserdataMemberTypeKey(key), sol::var(static_cast(memberType))); } inline void SetSignatureAndDocstring(sol::table &table, std::string_view key, const char *signature, const char *doc) { table.set(LuaSignatureKey(key), signature); table.set(LuaDocstringKey(key), doc); } } // namespace lua_metadoc_internal template void LuaSetDocFn(sol::usertype &table, std::string_view key, const char *signature, const char *doc, T &&value) { table.set_function(key, std::forward(value)); lua_metadoc_internal::SetUsertypeSignatureAndDocstring(table, key, signature, doc, LuaUserdataMemberType::MemberFunction); } template void LuaSetDocReadonlyProperty(sol::usertype &table, std::string_view key, const char *type, const char *doc, G &&getter) { table.set(key, sol::readonly_property(std::forward(getter))); lua_metadoc_internal::SetUsertypeSignatureAndDocstring(table, key, type, doc, LuaUserdataMemberType::ReadonlyProperty); } template void LuaSetDocProperty(sol::usertype &table, std::string_view key, const char *type, const char *doc, G &&getter, S &&setter) { table.set(key, sol::property(std::forward(getter), std::forward(setter))); lua_metadoc_internal::SetUsertypeSignatureAndDocstring(table, key, type, doc, LuaUserdataMemberType::Property); } template void LuaSetDocProperty(sol::usertype &table, std::string_view key, const char *type, const char *doc, F U::*&&value) { table.set(key, value); lua_metadoc_internal::SetUsertypeSignatureAndDocstring(table, key, type, doc, LuaUserdataMemberType::Property); } template void LuaSetDoc(sol::table &table, std::string_view key, const char *signature, const char *doc, T &&value) { table.set(key, std::forward(value)); lua_metadoc_internal::SetSignatureAndDocstring(table, key, signature, doc); } template void LuaSetDocFn(sol::table &table, std::string_view key, const char *signature, const char *doc, T &&value) { table.set_function(key, std::forward(value)); lua_metadoc_internal::SetSignatureAndDocstring(table, key, signature, doc); } template void LuaSetDocFn(sol::table &table, std::string_view key, const char *signature, T &&value) { table.set_function(key, std::forward(value)); table.set(LuaSignatureKey(key), signature); } inline std::optional GetSignature(const sol::table &table, std::string_view key) { return table.get>(LuaSignatureKey(key)); } inline std::optional GetDocstring(const sol::table &table, std::string_view key) { return table.get>(LuaDocstringKey(key)); } inline std::optional GetLuaUserdataSignature(const sol::userdata &obj, std::string_view key) { return obj.get>(LuaSignatureKey(key)); } inline std::optional GetLuaUserdataDocstring(const sol::userdata &obj, std::string_view key) { return obj.get>(LuaDocstringKey(key)); } inline std::optional GetLuaUserdataMemberType(const sol::userdata &obj, std::string_view key, const sol::object &value) { std::optional result = obj.get>(LuaUserdataMemberTypeKey(key)); if (!result.has_value()) { if (value.get_type() == sol::type::userdata) return LuaUserdataMemberType::Property; if (value.get_type() == sol::type::function && key == "new") return LuaUserdataMemberType::Constructor; return std::nullopt; } return static_cast(*result); } } // namespace devilution ================================================ FILE: Source/lua/modules/audio.cpp ================================================ #include "lua/modules/audio.hpp" #include #include #include "effects.h" #include "lua/metadoc.hpp" #include "sound_effect_enums.h" namespace devilution { namespace { bool IsValidSfx(int16_t psfx) { return psfx >= 0 && psfx <= static_cast(SfxID::LAST); } void RegisterSfxIDEnum(sol::state_view &lua) { constexpr auto enumValues = magic_enum::enum_values(); sol::table enumTable = lua.create_table(); for (const auto enumValue : enumValues) { const std::string_view name = magic_enum::enum_name(enumValue); if (!name.empty() && name != "LAST" && name != "None") { enumTable[name] = static_cast(enumValue); } } // Add LAST and None explicitly enumTable["LAST"] = static_cast(SfxID::LAST); enumTable["None"] = static_cast(SfxID::None); lua["SfxID"] = enumTable; } } // namespace sol::table LuaAudioModule(sol::state_view &lua) { RegisterSfxIDEnum(lua); sol::table table = lua.create_table(); LuaSetDocFn(table, "playSfx", "(id: number)", [](int16_t psfx) { if (IsValidSfx(psfx)) PlaySFX(static_cast(psfx)); }); LuaSetDocFn(table, "playSfxLoc", "(id: number, x: number, y: number)", [](int16_t psfx, int x, int y) { if (IsValidSfx(psfx)) PlaySfxLoc(static_cast(psfx), { x, y }); }); // Expose SfxID enum through the module table table["SfxID"] = lua["SfxID"]; return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/audio.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaAudioModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/dev/display.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/display.hpp" #include #include #include #include #include "debug.h" #include "lighting.h" #include "lua/metadoc.hpp" #include "player.h" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdShowGrid(std::optional on) { DebugGrid = on.value_or(!DebugGrid); return StrCat("Tile grid highlighting: ", DebugGrid ? "On" : "Off"); } std::string DebugCmdVision(std::optional on) { DebugVision = on.value_or(!DebugVision); return StrCat("Vision highlighting: ", DebugVision ? "On" : "Off"); } std::string DebugCmdPath(std::optional on) { DebugPath = on.value_or(!DebugPath); return StrCat("Path highlighting: ", DebugPath ? "On" : "Off"); } std::string DebugCmdFullbright(std::optional on) { ToggleLighting(); return StrCat("Fullbright: ", DisableLighting ? "On" : "Off"); } std::string DebugCmdShowTileData(std::optional dataType) { static const std::array DataTypes { "microTiles", "dPiece", "dTransVal", "dLight", "dPreLight", "dFlags", "dPlayer", "dMonster", "missiles", "dCorpse", "dObject", "dItem", "dSpecial", "coords", "cursorcoords", "objectindex", "solid", "transparent", "trap", "AutomapView", "dungeon", "pdungeon", "Protected", }; if (!dataType.has_value()) { std::string result = "Valid values for the first argument:\nclear"; for (const std::string_view &str : DataTypes) StrAppend(result, ", ", str); return result; } if (*dataType == "clear") { SetDebugGridTextType(DebugGridTextItem::None); return "Tile data cleared."; } bool found = false; int index = 0; for (const std::string_view ¶m : DataTypes) { index++; if (*dataType != param) continue; found = true; auto newGridText = static_cast(index); if (newGridText == GetDebugGridTextType()) { SetDebugGridTextType(DebugGridTextItem::None); return "Tile data: Off"; } SetDebugGridTextType(newGridText); break; } if (!found) { std::string result = "Invalid name! Valid names are:\nclear"; for (const std::string_view &str : DataTypes) StrAppend(result, ", ", str); return result; } return "Tile data: On"; } std::string DebugCmdScrollView(std::optional on) { DebugScrollViewEnabled = on.value_or(!DebugScrollViewEnabled); if (!DebugScrollViewEnabled) InitMultiView(); return StrCat("Scroll view: ", DebugScrollViewEnabled ? "On" : "Off"); } std::string DebugCmdToggleFPS(std::optional on) { frameflag = on.value_or(!frameflag); return StrCat("FPS counter: ", frameflag ? "On" : "Off"); } } // namespace sol::table LuaDevDisplayModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "fps", "(name: string = nil)", "Toggle FPS display.", &DebugCmdToggleFPS); LuaSetDocFn(table, "fullbright", "(on: boolean = nil)", "Toggle light shading.", &DebugCmdFullbright); LuaSetDocFn(table, "grid", "(on: boolean = nil)", "Toggle showing the grid.", &DebugCmdShowGrid); LuaSetDocFn(table, "path", "(on: boolean = nil)", "Toggle path debug rendering.", &DebugCmdPath); LuaSetDocFn(table, "scrollView", "(on: boolean = nil)", "Toggle view scrolling via Shift+Mouse.", &DebugCmdScrollView); LuaSetDocFn(table, "tileData", "(name: string = nil)", "Toggle showing tile data.", &DebugCmdShowTileData); LuaSetDocFn(table, "vision", "(on: boolean = nil)", "Toggle vision debug rendering.", &DebugCmdVision); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/display.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevDisplayModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/items.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/items.hpp" #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "cursor.h" #include "engine/random.hpp" #include "items.h" #include "lua/metadoc.hpp" #include "pack.h" #include "player.h" #include "utils/is_of.hpp" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { const Item *DebugCmdGetItem() { const Player &myPlayer = *MyPlayer; if (!myPlayer.HoldItem.isEmpty()) return &myPlayer.HoldItem; if (pcursinvitem != -1) { if (pcursinvitem < INVITEM_INV_FIRST) return &myPlayer.InvBody[pcursinvitem]; if (pcursinvitem <= INVITEM_INV_LAST) return &myPlayer.InvList[pcursinvitem - INVITEM_INV_FIRST]; return &myPlayer.SpdList[pcursinvitem - INVITEM_BELT_FIRST]; } if (pcursitem != -1) return &Items[pcursitem]; return nullptr; } std::string DebugCmdItemInfo() { const Item *pItem = DebugCmdGetItem(); if (pItem != nullptr) { const Player &myPlayer = *MyPlayer; std::string_view netPackValidation { "N/A" }; if (gbIsMultiplayer) { ItemNetPack itemPack; Item unpacked; PackNetItem(*pItem, itemPack); netPackValidation = UnPackNetItem(myPlayer, itemPack, unpacked) ? "Success" : "Failure"; } return StrCat("Name: ", pItem->_iIName, "\nIDidx: ", pItem->IDidx, " (", AllItemsList[pItem->IDidx].iName, ")", "\nSeed: ", pItem->_iSeed, "\nCreateInfo: ", pItem->_iCreateInfo, "\nLevel: ", pItem->_iCreateInfo & CF_LEVEL, "\nOnly Good: ", ((pItem->_iCreateInfo & CF_ONLYGOOD) == 0) ? "False" : "True", "\nUnique Monster: ", ((pItem->_iCreateInfo & CF_UPER15) == 0) ? "False" : "True", "\nDungeon Item: ", ((pItem->_iCreateInfo & CF_UPER1) == 0) ? "False" : "True", "\nUnique Item: ", ((pItem->_iCreateInfo & CF_UNIQUE) == 0) ? "False" : "True", "\nSmith: ", ((pItem->_iCreateInfo & CF_SMITH) == 0) ? "False" : "True", "\nSmith Premium: ", ((pItem->_iCreateInfo & CF_SMITHPREMIUM) == 0) ? "False" : "True", "\nBoy: ", ((pItem->_iCreateInfo & CF_BOY) == 0) ? "False" : "True", "\nWitch: ", ((pItem->_iCreateInfo & CF_WITCH) == 0) ? "False" : "True", "\nHealer: ", ((pItem->_iCreateInfo & CF_HEALER) == 0) ? "False" : "True", "\nPregen: ", ((pItem->_iCreateInfo & CF_PREGEN) == 0) ? "False" : "True", "\nNet Validation: ", netPackValidation); } return StrCat("Num items: ", ActiveItemCount); } std::mt19937 BetterRng; std::string DebugSpawnItem(std::string itemName) { if (ActiveItemCount >= MAXITEMS) return "No space to generate the item!"; const int max_time = 3000; const int max_iter = 1000000; AsciiStrToLower(itemName); Item testItem; uint32_t begin = SDL_GetTicks(); int i = 0; for (;; i++) { // using a better rng here to seed the item to prevent getting stuck repeating same values using old one std::uniform_int_distribution dist(0, INT_MAX); SetRndSeed(dist(BetterRng)); if (SDL_GetTicks() - begin > max_time) return StrCat("Item not found in ", max_time / 1000, " seconds!"); if (i > max_iter) return StrCat("Item not found in ", max_iter, " tries!"); const int8_t monsterLevel = dist(BetterRng) % CF_LEVEL + 1; _item_indexes idx = RndItemForMonsterLevel(monsterLevel); if (IsAnyOf(idx, IDI_NONE, IDI_GOLD)) continue; testItem = {}; SetupAllItems(*MyPlayer, testItem, idx, AdvanceRndSeed(), monsterLevel, 1, false, false); TryRandomUniqueItem(testItem, idx, monsterLevel, 1, false, false); SetupItem(testItem); std::string tmp = AsciiStrToLower(testItem._iIName); if (tmp.find(itemName) != std::string::npos) break; } int ii = AllocateItem(); auto &item = Items[ii]; item = testItem.pop(); item._iIdentified = true; Point pos = MyPlayer->position.tile; GetSuperItemSpace(pos, ii); NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); return StrCat("Item generated successfully - iterations: ", i); } std::string DebugSpawnUniqueItem(std::string itemName) { if (ActiveItemCount >= MAXITEMS) return "No space to generate the item!"; AsciiStrToLower(itemName); UniqueItem uniqueItem; bool foundUnique = false; int uniqueIndex = 0; for (const auto &item : UniqueItems) { const std::string tmp = AsciiStrToLower(std::string_view(item.UIName)); if (tmp.find(itemName) != std::string::npos) { itemName = tmp; uniqueItem = item; foundUnique = true; break; } ++uniqueIndex; } if (!foundUnique) return "No unique item found!"; _item_indexes uniqueBaseIndex = IDI_GOLD; for (size_t j = 0; j < AllItemsList.size(); j++) { if (!IsItemAvailable(static_cast(j))) continue; if (AllItemsList[j].iItemId == uniqueItem.UIItemId) { uniqueBaseIndex = static_cast<_item_indexes>(j); break; } } if (uniqueBaseIndex == IDI_GOLD) return "Base item not available!"; auto &baseItemData = AllItemsList[static_cast(uniqueBaseIndex)]; Item testItem; int i = 0; for (uint32_t begin = SDL_GetTicks();; i++) { constexpr int max_time = 3000; if (SDL_GetTicks() - begin > max_time) return StrCat("Item not found in ", max_time / 1000, " seconds!"); constexpr int max_iter = 1000000; if (i > max_iter) return StrCat("Item not found in ", max_iter, " tries!"); testItem = {}; testItem._iMiscId = baseItemData.iMiscId; std::uniform_int_distribution dist(0, INT_MAX); SetRndSeed(dist(BetterRng)); for (auto &flag : UniqueItemFlags) flag = true; UniqueItemFlags[uniqueIndex] = false; SetupAllItems(*MyPlayer, testItem, uniqueBaseIndex, testItem._iMiscId == IMISC_UNIQUE ? uniqueIndex : AdvanceRndSeed(), uniqueItem.UIMinLvl, 1, false, false); TryRandomUniqueItem(testItem, uniqueBaseIndex, uniqueItem.UIMinLvl, 1, false, false); SetupItem(testItem); for (auto &flag : UniqueItemFlags) flag = false; if (testItem._iMagical != ITEM_QUALITY_UNIQUE) continue; const std::string tmp = AsciiStrToLower(testItem._iIName); if (tmp.find(itemName) != std::string::npos) break; } int ii = AllocateItem(); auto &item = Items[ii]; item = testItem.pop(); Point pos = MyPlayer->position.tile; GetSuperItemSpace(pos, ii); item._iIdentified = true; NetSendCmdPItem(false, CMD_SPAWNITEM, item.position, item); return StrCat("Item generated successfully - iterations: ", i); } } // namespace sol::table LuaDevItemsModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "get", "() -> Item", "Get the currently selected item.", &DebugCmdGetItem); LuaSetDocFn(table, "info", "()", "Show info of currently selected item.", &DebugCmdItemInfo); LuaSetDocFn(table, "spawn", "(name: string)", "Attempt to generate an item.", &DebugSpawnItem); LuaSetDocFn(table, "spawnUnique", "(name: string)", "Attempt to generate a unique item.", &DebugSpawnUniqueItem); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/items.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevItemsModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level/map.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/level/map.hpp" #include #include #include "automap.h" #include "lua/metadoc.hpp" namespace devilution { namespace { std::string DebugCmdMapReveal() { for (int x = 0; x < DMAXX; x++) for (int y = 0; y < DMAXY; y++) UpdateAutomapExplorer({ x, y }, MAP_EXP_SHRINE); return "Automap fully explored."; } std::string DebugCmdMapHide() { for (int x = 0; x < DMAXX; x++) for (int y = 0; y < DMAXY; y++) AutomapView[x][y] = MAP_EXP_NONE; return "Automap exploration removed."; } } // namespace sol::table LuaDevLevelMapModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "hide", "()", "Hide the map.", &DebugCmdMapHide); LuaSetDocFn(table, "reveal", "()", "Reveal the map.", &DebugCmdMapReveal); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level/map.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevLevelMapModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level/warp.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/level/warp.hpp" #include #include #include #include #include "debug.h" #include "interfac.h" #include "levels/setmaps.h" #include "lua/metadoc.hpp" #include "player.h" #include "quests.h" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdWarpToDungeonLevel(uint8_t level) { Player &myPlayer = *MyPlayer; if (level > (gbIsHellfire ? 24 : 16)) return StrCat("Level ", level, " does not exist!"); if (!setlevel && myPlayer.isOnLevel(level)) return StrCat("You are already on level ", level, "!"); StartNewLvl(myPlayer, (level != 21) ? interface_mode::WM_DIABNEXTLVL : interface_mode::WM_DIABTOWNWARP, level); return StrCat("Moved you to level ", level, "."); } std::string DebugCmdWarpToQuestLevel(uint8_t level) { if (level < 1) return StrCat("Quest level number must be 1 or higher!"); if (setlevel && setlvlnum == level) return StrCat("You are already on quest level", level, "!"); for (Quest &quest : Quests) { if (level != quest._qslvl) continue; setlvltype = quest._qlvltype; StartNewLvl(*MyPlayer, WM_DIABSETLVL, level); return StrCat("Moved you to quest level ", QuestLevelNames[level], "."); } return StrCat("Quest level ", level, " does not exist!"); } std::string DebugCmdWarpToCustomMap(std::string_view path, int dunType, int x, int y) { if (path.empty()) return "path is required"; if (dunType < DTYPE_CATHEDRAL || dunType > DTYPE_LAST) return "invalid dunType"; const Point spawn { x, y }; if (!InDungeonBounds(spawn)) return "spawn location is out of bounds"; TestMapPath = StrCat(path, ".dun"); setlvltype = static_cast(dunType); ViewPosition = spawn; StartNewLvl(*MyPlayer, WM_DIABSETLVL, SL_NONE); return StrCat("Moved you to ", TestMapPath, "."); } } // namespace sol::table LuaDevLevelWarpModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "dungeon", "(n: number)", "Go to dungeon level (0 for town).", &DebugCmdWarpToDungeonLevel); LuaSetDocFn(table, "map", "(path: string, dunType: number, x: number, y: number)", "Go to custom {path}.dun level", &DebugCmdWarpToCustomMap); LuaSetDocFn(table, "quest", "(n: number)", "Go to quest level.", &DebugCmdWarpToQuestLevel); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level/warp.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevLevelWarpModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/level.hpp" #include #include #include #include #include #include "diablo.h" #include "levels/gendung.h" #include "lua/metadoc.hpp" #include "lua/modules/dev/level/map.hpp" #include "lua/modules/dev/level/warp.hpp" #include "monster.h" #include "objects.h" #include "player.h" #include "utils/endian_stream.hpp" #include "utils/file_util.h" namespace devilution { namespace { std::string ExportDun() { const std::string levelName = StrCat(currlevel, "-", DungeonSeeds[currlevel], ".dun"); FILE *dunFile = OpenFile(levelName.c_str(), "ab"); WriteLE16(dunFile, DMAXX); WriteLE16(dunFile, DMAXY); /** Tiles. */ for (int y = 0; y < DMAXY; y++) { for (int x = 0; x < DMAXX; x++) { WriteLE16(dunFile, dungeon[x][y]); } } /** Padding */ for (int y = 16; y < MAXDUNY - 16; y++) { for (int x = 16; x < MAXDUNX - 16; x++) { WriteLE16(dunFile, 0); } } /** Monsters */ for (int y = 16; y < MAXDUNY - 16; y++) { for (int x = 16; x < MAXDUNX - 16; x++) { uint16_t monsterId = 0; if (dMonster[x][y] > 0) { for (int i = 0; i < 157; i++) { if (MonstConvTbl[i] == Monsters[std::abs(dMonster[x][y]) - 1].type().type) { monsterId = i + 1; break; } } } WriteLE16(dunFile, monsterId); } } /** Objects */ for (int y = 16; y < MAXDUNY - 16; y++) { for (int x = 16; x < MAXDUNX - 16; x++) { uint16_t objectId = 0; Object *object = FindObjectAtPosition({ x, y }, false); if (object != nullptr) { for (int i = 0; i < 147; i++) { if (ObjTypeConv[i] == object->_otype) { objectId = i; break; } } } WriteLE16(dunFile, objectId); } } /** Transparency */ for (int y = 16; y < MAXDUNY - 16; y++) { for (int x = 16; x < MAXDUNX - 16; x++) { WriteLE16(dunFile, dTransVal[x][y]); } } std::fclose(dunFile); return StrCat("Successfully exported ", levelName, "."); } std::string DebugCmdResetLevel(uint8_t level, std::optional seed) { Player &myPlayer = *MyPlayer; if (level > (gbIsHellfire ? 24 : 16)) return StrCat("Level ", level, " does not exist!"); if (myPlayer.isOnLevel(level)) return "Unable to reset dungeon levels occupied by players!"; myPlayer._pLvlVisited[level] = false; DeltaClearLevel(level); if (seed.has_value()) { DungeonSeeds[level] = *seed; LevelSeeds[level] = std::nullopt; return StrCat("Successfully reset level ", level, " with seed ", *seed, "."); } return StrCat("Successfully reset level ", level, "."); } std::string DebugCmdLevelSeed(std::optional level) { constexpr size_t NumLevels = sizeof(DungeonSeeds) / sizeof(DungeonSeeds[0]); if (level.has_value() && *level >= NumLevels) { return StrCat("level out of range, max: ", NumLevels - 1); } return StrCat(DungeonSeeds[level.value_or(currlevel)]); } } // namespace sol::table LuaDevLevelModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "exportDun", "()", "Save the current level as a dun-file.", &ExportDun); LuaSetDocFn(table, "map", "", "Automap-related commands.", LuaDevLevelMapModule(lua)); LuaSetDocFn(table, "reset", "(n: number, seed: number = nil)", "Resets specified level.", &DebugCmdResetLevel); LuaSetDocFn(table, "seed", "(level: number = nil)", "Get the seed of the current or given level.", &DebugCmdLevelSeed); LuaSetDocFn(table, "warp", "", "Warp to a level or a custom map.", LuaDevLevelWarpModule(lua)); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/level.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevLevelModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/monsters.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/monsters.hpp" #include #include #include #include "crawl.hpp" #include "levels/gendung.h" #include "levels/tile_properties.hpp" #include "lighting.h" #include "lua/metadoc.hpp" #include "monster.h" #include "player.h" #include "tables/monstdat.h" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdSpawnUniqueMonster(std::string name, std::optional countOpt) { if (leveltype == DTYPE_TOWN) return "Can't spawn monsters in town"; if (name.empty()) return "name is required"; const unsigned count = countOpt.value_or(1); if (count < 1) return "count must be positive"; AsciiStrToLower(name); int mtype = -1; UniqueMonsterType uniqueIndex = UniqueMonsterType::None; for (size_t i = 0; i < UniqueMonstersData.size(); ++i) { const auto &mondata = UniqueMonstersData[i]; const std::string monsterName = AsciiStrToLower(std::string_view(mondata.mName)); if (monsterName.find(name) == std::string::npos) continue; mtype = mondata.mtype; uniqueIndex = static_cast(i); if (monsterName == name) // to support partial name matching but always choose the correct monster if full name is given break; } if (mtype == -1) return "Monster not found"; size_t id = MaxLvlMTypes - 1; bool found = false; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (LevelMonsterTypes[i].type == mtype) { id = i; found = true; break; } } if (!found) { if (LevelMonsterTypeCount == MaxLvlMTypes) LevelMonsterTypeCount--; // we are running out of monster types, so override last used monster type tl::expected idResult = AddMonsterType(uniqueIndex, PLACE_SCATTER); if (!idResult.has_value()) return std::move(idResult).error(); id = idResult.value(); CMonster &monsterType = LevelMonsterTypes[id]; InitMonsterGFX(monsterType); monsterType.corpseId = 1; } Player &myPlayer = *MyPlayer; unsigned spawnedMonster = 0; auto ret = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional { Point pos = myPlayer.position.tile + displacement; if (dPlayer[pos.x][pos.y] != 0 || dMonster[pos.x][pos.y] != 0) return {}; if (!IsTileWalkable(pos)) return {}; Monster *monster = AddMonster(pos, myPlayer._pdir, id, true); if (monster == nullptr) return StrCat("Spawned ", spawnedMonster, " monsters. (Unable to spawn more)"); PrepareUniqueMonst(*monster, uniqueIndex, 0, 0, UniqueMonstersData[static_cast(uniqueIndex)]); monster->corpseId = 1; spawnedMonster += 1; if (spawnedMonster >= count) return StrCat("Spawned ", spawnedMonster, " monsters."); return {}; }); if (!ret.has_value()) ret = StrCat("Spawned ", spawnedMonster, " monsters. (Unable to spawn more)"); return *ret; } std::string DebugCmdSpawnMonster(std::string name, std::optional countOpt) { if (leveltype == DTYPE_TOWN) return "Can't spawn monsters in town"; if (name.empty()) return "name is required"; const unsigned count = countOpt.value_or(1); if (count < 1) return "count must be positive"; AsciiStrToLower(name); int mtype = -1; for (size_t i = 0; i < MonstersData.size(); i++) { const auto &mondata = MonstersData[i]; const std::string monsterName = AsciiStrToLower(std::string_view(mondata.name)); if (monsterName.find(name) == std::string::npos) continue; mtype = static_cast(i); if (monsterName == name) // to support partial name matching but always choose the correct monster if full name is given break; } if (mtype == -1) return "Monster not found"; if (!MyPlayer->isLevelOwnedByLocalClient()) return "You are not the level owner."; size_t id = MaxLvlMTypes - 1; bool found = false; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (LevelMonsterTypes[i].type == mtype) { id = i; found = true; break; } } if (!found) { if (LevelMonsterTypeCount == MaxLvlMTypes) LevelMonsterTypeCount--; // we are running out of monster types, so override last used monster type tl::expected idResult = AddMonsterType(static_cast<_monster_id>(mtype), PLACE_SCATTER); if (!idResult.has_value()) return std::move(idResult).error(); id = idResult.value(); CMonster &monsterType = LevelMonsterTypes[id]; InitMonsterGFX(monsterType); monsterType.corpseId = 1; } Player &myPlayer = *MyPlayer; size_t monstersToSpawn = std::min(MaxMonsters - ActiveMonsterCount, count); if (monstersToSpawn == 0) return "Can't spawn any monsters"; size_t spawnedMonster = 0; Crawl(0, MaxCrawlRadius, [&](Displacement displacement) { Point pos = myPlayer.position.tile + displacement; if (dPlayer[pos.x][pos.y] != 0 || dMonster[pos.x][pos.y] != 0) return false; if (!IsTileWalkable(pos)) return false; SpawnMonster(pos, myPlayer._pdir, id); spawnedMonster += 1; return spawnedMonster == monstersToSpawn; }); if (monstersToSpawn != count) return StrCat("Spawned ", spawnedMonster, " monsters. (Unable to spawn more)"); return StrCat("Spawned ", spawnedMonster, " monsters."); } } // namespace sol::table LuaDevMonstersModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "spawn", "(name: string, count: number = 1)", "Spawn monster(s)", &DebugCmdSpawnMonster); LuaSetDocFn(table, "spawnUnique", "(name: string, count: number = 1)", "Spawn unique monster(s)", &DebugCmdSpawnUniqueMonster); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/monsters.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevMonstersModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/gold.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/player/gold.hpp" #include #include #include #include #include "items.h" #include "lua/metadoc.hpp" #include "player.h" namespace devilution { namespace { std::string DebugCmdGiveGoldCheat(std::optional amount) { int goldToAdd = amount.value_or(GOLD_MAX_LIMIT * InventoryGridCells); if (goldToAdd <= 0) return "amount must be positive"; Player &myPlayer = *MyPlayer; const int goldAmountBefore = myPlayer._pGold; for (int8_t &itemIndex : myPlayer.InvGrid) { if (itemIndex < 0) continue; Item &item = myPlayer.InvList[itemIndex != 0 ? itemIndex - 1 : myPlayer._pNumInv]; if (itemIndex != 0) { if ((!item.isGold() && !item.isEmpty()) || (item.isGold() && item._ivalue == GOLD_MAX_LIMIT)) continue; } else { if (item.isEmpty()) { MakeGoldStack(item, 0); myPlayer._pNumInv++; itemIndex = myPlayer._pNumInv; } } int goldThatCanBeAdded = (GOLD_MAX_LIMIT - item._ivalue); if (goldThatCanBeAdded >= goldToAdd) { item._ivalue += goldToAdd; myPlayer._pGold += goldToAdd; break; } item._ivalue += goldThatCanBeAdded; goldToAdd -= goldThatCanBeAdded; myPlayer._pGold += goldThatCanBeAdded; } CalcPlrInv(myPlayer, true); return StrCat("Set your gold to ", myPlayer._pGold, ", added ", myPlayer._pGold - goldAmountBefore, "."); } std::string DebugCmdTakeGoldCheat(std::optional amount) { Player &myPlayer = *MyPlayer; int goldToRemove = amount.value_or(GOLD_MAX_LIMIT * InventoryGridCells); if (goldToRemove <= 0) return "amount must be positive"; const int goldAmountBefore = myPlayer._pGold; for (auto itemIndex : myPlayer.InvGrid) { itemIndex -= 1; if (itemIndex < 0) continue; Item &item = myPlayer.InvList[itemIndex]; if (!item.isGold()) continue; if (item._ivalue >= goldToRemove) { myPlayer._pGold -= goldToRemove; item._ivalue -= goldToRemove; if (item._ivalue == 0) myPlayer.RemoveInvItem(itemIndex); break; } myPlayer._pGold -= item._ivalue; goldToRemove -= item._ivalue; myPlayer.RemoveInvItem(itemIndex); } return StrCat("Set your gold to ", myPlayer._pGold, ", removed ", goldAmountBefore - myPlayer._pGold, "."); } } // namespace sol::table LuaDevPlayerGoldModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "give", "(amount: number = MAX)", "Gives the player gold.", &DebugCmdGiveGoldCheat); LuaSetDocFn(table, "take", "(amount: number = MAX)", "Takes the player's gold away.", &DebugCmdTakeGoldCheat); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/gold.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevPlayerGoldModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/spells.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/player/spells.hpp" #include #include #include #include "lua/metadoc.hpp" #include "msg.h" #include "spells.h" #include "tables/spelldat.h" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdSetSpellsLevel(uint8_t level) { for (uint8_t i = static_cast(SpellID::Firebolt); i < SpellsData.size(); i++) { if (GetSpellBookLevel(static_cast(i)) != -1) { NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, i, level); } } if (level == 0) MyPlayer->_pMemSpells = 0; return StrCat("Set all spell levels to ", level); } } // namespace sol::table LuaDevPlayerSpellsModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "setLevel", "(level: number)", "Set spell level for all spells.", &DebugCmdSetSpellsLevel); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/spells.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevPlayerSpellsModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/stats.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/player/stats.hpp" #include #include #include "engine/backbuffer_state.hpp" #include "lua/metadoc.hpp" #include "player.h" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdLevelUp(std::optional levels) { const int levelsToAdd = levels.value_or(1); if (levelsToAdd <= 0) return "amount must be positive"; Player &myPlayer = *MyPlayer; for (int i = 0; i < levelsToAdd; i++) NetSendCmd(true, CMD_CHEAT_EXPERIENCE); return StrCat("New character level: ", myPlayer.getCharacterLevel() + levelsToAdd); } std::string DebugCmdMaxStats() { Player &myPlayer = *MyPlayer; ModifyPlrStr(myPlayer, myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength) - myPlayer._pBaseStr); ModifyPlrMag(myPlayer, myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic) - myPlayer._pBaseMag); ModifyPlrDex(myPlayer, myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity) - myPlayer._pBaseDex); ModifyPlrVit(myPlayer, myPlayer.GetMaximumAttributeValue(CharacterAttribute::Vitality) - myPlayer._pBaseVit); return "Set all character base attributes to maximum."; } std::string DebugCmdMinStats() { Player &myPlayer = *MyPlayer; ModifyPlrStr(myPlayer, -myPlayer._pBaseStr); ModifyPlrMag(myPlayer, -myPlayer._pBaseMag); ModifyPlrDex(myPlayer, -myPlayer._pBaseDex); ModifyPlrVit(myPlayer, -myPlayer._pBaseVit); return "Set all character base attributes to minimum."; } std::string DebugCmdRefillHealthMana() { Player &myPlayer = *MyPlayer; myPlayer.RestoreFullLife(); myPlayer.RestoreFullMana(); RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); return StrCat("Restored life and mana to full."); } std::string DebugCmdChangeHealth(int change) { Player &myPlayer = *MyPlayer; if (change == 0) return StrCat("Enter a value not equal to 0 to change life!"); int newHealth = myPlayer._pHitPoints + (change * 64); SetPlayerHitPoints(myPlayer, newHealth); if (newHealth <= 0) SyncPlrKill(myPlayer, DeathReason::MonsterOrTrap); return StrCat("Changed life by ", change); } std::string DebugCmdChangeMana(int change) { Player &myPlayer = *MyPlayer; if (change == 0) return StrCat("Enter a value not equal to 0 to change mana!"); int newMana = myPlayer._pMana + (change * 64); myPlayer._pMana = newMana; myPlayer._pManaBase = myPlayer._pMana + myPlayer._pMaxManaBase - myPlayer._pMaxMana; RedrawComponent(PanelDrawComponent::Mana); return StrCat("Changed mana by ", change); } } // namespace sol::table LuaDevPlayerStatsModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "adjustHealth", "(amount: number)", "Adjust HP (amount can be negative)", &DebugCmdChangeHealth); LuaSetDocFn(table, "adjustMana", "(amount: number)", "Adjust mana (amount can be negative)", &DebugCmdChangeMana); LuaSetDocFn(table, "levelUp", "(amount: number = 1)", "Level the player up.", &DebugCmdLevelUp); LuaSetDocFn(table, "rejuvenate", "()", "Refill health", &DebugCmdRefillHealthMana); LuaSetDocFn(table, "setAttrToMax", "()", "Set Str, Mag, Dex, and Vit to maximum.", &DebugCmdMaxStats); LuaSetDocFn(table, "setAttrToMin", "()", "Set Str, Mag, Dex, and Vit to minimum.", &DebugCmdMinStats); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player/stats.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevPlayerStatsModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/player.hpp" #include #include #include #include "debug.h" #include "engine/assets.hpp" #include "lua/metadoc.hpp" #include "lua/modules/dev/player/gold.hpp" #include "lua/modules/dev/player/spells.hpp" #include "lua/modules/dev/player/stats.hpp" #include "player.h" namespace devilution { namespace { std::string DebugCmdArrow(std::string_view effect) { Player &myPlayer = *MyPlayer; myPlayer._pIFlags &= ~ItemSpecialEffect::FireArrows; myPlayer._pIFlags &= ~ItemSpecialEffect::LightningArrows; if (effect == "normal") { // we removed the parameter at the top } else if (effect == "fire") { myPlayer._pIFlags |= ItemSpecialEffect::FireArrows; } else if (effect == "lightning") { myPlayer._pIFlags |= ItemSpecialEffect::LightningArrows; } else if (effect == "spectral") { myPlayer._pIFlags |= (ItemSpecialEffect::FireArrows | ItemSpecialEffect::LightningArrows); } else { return "Invalid effect!"; } return StrCat("Arrows changed to: ", effect); } std::string DebugCmdGodMode(std::optional on) { DebugGodMode = on.value_or(!DebugGodMode); return StrCat("God mode: ", DebugGodMode ? "On" : "Off"); } std::string DebugCmdPlayerInfo(std::optional id) { const uint8_t playerId = id.value_or(0); if (playerId >= Players.size()) return StrCat("Invalid player ID (max: ", Players.size() - 1, ")"); Player &player = Players[playerId]; if (!player.plractive) return StrCat("Player ", playerId, " is not active!"); const Point target = player.GetTargetPosition(); return StrCat("Plr ", playerId, " is ", player._pName, "\nLvl: ", player.plrlevel, " Changing: ", player._pLvlChanging, "\nTile.x: ", player.position.tile.x, " Tile.y: ", player.position.tile.y, " Target.x: ", target.x, " Target.y: ", target.y, "\nMode: ", player._pmode, " destAction: ", player.destAction, " walkpath[0]: ", player.walkpath[0], "\nInvincible: ", player._pInvincible ? 1 : 0, " HitPoints: ", player._pHitPoints); } std::string DebugSetPlayerTrn(std::string_view path) { if (!path.empty()) { if (const AssetRef ref = FindAsset(path); !ref.ok()) { const char *error = ref.error(); return error == nullptr || *error == '\0' ? StrCat("File not found: ", path) : error; } } debugTRN = path; Player &player = *MyPlayer; InitPlayerGFX(player); StartStand(player, player._pdir); return path.empty() ? "TRN unset" : "TRN set"; } sol::table LuaDevPlayerTrnModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "mon", "(name: string)", "Set player TRN to monsters\\${name}.trn", [](std::string_view name) { return DebugSetPlayerTrn(StrCat("monsters\\", name, ".trn")); }); LuaSetDocFn(table, "plr", "(name: string)", "Set player TRN to plrgfx\\${name}.trn", [](std::string_view name) { return DebugSetPlayerTrn(StrCat("plrgfx\\", name, ".trn")); }); LuaSetDocFn(table, "clear", "()", "Unset player TRN", []() { return DebugSetPlayerTrn(""); }); return table; } std::string DebugCmdInvisible(std::optional on) { DebugInvisible = on.value_or(!DebugInvisible); return StrCat("Invisible: ", DebugInvisible ? "On" : "Off"); } } // namespace sol::table LuaDevPlayerModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "arrow", "(effect: 'normal'|'fire'|'lightning'|'explosion')", "Set arrow effect.", &DebugCmdArrow); LuaSetDocFn(table, "god", "(on: boolean = nil)", "Toggle god mode.", &DebugCmdGodMode); LuaSetDoc(table, "gold", "", "Adjust player gold.", LuaDevPlayerGoldModule(lua)); LuaSetDocFn(table, "info", "(id: number = 0)", "Show player info.", &DebugCmdPlayerInfo); LuaSetDoc(table, "spells", "", "Adjust player spells.", LuaDevPlayerSpellsModule(lua)); LuaSetDoc(table, "stats", "", "Adjust player stats (Strength, HP, etc).", LuaDevPlayerStatsModule(lua)); LuaSetDoc(table, "trn", "", "Set player TRN to '${name}.trn'", LuaDevPlayerTrnModule(lua)); LuaSetDocFn(table, "invisible", "(on: boolean = nil)", "Toggle invisibility.", &DebugCmdInvisible); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/player.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevPlayerModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/quests.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/quests.hpp" #include #include #include #include "engine/render/primitive_render.hpp" #include "lua/metadoc.hpp" #include "quests.h" #include "utils/is_of.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdEnableQuest(uint8_t questId) { if (questId >= MAXQUESTS) return StrCat("Quest ", questId, " does not exist!"); Quest &quest = Quests[questId]; if (IsNoneOf(quest._qactive, QUEST_NOTAVAIL, QUEST_INIT)) return StrCat(QuestsData[questId]._qlstr, " is already active!"); quest._qactive = QUEST_ACTIVE; quest._qlog = true; return StrCat(QuestsData[questId]._qlstr, " activated."); } std::string DebugCmdEnableQuests() { for (Quest &quest : Quests) { if (IsNoneOf(quest._qactive, QUEST_NOTAVAIL, QUEST_INIT)) continue; quest._qactive = QUEST_ACTIVE; quest._qlog = true; } return "Activated all quests."; } std::string DebugCmdQuestInfo(const uint8_t questId) { if (questId >= MAXQUESTS) return StrCat("Quest ", questId, " does not exist!"); const Quest &quest = Quests[questId]; return StrCat("Quest id=", quest._qidx, " ", QuestsData[quest._qidx]._qlstr, " active=", quest._qactive, " var1=", quest._qvar1, " var2=", quest._qvar2); } std::string DebugCmdQuestsInfo() { std::string ret; for (const Quest &quest : Quests) { StrAppend(ret, "Quest id=", quest._qidx, " ", QuestsData[quest._qidx]._qlstr, " active=", quest._qactive, " var1=", quest._qvar1, " var2=", quest._qvar2, "\n"); } if (!ret.empty()) ret.pop_back(); return ret; } } // namespace sol::table LuaDevQuestsModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "activate", "(id: number)", "Activate the given quest.", &DebugCmdEnableQuest); LuaSetDocFn(table, "activateAll", "()", "Activate all available quests.", &DebugCmdEnableQuests); LuaSetDocFn(table, "all", "()", "Information on all available quest.", &DebugCmdQuestsInfo); LuaSetDocFn(table, "info", "(id: number)", "Information on the given quest.", &DebugCmdQuestInfo); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/quests.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevQuestsModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/search.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/quests.hpp" #include #include #include "debug.h" #include "lua/metadoc.hpp" #include "utils/str_case.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { std::string DebugCmdSearchMonster(std::string_view name) { if (name.empty()) return "Missing monster name!"; AddDebugAutomapMonsterHighlight(AsciiStrToLower(name)); return StrCat("Added automap marker for monster ", name, "."); } std::string DebugCmdSearchItem(std::string_view name) { if (name.empty()) return "Missing item name!"; AddDebugAutomapItemHighlight(AsciiStrToLower(name)); return StrCat("Added automap marker for item ", name, "."); } std::string DebugCmdSearchObject(std::string_view name) { if (name.empty()) return "Missing object name!"; AddDebugAutomapObjectHighlight(AsciiStrToLower(name)); return StrCat("Added automap marker for object ", name, "."); } std::string DebugCmdClearSearch() { ClearDebugAutomapHighlights(); return "Removed all automap search markers."; } } // namespace sol::table LuaDevSearchModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "clear", "()", "Clear search results from the map.", &DebugCmdClearSearch); LuaSetDocFn(table, "item", "(name: string)", "Search the map for an item.", &DebugCmdSearchItem); LuaSetDocFn(table, "monster", "(name: string)", "Search the map for a monster.", &DebugCmdSearchMonster); LuaSetDocFn(table, "object", "(name: string)", "Search the map for an object.", &DebugCmdSearchObject); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/search.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevSearchModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/towners.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev/towners.hpp" #include #include #include #include "lua/metadoc.hpp" #include "player.h" #include "spells.h" #include "towners.h" #include "utils/str_cat.hpp" namespace devilution { namespace { ankerl::unordered_dense::map TownerShortNameToTownerId = { { "griswold", _talker_id::TOWN_SMITH }, { "smith", _talker_id::TOWN_SMITH }, { "pepin", _talker_id::TOWN_HEALER }, { "healer", _talker_id::TOWN_HEALER }, { "ogden", _talker_id::TOWN_TAVERN }, { "tavern", _talker_id::TOWN_TAVERN }, { "cain", _talker_id::TOWN_STORY }, { "story", _talker_id::TOWN_STORY }, { "farnham", _talker_id::TOWN_DRUNK }, { "drunk", _talker_id::TOWN_DRUNK }, { "adria", _talker_id::TOWN_WITCH }, { "witch", _talker_id::TOWN_WITCH }, { "gillian", _talker_id::TOWN_BMAID }, { "bmaid", _talker_id::TOWN_BMAID }, { "wirt", _talker_id ::TOWN_PEGBOY }, { "pegboy", _talker_id ::TOWN_PEGBOY }, { "lester", _talker_id ::TOWN_FARMER }, { "farmer", _talker_id ::TOWN_FARMER }, { "girl", _talker_id ::TOWN_GIRL }, { "nut", _talker_id::TOWN_COWFARM }, { "cowfarm", _talker_id::TOWN_COWFARM }, }; std::string DebugCmdVisitTowner(std::string_view name) { Player &myPlayer = *MyPlayer; if (setlevel || !myPlayer.isOnLevel(0)) return StrCat("This command is only available in Town!"); if (name.empty()) { std::string ret; ret = StrCat("Please provide the name of a Towner: "); for (const auto &[name, _] : TownerShortNameToTownerId) { ret += ' '; ret.append(name); } return ret; } auto it = TownerShortNameToTownerId.find(name); if (it == TownerShortNameToTownerId.end()) return StrCat(name, " is invalid!"); for (const Towner &towner : Towners) { if (towner._ttype != it->second) continue; CastSpell( *MyPlayer, SpellID::Teleport, myPlayer.position.tile, towner.position, /*spllvl=*/1); return StrCat("Moved you to ", name, "."); } return StrCat("Unable to locate ", name, "!"); } std::string DebugCmdTalkToTowner(std::string_view name) { if (name.empty()) { std::string ret; ret = StrCat("Please provide the name of a Towner: "); for (const auto &[name, _] : TownerShortNameToTownerId) { ret += ' '; ret.append(name); } return ret; } auto it = TownerShortNameToTownerId.find(name); if (it == TownerShortNameToTownerId.end()) return StrCat(name, " is invalid!"); if (!DebugTalkToTowner(it->second)) return StrCat("Towner not found!"); return StrCat("Opened ", name, " talk window."); } } // namespace sol::table LuaDevTownersModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "talk", "(name: string)", "Talk to towner.", &DebugCmdTalkToTowner); LuaSetDocFn(table, "visit", "(name: string)", "Teleport to towner.", &DebugCmdVisitTowner); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev/towners.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevTownersModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev.cpp ================================================ #ifdef _DEBUG #include "lua/modules/dev.hpp" #include #include "lua/metadoc.hpp" #include "lua/modules/dev/display.hpp" #include "lua/modules/dev/items.hpp" #include "lua/modules/dev/level.hpp" #include "lua/modules/dev/monsters.hpp" #include "lua/modules/dev/player.hpp" #include "lua/modules/dev/quests.hpp" #include "lua/modules/dev/search.hpp" #include "lua/modules/dev/towners.hpp" namespace devilution { sol::table LuaDevModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDoc(table, "display", "", "Debugging HUD and rendering commands.", LuaDevDisplayModule(lua)); LuaSetDoc(table, "items", "", "Item-related commands.", LuaDevItemsModule(lua)); LuaSetDoc(table, "level", "", "Level-related commands.", LuaDevLevelModule(lua)); LuaSetDoc(table, "monsters", "", "Monster-related commands.", LuaDevMonstersModule(lua)); LuaSetDoc(table, "player", "", "Player-related commands.", LuaDevPlayerModule(lua)); LuaSetDoc(table, "quests", "", "Quest-related commands.", LuaDevQuestsModule(lua)); LuaSetDoc(table, "search", "", "Search the map for monsters / items / objects.", LuaDevSearchModule(lua)); LuaSetDoc(table, "towners", "", "Town NPC commands.", LuaDevTownersModule(lua)); return table; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/dev.hpp ================================================ #pragma once #ifdef _DEBUG #include namespace devilution { sol::table LuaDevModule(sol::state_view &lua); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/modules/floatingnumbers.cpp ================================================ #include "lua/modules/floatingnumbers.hpp" #include #include "engine/point.hpp" #include "lua/metadoc.hpp" #include "qol/floatingnumbers.h" namespace devilution { sol::table LuaFloatingNumbersModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "add", "(text: string, pos: Point, style: UiFlags, id: integer = 0, reverseDirection: boolean = false)", "Add a floating number", [](const std::string &text, Point pos, UiFlags style, std::optional id, std::optional reverseDirection) { AddFloatingNumber(pos, { 0, 0 }, text, style, id.value_or(0), reverseDirection.value_or(false)); }); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/floatingnumbers.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaFloatingNumbersModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/hellfire.cpp ================================================ #include "lua/modules/hellfire.hpp" #include #include "engine/assets.hpp" #include "lua/metadoc.hpp" namespace devilution { sol::table LuaHellfireModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "loadData", "()", LoadHellfireArchives); LuaSetDocFn(table, "enable", "()", []() { gbIsHellfire = true; }); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/hellfire.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaHellfireModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/i18n.cpp ================================================ #include "lua/modules/i18n.hpp" #include #include #include #include "lua/metadoc.hpp" #include "utils/language.h" namespace devilution { sol::table LuaI18nModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "language_code", "()", "Returns the current language code", GetLanguageCode); LuaSetDocFn(table, "translate", "(text: string)", "Translates the given string", [](const char *key) { return LanguageTranslate(key); }); LuaSetDocFn(table, "plural_translate", "(singular: string, plural: string, count: integer)", "Returns a singular or plural translation for the given keys and count", [](const char *singular, std::string_view plural, int count) { return fmt::format(fmt::runtime(LanguagePluralTranslate(singular, plural, count)), count); }); LuaSetDocFn(table, "particular_translate", "(context: string, text: string)", "Returns the translation for the given context identifier and key.", LanguageParticularTranslate); LuaSetDocFn(table, "is_small_font_tall", "()", "Whether the language's small font is tall (16px)", IsSmallFontTall); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/i18n.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaI18nModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/items.cpp ================================================ #include "lua/modules/items.hpp" #include #include #include #include "data/file.hpp" #include "items.h" #include "lua/metadoc.hpp" #include "player.h" #include "tables/itemdat.h" #include "utils/utf8.hpp" namespace devilution { namespace { void InitItemUserType(sol::state_view &lua) { // Create a new usertype for Item with no constructor sol::usertype itemType = lua.new_usertype(sol::no_constructor); // Member variables LuaSetDocProperty(itemType, "seed", "number", "Randomly generated identifier", &Item::_iSeed); LuaSetDocProperty(itemType, "createInfo", "number", "Creation flags", &Item::_iCreateInfo); LuaSetDocProperty(itemType, "type", "ItemType", "Item type", &Item::_itype); LuaSetDocProperty(itemType, "animFlag", "boolean", "Animation flag", &Item::_iAnimFlag); LuaSetDocProperty(itemType, "position", "Point", "Item world position", &Item::position); // TODO: Add AnimationInfo usertype // LuaSetDocProperty(itemType, "animInfo", "AnimationInfo", "Animation information", &Item::AnimInfo); LuaSetDocProperty(itemType, "delFlag", "boolean", "Deletion flag", &Item::_iDelFlag); LuaSetDocProperty(itemType, "selectionRegion", "number", "Selection region", &Item::selectionRegion); LuaSetDocProperty(itemType, "postDraw", "boolean", "Post-draw flag", &Item::_iPostDraw); LuaSetDocProperty(itemType, "identified", "boolean", "Identified flag", &Item::_iIdentified); LuaSetDocProperty(itemType, "magical", "number", "Item quality", &Item::_iMagical); LuaSetDocProperty(itemType, "name", "string", "Item name", [](const Item &i) { return std::string_view(i._iName); }, [](Item &i, std::string_view val) { CopyUtf8(i._iName, val, sizeof(i._iName)); }); LuaSetDocProperty(itemType, "iName", "string", "Identified item name", [](const Item &i) { return std::string_view(i._iIName); }, [](Item &i, std::string_view val) { CopyUtf8(i._iIName, val, sizeof(i._iIName)); }); LuaSetDocProperty(itemType, "loc", "ItemEquipType", "Equipment location", &Item::_iLoc); LuaSetDocProperty(itemType, "class", "ItemClass", "Item class", &Item::_iClass); LuaSetDocProperty(itemType, "curs", "number", "Cursor index", &Item::_iCurs); LuaSetDocProperty(itemType, "value", "number", "Item value", &Item::_ivalue); LuaSetDocProperty(itemType, "ivalue", "number", "Identified item value", &Item::_iIvalue); LuaSetDocProperty(itemType, "minDam", "number", "Minimum damage", &Item::_iMinDam); LuaSetDocProperty(itemType, "maxDam", "number", "Maximum damage", &Item::_iMaxDam); LuaSetDocProperty(itemType, "AC", "number", "Armor class", &Item::_iAC); LuaSetDocProperty(itemType, "flags", "ItemSpecialEffect", "Special effect flags", &Item::_iFlags); LuaSetDocProperty(itemType, "miscId", "ItemMiscID", "Miscellaneous ID", &Item::_iMiscId); LuaSetDocProperty(itemType, "spell", "SpellID", "Spell", &Item::_iSpell); LuaSetDocProperty(itemType, "IDidx", "ItemIndex", "Base item index", &Item::IDidx); LuaSetDocProperty(itemType, "charges", "number", "Number of charges", &Item::_iCharges); LuaSetDocProperty(itemType, "maxCharges", "number", "Maximum charges", &Item::_iMaxCharges); LuaSetDocProperty(itemType, "durability", "number", "Durability", &Item::_iDurability); LuaSetDocProperty(itemType, "maxDur", "number", "Maximum durability", &Item::_iMaxDur); LuaSetDocProperty(itemType, "PLDam", "number", "Damage % bonus", &Item::_iPLDam); LuaSetDocProperty(itemType, "PLToHit", "number", "Chance to hit bonus", &Item::_iPLToHit); LuaSetDocProperty(itemType, "PLAC", "number", "Armor class % bonus", &Item::_iPLAC); LuaSetDocProperty(itemType, "PLStr", "number", "Strength bonus", &Item::_iPLStr); LuaSetDocProperty(itemType, "PLMag", "number", "Magic bonus", &Item::_iPLMag); LuaSetDocProperty(itemType, "PLDex", "number", "Dexterity bonus", &Item::_iPLDex); LuaSetDocProperty(itemType, "PLVit", "number", "Vitality bonus", &Item::_iPLVit); LuaSetDocProperty(itemType, "PLFR", "number", "Fire resistance bonus", &Item::_iPLFR); LuaSetDocProperty(itemType, "PLLR", "number", "Lightning resistance bonus", &Item::_iPLLR); LuaSetDocProperty(itemType, "PLMR", "number", "Magic resistance bonus", &Item::_iPLMR); LuaSetDocProperty(itemType, "PLMana", "number", "Mana bonus", &Item::_iPLMana); LuaSetDocProperty(itemType, "PLHP", "number", "Life bonus", &Item::_iPLHP); LuaSetDocProperty(itemType, "PLDamMod", "number", "Damage modifier bonus", &Item::_iPLDamMod); LuaSetDocProperty(itemType, "PLGetHit", "number", "Damage from enemies bonus", &Item::_iPLGetHit); LuaSetDocProperty(itemType, "PLLight", "number", "Light bonus", &Item::_iPLLight); LuaSetDocProperty(itemType, "splLvlAdd", "number", "Spell level bonus", &Item::_iSplLvlAdd); LuaSetDocProperty(itemType, "request", "boolean", "Request flag", &Item::_iRequest); LuaSetDocProperty(itemType, "uid", "number", "Unique item ID", &Item::_iUid); LuaSetDocProperty(itemType, "fMinDam", "number", "Fire minimum damage", &Item::_iFMinDam); LuaSetDocProperty(itemType, "fMaxDam", "number", "Fire maximum damage", &Item::_iFMaxDam); LuaSetDocProperty(itemType, "lMinDam", "number", "Lightning minimum damage", &Item::_iLMinDam); LuaSetDocProperty(itemType, "lMaxDam", "number", "Lightning maximum damage", &Item::_iLMaxDam); LuaSetDocProperty(itemType, "PLEnAc", "number", "Damage target AC bonus", &Item::_iPLEnAc); LuaSetDocProperty(itemType, "prePower", "ItemEffectType", "Prefix power", &Item::_iPrePower); LuaSetDocProperty(itemType, "sufPower", "ItemEffectType", "Suffix power", &Item::_iSufPower); LuaSetDocProperty(itemType, "vAdd1", "number", "Value addition 1", &Item::_iVAdd1); LuaSetDocProperty(itemType, "vMult1", "number", "Value multiplier 1", &Item::_iVMult1); LuaSetDocProperty(itemType, "vAdd2", "number", "Value addition 2", &Item::_iVAdd2); LuaSetDocProperty(itemType, "vMult2", "number", "Value multiplier 2", &Item::_iVMult2); LuaSetDocProperty(itemType, "minStr", "number", "Minimum strength required", &Item::_iMinStr); LuaSetDocProperty(itemType, "minMag", "number", "Minimum magic required", &Item::_iMinMag); LuaSetDocProperty(itemType, "minDex", "number", "Minimum dexterity required", &Item::_iMinDex); LuaSetDocProperty(itemType, "statFlag", "boolean", "Equippable flag", &Item::_iStatFlag); LuaSetDocProperty(itemType, "damAcFlags", "ItemSpecialEffectHf", "Secondary special effect flags", &Item::_iDamAcFlags); LuaSetDocProperty(itemType, "buff", "number", "Secondary creation flags", &Item::dwBuff); // Member functions LuaSetDocFn(itemType, "pop", "() -> Item", "Clears this item and returns the old value", &Item::pop); LuaSetDocFn(itemType, "clear", "() -> void", "Resets the item", &Item::clear); LuaSetDocFn(itemType, "isEmpty", "() -> boolean", "Checks whether this item is empty", &Item::isEmpty); LuaSetDocFn(itemType, "isEquipment", "() -> boolean", "Checks if item is equipment", &Item::isEquipment); LuaSetDocFn(itemType, "isWeapon", "() -> boolean", "Checks if item is a weapon", &Item::isWeapon); LuaSetDocFn(itemType, "isArmor", "() -> boolean", "Checks if item is armor", &Item::isArmor); LuaSetDocFn(itemType, "isGold", "() -> boolean", "Checks if item is gold", &Item::isGold); LuaSetDocFn(itemType, "isHelm", "() -> boolean", "Checks if item is a helm", &Item::isHelm); LuaSetDocFn(itemType, "isShield", "() -> boolean", "Checks if item is a shield", &Item::isShield); LuaSetDocFn(itemType, "isJewelry", "() -> boolean", "Checks if item is jewelry", &Item::isJewelry); LuaSetDocFn(itemType, "isScroll", "() -> boolean", "Checks if item is a scroll", &Item::isScroll); LuaSetDocFn(itemType, "isScrollOf", "(spell: SpellID) -> boolean", "Checks if item is a scroll of a given spell", &Item::isScrollOf); LuaSetDocFn(itemType, "isRune", "() -> boolean", "Checks if item is a rune", &Item::isRune); LuaSetDocFn(itemType, "isRuneOf", "(spell: SpellID) -> boolean", "Checks if item is a rune of a given spell", &Item::isRuneOf); LuaSetDocFn(itemType, "isUsable", "() -> boolean", "Checks if item is usable", &Item::isUsable); LuaSetDocFn(itemType, "keyAttributesMatch", "(seed: number, idx: number, createInfo: number) -> boolean", "Checks if key attributes match", &Item::keyAttributesMatch); LuaSetDocFn(itemType, "getTextColor", "() -> UiFlags", "Gets the text color", &Item::getTextColor); LuaSetDocFn(itemType, "getTextColorWithStatCheck", "() -> UiFlags", "Gets the text color with stat check", &Item::getTextColorWithStatCheck); LuaSetDocFn(itemType, "setNewAnimation", "(showAnimation: boolean) -> void", "Sets the new animation", &Item::setNewAnimation); LuaSetDocFn(itemType, "updateRequiredStatsCacheForPlayer", "(player: Player) -> void", "Updates the required stats cache", &Item::updateRequiredStatsCacheForPlayer); LuaSetDocFn(itemType, "getName", "() -> string", "Gets the translated item name", &Item::getName); } void RegisterItemTypeEnum(sol::state_view &lua) { lua.new_enum("ItemType", { { "Misc", ItemType::Misc }, { "Sword", ItemType::Sword }, { "Axe", ItemType::Axe }, { "Bow", ItemType::Bow }, { "Mace", ItemType::Mace }, { "Shield", ItemType::Shield }, { "LightArmor", ItemType::LightArmor }, { "Helm", ItemType::Helm }, { "MediumArmor", ItemType::MediumArmor }, { "HeavyArmor", ItemType::HeavyArmor }, { "Staff", ItemType::Staff }, { "Gold", ItemType::Gold }, { "Ring", ItemType::Ring }, { "Amulet", ItemType::Amulet }, { "None", ItemType::None }, }); } void RegisterItemEquipTypeEnum(sol::state_view &lua) { lua.new_enum("ItemEquipType", { { "None", ILOC_NONE }, { "OneHand", ILOC_ONEHAND }, { "TwoHand", ILOC_TWOHAND }, { "Armor", ILOC_ARMOR }, { "Helm", ILOC_HELM }, { "Ring", ILOC_RING }, { "Amulet", ILOC_AMULET }, { "Unequipable", ILOC_UNEQUIPABLE }, { "Belt", ILOC_BELT }, { "Invalid", ILOC_INVALID }, }); } void RegisterItemClassEnum(sol::state_view &lua) { lua.new_enum("ItemClass", { { "None", ICLASS_NONE }, { "Weapon", ICLASS_WEAPON }, { "Armor", ICLASS_ARMOR }, { "Misc", ICLASS_MISC }, { "Gold", ICLASS_GOLD }, { "Quest", ICLASS_QUEST }, }); } void RegisterItemSpecialEffectEnum(sol::state_view &lua) { lua.new_enum("ItemSpecialEffect", { { "None", ItemSpecialEffect::None }, { "RandomStealLife", ItemSpecialEffect::RandomStealLife }, { "RandomArrowVelocity", ItemSpecialEffect::RandomArrowVelocity }, { "FireArrows", ItemSpecialEffect::FireArrows }, { "FireDamage", ItemSpecialEffect::FireDamage }, { "LightningDamage", ItemSpecialEffect::LightningDamage }, { "DrainLife", ItemSpecialEffect::DrainLife }, { "MultipleArrows", ItemSpecialEffect::MultipleArrows }, { "Knockback", ItemSpecialEffect::Knockback }, { "StealMana3", ItemSpecialEffect::StealMana3 }, { "StealMana5", ItemSpecialEffect::StealMana5 }, { "StealLife3", ItemSpecialEffect::StealLife3 }, { "StealLife5", ItemSpecialEffect::StealLife5 }, { "QuickAttack", ItemSpecialEffect::QuickAttack }, { "FastAttack", ItemSpecialEffect::FastAttack }, { "FasterAttack", ItemSpecialEffect::FasterAttack }, { "FastestAttack", ItemSpecialEffect::FastestAttack }, { "FastHitRecovery", ItemSpecialEffect::FastHitRecovery }, { "FasterHitRecovery", ItemSpecialEffect::FasterHitRecovery }, { "FastestHitRecovery", ItemSpecialEffect::FastestHitRecovery }, { "FastBlock", ItemSpecialEffect::FastBlock }, { "LightningArrows", ItemSpecialEffect::LightningArrows }, { "Thorns", ItemSpecialEffect::Thorns }, { "NoMana", ItemSpecialEffect::NoMana }, { "HalfTrapDamage", ItemSpecialEffect::HalfTrapDamage }, { "TripleDemonDamage", ItemSpecialEffect::TripleDemonDamage }, { "ZeroResistance", ItemSpecialEffect::ZeroResistance }, }); } void RegisterItemMiscIDEnum(sol::state_view &lua) { lua.new_enum("ItemMiscID", { { "None", IMISC_NONE }, { "FullHeal", IMISC_FULLHEAL }, { "Heal", IMISC_HEAL }, { "Mana", IMISC_MANA }, { "FullMana", IMISC_FULLMANA }, { "ElixirStr", IMISC_ELIXSTR }, { "ElixirMag", IMISC_ELIXMAG }, { "ElixirDex", IMISC_ELIXDEX }, { "ElixirVit", IMISC_ELIXVIT }, { "Rejuv", IMISC_REJUV }, { "FullRejuv", IMISC_FULLREJUV }, { "Scroll", IMISC_SCROLL }, { "ScrollT", IMISC_SCROLLT }, { "Staff", IMISC_STAFF }, { "Book", IMISC_BOOK }, { "Ring", IMISC_RING }, { "Amulet", IMISC_AMULET }, { "Unique", IMISC_UNIQUE }, }); } void RegisterSpellIDEnum(sol::state_view &lua) { lua.new_enum("SpellID", { { "Null", SpellID::Null }, { "Firebolt", SpellID::Firebolt }, { "Healing", SpellID::Healing }, { "Lightning", SpellID::Lightning }, { "Flash", SpellID::Flash }, { "Identify", SpellID::Identify }, { "FireWall", SpellID::FireWall }, { "TownPortal", SpellID::TownPortal }, { "StoneCurse", SpellID::StoneCurse }, { "Infravision", SpellID::Infravision }, { "Phasing", SpellID::Phasing }, { "ManaShield", SpellID::ManaShield }, { "Fireball", SpellID::Fireball }, { "Guardian", SpellID::Guardian }, { "ChainLightning", SpellID::ChainLightning }, { "FlameWave", SpellID::FlameWave }, { "DoomSerpents", SpellID::DoomSerpents }, { "BloodRitual", SpellID::BloodRitual }, { "Nova", SpellID::Nova }, { "Invisibility", SpellID::Invisibility }, { "Inferno", SpellID::Inferno }, { "Golem", SpellID::Golem }, { "Rage", SpellID::Rage }, { "Teleport", SpellID::Teleport }, { "Apocalypse", SpellID::Apocalypse }, { "Etherealize", SpellID::Etherealize }, { "ItemRepair", SpellID::ItemRepair }, { "StaffRecharge", SpellID::StaffRecharge }, { "TrapDisarm", SpellID::TrapDisarm }, { "Elemental", SpellID::Elemental }, { "ChargedBolt", SpellID::ChargedBolt }, { "HolyBolt", SpellID::HolyBolt }, { "Resurrect", SpellID::Resurrect }, { "Telekinesis", SpellID::Telekinesis }, { "HealOther", SpellID::HealOther }, { "BloodStar", SpellID::BloodStar }, { "BoneSpirit", SpellID::BoneSpirit }, { "Mana", SpellID::Mana }, { "Magi", SpellID::Magi }, { "Jester", SpellID::Jester }, { "LightningWall", SpellID::LightningWall }, { "Immolation", SpellID::Immolation }, { "Warp", SpellID::Warp }, { "Reflect", SpellID::Reflect }, { "Berserk", SpellID::Berserk }, { "RingOfFire", SpellID::RingOfFire }, { "Search", SpellID::Search }, { "RuneOfFire", SpellID::RuneOfFire }, { "RuneOfLight", SpellID::RuneOfLight }, { "RuneOfNova", SpellID::RuneOfNova }, { "RuneOfImmolation", SpellID::RuneOfImmolation }, { "RuneOfStone", SpellID::RuneOfStone }, { "Invalid", SpellID::Invalid }, }); } void RegisterItemIndexEnum(sol::state_view &lua) { lua.new_enum<_item_indexes>("ItemIndex", { { "Gold", IDI_GOLD }, { "Warrior", IDI_WARRIOR }, { "WarriorShield", IDI_WARRSHLD }, { "WarriorClub", IDI_WARRCLUB }, { "Rogue", IDI_ROGUE }, { "Sorcerer", IDI_SORCERER }, { "Cleaver", IDI_CLEAVER }, { "FirstQuest", IDI_FIRSTQUEST }, { "SkeletonKingCrown", IDI_SKCROWN }, { "InfravisionRing", IDI_INFRARING }, { "Rock", IDI_ROCK }, { "OpticAmulet", IDI_OPTAMULET }, { "TruthRing", IDI_TRING }, { "Banner", IDI_BANNER }, { "HarlequinCrest", IDI_HARCREST }, { "SteelVeil", IDI_STEELVEIL }, { "GoldenElixir", IDI_GLDNELIX }, { "Anvil", IDI_ANVIL }, { "Mushroom", IDI_MUSHROOM }, { "Brain", IDI_BRAIN }, { "FungalTome", IDI_FUNGALTM }, { "SpectralElixir", IDI_SPECELIX }, { "BloodStone", IDI_BLDSTONE }, { "MapOfDoom", IDI_MAPOFDOOM }, { "LastQuest", IDI_LASTQUEST }, { "Ear", IDI_EAR }, { "Heal", IDI_HEAL }, { "Mana", IDI_MANA }, { "Identify", IDI_IDENTIFY }, { "Portal", IDI_PORTAL }, { "ArmorOfValor", IDI_ARMOFVAL }, { "FullHeal", IDI_FULLHEAL }, { "FullMana", IDI_FULLMANA }, { "Griswold", IDI_GRISWOLD }, { "Lightforge", IDI_LGTFORGE }, { "LazarusStaff", IDI_LAZSTAFF }, { "Resurrect", IDI_RESURRECT }, { "Oil", IDI_OIL }, { "ShortStaff", IDI_SHORTSTAFF }, { "BardSword", IDI_BARDSWORD }, { "BardDagger", IDI_BARDDAGGER }, { "RuneBomb", IDI_RUNEBOMB }, { "Theodore", IDI_THEODORE }, { "Auric", IDI_AURIC }, { "Note1", IDI_NOTE1 }, { "Note2", IDI_NOTE2 }, { "Note3", IDI_NOTE3 }, { "FullNote", IDI_FULLNOTE }, { "BrownSuit", IDI_BROWNSUIT }, { "GreySuit", IDI_GREYSUIT }, { "Book1", IDI_BOOK1 }, { "Book2", IDI_BOOK2 }, { "Book3", IDI_BOOK3 }, { "Book4", IDI_BOOK4 }, { "Barbarian", IDI_BARBARIAN }, { "ShortBattleBow", IDI_SHORT_BATTLE_BOW }, { "RuneOfStone", IDI_RUNEOFSTONE }, { "SorcererDiablo", IDI_SORCERER_DIABLO }, { "ArenaPotion", IDI_ARENAPOT }, { "None", IDI_NONE }, }); } void RegisterItemEffectTypeEnum(sol::state_view &lua) { lua.new_enum("ItemEffectType", { { "ToHit", IPL_TOHIT }, { "ToHitCurse", IPL_TOHIT_CURSE }, { "DamagePercent", IPL_DAMP }, { "DamagePercentCurse", IPL_DAMP_CURSE }, { "ToHitDamagePercent", IPL_TOHIT_DAMP }, { "ToHitDamagePercentCurse", IPL_TOHIT_DAMP_CURSE }, { "ArmorClassPercent", IPL_ACP }, { "ArmorClassPercentCurse", IPL_ACP_CURSE }, { "FireResist", IPL_FIRERES }, { "LightningResist", IPL_LIGHTRES }, { "MagicResist", IPL_MAGICRES }, { "AllResist", IPL_ALLRES }, { "SpellLevelAdd", IPL_SPLLVLADD }, { "Charges", IPL_CHARGES }, { "FireDamage", IPL_FIREDAM }, { "LightningDamage", IPL_LIGHTDAM }, { "Strength", IPL_STR }, { "StrengthCurse", IPL_STR_CURSE }, { "Magic", IPL_MAG }, { "MagicCurse", IPL_MAG_CURSE }, { "Dexterity", IPL_DEX }, { "DexterityCurse", IPL_DEX_CURSE }, { "Vitality", IPL_VIT }, { "VitalityCurse", IPL_VIT_CURSE }, { "Attributes", IPL_ATTRIBS }, { "AttributesCurse", IPL_ATTRIBS_CURSE }, { "GetHitCurse", IPL_GETHIT_CURSE }, { "GetHit", IPL_GETHIT }, { "Life", IPL_LIFE }, { "LifeCurse", IPL_LIFE_CURSE }, { "Mana", IPL_MANA }, { "ManaCurse", IPL_MANA_CURSE }, { "Durability", IPL_DUR }, { "DurabilityCurse", IPL_DUR_CURSE }, { "Indestructible", IPL_INDESTRUCTIBLE }, { "Light", IPL_LIGHT }, { "LightCurse", IPL_LIGHT_CURSE }, { "MultipleArrows", IPL_MULT_ARROWS }, { "FireArrows", IPL_FIRE_ARROWS }, { "LightningArrows", IPL_LIGHT_ARROWS }, { "Thorns", IPL_THORNS }, { "NoMana", IPL_NOMANA }, { "Fireball", IPL_FIREBALL }, { "AbsorbHalfTrap", IPL_ABSHALFTRAP }, { "Knockback", IPL_KNOCKBACK }, { "StealMana", IPL_STEALMANA }, { "StealLife", IPL_STEALLIFE }, { "TargetArmorClass", IPL_TARGAC }, { "FastAttack", IPL_FASTATTACK }, { "FastRecover", IPL_FASTRECOVER }, { "FastBlock", IPL_FASTBLOCK }, { "DamageModifier", IPL_DAMMOD }, { "RandomArrowVelocity", IPL_RNDARROWVEL }, { "SetDamage", IPL_SETDAM }, { "SetDurability", IPL_SETDUR }, { "NoMinimumStrength", IPL_NOMINSTR }, { "Spell", IPL_SPELL }, { "OneHand", IPL_ONEHAND }, { "3xDamageVsDemons", IPL_3XDAMVDEM }, { "AllResistZero", IPL_ALLRESZERO }, { "DrainLife", IPL_DRAINLIFE }, { "RandomStealLife", IPL_RNDSTEALLIFE }, { "SetArmorClass", IPL_SETAC }, { "AddArmorClassLife", IPL_ADDACLIFE }, { "AddManaArmorClass", IPL_ADDMANAAC }, { "ArmorClassCurse", IPL_AC_CURSE }, { "LastDiablo", IPL_LASTDIABLO }, { "FireResistCurse", IPL_FIRERES_CURSE }, { "LightResistCurse", IPL_LIGHTRES_CURSE }, { "MagicResistCurse", IPL_MAGICRES_CURSE }, { "Devastation", IPL_DEVASTATION }, { "Decay", IPL_DECAY }, { "Peril", IPL_PERIL }, { "Jesters", IPL_JESTERS }, { "Crystalline", IPL_CRYSTALLINE }, { "Doppelganger", IPL_DOPPELGANGER }, { "ArmorClassDemon", IPL_ACDEMON }, { "ArmorClassUndead", IPL_ACUNDEAD }, { "ManaToLife", IPL_MANATOLIFE }, { "LifeToMana", IPL_LIFETOMANA }, { "Invalid", IPL_INVALID }, }); } void RegisterItemSpecialEffectHfEnum(sol::state_view &lua) { lua.new_enum("ItemSpecialEffectHf", { { "None", ItemSpecialEffectHf::None }, { "Devastation", ItemSpecialEffectHf::Devastation }, { "Decay", ItemSpecialEffectHf::Decay }, { "Peril", ItemSpecialEffectHf::Peril }, { "Jesters", ItemSpecialEffectHf::Jesters }, { "Doppelganger", ItemSpecialEffectHf::Doppelganger }, { "ACAgainstDemons", ItemSpecialEffectHf::ACAgainstDemons }, { "ACAgainstUndead", ItemSpecialEffectHf::ACAgainstUndead }, }); } void AddItemDataFromTsv(const std::string_view path, const int32_t baseMappingId) { DataFile dataFile = DataFile::loadOrDie(path); LoadItemDatFromFile(dataFile, path, baseMappingId); } void AddUniqueItemDataFromTsv(const std::string_view path, const int32_t baseMappingId) { DataFile dataFile = DataFile::loadOrDie(path); LoadUniqueItemDatFromFile(dataFile, path, baseMappingId); } } // namespace sol::table LuaItemModule(sol::state_view &lua) { InitItemUserType(lua); RegisterItemTypeEnum(lua); RegisterItemEquipTypeEnum(lua); RegisterItemClassEnum(lua); RegisterItemSpecialEffectEnum(lua); RegisterItemMiscIDEnum(lua); RegisterSpellIDEnum(lua); RegisterItemIndexEnum(lua); RegisterItemEffectTypeEnum(lua); RegisterItemSpecialEffectHfEnum(lua); sol::table table = lua.create_table(); LuaSetDocFn(table, "addItemDataFromTsv", "(path: string, baseMappingId: number)", AddItemDataFromTsv); LuaSetDocFn(table, "addUniqueItemDataFromTsv", "(path: string, baseMappingId: number)", AddUniqueItemDataFromTsv); // Expose enums through the module table table["ItemIndex"] = lua["ItemIndex"]; table["ItemType"] = lua["ItemType"]; table["ItemClass"] = lua["ItemClass"]; table["ItemEquipType"] = lua["ItemEquipType"]; table["ItemMiscID"] = lua["ItemMiscID"]; table["SpellID"] = lua["SpellID"]; table["ItemEffectType"] = lua["ItemEffectType"]; table["ItemSpecialEffect"] = lua["ItemSpecialEffect"]; table["ItemSpecialEffectHf"] = lua["ItemSpecialEffectHf"]; return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/items.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaItemModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/log.cpp ================================================ #include "lua/modules/log.hpp" #ifdef USE_SDL3 #include #else #include #endif #include #include #include #include #include "utils/log.hpp" #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { namespace { void LuaLogMessage(LogPriority priority, std::string_view fmt, sol::variadic_args args) { std::string formatted; FMT_TRY { fmt::dynamic_format_arg_store store; for (const sol::stack_proxy arg : args) { switch (arg.get_type()) { case sol::type::boolean: store.push_back(arg.as()); break; case sol::type::number: if (lua_isinteger(arg.lua_state(), arg.stack_index())) { store.push_back(lua_tointeger(arg.lua_state(), arg.stack_index())); } else { store.push_back(lua_tonumber(arg.lua_state(), arg.stack_index())); } break; case sol::type::string: store.push_back(arg.as()); break; default: store.push_back(sol::utility::to_string(sol::stack_object(arg))); break; } } formatted = fmt::vformat(fmt, store); } #if FMT_EXCEPTIONS FMT_CATCH(const fmt::format_error &e) { // e.what() is undefined if exceptions are disabled, so we wrap it // with an `FMT_EXCEPTIONS` check. std::string error = e.what(); #else FMT_CATCH(const fmt::format_error &) { std::string error = "unknown (FMT_EXCEPTIONS disabled)"; #endif std::string fullError = StrCat("Format error, fmt: ", fmt, " error: ", error); SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "%s", fullError.c_str()); return; } SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, static_cast(priority), "%s", formatted.c_str()); } void LuaLogInfo(std::string_view fmt, sol::variadic_args args) { LuaLogMessage(LogPriority::Info, fmt, std::move(args)); } void LuaLogVerbose(std::string_view fmt, sol::variadic_args args) { LuaLogMessage(LogPriority::Verbose, fmt, std::move(args)); } void LuaLogDebug(std::string_view fmt, sol::variadic_args args) { LuaLogMessage(LogPriority::Debug, fmt, std::move(args)); } void LuaLogWarn(std::string_view fmt, sol::variadic_args args) { LuaLogMessage(LogPriority::Warn, fmt, std::move(args)); } void LuaLogError(std::string_view fmt, sol::variadic_args args) { LuaLogMessage(LogPriority::Error, fmt, std::move(args)); } } // namespace sol::table LuaLogModule(sol::state_view &lua) { return lua.create_table_with( "info", LuaLogInfo, "verbose", LuaLogVerbose, "debug", LuaLogDebug, "warn", LuaLogWarn, "error", LuaLogError); } } // namespace devilution ================================================ FILE: Source/lua/modules/log.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaLogModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/monsters.cpp ================================================ #include "lua/modules/monsters.hpp" #include #include #include #include "data/file.hpp" #include "engine/point.hpp" #include "lua/metadoc.hpp" #include "monster.h" #include "tables/monstdat.h" #include "utils/language.h" #include "utils/str_split.hpp" namespace devilution { namespace { void AddMonsterDataFromTsv(const std::string_view path) { DataFile dataFile = DataFile::loadOrDie(path); LoadMonstDatFromFile(dataFile, path, true); } void AddUniqueMonsterDataFromTsv(const std::string_view path) { DataFile dataFile = DataFile::loadOrDie(path); LoadUniqueMonstDatFromFile(dataFile, path); } void InitMonsterUserType(sol::state_view &lua) { sol::usertype monsterType = lua.new_usertype(sol::no_constructor); LuaSetDocReadonlyProperty(monsterType, "position", "Point", "Monster's current position (readonly)", [](const Monster &monster) { return Point { monster.position.tile }; }); LuaSetDocReadonlyProperty(monsterType, "id", "integer", "Monster's unique ID (readonly)", [](const Monster &monster) { return static_cast(reinterpret_cast(&monster)); }); } } // namespace sol::table LuaMonstersModule(sol::state_view &lua) { InitMonsterUserType(lua); sol::table table = lua.create_table(); LuaSetDocFn(table, "addMonsterDataFromTsv", "(path: string)", AddMonsterDataFromTsv); LuaSetDocFn(table, "addUniqueMonsterDataFromTsv", "(path: string)", AddUniqueMonsterDataFromTsv); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/monsters.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaMonstersModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/player.cpp ================================================ #include "lua/modules/player.hpp" #include #include #include "effects.h" #include "engine/backbuffer_state.hpp" #include "engine/point.hpp" #include "engine/random.hpp" #include "inv.h" #include "items.h" #include "lua/metadoc.hpp" #include "player.h" namespace devilution { namespace { void InitPlayerUserType(sol::state_view &lua) { sol::usertype playerType = lua.new_usertype(sol::no_constructor); LuaSetDocReadonlyProperty(playerType, "name", "string", "Player's name (readonly)", &Player::name); LuaSetDocReadonlyProperty(playerType, "id", "integer", "Player's unique ID (readonly)", [](const Player &player) { return static_cast(reinterpret_cast(&player)); }); LuaSetDocReadonlyProperty(playerType, "position", "Point", "Player's current position (readonly)", [](const Player &player) -> Point { return Point { player.position.tile }; }); LuaSetDocFn(playerType, "addExperience", "(experience: integer, monsterLevel: integer = nil)", "Adds experience to this player based on the current game mode", [](Player &player, uint32_t experience, std::optional monsterLevel) { if (monsterLevel.has_value()) { player.addExperience(experience, *monsterLevel); } else { player.addExperience(experience); } }); LuaSetDocProperty(playerType, "characterLevel", "number", "Character level (writeable)", &Player::getCharacterLevel, &Player::setCharacterLevel); LuaSetDocFn(playerType, "addItem", "(itemId: integer, count: integer = 1)", "Add an item to the player's inventory", [](Player &player, int itemId, std::optional count) -> bool { const _item_indexes itemIndex = static_cast<_item_indexes>(itemId); const int itemCount = count.value_or(1); for (int i = 0; i < itemCount; i++) { Item tempItem {}; SetupAllItems(player, tempItem, itemIndex, AdvanceRndSeed(), 1, 1, true, false); if (!AutoPlaceItemInInventory(player, tempItem, true)) { return false; } } CalcPlrInv(player, true); return true; }); LuaSetDocFn(playerType, "hasItem", "(itemId: integer)", "Check if the player has an item with the given ID", [](const Player &player, int itemId) -> bool { return HasInventoryOrBeltItemWithId(player, static_cast<_item_indexes>(itemId)); }); LuaSetDocFn(playerType, "removeItem", "(itemId: integer, count: integer = 1)", "Remove an item from the player's inventory", [](Player &player, int itemId, std::optional count) -> int { const _item_indexes targetId = static_cast<_item_indexes>(itemId); const int itemCount = count.value_or(1); int removed = 0; // Remove from inventory for (int i = player._pNumInv - 1; i >= 0 && removed < itemCount; i--) { if (player.InvList[i].IDidx == targetId) { player.RemoveInvItem(i); removed++; } } // Remove from belt if needed for (int i = MaxBeltItems - 1; i >= 0 && removed < itemCount; i--) { if (!player.SpdList[i].isEmpty() && player.SpdList[i].IDidx == targetId) { player.RemoveSpdBarItem(i); removed++; } } if (removed > 0) { CalcPlrInv(player, true); } return removed; }); LuaSetDocFn(playerType, "restoreFullLife", "()", "Restore player's HP to maximum", [](Player &player) { player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; }); LuaSetDocFn(playerType, "restoreFullMana", "()", "Restore player's mana to maximum", [](Player &player) { player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; }); LuaSetDocReadonlyProperty(playerType, "mana", "number", "Current mana (readonly)", [](Player &player) { return player._pMana >> 6; }); LuaSetDocReadonlyProperty(playerType, "maxMana", "number", "Maximum mana (readonly)", [](Player &player) { return player._pMaxMana >> 6; }); } } // namespace sol::table LuaPlayerModule(sol::state_view &lua) { InitPlayerUserType(lua); sol::table table = lua.create_table(); LuaSetDocFn(table, "self", "()", "The current player", []() { return MyPlayer; }); LuaSetDocFn(table, "walk_to", "(x: integer, y: integer)", "Walk to the given coordinates", [](int x, int y) { NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, Point { x, y }); }); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/player.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaPlayerModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/render.cpp ================================================ #include "lua/modules/render.hpp" #include #include "DiabloUI/ui_flags.hpp" #include "engine/dx.h" #include "engine/render/text_render.hpp" #include "lua/metadoc.hpp" #include "utils/display.h" namespace devilution { sol::table LuaRenderModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "string", "(text: string, x: integer, y: integer)", "Renders a string at the given coordinates", [](std::string_view text, int x, int y) { DrawString(GlobalBackBuffer(), text, { x, y }); }); LuaSetDocFn(table, "screen_width", "()", "Returns the screen width", []() { return gnScreenWidth; }); LuaSetDocFn(table, "screen_height", "()", "Returns the screen height", []() { return gnScreenHeight; }); auto uiFlags = lua.create_table(); uiFlags["None"] = UiFlags::None; uiFlags["FontSize12"] = UiFlags::FontSize12; uiFlags["FontSize24"] = UiFlags::FontSize24; uiFlags["FontSize30"] = UiFlags::FontSize30; uiFlags["FontSize42"] = UiFlags::FontSize42; uiFlags["FontSize46"] = UiFlags::FontSize46; uiFlags["FontSizeDialog"] = UiFlags::FontSizeDialog; uiFlags["ColorUiGold"] = UiFlags::ColorUiGold; uiFlags["ColorUiSilver"] = UiFlags::ColorUiSilver; uiFlags["ColorUiGoldDark"] = UiFlags::ColorUiGoldDark; uiFlags["ColorUiSilverDark"] = UiFlags::ColorUiSilverDark; uiFlags["ColorDialogWhite"] = UiFlags::ColorDialogWhite; uiFlags["ColorDialogYellow"] = UiFlags::ColorDialogYellow; uiFlags["ColorDialogRed"] = UiFlags::ColorDialogRed; uiFlags["ColorYellow"] = UiFlags::ColorYellow; uiFlags["ColorGold"] = UiFlags::ColorGold; uiFlags["ColorBlack"] = UiFlags::ColorBlack; uiFlags["ColorWhite"] = UiFlags::ColorWhite; uiFlags["ColorWhitegold"] = UiFlags::ColorWhitegold; uiFlags["ColorRed"] = UiFlags::ColorRed; uiFlags["ColorBlue"] = UiFlags::ColorBlue; uiFlags["ColorOrange"] = UiFlags::ColorOrange; uiFlags["ColorButtonface"] = UiFlags::ColorButtonface; uiFlags["ColorButtonpushed"] = UiFlags::ColorButtonpushed; uiFlags["AlignCenter"] = UiFlags::AlignCenter; uiFlags["AlignRight"] = UiFlags::AlignRight; uiFlags["VerticalCenter"] = UiFlags::VerticalCenter; uiFlags["KerningFitSpacing"] = UiFlags::KerningFitSpacing; uiFlags["ElementDisabled"] = UiFlags::ElementDisabled; uiFlags["ElementHidden"] = UiFlags::ElementHidden; uiFlags["PentaCursor"] = UiFlags::PentaCursor; uiFlags["Outlined"] = UiFlags::Outlined; uiFlags["NeedsNextElement"] = UiFlags::NeedsNextElement; table["UiFlags"] = uiFlags; return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/render.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaRenderModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/system.cpp ================================================ #include "lua/modules/system.hpp" #include #ifdef USE_SDL3 #include #else #include #endif #include "lua/metadoc.hpp" namespace devilution { sol::table LuaSystemModule(sol::state_view &lua) { sol::table table = lua.create_table(); LuaSetDocFn(table, "get_ticks", "() -> integer", "Returns the number of milliseconds since the game started.", []() { return static_cast(SDL_GetTicks()); }); return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/system.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaSystemModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/modules/towners.cpp ================================================ #include "lua/modules/towners.hpp" #include #include #include #include #include "engine/point.hpp" #include "lua/metadoc.hpp" #include "player.h" #include "towners.h" namespace devilution { namespace { // Map from towner type enum to Lua table name const std::unordered_map<_talker_id, const char *> TownerTableNames = { { TOWN_SMITH, "griswold" }, { TOWN_HEALER, "pepin" }, { TOWN_DEADGUY, "deadguy" }, { TOWN_TAVERN, "ogden" }, { TOWN_STORY, "cain" }, { TOWN_DRUNK, "farnham" }, { TOWN_WITCH, "adria" }, { TOWN_BMAID, "gillian" }, { TOWN_PEGBOY, "wirt" }, { TOWN_COW, "cow" }, { TOWN_FARMER, "lester" }, { TOWN_GIRL, "celia" }, { TOWN_COWFARM, "nut" }, }; void PopulateTownerTable(_talker_id townerId, sol::table &out) { LuaSetDocFn(out, "position", "()", "Returns towner coordinates", [townerId]() -> std::optional> { const Towner *towner = GetTowner(townerId); if (towner == nullptr) return std::nullopt; return std::make_pair(towner->position.x, towner->position.y); }); } } // namespace sol::table LuaTownersModule(sol::state_view &lua) { sol::table table = lua.create_table(); // Iterate over all towner types found in TSV data for (const auto &[townerId, name] : TownerLongNames) { auto tableNameIt = TownerTableNames.find(townerId); if (tableNameIt == TownerTableNames.end()) continue; // Skip if no table name mapping sol::table townerTable = lua.create_table(); PopulateTownerTable(townerId, townerTable); LuaSetDoc(table, tableNameIt->second, /*signature=*/"", name.c_str(), std::move(townerTable)); } return table; } } // namespace devilution ================================================ FILE: Source/lua/modules/towners.hpp ================================================ #pragma once #include namespace devilution { sol::table LuaTownersModule(sol::state_view &lua); } // namespace devilution ================================================ FILE: Source/lua/repl.cpp ================================================ #ifdef _DEBUG #include "lua/repl.hpp" #include #include #include #include #include #include #include #include "lua/lua_global.hpp" #include "panels/console.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { std::optional replEnv; void LuaConsoleWarn(void *userData, const char *message, int continued) { static std::string warnBuffer; warnBuffer.append(message); if (continued != 0) return; PrintWarningToConsole(warnBuffer); warnBuffer.clear(); } int LuaPrintToConsole(lua_State *state) { std::string result; const int n = lua_gettop(state); for (int i = 1; i <= n; i++) { size_t l; const char *s = luaL_tolstring(state, i, &l); if (i > 1) result += '\t'; result.append(s, l); lua_pop(state, 1); } PrintToConsole(result); return 0; } void CreateReplEnvironment() { sol::environment env = CreateLuaSandbox(); env["print"] = LuaPrintToConsole; lua_setwarnf(env.lua_state(), LuaConsoleWarn, /*ud=*/nullptr); replEnv.emplace(env); } sol::protected_function_result TryRunLuaAsExpressionThenStatement(std::string_view code) { // Try to compile as an expression first. This also how the `lua` repl is implemented. sol::state &lua = GetLuaState(); std::string expression = StrCat("return ", code, ";"); sol::detail::typical_chunk_name_t basechunkname = {}; sol::load_status status = static_cast( luaL_loadbufferx(lua.lua_state(), expression.data(), expression.size(), sol::detail::make_chunk_name(expression, sol::detail::default_chunk_name(), basechunkname), "text")); if (status != sol::load_status::ok) { // Try as a statement: status = static_cast( luaL_loadbufferx(lua.lua_state(), code.data(), code.size(), sol::detail::make_chunk_name(code, sol::detail::default_chunk_name(), basechunkname), "text")); if (status != sol::load_status::ok) { return sol::protected_function_result( lua.lua_state(), sol::absolute_index(lua.lua_state(), -1), 0, 1, static_cast(status)); } } sol::stack_aligned_protected_function fn(lua.lua_state(), -1); sol::set_environment(GetLuaReplEnvironment(), fn); return fn(); } } // namespace sol::environment &GetLuaReplEnvironment() { if (!replEnv.has_value()) CreateReplEnvironment(); return *replEnv; } tl::expected RunLuaReplLine(std::string_view code) { const sol::protected_function_result result = TryRunLuaAsExpressionThenStatement(code); if (!result.valid()) { if (result.get_type() == sol::type::string) { return tl::make_unexpected(result.get()); } return tl::make_unexpected("Unknown Lua error"); } if (result.get_type() == sol::type::none) { return std::string {}; } return sol::utility::to_string(sol::stack_object(result)); } void LuaReplShutdown() { replEnv = std::nullopt; } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/lua/repl.hpp ================================================ #pragma once #ifdef _DEBUG #include #include #include #include namespace devilution { tl::expected RunLuaReplLine(std::string_view code); sol::environment &GetLuaReplEnvironment(); void LuaReplShutdown(); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/main.cpp ================================================ #ifdef USE_SDL3 #include #else #include #include #endif #ifdef __SWITCH__ #include "platform/switch/network.h" #include "platform/switch/random.hpp" #include "platform/switch/romfs.hpp" #endif #ifdef __3DS__ #include "platform/ctr/system.h" #endif #ifdef __vita__ #include "platform/vita/network.h" #include "platform/vita/random.hpp" #endif #ifdef NXDK #include #endif #ifdef GPERF_HEAP_MAIN #include #endif #include "diablo.h" #if !defined(__APPLE__) extern "C" const char *__asan_default_options() // NOLINT(bugprone-reserved-identifier, readability-identifier-naming) { return "halt_on_error=0"; } #endif extern "C" int main(int argc, char **argv) { #ifdef __SWITCH__ switch_romfs_init(); switch_enable_network(); #ifdef PACKET_ENCRYPTION randombytes_switchrandom_init(); #endif #endif #ifdef __3DS__ ctr_sys_init(); #endif #ifdef __vita__ vita_enable_network(); #ifdef PACKET_ENCRYPTION randombytes_vitarandom_init(); #endif #endif #ifdef NXDK nxMountDrive('E', "\\Device\\Harddisk0\\Partition1\\"); #endif #ifdef GPERF_HEAP_MAIN HeapProfilerStart("main"); #endif const int result = devilution::DiabloMain(argc, argv); #ifdef GPERF_HEAP_MAIN HeapProfilerStop(); #endif return result; } ================================================ FILE: Source/menu.cpp ================================================ /** * @file mainmenu.cpp * * Implementation of functions for interacting with the main menu. */ #include #ifdef USE_SDL3 #include #include #else #include #endif #include "DiabloUI/diabloui.h" #include "DiabloUI/settingsmenu.h" #include "engine/assets.hpp" #include "engine/demomode.h" #include "game_mode.hpp" #include "init.hpp" #include "movie.h" #include "options.h" #include "pfile.h" #include "storm/storm_net.hpp" #include "utils/language.h" namespace devilution { uint32_t gSaveNumber; namespace { _music_id NextTrack() { if (gbIsSpawn) { return TMUSIC_INTRO; } switch (sgnMusicTrack) { case TMUSIC_INTRO: return TMUSIC_CATACOMBS; case TMUSIC_CATACOMBS: return TMUSIC_CAVES; case TMUSIC_CAVES: return TMUSIC_HELL; case TMUSIC_HELL: return gbIsHellfire ? TMUSIC_NEST : TMUSIC_INTRO; case TMUSIC_NEST: return gbIsHellfire ? TMUSIC_CRYPT : TMUSIC_INTRO; default: return TMUSIC_INTRO; } } void RefreshMusic() { music_start(NextTrack()); } bool InitMenu(_selhero_selections type) { bool success; if (type == SELHERO_PREVIOUS) return true; success = StartGame(type != SELHERO_CONTINUE, type != SELHERO_CONNECT); if (success) RefreshMusic(); return success; } bool InitSinglePlayerMenu() { gbIsMultiplayer = false; return InitMenu(SELHERO_NEW_DUNGEON); } bool InitMultiPlayerMenu() { gbIsMultiplayer = true; return InitMenu(SELHERO_CONNECT); } void PlayIntro() { music_stop(); if (gbIsHellfire) play_movie("gendata\\Hellfire.smk", true); else play_movie("gendata\\diablo1.smk", true); RefreshMusic(); } bool DummyGetHeroInfo(_uiheroinfo * /*pInfo*/) { return true; } } // namespace bool mainmenu_select_hero_dialog(GameData *gameData) { OptionEntryInt *pSaveNumberFromOptions = nullptr; _selhero_selections dlgresult = SELHERO_NEW_DUNGEON; if (demo::IsRunning()) { pfile_ui_set_hero_infos(DummyGetHeroInfo); gbLoadGame = true; } else if (!gbIsMultiplayer) { pSaveNumberFromOptions = gbIsHellfire ? &GetOptions().Hellfire.lastSinglePlayerHero : &GetOptions().Diablo.lastSinglePlayerHero; gSaveNumber = **pSaveNumberFromOptions; UiSelHeroSingDialog( pfile_ui_set_hero_infos, pfile_ui_save_create, pfile_delete_save, pfile_ui_set_class_stats, &dlgresult, &gSaveNumber, &gameData->nDifficulty); gbLoadGame = (dlgresult == SELHERO_CONTINUE); } else { pSaveNumberFromOptions = gbIsHellfire ? &GetOptions().Hellfire.lastMultiplayerHero : &GetOptions().Diablo.lastMultiplayerHero; gSaveNumber = **pSaveNumberFromOptions; UiSelHeroMultDialog( pfile_ui_set_hero_infos, pfile_ui_save_create, pfile_delete_save, pfile_ui_set_class_stats, &dlgresult, &gSaveNumber); } if (dlgresult == SELHERO_PREVIOUS) { return false; } if (pSaveNumberFromOptions != nullptr) pSaveNumberFromOptions->SetValue(gSaveNumber); return true; } void mainmenu_wait_for_button_sound() { SDL_FillSurfaceRect(DiabloUiSurface(), nullptr, 0); UiFadeIn(); SDL_Delay(350); // delay to let button pressed sound finish playing } void mainmenu_loop() { bool done; RefreshMusic(); done = false; do { _mainmenu_selections menu = MAINMENU_NONE; if (demo::IsRunning()) menu = MAINMENU_SINGLE_PLAYER; else if (!UiMainMenuDialog(gszProductName, &menu, 30)) app_fatal(_("Unable to display mainmenu")); switch (menu) { case MAINMENU_NONE: break; case MAINMENU_SINGLE_PLAYER: if (!InitSinglePlayerMenu()) done = true; break; case MAINMENU_MULTIPLAYER: if (!InitMultiPlayerMenu()) done = true; break; case MAINMENU_ATTRACT_MODE: if (gbIsSpawn && !HaveIntro()) done = false; else if (gbActive) PlayIntro(); break; case MAINMENU_SHOW_CREDITS: UiCreditsDialog(); break; case MAINMENU_SHOW_SUPPORT: UiSupportDialog(); break; case MAINMENU_EXIT_DIABLO: mainmenu_wait_for_button_sound(); done = true; break; case MAINMENU_SETTINGS: UiSettingsMenu(); break; } } while (!done); music_stop(); } } // namespace devilution ================================================ FILE: Source/menu.h ================================================ /** * @file menu.h * * Interface of functions for interacting with the main menu. */ #pragma once #include #include "multi.h" namespace devilution { extern uint32_t gSaveNumber; bool mainmenu_select_hero_dialog(GameData *gameData); void mainmenu_loop(); } // namespace devilution ================================================ FILE: Source/minitext.cpp ================================================ /** * @file minitext.cpp * * Implementation of scrolling dialog text. */ #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/dx.h" #include "engine/load_cel.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "tables/playerdat.hpp" #include "tables/textdat.h" #include "utils/language.h" #include "utils/timer.hpp" namespace devilution { bool qtextflag; namespace { /** Vertical speed of the scrolling text in ms/px */ int qtextSpd; /** Start time of scrolling */ uint32_t ScrollStart; /** Graphics for the window border */ OptionalOwnedClxSpriteList pTextBoxCels; /** Pixels for a line of text and the empty space under it. */ const int LineHeight = 38; std::vector TextLines; void LoadText(std::string_view text) { TextLines.clear(); const std::string paragraphs = WordWrapString(text, 543, GameFont30); size_t previous = 0; while (true) { const size_t next = paragraphs.find('\n', previous); TextLines.emplace_back(paragraphs.substr(previous, next - previous)); if (next == std::string::npos) break; previous = next + 1; } } /** * @brief Calculate the speed the current text should scroll to match the given audio * @param nSFX The index of the sound in the sgSFX table * @return ms/px */ uint32_t CalculateTextSpeed(SfxID nSFX) { const auto numLines = static_cast(TextLines.size()); #ifndef NOSOUND uint32_t sfxFrames = GetSFXLength(nSFX); #else // Sound is disabled -- estimate length from the number of lines. uint32_t sfxFrames = numLines * 3000; #endif assert(sfxFrames != 0); uint32_t textHeight = LineHeight * numLines; textHeight += LineHeight * 5; // adjust so when speaker is done two line are left assert(textHeight != 0); return sfxFrames / textHeight; } int CalculateTextPosition() { const uint32_t currTime = GetMillisecondsSinceStartup(); const int y = (currTime - ScrollStart) / qtextSpd - 260; const auto textHeight = static_cast(LineHeight * TextLines.size()); if (y >= textHeight) qtextflag = false; return y; } /** * @brief Draw the current text in the quest dialog window */ void DrawQTextContent(const Surface &out) { const int y = CalculateTextPosition(); const int sx = GetUIRectangle().position.x + 48; const int sy = 0 - (y % LineHeight); const unsigned int skipLines = y / LineHeight; for (int i = 0; i < 8; i++) { const unsigned int lineNumber = skipLines + i; if (lineNumber >= TextLines.size()) { continue; } const std::string &line = TextLines[lineNumber]; if (line.empty()) { continue; } DrawString(out, line, { { sx, sy + i * LineHeight }, { 543, LineHeight } }, { .flags = UiFlags::FontSize30 | UiFlags::ColorGold }); } } } // namespace void FreeQuestText() { pTextBoxCels = std::nullopt; } void InitQuestText() { pTextBoxCels = LoadCel("data\\textbox", 591); } void InitQTextMsg(_speech_id m) { SfxID sfxnr = Speeches[m].sfxnr; switch (sfxnr) { case SfxID::Warrior1: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::ChamberOfBoneLore); break; case SfxID::Warrior10: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::ValorLore); break; case SfxID::Warrior11: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::HallsOfTheBlindLore); break; case SfxID::Warrior12: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::WarlordOfBloodLore); break; case SfxID::Warrior54: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::InSpirituSanctum); break; case SfxID::Warrior55: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::PraedictumOtium); break; case SfxID::Warrior56: sfxnr = GetHeroSound(MyPlayer->_pClass, HeroSpeech::EfficioObitusUtInimicus); break; default: break; } if (Speeches[m].scrlltxt) { QuestLogIsOpen = false; LoadText(_(Speeches[m].txtstr)); qtextflag = true; qtextSpd = CalculateTextSpeed(sfxnr); ScrollStart = GetMillisecondsSinceStartup(); } PlaySFX(sfxnr); } void DrawQTextBack(const Surface &out) { const Point uiPosition = GetUIRectangle().position; ClxDraw(out, uiPosition + Displacement { 24, 327 }, (*pTextBoxCels)[0]); DrawHalfTransparentRectTo(out, uiPosition.x + 27, uiPosition.y + 28, 585, 297); } void DrawQText(const Surface &out) { DrawQTextBack(out); DrawQTextContent(out.subregionY(GetUIRectangle().position.y + 49, 260)); } } // namespace devilution ================================================ FILE: Source/minitext.h ================================================ /** * @file minitext.h * * Interface of scrolling dialog text. */ #pragma once #include "engine/surface.hpp" #include "tables/textdat.h" namespace devilution { /** Specify if the quest dialog window is being shown */ extern bool qtextflag; /** * @brief Free the resources used by the quest dialog window */ void FreeQuestText(); /** * @brief Load the resources used by the quest dialog window, and initialize it's state */ void InitQuestText(); /** * @brief Start the given naration * @param m Index of narration from the Texts table */ void InitQTextMsg(_speech_id m); /** * @brief Draw the quest dialog window decoration and background. */ void DrawQTextBack(const Surface &out); /** * @brief Draw the quest dialog window text. */ void DrawQText(const Surface &out); } // namespace devilution ================================================ FILE: Source/missiles.cpp ================================================ /** * @file missiles.cpp * * Implementation of missile functionality. */ #include "missiles.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "appfat.h" #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "crawl.hpp" #include "cursor.h" #include "dead.h" #include "diablo.h" #include "effects.h" #include "engine/clx_sprite.hpp" #include "engine/direction.hpp" #include "engine/displacement.hpp" #include "engine/lighting_defs.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "engine/render/scrollrt.h" #include "engine/world_tile.hpp" #include "function_ref.hpp" #include "interfac.h" #include "items.h" #include "levels/gendung.h" #include "levels/gendung_defs.hpp" #include "msg.h" #include "multi.h" #include "objects.h" #include "player.h" #include "sound_effect_enums.h" #include "tables/itemdat.h" #include "tables/misdat.h" #include "tables/monstdat.h" #include "tables/playerdat.hpp" #include "tables/spelldat.h" #include "utils/enum_traits.h" #ifdef _DEBUG #include "debug.h" #endif #include "engine/backbuffer_state.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "inv.h" #include "levels/dun_tile.hpp" #include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "monster.h" #include "utils/is_of.hpp" #include "utils/str_cat.hpp" namespace devilution { std::list Missiles; bool MissilePreFlag; void Missile::setAnimation(MissileGraphicID animtype) { const int dir = _mimfnum; if (animtype >= MissileGraphicID::None) { _miAnimType = MissileGraphicID::None; _miAnimData = std::nullopt; _miAnimWidth = 0; _miAnimWidth2 = 0; _miAnimFlags = MissileGraphicsFlags::None; _miAnimDelay = 0; _miAnimLen = 0; _miAnimCnt = 0; _miAnimFrame = 1; return; } const MissileFileData &missileData = GetMissileSpriteData(animtype); _miAnimType = animtype; _miAnimFlags = missileData.flags; if (!HeadlessMode) { _miAnimData = missileData.spritesForDirection(static_cast(dir)); } _miAnimDelay = missileData.animDelay(dir); _miAnimLen = missileData.animLen(dir); _miAnimWidth = missileData.animWidth; _miAnimWidth2 = missileData.animWidth2; _miAnimCnt = 0; _miAnimFrame = 1; } namespace { int AddClassHealingBonus(int hp, HeroClass heroClass) { switch (heroClass) { case HeroClass::Warrior: case HeroClass::Monk: case HeroClass::Barbarian: return hp * 2; case HeroClass::Rogue: case HeroClass::Bard: return hp + (hp / 2); default: return hp; } } int ScaleSpellEffect(int base, int spellLevel) { for (int i = 0; i < spellLevel; i++) { base += base / 8; } return base; } int GenerateRndSum(int range, int iterations) { int value = 0; for (int i = 0; i < iterations; i++) { value += GenerateRnd(range); } return value; } bool CheckBlock(Point from, Point to) { while (from != to) { from += GetDirection(from, to); if (TileHasAny(from, TileProperties::Solid)) return true; } return false; } Monster *FindClosest(Point source, int rad) { std::optional monsterPosition = FindClosestValidPosition( [&source](Point target) { // search for a monster with clear line of sight return InDungeonBounds(target) && dMonster[target.x][target.y] > 0 && !CheckBlock(source, target); }, source, 1, rad); if (monsterPosition) { const int mid = dMonster[monsterPosition->x][monsterPosition->y]; return &Monsters[mid - 1]; } return nullptr; } constexpr Direction16 Direction16Flip(Direction16 x, Direction16 pivot) { const std::underlying_type_t ret = (2 * static_cast>(pivot) + 16 - static_cast>(x)) % 16; return static_cast(ret); } void UpdateMissileVelocity(Missile &missile, WorldTilePosition destination, int velocityInPixels) { missile.position.velocity = { 0, 0 }; if (missile.position.tile == destination) return; // Get the normalized vector in isometric projection const Displacement fixed16NormalVector = (Point { missile.position.tile } - Point { destination }).worldToNormalScreen(); // Multiplying by the target velocity gives us a scaled velocity vector. missile.position.velocity = fixed16NormalVector * velocityInPixels; } /** * @brief Add the missile to the lookup tables * @param missile The missile to add */ void PutMissile(Missile &missile) { const Point position = missile.position.tile; if (!InDungeonBounds(position)) missile._miDelFlag = true; if (missile._miDelFlag) { return; } DungeonFlag &flags = dFlags[position.x][position.y]; flags |= DungeonFlag::Missile; if (missile._mitype == MissileID::FireWall) flags |= DungeonFlag::MissileFireWall; if (missile._mitype == MissileID::LightningWall) flags |= DungeonFlag::MissileLightningWall; if (missile._miPreFlag) MissilePreFlag = true; } void UpdateMissilePos(Missile &missile) { const Displacement pixelsTravelled = missile.position.traveled >> 16; const Displacement tileOffset = pixelsTravelled.screenToMissile(); missile.position.tile = missile.position.start + tileOffset; missile.position.offset = pixelsTravelled + tileOffset.worldToScreen(); const Displacement absoluteLightOffset = pixelsTravelled.screenToLight(); ChangeLightOffset(missile._mlid, absoluteLightOffset - tileOffset * 8); } /** * @brief Dodgy hack used to correct the position for charging monsters. * * If the monster represented by this missile is *not* facing north in some way it gets shifted to the south. * This appears to compensate for some visual oddity or invalid calculation earlier in the ProcessRhino logic. * @param missile MissileStruct representing a charging monster. */ void MoveMissilePos(Missile &missile) { Direction moveDirection; switch (missile.getDirection()) { case Direction::East: moveDirection = Direction::SouthEast; break; case Direction::West: moveDirection = Direction::SouthWest; break; case Direction::South: case Direction::SouthWest: case Direction::SouthEast: moveDirection = Direction::South; break; default: return; } auto target = missile.position.tile + moveDirection; if (IsTileAvailable(*missile.sourceMonster(), target)) { missile.position.tile = target; missile.position.offset += Displacement(moveDirection).worldToScreen(); } } int ProjectileMonsterDamage(Missile &missile) { const Monster &monster = *missile.sourceMonster(); return RandomIntBetween(monster.minDamage, monster.maxDamage); } int ProjectileTrapDamage() { return currlevel + GenerateRnd(2 * currlevel); } bool MonsterMHit(const Player &player, Monster &monster, int mindam, int maxdam, int dist, MissileID t, WorldTilePosition startPos, DamageType damageType, bool shift) { if (!monster.isPossibleToHit() || monster.isImmune(t, damageType)) return false; int hit = RandomIntLessThan(100); int hper = 0; const MissileData &missileData = GetMissileData(t); if (missileData.isArrow()) { hper = player.GetRangedPiercingToHit(); hper -= player.CalculateArmorPierce(monster.armorClass, false); hper -= (dist * dist) / 2; } else { hper = player.GetMagicToHit() - (monster.level(sgGameInitInfo.nDifficulty) * 2) - dist; } hper = std::clamp(hper, 5, 95); if (monster.mode == MonsterMode::Petrified) hit = 0; if (monster.tryLiftGargoyle()) return true; if (hit >= hper) { #ifdef _DEBUG if (!DebugGodMode) #endif return false; } int dam; if (t == MissileID::BoneSpirit) { dam = monster.hitPoints / 3 >> 6; } else { dam = RandomIntBetween(mindam, maxdam); } if (missileData.isArrow() && damageType == DamageType::Physical) { dam = player._pIBonusDamMod + dam * player._pIBonusDam / 100 + dam; if (player._pClass == HeroClass::Rogue) dam += player._pDamageMod; else dam += player._pDamageMod / 2; if (monster.data().monsterClass == MonsterClass::Demon && HasAnyOf(player._pIFlags, ItemSpecialEffect::TripleDemonDamage)) dam *= 3; } const bool resist = monster.isResistant(t, damageType); if (!shift) dam <<= 6; if (resist) dam >>= 2; if (&player == MyPlayer) ApplyMonsterDamage(damageType, monster, dam); if (monster.hasNoLife()) { M_StartKill(monster, player); } else if (resist) { monster.tag(player); PlayEffect(monster, MonsterSound::Hit); } else { if (monster.mode != MonsterMode::Petrified && missileData.isArrow() && HasAnyOf(player._pIFlags, ItemSpecialEffect::Knockback)) M_GetKnockback(monster, startPos); if (monster.type().type != MT_GOLEM) M_StartHit(monster, player, dam); } if (monster.activeForTicks == 0) { monster.activeForTicks = UINT8_MAX; monster.position.last = player.position.tile; } return true; } bool Plr2PlrMHit(const Player &player, Player &target, int mindam, int maxdam, int dist, MissileID mtype, DamageType damageType, bool shift, bool *blocked) { if (sgGameInitInfo.bFriendlyFire == 0 && player.friendlyMode) return false; *blocked = false; if (target.isOnArenaLevel() && target._pmode == PM_WALK_SIDEWAYS) return false; if (target._pInvincible) { return false; } if (mtype == MissileID::HolyBolt) { return false; } const MissileData &missileData = GetMissileData(mtype); if (HasAnyOf(target._pSpellFlags, SpellFlag::Etherealize) && missileData.isArrow()) { return false; } int8_t resper; switch (damageType) { case DamageType::Fire: resper = target._pFireResist; break; case DamageType::Lightning: resper = target._pLghtResist; break; case DamageType::Magic: case DamageType::Acid: resper = target._pMagResist; break; default: resper = 0; break; } const int hper = GenerateRnd(100); int hit; if (missileData.isArrow()) { hit = player.GetRangedToHit() - (dist * dist / 2) - target.GetArmor(); } else { hit = player.GetMagicToHit() - (target.getCharacterLevel() * 2) - dist; } hit = std::clamp(hit, 5, 95); if (hper >= hit) { return false; } int blkper = 100; if (!shift && (target._pmode == PM_STAND || target._pmode == PM_ATTACK) && target._pBlockFlag) { blkper = GenerateRnd(100); } int blk = target.GetBlockChance() - (player.getCharacterLevel() * 2); blk = std::clamp(blk, 0, 100); int dam; if (mtype == MissileID::BoneSpirit) { dam = target._pHitPoints / 3; } else { dam = RandomIntBetween(mindam, maxdam); if (missileData.isArrow() && damageType == DamageType::Physical) { const int damMod = IsAnyOf(player._pClass, HeroClass::Rogue) ? player._pDamageMod : player._pDamageMod / 2; dam += player._pIBonusDamMod + damMod + dam * player._pIBonusDam / 100; } if (!shift) dam <<= 6; } if (!missileData.isArrow()) dam /= 2; if (resper > 0) { dam -= (dam * resper) / 100; if (&player == MyPlayer) NetSendCmdDamage(true, target, dam, damageType); target.Say(HeroSpeech::ArghClang); return true; } if (blkper < blk) { StartPlrBlock(target, GetDirection(target.position.tile, player.position.tile)); *blocked = true; } else { if (&player == MyPlayer) NetSendCmdDamage(true, target, dam, damageType); StartPlrHit(target, dam, false); } return true; } void RotateBlockedMissile(Missile &missile) { const int rotation = PickRandomlyAmong({ -1, 1 }); if (missile._miAnimType == MissileGraphicID::Arrow) { const int dir = missile._miAnimFrame + rotation; missile._miAnimFrame = (dir + 15) % 16 + 1; return; } int dir = missile.getFrameGroupRaw() + rotation; const int mAnimFAmt = GetMissileSpriteData(missile._miAnimType).animFAmt; if (dir < 0) dir = mAnimFAmt - 1; else if (dir >= mAnimFAmt) dir = 0; missile.setFrameGroupRaw(dir); } bool CheckCanHitOnlyWalking(const Missile &missile, const ActorPosition &position, Direction wallDir) { Point other = missile.position.tile + wallDir; if (missile.position.tile == position.tile && other == position.future) return true; if (missile.position.tile == position.future && other == position.tile) return true; return false; } void CheckMissileCol(Missile &missile, DamageType damageType, int minDamage, int maxDamage, bool isDamageShifted, Point position, bool dontDeleteOnCollision, std::optional onlyHitWalking = {}) { if (!InDungeonBounds(position)) return; bool isMonsterHit = false; int mid = dMonster[position.x][position.y]; if (mid != 0) { Monster &monster = Monsters[std::abs(mid) - 1]; if (onlyHitWalking.has_value() ? (monster.isWalking() && CheckCanHitOnlyWalking(missile, monster.position, *onlyHitWalking)) : (mid > 0 || monster.mode == MonsterMode::Petrified)) { if (missile.IsTrap() || (missile._micaster == TARGET_PLAYERS && ( // or was fired by a monster and monster.isPlayerMinion() != Monsters[missile._misource].isPlayerMinion() // the monsters are on opposing factions || (Monsters[missile._misource].flags & MFLAG_BERSERK) != 0 // or the attacker is berserked || (monster.flags & MFLAG_BERSERK) != 0 // or the target is berserked ))) { // then the missile can potentially hit this target isMonsterHit = MonsterTrapHit(monster, minDamage, maxDamage, missile._midist, missile._mitype, damageType, isDamageShifted); } else if (IsAnyOf(missile._micaster, TARGET_BOTH, TARGET_MONSTERS)) { isMonsterHit = MonsterMHit(*missile.sourcePlayer(), monster, minDamage, maxDamage, missile._midist, missile._mitype, missile.position.start, damageType, isDamageShifted); } } } if (isMonsterHit) { if (!dontDeleteOnCollision) missile.duration = 0; missile._miHitFlag = true; } bool isPlayerHit = false; bool blocked = false; Player *player = PlayerAtPosition(position, !onlyHitWalking.has_value()); if (player != nullptr && (onlyHitWalking.has_value() ? (player->isWalking() && CheckCanHitOnlyWalking(missile, player->position, *onlyHitWalking)) : true)) { if (missile._micaster != TARGET_BOTH && !missile.IsTrap()) { if (missile._micaster == TARGET_MONSTERS) { if (player->getId() != missile._misource) isPlayerHit = Plr2PlrMHit(Players[missile._misource], *player, minDamage, maxDamage, missile._midist, missile._mitype, damageType, isDamageShifted, &blocked); } else { Monster &monster = Monsters[missile._misource]; isPlayerHit = PlayerMHit(*player, &monster, missile._midist, minDamage, maxDamage, missile._mitype, damageType, isDamageShifted, DeathReason::MonsterOrTrap, &blocked); } } else { const DeathReason deathReason = missile.sourceType() == MissileSource::Player ? DeathReason::Player : DeathReason::MonsterOrTrap; isPlayerHit = PlayerMHit(*player, nullptr, missile._midist, minDamage, maxDamage, missile._mitype, damageType, isDamageShifted, deathReason, &blocked); } } if (isPlayerHit) { if (gbIsHellfire && blocked) { RotateBlockedMissile(missile); } else if (!dontDeleteOnCollision) { missile.duration = 0; } missile._miHitFlag = true; } if (IsMissileBlockedByTile(position)) { Object *object = FindObjectAtPosition(position); if (object != nullptr && object->IsBreakable()) { BreakObjectMissile(missile.sourcePlayer(), *object); } if (!dontDeleteOnCollision) missile.duration = 0; missile._miHitFlag = false; } const MissileData &missileData = GetMissileData(missile._mitype); if (missile.duration == 0 && missileData.hitSound != SfxID::None) PlaySfxLoc(missileData.hitSound, missile.position.tile); } bool MoveMissile(Missile &missile, tl::function_ref checkTile, bool ifCheckTileFailsDontMoveToTile = false) { Point prevTile = missile.position.tile; missile.position.traveled += missile.position.velocity; UpdateMissilePos(missile); int possibleVisitTiles; if (missile.position.velocity.deltaX == 0 || missile.position.velocity.deltaY == 0) possibleVisitTiles = prevTile.WalkingDistance(missile.position.tile); else possibleVisitTiles = prevTile.ManhattanDistance(missile.position.tile); if (possibleVisitTiles == 0) return false; // Did the missile skip a tile? if (possibleVisitTiles > 1) { auto speed = abs(missile.position.velocity); const auto denominator = static_cast((2 * speed.deltaY >= speed.deltaX) ? 2 * speed.deltaY : speed.deltaX); auto incVelocity = missile.position.velocity * ((32 << 16) / denominator); auto traveled = missile.position.traveled - missile.position.velocity; // Adjust the traveled vector to start on the next smallest multiple of incVelocity if (incVelocity.deltaY != 0) traveled.deltaY = (traveled.deltaY / incVelocity.deltaY) * incVelocity.deltaY; if (incVelocity.deltaX != 0) traveled.deltaX = (traveled.deltaX / incVelocity.deltaX) * incVelocity.deltaX; do { auto initialDiff = missile.position.traveled - traveled; traveled += incVelocity; auto incDiff = missile.position.traveled - traveled; // we are at the original calculated position => resume with normal logic if ((initialDiff.deltaX < 0) != (incDiff.deltaX < 0)) break; if ((initialDiff.deltaY < 0) != (incDiff.deltaY < 0)) break; // calculate in-between tile const Displacement pixelsTraveled = traveled >> 16; const Displacement tileOffset = pixelsTraveled.screenToMissile(); const Point tile = missile.position.start + tileOffset; // we haven't quite reached the missile's current position, // but we can break early to avoid checking collisions in this tile twice if (tile == missile.position.tile) break; // skip collision logic if the missile is on a corner between tiles if (pixelsTraveled.deltaY % 16 == 0 && pixelsTraveled.deltaX % 32 == 0 && std::abs(pixelsTraveled.deltaY / 16) % 2 != std::abs(pixelsTraveled.deltaX / 32) % 2) { continue; } // don't call checkTile more than once for a tile if (prevTile == tile) continue; prevTile = tile; if (!checkTile(tile)) { missile.position.traveled = traveled; if (ifCheckTileFailsDontMoveToTile) { missile.position.traveled -= incVelocity; UpdateMissilePos(missile); missile.position.StopMissile(); } else { UpdateMissilePos(missile); } return true; } } while (true); } if (!checkTile(missile.position.tile) && ifCheckTileFailsDontMoveToTile) { missile.position.traveled -= missile.position.velocity; UpdateMissilePos(missile); missile.position.StopMissile(); } return true; } void MoveMissileAndCheckMissileCol(Missile &missile, DamageType damageType, int mindam, int maxdam, bool ignoreStart, bool ifCollidesDontMoveToHitTile) { auto checkTile = [&](Point tile) { if (ignoreStart && missile.position.start == tile) return true; CheckMissileCol(missile, damageType, mindam, maxdam, false, tile, false); // Did missile hit anything? if (missile.duration != 0) return true; if (missile._miHitFlag && GetMissileData(missile._mitype).movementDistribution == MissileMovementDistribution::Blockable) return false; return !IsMissileBlockedByTile(tile); }; const bool tileChanged = MoveMissile(missile, checkTile, ifCollidesDontMoveToHitTile); const int16_t tileTargetHash = dMonster[missile.position.tile.x][missile.position.tile.y] ^ dPlayer[missile.position.tile.x][missile.position.tile.y]; // missile didn't change the tile... check that we perform CheckMissileCol only once for any monster/player to avoid multiple hits for slow missiles if (!tileChanged && missile.lastCollisionTargetHash != tileTargetHash) { CheckMissileCol(missile, damageType, mindam, maxdam, false, missile.position.tile, false); } // remember what target CheckMissileCol was checked against missile.lastCollisionTargetHash = tileTargetHash; } void AddRune(Missile &missile, Point dst, MissileID missileID) { if (LineClearMissile(missile.position.start, dst)) { std::optional runePosition = FindClosestValidPosition( [](Point target) { if (!InDungeonBounds(target)) { return false; } if (IsObjectAtPosition(target)) { return false; } if (TileContainsMissile(target)) { return false; } if (TileHasAny(target, TileProperties::Solid)) { return false; } return true; }, dst, 0, 9); if (runePosition) { missile.position.tile = *runePosition; missile.var1 = static_cast(missileID); missile._mlid = AddLight(missile.position.tile, 8); return; } } missile._miDelFlag = true; } bool CheckIfTrig(Point position) { for (int i = 0; i < numtrigs; i++) { if (trigs[i].position.WalkingDistance(position) < 2) return true; } return false; } bool GuardianTryFireAt(Missile &missile, Point target) { const Point position = missile.position.tile; if (!LineClearMovingMissile(position, target)) return false; const int mid = dMonster[target.x][target.y] - 1; if (mid < 0) return false; const Monster &monster = Monsters[mid]; if (monster.isPlayerMinion()) return false; if (monster.hasNoLife()) return false; const Player &player = Players[missile._misource]; int dmg = GenerateRnd(10) + (player.getCharacterLevel() / 2) + 1; dmg = ScaleSpellEffect(dmg, missile._mispllvl); const Direction dir = GetDirection(position, target); AddMissile(position, target, dir, MissileID::Firebolt, TARGET_MONSTERS, missile._misource, missile._midam, missile.sourcePlayer()->GetSpellLevel(SpellID::Guardian), &missile); missile.setFrameGroup(GuardianFrame::Attack); missile.var2 = 3; return true; } bool CanPlaceWall(Point position) { if (!InDungeonBounds(position)) return false; [[maybe_unused]] const int dp = dPiece[position.x][position.y]; assert(dp <= MAXTILES && dp >= 0); return !TileHasAny(position, TileProperties::BlockMissile); } Missile *PlaceWall(int id, MissileID type, Point position, Direction direction, int spellLevel, int damage) { return AddMissile(position, position + direction, direction, type, TARGET_BOTH, id, damage, spellLevel); } bool TryGrowWall(int id, MissileID type, Point position, Direction growDirection, Direction direction, int spellLevel, int damage) { switch (growDirection) { case Direction::South: case Direction::West: case Direction::North: case Direction::East: { Point gapPos = position + Displacement(Right(growDirection)) - Displacement(growDirection); if (CanPlaceWall(gapPos)) { Missile *missile = PlaceWall(id, type, gapPos, direction, spellLevel, damage); if (missile != nullptr) { Displacement travelled = Displacement(Right(Right(growDirection))).worldToNormalScreen() * 30; // Move the wall to the edge to the next tile, but not over it missile->position.traveled += travelled; missile->_miDrawFlag = false; missile->var3 = static_cast(Left(Left(growDirection))) + 1; UpdateMissilePos(*missile); assert(gapPos == missile->position.tile); // Check that the tile we checked against (CanPlaceWall) didn't change } } break; } default: break; } if (!CanPlaceWall(position)) return false; PlaceWall(id, type, position, direction, spellLevel, damage); return true; } std::optional MoveWallToNextPosition(Point position, Direction growDirection) { Point nextPosition = position + growDirection; if (!InDungeonBounds(nextPosition)) return std::nullopt; return nextPosition; } /** @brief Sync missile position with parent missile */ void SyncPositionWithParent(Missile &missile, const AddMissileParameter ¶meter) { const Missile *parent = parameter.pParent; if (parent == nullptr) return; missile.position.offset = parent->position.offset; missile.position.traveled = parent->position.traveled; } void SpawnLightning(Missile &missile, int dam) { missile.duration--; MoveMissile( missile, [&](Point tile) { assert(InDungeonBounds(tile)); [[maybe_unused]] const int pn = dPiece[tile.x][tile.y]; assert(pn >= 0 && pn <= MAXTILES); if (!missile.IsTrap() || tile != missile.position.start) { if (TileHasAny(tile, TileProperties::BlockMissile)) { missile.duration = 0; return false; } } return true; }); auto position = missile.position.tile; if (!TileHasAny(position, TileProperties::BlockMissile)) { if (position != Point { missile.var1, missile.var2 } && InDungeonBounds(position)) { MissileID type = MissileID::Lightning; if (missile.sourceType() == MissileSource::Monster && IsAnyOf(missile.sourceMonster()->type().type, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL)) { type = MissileID::ThinLightning; } AddMissile( position, missile.position.start, Direction::South, type, missile._micaster, missile._misource, dam, missile._mispllvl, &missile); missile.var1 = position.x; missile.var2 = position.y; } } if (missile.duration == 0) { missile._miDelFlag = true; } } } // namespace #ifdef BUILD_TESTING void TestRotateBlockedMissile(Missile &missile) { RotateBlockedMissile(missile); } #endif bool IsMissileBlockedByTile(Point tile) { if (!InDungeonBounds(tile)) { return true; } if (TileHasAny(tile, TileProperties::BlockMissile)) { return true; } Object *object = FindObjectAtPosition(tile); // _oMissFlag is true if the object allows missiles to pass through so we need to invert the check here... return object != nullptr && !object->_oMissFlag; } DamageRange GetDamageAmt(SpellID spell, int spellLevel) { assert(MyPlayer != nullptr); assert(spell >= SpellID::FIRST && spell <= SpellID::LAST); const Player &myPlayer = *MyPlayer; switch (spell) { case SpellID::Firebolt: { const int min = (myPlayer._pMagic / 8) + spellLevel + 1; return { min, min + 9 }; } case SpellID::Healing: case SpellID::HealOther: /// BUGFIX: healing calculation is unused return { AddClassHealingBonus(myPlayer.getCharacterLevel() + spellLevel + 1, myPlayer._pClass) - 1, AddClassHealingBonus((4 * myPlayer.getCharacterLevel()) + (6 * spellLevel) + 10, myPlayer._pClass) - 1 }; case SpellID::RuneOfLight: case SpellID::Lightning: return { 2, 2 + myPlayer.getCharacterLevel() }; case SpellID::Flash: { int min = ScaleSpellEffect(myPlayer.getCharacterLevel(), spellLevel); min += min / 2; return { min, min * 2 }; }; case SpellID::Identify: case SpellID::TownPortal: case SpellID::StoneCurse: case SpellID::Infravision: case SpellID::Phasing: case SpellID::ManaShield: case SpellID::DoomSerpents: case SpellID::BloodRitual: case SpellID::Invisibility: case SpellID::Rage: case SpellID::Teleport: case SpellID::Etherealize: case SpellID::ItemRepair: case SpellID::StaffRecharge: case SpellID::TrapDisarm: case SpellID::Resurrect: case SpellID::Telekinesis: case SpellID::BoneSpirit: case SpellID::Warp: case SpellID::Reflect: case SpellID::Berserk: case SpellID::Search: case SpellID::RuneOfStone: return { -1, -1 }; case SpellID::FireWall: case SpellID::LightningWall: case SpellID::RingOfFire: { const int min = 2 * myPlayer.getCharacterLevel() + 4; return { min, min + 36 }; } case SpellID::Fireball: case SpellID::RuneOfFire: { const int base = (2 * myPlayer.getCharacterLevel()) + 4; return { ScaleSpellEffect(base, spellLevel), ScaleSpellEffect(base + 36, spellLevel) }; } break; case SpellID::Guardian: { const int base = (myPlayer.getCharacterLevel() / 2) + 1; return { ScaleSpellEffect(base, spellLevel), ScaleSpellEffect(base + 9, spellLevel) }; } break; case SpellID::ChainLightning: return { 4, 4 + (2 * myPlayer.getCharacterLevel()) }; case SpellID::FlameWave: { const int min = 6 * (myPlayer.getCharacterLevel() + 1); return { min, min + 54 }; } case SpellID::Nova: case SpellID::Immolation: case SpellID::RuneOfImmolation: case SpellID::RuneOfNova: return { ScaleSpellEffect((myPlayer.getCharacterLevel() + 5) / 2, spellLevel) * 5, ScaleSpellEffect((myPlayer.getCharacterLevel() + 30) / 2, spellLevel) * 5 }; case SpellID::Inferno: { int max = myPlayer.getCharacterLevel() + 4; max += max / 2; return { 3, max }; } case SpellID::Golem: return { 11, 17 }; case SpellID::Apocalypse: return { myPlayer.getCharacterLevel(), myPlayer.getCharacterLevel() * 6 }; case SpellID::Elemental: /// BUGFIX: Divide min and max by 2 return { ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 4, spellLevel), ScaleSpellEffect(2 * myPlayer.getCharacterLevel() + 40, spellLevel) }; case SpellID::ChargedBolt: return { 1, 1 + (myPlayer._pMagic / 4) }; case SpellID::HolyBolt: return { myPlayer.getCharacterLevel() + 9, myPlayer.getCharacterLevel() + 18 }; case SpellID::BloodStar: { const int min = (myPlayer._pMagic / 2) + 3 * spellLevel - (myPlayer._pMagic / 8); return { min, min }; } default: return { -1, -1 }; } } Direction16 GetDirection16(Point p1, Point p2) { const Displacement offset = p2 - p1; Displacement absolute = abs(offset); const bool flipY = offset.deltaX != absolute.deltaX; const bool flipX = offset.deltaY != absolute.deltaY; bool flipMedian = false; if (absolute.deltaX > absolute.deltaY) { std::swap(absolute.deltaX, absolute.deltaY); flipMedian = true; } Direction16 ret = Direction16::South; if (3 * absolute.deltaX <= (absolute.deltaY * 2)) { // mx/my <= 2/3, approximation of tan(33.75) if (5 * absolute.deltaX < absolute.deltaY) // mx/my < 0.2, approximation of tan(11.25) ret = Direction16::SouthWest; else ret = Direction16::South_SouthWest; } Direction16 medianPivot = Direction16::South; if (flipY) { ret = Direction16Flip(ret, Direction16::SouthWest); medianPivot = Direction16Flip(medianPivot, Direction16::SouthWest); } if (flipX) { ret = Direction16Flip(ret, Direction16::SouthEast); medianPivot = Direction16Flip(medianPivot, Direction16::SouthEast); } if (flipMedian) ret = Direction16Flip(ret, medianPivot); return ret; } bool MonsterTrapHit(Monster &monster, int mindam, int maxdam, int dist, MissileID t, DamageType damageType, bool shift) { if (!monster.isPossibleToHit() || monster.isImmune(t, damageType)) return false; const int hit = GenerateRnd(100); int hper = 90 - monster.armorClass - dist; hper = std::clamp(hper, 5, 95); if (monster.tryLiftGargoyle()) return true; if (hit >= hper && monster.mode != MonsterMode::Petrified) { #ifdef _DEBUG if (!DebugGodMode) #endif return false; } const bool resist = monster.isResistant(t, damageType); int dam = RandomIntBetween(mindam, maxdam); if (!shift) dam <<= 6; if (resist) dam /= 4; ApplyMonsterDamage(damageType, monster, dam); #ifdef _DEBUG if (DebugGodMode) monster.hitPoints = 0; #endif if (monster.hasNoLife()) { MonsterDeath(monster, monster.direction, true); } else if (resist) { PlayEffect(monster, MonsterSound::Hit); } else if (monster.type().type != MT_GOLEM) { M_StartHit(monster, dam); } return true; } bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd, MissileID mtype, DamageType damageType, bool shift, DeathReason deathReason, bool *blocked) { *blocked = false; if (player.hasNoLife()) { return false; } if (player._pInvincible) { return false; } const MissileData &missileData = GetMissileData(mtype); if (HasAnyOf(player._pSpellFlags, SpellFlag::Etherealize) && missileData.isArrow()) { return false; } int hit = GenerateRnd(100); #ifdef _DEBUG if (DebugGodMode) hit = 1000; #endif int hper = 40; if (missileData.isArrow()) { const int tac = player.GetArmor(); if (monster != nullptr) { hper = monster->toHit(sgGameInitInfo.nDifficulty) + ((monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2) + 30 - (dist * 2) - tac; } else { hper = 100 - (tac / 2) - (dist * 2); } } else if (monster != nullptr) { hper += (monster->level(sgGameInitInfo.nDifficulty) * 2) - (player.getCharacterLevel() * 2) - (dist * 2); } int minhit = 10; if (currlevel == 14) minhit = 20; if (currlevel == 15) minhit = 25; if (currlevel == 16) minhit = 30; hper = std::max(hper, minhit); int blk = 100; if ((player._pmode == PM_STAND || player._pmode == PM_ATTACK) && player._pBlockFlag) { blk = GenerateRnd(100); } if (shift) blk = 100; if (mtype == MissileID::AcidPuddle) blk = 100; int blkper = player.GetBlockChance(false); if (monster != nullptr) blkper -= (monster->level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) * 2; blkper = std::clamp(blkper, 0, 100); int8_t resper; switch (damageType) { case DamageType::Fire: resper = player._pFireResist; break; case DamageType::Lightning: resper = player._pLghtResist; break; case DamageType::Magic: case DamageType::Acid: resper = player._pMagResist; break; default: resper = 0; break; } if (hit >= hper) { return false; } int dam; if (mtype == MissileID::BoneSpirit) { dam = player._pHitPoints / 3; } else { if (!shift) { // New method fixes a bug which caused the maximum possible damage value to be 63/64ths too low. dam = RandomIntBetween(mind << 6, maxd << 6); if (monster == nullptr) if (HasAnyOf(player._pIFlags, ItemSpecialEffect::HalfTrapDamage)) dam /= 2; dam += player._pIGetHit * 64; } else { dam = RandomIntBetween(mind, maxd); if (monster == nullptr) if (HasAnyOf(player._pIFlags, ItemSpecialEffect::HalfTrapDamage)) dam /= 2; dam += player._pIGetHit; } dam = std::max(dam, 64); } if ((resper <= 0 || gbIsHellfire) && blk < blkper) { Direction dir = player._pdir; if (monster != nullptr) { dir = GetDirection(player.position.tile, monster->position.tile); } *blocked = true; StartPlrBlock(player, dir); return true; } if (monster != nullptr) { MonsterReducePlayerAttribute(*monster, player); } if (resper > 0) { dam -= dam * resper / 100; if (&player == MyPlayer) { ApplyPlrDamage(damageType, player, 0, 0, dam, deathReason); } if (!player.hasNoLife()) { player.Say(HeroSpeech::ArghClang); } return true; } if (&player == MyPlayer) { ApplyPlrDamage(damageType, player, 0, 0, dam, deathReason); } if (!player.hasNoLife()) { StartPlrHit(player, dam, false); } return true; } void InitMissiles() { Player &myPlayer = *MyPlayer; AutoMapShowItems = false; myPlayer._pSpellFlags &= ~SpellFlag::Etherealize; if (myPlayer._pInfraFlag) { for (auto &missile : Missiles) { if (missile._mitype == MissileID::Infravision) { if (missile.sourcePlayer() == MyPlayer) CalcPlrItemVals(myPlayer, true); } } } if (HasAnyOf(myPlayer._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown)) { myPlayer._pSpellFlags &= ~SpellFlag::RageActive; myPlayer._pSpellFlags &= ~SpellFlag::RageCooldown; for (auto &missile : Missiles) { if (missile._mitype == MissileID::Rage) { if (missile.sourcePlayer() == MyPlayer) { CalcPlrItemVals(myPlayer, true); ApplyPlrDamage(DamageType::Physical, myPlayer, missile._midam, 1); } } } } Missiles.clear(); for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) dFlags[i][j] &= ~(DungeonFlag::Missile | DungeonFlag::MissileFireWall | DungeonFlag::MissileLightningWall); } } } void AddOpenNest(Missile &missile, AddMissileParameter ¶meter) { for (const WorldTileCoord x : { 80, 81 }) { for (const WorldTileCoord y : { 62, 63 }) { AddMissile({ x, y }, { 80, 62 }, parameter.midir, MissileID::BigExplosion, missile._micaster, missile._misource, missile._midam, 0); } } missile._miDelFlag = true; } void AddRuneOfFire(Missile &missile, AddMissileParameter ¶meter) { AddRune(missile, parameter.dst, MissileID::BigExplosion); } void AddRuneOfLight(Missile &missile, AddMissileParameter ¶meter) { const int lvl = (missile.sourceType() == MissileSource::Player) ? missile.sourcePlayer()->getCharacterLevel() : 0; const int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2); missile._midam = dmg; AddRune(missile, parameter.dst, MissileID::LightningWall); } void AddRuneOfNova(Missile &missile, AddMissileParameter ¶meter) { AddRune(missile, parameter.dst, MissileID::Nova); } void AddRuneOfImmolation(Missile &missile, AddMissileParameter ¶meter) { AddRune(missile, parameter.dst, MissileID::Immolation); } void AddRuneOfStone(Missile &missile, AddMissileParameter ¶meter) { AddRune(missile, parameter.dst, MissileID::StoneCurse); } void AddReflect(Missile &missile, AddMissileParameter & /*parameter*/) { missile._miDelFlag = true; if (missile.sourceType() != MissileSource::Player) return; Player &player = *missile.sourcePlayer(); int add = (missile._mispllvl != 0 ? missile._mispllvl : 2) * player.getCharacterLevel(); if (player.wReflections + add >= std::numeric_limits::max()) add = 0; player.wReflections += add; if (&player == MyPlayer) NetSendCmdParam1(true, CMD_SETREFLECT, player.wReflections); } void AddBerserk(Missile &missile, AddMissileParameter ¶meter) { missile._miDelFlag = true; parameter.spellFizzled = true; if (missile.sourceType() == MissileSource::Trap) return; std::optional targetMonsterPosition = FindClosestValidPosition( [](Point target) { if (!InDungeonBounds(target)) { return false; } const int monsterId = std::abs(dMonster[target.x][target.y]) - 1; if (monsterId < 0) return false; const Monster &monster = Monsters[monsterId]; if (monster.isPlayerMinion()) return false; if ((monster.flags & MFLAG_BERSERK) != 0) return false; if (monster.isUnique() || monster.ai == MonsterAIID::Diablo) return false; if (IsAnyOf(monster.mode, MonsterMode::FadeIn, MonsterMode::FadeOut, MonsterMode::Charge)) return false; if ((monster.resistance & IMMUNE_MAGIC) != 0) return false; if ((monster.resistance & RESIST_MAGIC) != 0 && ((monster.resistance & RESIST_MAGIC) != 1 || !FlipCoin())) return false; return true; }, parameter.dst, 0, 5); if (targetMonsterPosition) { Monster &monster = Monsters[std::abs(dMonster[targetMonsterPosition->x][targetMonsterPosition->y]) - 1]; const Player &player = *missile.sourcePlayer(); const int slvl = player.GetSpellLevel(SpellID::Berserk); monster.flags |= MFLAG_BERSERK | MFLAG_GOLEM; monster.minDamage = (GenerateRnd(10) + 120) * monster.minDamage / 100 + slvl; monster.maxDamage = (GenerateRnd(10) + 120) * monster.maxDamage / 100 + slvl; monster.minDamageSpecial = (GenerateRnd(10) + 120) * monster.minDamageSpecial / 100 + slvl; monster.maxDamageSpecial = (GenerateRnd(10) + 120) * monster.maxDamageSpecial / 100 + slvl; const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3; monster.lightId = AddLight(monster.position.tile, lightRadius); parameter.spellFizzled = false; } } void AddHorkSpawn(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 8); missile.duration = 9; missile.var1 = static_cast(parameter.midir); PutMissile(missile); } void AddJester(Missile &missile, AddMissileParameter ¶meter) { MissileID spell = MissileID::Firebolt; switch (RandomIntLessThan(10)) { case 0: case 1: spell = MissileID::Firebolt; break; case 2: spell = MissileID::Fireball; break; case 3: spell = MissileID::FireWallControl; break; case 4: spell = MissileID::Guardian; break; case 5: spell = MissileID::ChainLightning; break; case 6: spell = MissileID::TownPortal; break; case 7: spell = MissileID::Teleport; break; case 8: spell = MissileID::Apocalypse; break; case 9: spell = MissileID::StoneCurse; break; } Missile *randomMissile = AddMissile(missile.position.start, parameter.dst, parameter.midir, spell, missile._micaster, missile._misource, 0, missile._mispllvl); parameter.spellFizzled = randomMissile == nullptr; missile._miDelFlag = true; } void AddStealPotions(Missile &missile, AddMissileParameter & /*parameter*/) { Crawl(0, 2, [&](Displacement displacement) { const Point target = missile.position.start + displacement; if (!InDungeonBounds(target)) return false; Player *player = PlayerAtPosition(target); if (player == nullptr) return false; bool hasPlayedSFX = false; for (int si = 0; si < MaxBeltItems; si++) { Item &beltItem = player->SpdList[si]; _item_indexes ii = IDI_NONE; if (beltItem._itype == ItemType::Misc) { if (FlipCoin()) continue; switch (beltItem._iMiscId) { case IMISC_FULLHEAL: ii = ItemMiscIdIdx(IMISC_HEAL); break; case IMISC_HEAL: case IMISC_MANA: player->RemoveSpdBarItem(si); break; case IMISC_FULLMANA: ii = ItemMiscIdIdx(IMISC_MANA); break; case IMISC_REJUV: ii = ItemMiscIdIdx(PickRandomlyAmong({ IMISC_HEAL, IMISC_MANA })); break; case IMISC_FULLREJUV: switch (GenerateRnd(3)) { case 0: ii = ItemMiscIdIdx(IMISC_FULLMANA); break; case 1: ii = ItemMiscIdIdx(IMISC_FULLHEAL); break; default: ii = ItemMiscIdIdx(IMISC_REJUV); break; } break; default: continue; } } if (ii != IDI_NONE) { auto seed = beltItem._iSeed; InitializeItem(beltItem, ii); beltItem._iSeed = seed; beltItem._iStatFlag = true; } if (!hasPlayedSFX) { PlaySfxLoc(SfxID::PodPop, target); hasPlayedSFX = true; } } RedrawEverything(); return false; }); missile._miDelFlag = true; } void AddStealMana(Missile &missile, AddMissileParameter & /*parameter*/) { std::optional trappedPlayerPosition = FindClosestValidPosition( [](Point target) { return InDungeonBounds(target) && dPlayer[target.x][target.y] != 0; }, missile.position.start, 0, 2); if (trappedPlayerPosition) { Player &player = Players[std::abs(dPlayer[trappedPlayerPosition->x][trappedPlayerPosition->y]) - 1]; player._pMana = 0; player._pManaBase = player._pMana + player._pMaxManaBase - player._pMaxMana; CalcPlrInv(player, false); RedrawComponent(PanelDrawComponent::Mana); PlaySfxLoc(SfxID::Pig, *trappedPlayerPosition); } missile._miDelFlag = true; } void AddSpectralArrow(Missile &missile, AddMissileParameter ¶meter) { int av = 0; if (missile.sourceType() == MissileSource::Player) { const Player &player = *missile.sourcePlayer(); if (player._pClass == HeroClass::Rogue) av += (player.getCharacterLevel() - 1) / 4; else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard) av += (player.getCharacterLevel() - 1) / 8; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) av++; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastAttack)) av += 2; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FasterAttack)) av += 4; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastestAttack)) av += 8; } missile.duration = 1; missile.var1 = parameter.dst.x; missile.var2 = parameter.dst.y; missile.var3 = av; } void AddWarp(Missile &missile, AddMissileParameter ¶meter) { int minDistanceSq = std::numeric_limits::max(); const int id = missile._misource; Player &player = Players[id]; Point tile = player.position.tile; for (int i = 0; i < numtrigs && i < MAXTRIGGERS; i++) { TriggerStruct *trg = &trigs[i]; if (IsNoneOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL, WM_DIABNEXTLVL, WM_DIABRTNLVL)) continue; Point candidate = trg->position; auto getTriggerOffset = [](TriggerStruct *trg) { switch (leveltype) { case DTYPE_CATHEDRAL: if (setlevel && setlvlnum == SL_VILEBETRAYER) return Displacement { 1, 1 }; // Portal if (IsAnyOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL, WM_DIABRTNLVL)) return Displacement { 1, 2 }; return Displacement { 0, 1 }; // WM_DIABNEXTLVL case DTYPE_CATACOMBS: if (IsAnyOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL)) return Displacement { 1, 1 }; return Displacement { 0, 1 }; // WM_DIABRTNLVL, WM_DIABNEXTLVL case DTYPE_CAVES: if (IsAnyOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL)) return Displacement { 0, 1 }; return Displacement { 1, 0 }; // WM_DIABRTNLVL, WM_DIABNEXTLVL case DTYPE_HELL: return Displacement { 1, 0 }; case DTYPE_NEST: if (IsAnyOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL, WM_DIABRTNLVL)) return Displacement { 0, 1 }; return Displacement { 1, 0 }; // WM_DIABNEXTLVL case DTYPE_CRYPT: if (IsAnyOf(trg->_tmsg, WM_DIABTWARPUP, WM_DIABPREVLVL, WM_DIABRTNLVL)) return Displacement { 1, 1 }; return Displacement { 0, 1 }; // WM_DIABNEXTLVL case DTYPE_TOWN: app_fatal("invalid leveltype: DTYPE_TOWN"); case DTYPE_NONE: app_fatal("leveltype not set"); } app_fatal(StrCat("invalid leveltype", static_cast(leveltype))); }; const Displacement triggerOffset = getTriggerOffset(trg); candidate += triggerOffset; const Displacement off = Point { player.position.tile } - candidate; const int distanceSq = (off.deltaY * off.deltaY) + (off.deltaX * off.deltaX); if (distanceSq < minDistanceSq) { minDistanceSq = distanceSq; tile = candidate; } } missile.duration = 2; std::optional teleportDestination = FindClosestValidPosition( [&player](Point target) { for (int i = 0; i < numtrigs; i++) { if (trigs[i].position == target) return false; } return PosOkPlayer(player, target); }, tile, 0, 5); if (teleportDestination) { missile.position.tile = *teleportDestination; } else { // No valid teleport destination found missile._miDelFlag = true; parameter.spellFizzled = true; } } void AddLightningWall(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 16); missile._miAnimFrame = RandomIntBetween(1, 8); missile.duration = 255 * (missile._mispllvl + 1); switch (missile.sourceType()) { case MissileSource::Trap: missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; break; case MissileSource::Player: { const Player &player = *missile.sourcePlayer(); missile.var1 = player.position.tile.x; missile.var2 = player.position.tile.y; } break; case MissileSource::Monster: assert(missile.sourceType() != MissileSource::Monster); break; } } void AddBigExplosion(Missile &missile, AddMissileParameter & /*parameter*/) { if (missile.sourceType() == MissileSource::Player) { int dmg = (2 * (missile.sourcePlayer()->getCharacterLevel() + GenerateRndSum(10, 2))) + 4; dmg = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam = dmg; const DamageType damageType = GetMissileData(missile._mitype).damageType(); for (const Point position : PointsInRectangleColMajor(Rectangle { missile.position.tile, 1 })) CheckMissileCol(missile, damageType, dmg, dmg, false, position, true); } missile._mlid = AddLight(missile.position.start, 8); missile.setDefaultFrameGroup(); missile.duration = missile._miAnimLen - 1; } void AddImmolation(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == parameter.dst) { dst += parameter.midir; } int sp = 16; if (missile._micaster == TARGET_MONSTERS) { sp += std::min(missile._mispllvl, 34); } UpdateMissileVelocity(missile, dst, sp); missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile._mlid = AddLight(missile.position.start, 8); } void AddLightningBow(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == parameter.dst) { dst += parameter.midir; } UpdateMissileVelocity(missile, dst, 32); missile._miAnimFrame = RandomIntBetween(1, 8); missile.duration = 255; if (missile._misource < 0) { missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; } else { missile.var1 = Players[missile._misource].position.tile.x; missile.var2 = Players[missile._misource].position.tile.y; } missile._midam <<= 6; } void AddMana(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; int manaAmount = (GenerateRnd(10) + 1) << 6; for (int i = 0; i < player.getCharacterLevel(); i++) { manaAmount += RandomIntBetween(1, 4) << 6; } for (int i = 0; i < missile._mispllvl; i++) { manaAmount += (GenerateRnd(6) + 1) << 6; } if (player._pClass == HeroClass::Sorcerer) manaAmount *= 2; if (player._pClass == HeroClass::Rogue || player._pClass == HeroClass::Bard) manaAmount += manaAmount / 2; player._pMana += manaAmount; player._pMana = std::min(player._pMana, player._pMaxMana); player._pManaBase += manaAmount; player._pManaBase = std::min(player._pManaBase, player._pMaxManaBase); missile._miDelFlag = true; RedrawComponent(PanelDrawComponent::Mana); } void AddMagi(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; missile._miDelFlag = true; RedrawComponent(PanelDrawComponent::Mana); } void AddRingOfFire(Missile &missile, AddMissileParameter & /*parameter*/) { missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile.duration = 7; } void AddSearch(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; if (&player == MyPlayer) AutoMapShowItems = true; int lvl = 2; if (missile._misource >= 0) lvl = player.getCharacterLevel() * 2; missile.duration = lvl + 10 * missile._mispllvl + 245; for (auto &other : Missiles) { if (&other != &missile && missile.isSameSource(other) && other._mitype == MissileID::Search) { const int r1 = missile.duration; const int r2 = other.duration; if (r2 < INT_MAX - r1) other.duration = r1 + r2; missile._miDelFlag = true; break; } } } void AddChargedBoltBow(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; missile._mirnd = GenerateRnd(15) + 1; if (missile._micaster != TARGET_MONSTERS) { missile._midam = 15; } if (missile.position.start == dst) { dst += parameter.midir; } missile._miAnimFrame = RandomIntBetween(1, 8); missile._mlid = AddLight(missile.position.start, 5); UpdateMissileVelocity(missile, dst, 8); missile.var1 = 5; missile.var2 = static_cast(parameter.midir); missile.duration = 256; } void AddElementalArrow(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } int av = 32; if (missile._micaster == TARGET_MONSTERS) { const Player &player = Players[missile._misource]; if (player._pClass == HeroClass::Rogue) av += (player.getCharacterLevel()) / 4; else if (IsAnyOf(player._pClass, HeroClass::Warrior, HeroClass::Bard)) av += (player.getCharacterLevel()) / 8; if (gbIsHellfire) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) av++; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastAttack)) av += 2; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FasterAttack)) av += 4; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastestAttack)) av += 8; } else { if (IsAnyOf(player._pClass, HeroClass::Rogue, HeroClass::Warrior, HeroClass::Bard)) av -= 1; } } UpdateMissileVelocity(missile, dst, av); missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 5); } void AddArrow(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } int av = 32; if (missile._micaster == TARGET_MONSTERS) { const Player &player = Players[missile._misource]; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::RandomArrowVelocity)) { av = RandomIntBetween(16, 47); } if (player._pClass == HeroClass::Rogue) av += (player.getCharacterLevel() - 1) / 4; else if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Bard) av += (player.getCharacterLevel() - 1) / 8; if (gbIsHellfire) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::QuickAttack)) av++; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastAttack)) av += 2; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FasterAttack)) av += 4; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastestAttack)) av += 8; } } UpdateMissileVelocity(missile, dst, av); missile._miAnimFrame = static_cast(GetDirection16(missile.position.start, dst)) + 1; missile.duration = 256; } void UpdateVileMissPos(Missile &missile, Point dst) { for (int k = 1; k < 50; k++) { for (int j = -k; j <= k; j++) { const int yy = j + dst.y; for (int i = -k; i <= k; i++) { const int xx = i + dst.x; if (PosOkPlayer(*MyPlayer, { xx, yy })) { missile.position.tile = WorldTilePosition(xx, yy); return; } } } } } void AddPhasing(Missile &missile, AddMissileParameter ¶meter) { missile.duration = 2; const Player &player = Players[missile._misource]; if (missile._micaster == TARGET_BOTH) { missile.position.tile = parameter.dst; if (!PosOkPlayer(player, parameter.dst)) UpdateVileMissPos(missile, parameter.dst); return; } std::array(4 * 9)> targets; int count = 0; for (int y = -6; y <= 6; y++) { for (int x = -6; x <= 6; x++) { if ((x >= -3 && x <= 3) || (y >= -3 && y <= 3)) continue; // Skip center const Point target = missile.position.start + Displacement { x, y }; if (!PosOkPlayer(player, target)) continue; targets[count] = target; count++; } } if (count == 0) { missile._miDelFlag = true; return; } missile.position.tile = targets[std::max(GenerateRnd(count), 0)]; } void AddFirebolt(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } int sp = 26; if (missile._micaster == TARGET_MONSTERS) { sp = 16; if (!missile.IsTrap()) { sp += std::min(missile._mispllvl * 2, 47); } } UpdateMissileVelocity(missile, dst, sp); missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); if (missile._midam == 0) { switch (missile.sourceType()) { case MissileSource::Player: { const Player &player = *missile.sourcePlayer(); missile._midam = GenerateRnd(10) + (player._pMagic / 8) + missile._mispllvl + 1; } break; case MissileSource::Monster: missile._midam = ProjectileMonsterDamage(missile); break; case MissileSource::Trap: missile._midam = ProjectileTrapDamage(); break; } } } void AddMagmaBall(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 16); missile.position.traveled.deltaX += 3 * missile.position.velocity.deltaX; missile.position.traveled.deltaY += 3 * missile.position.velocity.deltaY; UpdateMissilePos(missile); if (!gbIsHellfire || (missile.position.velocity.deltaX & 0xFFFF0000) != 0 || (missile.position.velocity.deltaY & 0xFFFF0000) != 0) missile.duration = 256; else missile.duration = 1; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); if (missile._midam == 0) { switch (missile.sourceType()) { case MissileSource::Player: // Not typically created by Players break; case MissileSource::Monster: missile._midam = ProjectileMonsterDamage(missile); break; case MissileSource::Trap: missile._midam = ProjectileTrapDamage(); break; } } } void AddTeleport(Missile &missile, AddMissileParameter ¶meter) { Player &player = Players[missile._misource]; std::optional teleportDestination = FindClosestValidPosition( [&player](Point target) { return PosOkPlayer(player, target); }, parameter.dst, 0, 5); if (teleportDestination) { missile.position.tile = *teleportDestination; missile.position.start = *teleportDestination; missile.duration = 2; } else { missile._miDelFlag = true; parameter.spellFizzled = true; } } void AddNovaBall(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 16); missile._miAnimFrame = RandomIntBetween(1, 8); missile.duration = 255; const WorldTilePosition position = missile._misource < 0 ? missile.position.start : Players[missile._misource].position.tile; missile.var1 = position.x; missile.var2 = position.y; } void AddFireWall(Missile &missile, AddMissileParameter ¶meter) { missile._midam = GenerateRndSum(10, 2) + 2; missile._midam += missile._misource >= 0 ? Players[missile._misource].getCharacterLevel() : currlevel; // BUGFIX: missing parenthesis around ternary (fixed) missile._midam <<= 3; UpdateMissileVelocity(missile, parameter.dst, 16); const int i = missile._mispllvl; missile.duration = 10; if (i > 0) missile.duration *= i + 1; if (missile._micaster == TARGET_PLAYERS || missile._misource < 0) missile.duration += currlevel; missile.duration *= 16; missile.var1 = missile.duration - missile._miAnimLen; } void AddFireball(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } int sp = 16; if (missile._micaster == TARGET_MONSTERS) { sp += std::min(missile._mispllvl * 2, 34); const Player &player = Players[missile._misource]; const int dmg = (2 * (player.getCharacterLevel() + GenerateRndSum(10, 2))) + 4; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); } UpdateMissileVelocity(missile, dst, sp); missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); } void AddLightningControl(Missile &missile, AddMissileParameter ¶meter) { missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; UpdateMissileVelocity(missile, parameter.dst, 32); missile._miAnimFrame = RandomIntBetween(1, 8); missile.duration = 256; } void AddLightning(Missile &missile, AddMissileParameter ¶meter) { missile.position.start = parameter.dst; SyncPositionWithParent(missile, parameter); missile._miAnimFrame = RandomIntBetween(1, 8); if (missile._micaster == TARGET_PLAYERS || missile.IsTrap()) { if (missile.IsTrap() || Monsters[missile._misource].type().type == MT_FAMILIAR) missile.duration = 8; else missile.duration = 10; } else { missile.duration = (missile._mispllvl / 2) + 6; } missile._mlid = AddLight(missile.position.tile, 4); } void AddMissileExplosion(Missile &missile, AddMissileParameter ¶meter) { if (missile._micaster != TARGET_MONSTERS && missile._misource >= 0) { switch (Monsters[missile._misource].type().type) { case MT_SUCCUBUS: missile.setAnimation(MissileGraphicID::BloodStarExplosion); break; case MT_SNOWWICH: missile.setAnimation(MissileGraphicID::BloodStarBlueExplosion); break; case MT_HLSPWN: missile.setAnimation(MissileGraphicID::BloodStarRedExplosion); break; case MT_SOLBRNR: missile.setAnimation(MissileGraphicID::BloodStarYellowExplosion); break; default: break; } } assert(parameter.pParent != nullptr); // AddMissileExplosion will always be called with a parent associated to the missile. auto &parent = *parameter.pParent; missile.position.tile = parent.position.tile; missile.position.start = parent.position.start; missile.position.offset = parent.position.offset; missile.position.traveled = parent.position.traveled; missile.duration = missile._miAnimLen; } void AddWeaponExplosion(Missile &missile, AddMissileParameter ¶meter) { missile.var2 = parameter.dst.x; if (parameter.dst.x == 1) missile.setAnimation(MissileGraphicID::MagmaBallExplosion); else missile.setAnimation(MissileGraphicID::ChargedBolt); missile.duration = missile._miAnimLen - 1; } void AddTownPortal(Missile &missile, AddMissileParameter ¶meter) { if (leveltype == DTYPE_TOWN) { missile.position.tile = parameter.dst; missile.position.start = parameter.dst; } else { std::optional targetPosition = FindClosestValidPosition( [](Point target) { if (!InDungeonBounds(target)) { return false; } if (IsObjectAtPosition(target)) { return false; } if (dPlayer[target.x][target.y] != 0) { return false; } if (TileContainsMissile(target)) { return false; } if (TileHasAny(target, TileProperties::Solid | TileProperties::BlockMissile)) { return false; } return !CheckIfTrig(target); }, parameter.dst, 0, 5); if (targetPosition) { missile.position.tile = *targetPosition; missile.position.start = *targetPosition; missile._miDelFlag = false; } else { missile._miDelFlag = true; } } missile.duration = 100; missile.var1 = missile.duration - missile._miAnimLen; for (auto &other : Missiles) { if (other._mitype == MissileID::TownPortal && &other != &missile && missile.isSameSource(other)) other.duration = 0; } PutMissile(missile); if (missile.sourcePlayer() == MyPlayer && !missile._miDelFlag && leveltype != DTYPE_TOWN) { if (!setlevel) { NetSendCmdLocParam3(true, CMD_ACTIVATEPORTAL, missile.position.tile, currlevel, leveltype, 0); } else { NetSendCmdLocParam3(true, CMD_ACTIVATEPORTAL, missile.position.tile, setlvlnum, leveltype, 1); } } } void AddFlashBottom(Missile &missile, AddMissileParameter & /*parameter*/) { switch (missile.sourceType()) { case MissileSource::Player: { const Player &player = *missile.sourcePlayer(); const int dmg = GenerateRndSum(20, player.getCharacterLevel() + 1) + player.getCharacterLevel() + 1; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam += missile._midam / 2; } break; case MissileSource::Monster: missile._midam = missile.sourceMonster()->level(sgGameInitInfo.nDifficulty) * 2; break; case MissileSource::Trap: missile._midam = currlevel / 2; break; } missile.duration = 19; } void AddFlashTop(Missile &missile, AddMissileParameter & /*parameter*/) { if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) { int dmg = Players[missile._misource].getCharacterLevel() + 1; dmg += GenerateRndSum(20, dmg); missile._midam = ScaleSpellEffect(dmg, missile._mispllvl); missile._midam += missile._midam / 2; } else { missile._midam = currlevel / 2; } } missile._miPreFlag = true; missile.duration = 19; } void AddManaShield(Missile &missile, AddMissileParameter ¶meter) { missile._miDelFlag = true; Player &player = Players[missile._misource]; if (player.pManaShield) { parameter.spellFizzled = true; return; } player.pManaShield = true; if (&player == MyPlayer) NetSendCmd(true, CMD_SETSHIELD); } void AddFlameWave(Missile &missile, AddMissileParameter ¶meter) { missile._midam = GenerateRnd(10) + Players[missile._misource].getCharacterLevel() + 1; UpdateMissileVelocity(missile, parameter.dst, 16); missile.duration = 255; // Adjust missile's position for rendering missile.position.tile += Direction::South; missile.position.offset.deltaY -= 32; } void AddGuardian(Missile &missile, AddMissileParameter ¶meter) { const Player &player = Players[missile._misource]; std::optional spawnPosition = FindClosestValidPosition( [start = missile.position.start](Point target) { if (!InDungeonBounds(target)) { return false; } if (dMonster[target.x][target.y] != 0) { return false; } if (IsObjectAtPosition(target)) { return false; } if (TileContainsMissile(target)) { return false; } if (TileHasAny(target, TileProperties::Solid | TileProperties::BlockMissile)) { return false; } return LineClearMissile(start, target); }, parameter.dst, 0, 5); if (!spawnPosition) { missile._miDelFlag = true; parameter.spellFizzled = true; return; } missile._miDelFlag = false; missile.position.tile = *spawnPosition; missile.position.start = *spawnPosition; missile._mlid = AddLight(missile.position.tile, 1); missile.duration = missile._mispllvl + (player.getCharacterLevel() / 2); missile.duration = std::min(missile.duration, 30); missile.duration <<= 4; missile.duration = std::max(missile.duration, 30); missile.var1 = missile.duration - missile._miAnimLen; missile.var3 = 1; } void AddChainLightning(Missile &missile, AddMissileParameter ¶meter) { missile.var1 = parameter.dst.x; missile.var2 = parameter.dst.y; missile.duration = 1; } namespace { void InitMissileAnimationFromMonster(Missile &mis, Direction midir, const Monster &mon, MonsterGraphic graphic) { const AnimStruct &anim = mon.type().getAnimData(graphic); mis.setDirection(midir); mis._miAnimFlags = MissileGraphicsFlags::None; const ClxSpriteList sprites = *anim.spritesForDirection(midir); const uint16_t width = sprites[0].width(); mis._miAnimData.emplace(sprites); mis._miAnimDelay = anim.rate; mis._miAnimLen = anim.frames; mis._miAnimWidth = width; mis._miAnimWidth2 = CalculateSpriteTileCenterX(width); mis._miAnimAdd = 1; mis.var1 = 0; mis.var2 = 0; mis._miLightFlag = true; mis.duration = 256; } } // namespace void AddRhino(Missile &missile, AddMissileParameter ¶meter) { const Monster &monster = Monsters[missile._misource]; MonsterGraphic graphic = MonsterGraphic::Walk; if (IsAnyOf(monster.type().type, MT_HORNED, MT_MUDRUN, MT_FROSTC, MT_OBLORD)) { graphic = MonsterGraphic::Special; } else if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) { graphic = MonsterGraphic::Attack; } UpdateMissileVelocity(missile, parameter.dst, 18); InitMissileAnimationFromMonster(missile, parameter.midir, monster, graphic); if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) missile._miAnimFrame = 7; if (monster.isUnique()) { missile._mlid = monster.lightId; } PutMissile(missile); } void AddGenericMagicMissile(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } UpdateMissileVelocity(missile, dst, 16); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); if (missile._micaster != TARGET_MONSTERS && missile._misource > 0) { const Monster &monster = Monsters[missile._misource]; if (monster.type().type == MT_SUCCUBUS) missile.setAnimation(MissileGraphicID::BloodStar); if (monster.type().type == MT_SNOWWICH) missile.setAnimation(MissileGraphicID::BloodStarBlue); if (monster.type().type == MT_HLSPWN) missile.setAnimation(MissileGraphicID::BloodStarRed); if (monster.type().type == MT_SOLBRNR) missile.setAnimation(MissileGraphicID::BloodStarYellow); } if (GetMissileSpriteData(missile._miAnimType).animFAmt == 16) { missile.setDirection(GetDirection16(missile.position.start, dst)); } if (missile._midam == 0) { switch (missile.sourceType()) { case MissileSource::Player: { const Player &player = *missile.sourcePlayer(); missile._midam = 3 * missile._mispllvl - (player._pMagic / 8) + (player._pMagic / 2); break; } case MissileSource::Monster: missile._midam = ProjectileMonsterDamage(missile); break; case MissileSource::Trap: missile._midam = ProjectileTrapDamage(); break; } } } void AddAcid(Missile &missile, AddMissileParameter ¶meter) { UpdateMissileVelocity(missile, parameter.dst, 16); missile.setDirection(GetDirection16(missile.position.start, parameter.dst)); if (!gbIsHellfire || (missile.position.velocity.deltaX & 0xFFFF0000) != 0 || (missile.position.velocity.deltaY & 0xFFFF0000) != 0) missile.duration = 5 * (Monsters[missile._misource].intelligence + 4); else missile.duration = 1; missile._mlid = NO_LIGHT; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; if (missile._midam == 0) { switch (missile.sourceType()) { case MissileSource::Player: // Not typically created by Players break; case MissileSource::Monster: missile._midam = ProjectileMonsterDamage(missile); break; case MissileSource::Trap: missile._midam = ProjectileTrapDamage(); break; } } PutMissile(missile); } void AddAcidPuddle(Missile &missile, AddMissileParameter & /*parameter*/) { missile._miLightFlag = true; const int monst = missile._misource; missile.duration = GenerateRnd(15) + 40 * (Monsters[monst].intelligence + 1); missile._miPreFlag = true; } void AddStoneCurse(Missile &missile, AddMissileParameter ¶meter) { std::optional targetMonsterPosition = FindClosestValidPosition( [](Point target) { if (!InDungeonBounds(target)) { return false; } const int monsterId = std::abs(dMonster[target.x][target.y]) - 1; if (monsterId < 0) { return false; } const Monster &monster = Monsters[monsterId]; if (IsAnyOf(monster.type().type, MT_GOLEM, MT_DIABLO, MT_NAKRUL)) { return false; } if (IsAnyOf(monster.mode, MonsterMode::FadeIn, MonsterMode::FadeOut, MonsterMode::Charge)) { return false; } return true; }, parameter.dst, 0, 5); if (!targetMonsterPosition) { missile._miDelFlag = true; parameter.spellFizzled = true; return; } // Petrify the targeted monster const int monsterId = std::abs(dMonster[targetMonsterPosition->x][targetMonsterPosition->y]) - 1; Monster &monster = Monsters[monsterId]; if (monster.mode == MonsterMode::Petrified) { // Monster is already petrified and StoneCurse doesn't stack missile._miDelFlag = true; return; } missile.var1 = static_cast(monster.mode); missile.var2 = monsterId; monster.petrify(); // And set up the missile to unpetrify it in the future missile.position.tile = *targetMonsterPosition; missile.position.start = missile.position.tile; missile.duration = missile._mispllvl + 6; missile.duration = std::min(missile.duration, 15); missile.duration <<= 4; } void AddGolem(Missile &missile, AddMissileParameter ¶meter) { missile._miDelFlag = true; const int playerId = missile._misource; Player &player = Players[playerId]; Monster *golem = FindGolemForPlayer(player); // Is Golem alive? if (golem != nullptr) { KillGolem(*golem); return; } std::optional spawnPosition = FindClosestValidPosition( [start = missile.position.start](Point target) { return !IsTileOccupied(target) && LineClearMissile(start, target); }, parameter.dst, 0, 5); if (!spawnPosition) return; if (&player != MyPlayer) return; const auto spellLevel = static_cast(missile._mispllvl); // The command is only executed for the level owner, to prevent desyncs in multiplayer. if (!MyPlayer->isLevelOwnedByLocalClient()) { // If we are not the level owner, request the level owner to spawn the golem for us NetSendCmdLocParam1(true, CMD_REQUESTSPAWNGOLEM, *spawnPosition, spellLevel); return; } SpawnGolem(player, *spawnPosition, spellLevel); } void AddApocalypseBoom(Missile &missile, AddMissileParameter ¶meter) { missile.position.tile = parameter.dst; missile.position.start = parameter.dst; missile.duration = missile._miAnimLen; } void AddHealing(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; int hp = GenerateRnd(10) + 1; hp += GenerateRndSum(4, player.getCharacterLevel()) + player.getCharacterLevel(); hp += GenerateRndSum(6, missile._mispllvl) + missile._mispllvl; hp <<= 6; if (player._pClass == HeroClass::Warrior || player._pClass == HeroClass::Barbarian || player._pClass == HeroClass::Monk) { hp *= 2; } else if (player._pClass == HeroClass::Rogue || player._pClass == HeroClass::Bard) { hp += hp / 2; } player._pHitPoints = std::min(player._pHitPoints + hp, player._pMaxHP); player._pHPBase = std::min(player._pHPBase + hp, player._pMaxHPBase); missile._miDelFlag = true; RedrawComponent(PanelDrawComponent::Health); } void AddHealOther(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) { NewCursor(CURSOR_HEALOTHER); if (ControlMode != ControlTypes::KeyboardAndMouse) TryIconCurs(); } } void AddElemental(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } const Player &player = Players[missile._misource]; const int dmg = (2 * (player.getCharacterLevel() + GenerateRndSum(10, 2))) + 4; missile._midam = ScaleSpellEffect(dmg, missile._mispllvl) / 2; UpdateMissileVelocity(missile, dst, 16); missile.setDirection(GetDirection(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile.var4 = dst.x; missile.var5 = dst.y; missile._mlid = AddLight(missile.position.start, 8); } void AddIdentify(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) { if (SpellbookFlag) SpellbookFlag = false; if (!invflag) { invflag = true; if (ControlMode != ControlTypes::KeyboardAndMouse) FocusOnInventory(); } NewCursor(CURSOR_IDENTIFY); } } void AddWallControl(Missile &missile, AddMissileParameter ¶meter) { std::optional spreadPosition = FindClosestValidPosition( [start = missile.position.start](Point target) { return start != target && !TileHasAny(target, TileProperties::BlockMissile) && LineClearMissile(start, target); }, parameter.dst, 0, 5); if (!spreadPosition) { missile._miDelFlag = true; parameter.spellFizzled = true; return; } missile._miDelFlag = false; missile.var1 = spreadPosition->x; missile.var2 = spreadPosition->y; missile.var5 = spreadPosition->x; missile.var6 = spreadPosition->y; missile.var3 = static_cast(Left(Left(parameter.midir))); missile.var4 = static_cast(Right(Right(parameter.midir))); missile.duration = 7; } void AddInfravision(Missile &missile, AddMissileParameter & /*parameter*/) { missile.duration = ScaleSpellEffect(1584, missile._mispllvl); } void AddFlameWaveControl(Missile &missile, AddMissileParameter ¶meter) { missile.var1 = parameter.dst.x; missile.var2 = parameter.dst.y; missile.duration = 1; missile._miAnimFrame = 4; } void AddNova(Missile &missile, AddMissileParameter ¶meter) { missile.var1 = parameter.dst.x; missile.var2 = parameter.dst.y; if (!missile.IsTrap()) { const Player &player = Players[missile._misource]; const int dmg = GenerateRndSum(6, 5) + player.getCharacterLevel() + 5; missile._midam = ScaleSpellEffect(dmg / 2, missile._mispllvl); } else { missile._midam = (currlevel / 2) + GenerateRndSum(3, 3); } missile.duration = 1; } void AddRage(Missile &missile, AddMissileParameter ¶meter) { Player &player = Players[missile._misource]; if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive | SpellFlag::RageCooldown) || player._pHitPoints <= player.getCharacterLevel() << 6) { missile._miDelFlag = true; parameter.spellFizzled = true; return; } missile._midam = player.getCharacterLevel() * 6; missile.duration = 245 + (player.getCharacterLevel() * 2); missile.var1 = missile.duration; player._pSpellFlags |= SpellFlag::RageActive; CalcPlrItemVals(player, true); player.Say(HeroSpeech::Aaaaargh); } void AddItemRepair(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) { if (SpellbookFlag) SpellbookFlag = false; if (!invflag) { invflag = true; if (ControlMode != ControlTypes::KeyboardAndMouse) FocusOnInventory(); } NewCursor(CURSOR_REPAIR); } } void AddStaffRecharge(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) { if (SpellbookFlag) SpellbookFlag = false; if (!invflag) { invflag = true; if (ControlMode != ControlTypes::KeyboardAndMouse) FocusOnInventory(); } NewCursor(CURSOR_RECHARGE); } } void AddTrapDisarm(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) { NewCursor(CURSOR_DISARM); if (ControlMode != ControlTypes::KeyboardAndMouse) { if (ObjectUnderCursor != nullptr) NetSendCmdLoc(MyPlayerId, true, CMD_DISARMXY, cursPosition); else NewCursor(CURSOR_HAND); } } } void AddApocalypse(Missile &missile, AddMissileParameter & /*parameter*/) { const Player &player = Players[missile._misource]; missile.var1 = 8; missile.var2 = std::max(missile.position.start.y - 8, 1); missile.var3 = std::min(missile.position.start.y + 8, MAXDUNY - 1); missile.var4 = std::max(missile.position.start.x - 8, 1); missile.var5 = std::min(missile.position.start.x + 8, MAXDUNX - 1); missile.var6 = missile.var4; const int playerLevel = player.getCharacterLevel(); missile._midam = GenerateRndSum(6, playerLevel) + playerLevel; missile.duration = 255; } void AddInferno(Missile &missile, AddMissileParameter ¶meter) { missile.var2 = 5 * missile._midam; missile.position.start = parameter.dst; SyncPositionWithParent(missile, parameter); missile.duration = missile.var2 + 20; missile._mlid = AddLight(missile.position.start, 1); if (missile._micaster == TARGET_MONSTERS) { const int i = GenerateRnd(Players[missile._misource].getCharacterLevel()) + GenerateRnd(2); missile._midam = 8 * i + 16 + ((8 * i + 16) / 2); } else { const Monster &monster = Monsters[missile._misource]; missile._midam = RandomIntBetween(monster.minDamage, monster.maxDamage); } } void AddInfernoControl(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == parameter.dst) { dst += parameter.midir; } UpdateMissileVelocity(missile, dst, 32); missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile.duration = 256; } void AddChargedBolt(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; missile._mirnd = GenerateRnd(15) + 1; missile._midam = (missile._micaster == TARGET_MONSTERS) ? (GenerateRnd(Players[missile._misource]._pMagic / 4) + 1) : 15; if (missile.position.start == dst) { dst += parameter.midir; } missile._miAnimFrame = RandomIntBetween(1, 8); missile._mlid = AddLight(missile.position.start, 5); UpdateMissileVelocity(missile, dst, 8); missile.var1 = 5; missile.var2 = static_cast(parameter.midir); missile.duration = 256; } void AddHolyBolt(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } int sp = 16; if (!missile.IsTrap()) { sp += std::min(missile._mispllvl * 2, 47); } const Player &player = Players[missile._misource]; UpdateMissileVelocity(missile, dst, sp); missile.setDirection(GetDirection16(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile._mlid = AddLight(missile.position.start, 8); missile._midam = GenerateRnd(10) + player.getCharacterLevel() + 9; } void AddResurrect(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; if (&player == MyPlayer) { NewCursor(CURSOR_RESURRECT); if (ControlMode != ControlTypes::KeyboardAndMouse) TryIconCurs(); } missile._miDelFlag = true; } void AddResurrectBeam(Missile &missile, AddMissileParameter ¶meter) { missile.position.tile = parameter.dst; missile.position.start = parameter.dst; missile.duration = GetMissileSpriteData(MissileGraphicID::Resurrect).animLen(0); } void AddTelekinesis(Missile &missile, AddMissileParameter & /*parameter*/) { Player &player = Players[missile._misource]; missile._miDelFlag = true; if (&player == MyPlayer) NewCursor(CURSOR_TELEKINESIS); } void AddBoneSpirit(Missile &missile, AddMissileParameter ¶meter) { WorldTilePosition dst = parameter.dst; if (missile.position.start == dst) { dst += parameter.midir; } UpdateMissileVelocity(missile, dst, 16); missile.setDirection(GetDirection(missile.position.start, dst)); missile.duration = 256; missile.var1 = missile.position.start.x; missile.var2 = missile.position.start.y; missile.var4 = dst.x; missile.var5 = dst.y; missile._mlid = AddLight(missile.position.start, 8); } void AddRedPortal(Missile &missile, AddMissileParameter & /*parameter*/) { missile.duration = 100; missile.var1 = 100 - missile._miAnimLen; PutMissile(missile); } void AddDiabloApocalypse(Missile &missile, AddMissileParameter & /*parameter*/) { for (const Player &player : Players) { if (!player.plractive) continue; if (!LineClearMissile(missile.position.start, player.position.future)) continue; AddMissile({ 0, 0 }, player.position.future, Direction::South, MissileID::DiabloApocalypseBoom, missile._micaster, missile._misource, missile._midam, 0); } missile._miDelFlag = true; } Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midir, MissileID mitype, mienemy_type micaster, int id, int midam, int spllvl, Missile *parent, std::optional lSFX) { if (Missiles.size() >= Missiles.max_size()) { return nullptr; } Missiles.emplace_back(); auto &missile = Missiles.back(); const MissileData &missileData = GetMissileData(mitype); missile._mitype = mitype; missile._micaster = micaster; missile._misource = id; missile._midam = midam; missile._mispllvl = spllvl; missile.position.tile = src; missile.position.start = src; missile._miAnimAdd = 1; missile._miAnimType = missileData.graphic; missile._miDrawFlag = missileData.isDrawn(); missile._mlid = NO_LIGHT; missile.lastCollisionTargetHash = 0; if (!missile.IsTrap() && micaster == TARGET_PLAYERS) { const Monster &monster = Monsters[id]; if (monster.isUnique()) { missile._miUniqTrans = monster.uniqTrans + 1; } } if (missile._miAnimType == MissileGraphicID::None || GetMissileSpriteData(missile._miAnimType).animFAmt < 8) missile.setDefaultFrameGroup(); else missile.setDirection(midir); if (!lSFX) { lSFX = missileData.castSound; } if (*lSFX != SfxID::None) { PlaySfxLoc(*lSFX, missile.position.start); } AddMissileParameter parameter = { dst, midir, parent, false }; missileData.addFn(missile, parameter); if (parameter.spellFizzled) { return nullptr; } return &missile; } void ProcessElementalArrow(Missile &missile) { missile.duration--; if (missile._miAnimType == MissileGraphicID::ChargedBolt || missile._miAnimType == MissileGraphicID::MagmaBallExplosion) { ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame + 5); } else { int mind; int maxd; const int p = missile._misource; missile._midist++; if (!missile.IsTrap()) { if (missile._micaster == TARGET_MONSTERS) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. const Player &player = Players[p]; mind = player._pIMinDam; maxd = player._pIMaxDam; } else { // BUGFIX: damage of missile should be encoded in missile struct; monster can be dead before missile arrives. const Monster &monster = Monsters[p]; mind = monster.minDamage; maxd = monster.maxDamage; } } else { mind = GenerateRnd(10) + 1 + currlevel; maxd = GenerateRnd(10) + 1 + currlevel * 2; } MoveMissileAndCheckMissileCol(missile, DamageType::Physical, mind, maxd, true, false); if (missile.duration == 0) { missile.setDefaultFrameGroup(); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); int eMind; int eMaxd; MissileGraphicID eAnim; DamageType damageType; switch (missile._mitype) { case MissileID::LightningArrow: if (!missile.IsTrap()) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. const Player &player = Players[p]; eMind = player._pILMinDam; eMaxd = player._pILMaxDam; } else { eMind = GenerateRnd(10) + 1 + currlevel; eMaxd = GenerateRnd(10) + 1 + currlevel * 2; } eAnim = MissileGraphicID::ChargedBolt; damageType = DamageType::Lightning; break; case MissileID::FireArrow: if (!missile.IsTrap()) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. const Player &player = Players[p]; eMind = player._pIFMinDam; eMaxd = player._pIFMaxDam; } else { eMind = GenerateRnd(10) + 1 + currlevel; eMaxd = GenerateRnd(10) + 1 + currlevel * 2; } eAnim = MissileGraphicID::MagmaBallExplosion; damageType = DamageType::Fire; break; default: app_fatal(StrCat("wrong missile ID ", static_cast(missile._mitype))); break; } missile.setAnimation(eAnim); CheckMissileCol(missile, damageType, eMind, eMaxd, false, missile.position.tile, true); } else { if (missile.position.tile != Point { missile.var1, missile.var2 }) { missile.var1 = missile.position.tile.x; missile.var2 = missile.position.tile.y; ChangeLight(missile._mlid, missile.position.tile, 5); } } } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessArrow(Missile &missile) { missile.duration--; missile._midist++; int mind; int maxd; switch (missile.sourceType()) { case MissileSource::Player: { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. const Player &player = *missile.sourcePlayer(); mind = player._pIMinDam; maxd = player._pIMaxDam; } break; case MissileSource::Monster: { // BUGFIX: damage of missile should be encoded in missile struct; monster can be dead before missile arrives. const Monster &monster = *missile.sourceMonster(); mind = monster.minDamage; maxd = monster.maxDamage; } break; case MissileSource::Trap: mind = currlevel; maxd = 2 * currlevel; break; } MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), mind, maxd, true, false); if (missile.duration == 0) missile._miDelFlag = true; PutMissile(missile); } void ProcessGenericProjectile(Missile &missile) { missile.duration--; MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, true); if (missile.duration == 0) { missile._miDelFlag = true; const Point dst = { 0, 0 }; const Direction dir = missile.getDirection(); switch (missile._mitype) { case MissileID::Firebolt: case MissileID::MagmaBall: AddMissile(missile.position.tile, dst, dir, MissileID::MagmaBallExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::BloodStar: AddMissile(missile.position.tile, dst, dir, MissileID::BloodStarExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::Acid: AddMissile(missile.position.tile, dst, dir, MissileID::AcidSplat, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::OrangeFlare: AddMissile(missile.position.tile, dst, dir, MissileID::OrangeExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::BlueFlare: AddMissile(missile.position.tile, dst, dir, MissileID::BlueExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::RedFlare: AddMissile(missile.position.tile, dst, dir, MissileID::RedExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::YellowFlare: AddMissile(missile.position.tile, dst, dir, MissileID::YellowExplosion, missile._micaster, missile._misource, 0, 0, &missile); break; case MissileID::BlueFlare2: AddMissile(missile.position.tile, dst, dir, MissileID::BlueExplosion2, missile._micaster, missile._misource, 0, 0, &missile); break; default: break; } if (missile._mlid != NO_LIGHT) AddUnLight(missile._mlid); PutMissile(missile); } else { if (missile.position.tile != Point { missile.var1, missile.var2 }) { missile.var1 = missile.position.tile.x; missile.var2 = missile.position.tile.y; if (missile._mlid != NO_LIGHT) ChangeLight(missile._mlid, missile.position.tile, 8); } PutMissile(missile); } } void ProcessNovaBall(Missile &missile) { const Point targetPosition = { missile.var1, missile.var2 }; missile.duration--; const int j = missile.duration; MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false); if (missile._miHitFlag) missile.duration = j; if (missile.position.tile == targetPosition) { Object *object = FindObjectAtPosition(targetPosition); if (object != nullptr && object->IsShrine()) { missile.duration = j; } } if (missile.duration == 0) missile._miDelFlag = true; PutMissile(missile); } void ProcessAcidPuddle(Missile &missile) { missile.duration--; const int range = missile.duration; CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false); missile.duration = range; if (range == 0) { if (missile.getFrameGroup() != AcidPuddleFrame::Idle) { missile._miDelFlag = true; } else { missile.setFrameGroup(AcidPuddleFrame::End); missile.duration = missile._miAnimLen; } } PutMissile(missile); } void ProcessFireWall(Missile &missile) { constexpr int ExpLightLen = 12; constexpr int ExpLight[ExpLightLen] = { 2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12 }; missile.duration--; if (missile.duration == missile.var1) { missile.setFrameGroup(FireWallFrame::Idle); missile._miAnimFrame = GenerateRnd(11) + 1; } if (missile.duration == missile._miAnimLen - 1) { missile.setFrameGroup(FireWallFrame::Start); missile._miAnimFrame = 13; missile._miAnimAdd = -1; } std::optional onlyHitWalking = {}; if (missile.var3 != 0) onlyHitWalking = static_cast(missile.var3 - 1); CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, true, onlyHitWalking); if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } constexpr int MaxExpLightIndex = ExpLightLen - 1; if (missile.getFrameGroup() == FireWallFrame::Start && missile.duration != 0 && missile.var2 <= MaxExpLightIndex * 2) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, ExpLight[0]); const int expLightIndex = MaxExpLightIndex - std::abs(missile.var2 - MaxExpLightIndex); ChangeLight(missile._mlid, missile.position.tile, ExpLight[expLightIndex]); missile.var2++; } PutMissile(missile); } void ProcessFireball(Missile &missile) { missile.duration--; if (missile._miAnimType == MissileGraphicID::BigExplosion) { if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } } else { int minDam = missile._midam; int maxDam = missile._midam; if (missile._micaster != TARGET_MONSTERS) { const Monster &monster = Monsters[missile._misource]; minDam = monster.minDamage; maxDam = monster.maxDamage; } const DamageType damageType = GetMissileData(missile._mitype).damageType(); MoveMissileAndCheckMissileCol(missile, damageType, minDam, maxDam, true, false); if (missile.duration == 0) { const Point missilePosition = missile.position.tile; ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame); constexpr Direction Offsets[] = { Direction::NoDirection, Direction::SouthWest, Direction::NorthEast, Direction::SouthEast, Direction::East, Direction::South, Direction::NorthWest, Direction::West, Direction::North }; for (const Direction offset : Offsets) { if (!CheckBlock(missile.position.start, missilePosition + offset)) CheckMissileCol(missile, damageType, minDam, maxDam, false, missilePosition + offset, true); } if (!TransList[dTransVal[missilePosition.x][missilePosition.y]] || (missile.position.velocity.deltaX < 0 && ((TransList[dTransVal[missilePosition.x][missilePosition.y + 1]] && TileHasAny(missilePosition + Direction::SouthWest, TileProperties::Solid)) || (TransList[dTransVal[missilePosition.x][missilePosition.y - 1]] && TileHasAny(missilePosition + Direction::NorthEast, TileProperties::Solid))))) { missile.position.tile += Direction::South; missile.position.offset.deltaY -= 32; } if (missile.position.velocity.deltaY > 0 && ((TransList[dTransVal[missilePosition.x + 1][missilePosition.y]] && TileHasAny(missilePosition + Direction::SouthEast, TileProperties::Solid)) || (TransList[dTransVal[missilePosition.x - 1][missilePosition.y]] && TileHasAny(missilePosition + Direction::NorthWest, TileProperties::Solid)))) { missile.position.offset.deltaY -= 32; } if (missile.position.velocity.deltaX > 0 && ((TransList[dTransVal[missilePosition.x][missilePosition.y + 1]] && TileHasAny(missilePosition + Direction::SouthWest, TileProperties::Solid)) || (TransList[dTransVal[missilePosition.x][missilePosition.y - 1]] && TileHasAny(missilePosition + Direction::NorthEast, TileProperties::Solid)))) { missile.position.offset.deltaX -= 32; } missile.setDefaultFrameGroup(); missile.setAnimation(MissileGraphicID::BigExplosion); missile.duration = missile._miAnimLen - 1; missile.position.velocity = {}; } else if (missile.position.tile != Point { missile.var1, missile.var2 }) { missile.var1 = missile.position.tile.x; missile.var2 = missile.position.tile.y; ChangeLight(missile._mlid, missile.position.tile, 8); } } PutMissile(missile); } void ProcessHorkSpawn(Missile &missile) { missile.duration--; CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), 0, 0, false, missile.position.tile, false); if (missile.duration <= 0) { missile._miDelFlag = true; std::optional spawnPosition = FindClosestValidPosition( [](Point target) { return !IsTileOccupied(target); }, missile.position.tile, 0, 1); if (spawnPosition) { auto facing = static_cast(missile.var1); SpawnMonster(*spawnPosition, facing, 1); } } else { missile._midist++; missile.position.traveled += missile.position.velocity; UpdateMissilePos(missile); } PutMissile(missile); } void ProcessRune(Missile &missile) { const Point position = missile.position.tile; const int mid = dMonster[position.x][position.y]; Player *player = PlayerAtPosition(position); if (mid != 0 || player != nullptr) { const Point targetPosition = mid != 0 ? Monsters[std::abs(mid) - 1].position.tile : player->position.tile; const Direction dir = GetDirection(position, targetPosition); missile._miDelFlag = true; AddUnLight(missile._mlid); AddMissile(position, position, dir, static_cast(missile.var1), TARGET_BOTH, missile._misource, missile._midam, missile._mispllvl); } PutMissile(missile); } void ProcessLightningWall(Missile &missile) { missile.duration--; const int range = missile.duration; std::optional onlyHitWalking = {}; if (missile.var3 != 0) onlyHitWalking = static_cast(missile.var3 - 1); CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false, onlyHitWalking); if (missile._miHitFlag) missile.duration = range; if (missile.duration == 0) missile._miDelFlag = true; PutMissile(missile); } void ProcessBigExplosion(Missile &missile) { missile.duration--; if (missile.duration <= 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessLightningBow(Missile &missile) { SpawnLightning(missile, missile._midam); } void ProcessRingOfFire(Missile &missile) { missile._miDelFlag = true; int8_t src = missile._misource; const uint8_t lvl = missile._micaster == TARGET_MONSTERS ? Players[src].getCharacterLevel() : currlevel; int dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2) / 2; if (missile.limitReached) return; Crawl(3, [&](Displacement displacement) { const Point target = Point { missile.var1, missile.var2 } + displacement; if (!InDungeonBounds(target)) return false; if (TileHasAny(target, TileProperties::Solid)) return false; if (IsObjectAtPosition(target)) return false; if (!LineClearMissile(missile.position.tile, target)) return false; if (TileHasAny(target, TileProperties::BlockMissile)) { missile.limitReached = true; return true; } AddMissile(target, target, Direction::South, MissileID::FireWall, TARGET_BOTH, src, dmg, missile._mispllvl); return false; }); } void ProcessSearch(Missile &missile) { missile.duration--; if (missile.duration != 0) return; const Player &player = Players[missile._misource]; missile._miDelFlag = true; PlaySfxLoc(SfxID::SpellEnd, player.position.tile); if (&player == MyPlayer) AutoMapShowItems = false; } void ProcessNovaCommon(Missile &missile, MissileID projectileType) { const int id = missile._misource; const int dam = missile._midam; const Point src = missile.position.tile; Direction dir = Direction::South; mienemy_type en = TARGET_PLAYERS; if (!missile.IsTrap()) { dir = Players[id]._pdir; en = TARGET_MONSTERS; } constexpr std::array quarterRadius = { { { 4, 0 }, { 4, 1 }, { 4, 2 }, { 4, 3 }, { 4, 4 }, { 3, 4 }, { 2, 4 }, { 1, 4 }, { 0, 4 } } }; for (const WorldTileDisplacement quarterOffset : quarterRadius) { // This ends up with two missiles targeting offsets 4,0, 0,4, -4,0, 0,-4. const std::array offsets { quarterOffset, quarterOffset.flipXY(), quarterOffset.flipX(), quarterOffset.flipY() }; for (const WorldTileDisplacement offset : offsets) AddMissile(src, src + offset, dir, projectileType, en, id, dam, missile._mispllvl); } missile.duration--; if (missile.duration == 0) missile._miDelFlag = true; } void ProcessImmolation(Missile &missile) { ProcessNovaCommon(missile, MissileID::FireballBow); } void ProcessNova(Missile &missile) { ProcessNovaCommon(missile, MissileID::NovaBall); } void ProcessSpectralArrow(Missile &missile) { const int id = missile._misource; const int dam = missile._midam; const Point src = missile.position.tile; const Point dst = { missile.var1, missile.var2 }; const int spllvl = missile.var3; MissileID mitype = MissileID::Arrow; Direction dir = Direction::South; mienemy_type micaster = TARGET_PLAYERS; if (!missile.IsTrap()) { const Player &player = Players[id]; dir = player._pdir; micaster = TARGET_MONSTERS; switch (player._pILMinDam) { case 0: mitype = MissileID::FireballBow; break; case 1: mitype = MissileID::LightningBow; break; case 2: mitype = MissileID::ChargedBoltBow; break; case 3: mitype = MissileID::HolyBoltBow; break; } } AddMissile(src, dst, dir, mitype, micaster, id, dam, spllvl); if (mitype == MissileID::ChargedBoltBow) { AddMissile(src, dst, dir, mitype, micaster, id, dam, spllvl); AddMissile(src, dst, dir, mitype, micaster, id, dam, spllvl); } missile.duration--; if (missile.duration == 0) missile._miDelFlag = true; } void ProcessLightningControl(Missile &missile) { missile.duration--; int dam; if (missile.IsTrap()) { // BUGFIX: damage of missile should be encoded in missile struct; monster can be dead before missile arrives. dam = GenerateRnd(currlevel) + 2 * currlevel; } else if (missile._micaster == TARGET_MONSTERS) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. dam = (GenerateRnd(2) + GenerateRnd(Players[missile._misource].getCharacterLevel()) + 2) << 6; } else { const Monster &monster = Monsters[missile._misource]; dam = 2 * RandomIntBetween(monster.minDamage, monster.maxDamage); } SpawnLightning(missile, dam); } void ProcessLightning(Missile &missile) { missile.duration--; const int j = missile.duration; if (missile.position.tile != missile.position.start) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false); if (missile._miHitFlag) missile.duration = j; if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessTownPortal(Missile &missile) { const int expLight[17] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15 }; if (missile.duration > 1) missile.duration--; if (missile.duration == missile.var1) missile.setFrameGroup(PortalFrame::Idle); if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != PortalFrame::Idle && missile.duration != 0) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, 1); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); missile.var2++; } for (Player &player : Players) { if (player.plractive && player.isOnActiveLevel() && !player._pLvlChanging && player._pmode == PM_STAND && player.position.tile == missile.position.tile) { ClrPlrPath(player); if (&player == MyPlayer) { NetSendCmdParam1(true, CMD_WARP, missile._misource); player._pmode = PM_NEWLVL; } } } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessFlashBottom(Missile &missile) { if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) Players[missile._misource]._pInvincible = true; } missile.duration--; constexpr Direction Offsets[] = { Direction::NorthWest, Direction::NoDirection, Direction::SouthEast, Direction::West, Direction::SouthWest, Direction::South }; for (const Direction offset : Offsets) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile + offset, true); if (missile.duration == 0) { missile._miDelFlag = true; if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) Players[missile._misource]._pInvincible = false; } } PutMissile(missile); } void ProcessFlashTop(Missile &missile) { if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) Players[missile._misource]._pInvincible = true; } missile.duration--; constexpr Direction Offsets[] = { Direction::North, Direction::NorthEast, Direction::East }; for (const Direction offset : Offsets) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile + offset, true); if (missile.duration == 0) { missile._miDelFlag = true; if (missile._micaster == TARGET_MONSTERS) { if (!missile.IsTrap()) Players[missile._misource]._pInvincible = false; } } PutMissile(missile); } void ProcessFlameWave(Missile &missile) { constexpr int ExpLightLen = 12; constexpr int ExpLight[ExpLightLen] = { 2, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12 }; // Adjust missile's position for processing missile.position.tile += Direction::North; missile.position.offset.deltaY += 32; missile.var1++; if (missile.var1 == missile._miAnimLen) { missile.setFrameGroup(FireWallFrame::Idle); missile._miAnimFrame = GenerateRnd(11) + 1; } const int j = missile.duration; MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false); if (missile._miHitFlag) missile.duration = j; if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } else { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, ExpLight[0]); ChangeLight(missile._mlid, missile.position.tile, ExpLight[missile.var2]); if (missile.var2 < ExpLightLen - 1) missile.var2++; } // Adjust missile's position for rendering missile.position.tile += Direction::South; missile.position.offset.deltaY -= 32; PutMissile(missile); } void ProcessGuardian(Missile &missile) { missile.duration--; if (missile.var2 > 0) { missile.var2--; } if (missile.duration == missile.var1 || (missile.getFrameGroup() == GuardianFrame::Attack && missile.var2 == 0)) { missile.setFrameGroup(GuardianFrame::Idle); } const Point position = missile.position.tile; if ((missile.duration % 16) == 0) { // Guardians pick a target by working backwards along lines originally based on VisionCrawlTable. // Because of their rather unique behaviour the points checked have been unrolled here constexpr std::array guardianArc { { // clang-format off { 6, 0 }, { 5, 0 }, { 4, 0 }, { 3, 0 }, { 2, 0 }, { 1, 0 }, { 6, 1 }, { 5, 1 }, { 4, 1 }, { 3, 1 }, { 6, 2 }, { 2, 1 }, { 5, 2 }, { 6, 3 }, { 4, 2 }, { 5, 3 }, { 3, 2 }, { 1, 1 }, { 6, 4 }, { 6, 5 }, { 5, 4 }, { 4, 3 }, { 2, 2 }, { 5, 5 }, { 4, 4 }, { 3, 3 }, { 6, 6 }, { 5, 6 }, { 4, 5 }, { 3, 4 }, { 2, 3 }, { 4, 6 }, { 3, 5 }, { 2, 4 }, { 1, 2 }, { 3, 6 }, { 2, 5 }, { 1, 3 }, { 0, 1 }, { 2, 6 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 0, 2 }, { 0, 3 }, { 0, 6 }, { 0, 5 }, { 0, 4 }, // clang-format on } }; for (const WorldTileDisplacement offset : guardianArc) { if (GuardianTryFireAt(missile, position + offset) || GuardianTryFireAt(missile, position + offset.flipXY()) || GuardianTryFireAt(missile, position + offset.flipY()) || GuardianTryFireAt(missile, position + offset.flipX())) break; } } if (missile.duration == 14) { missile.setFrameGroup(GuardianFrame::Start); missile._miAnimFrame = 15; missile._miAnimAdd = -1; } missile.var3 += missile._miAnimAdd; if (missile.var3 > 15) { missile.var3 = 15; } else if (missile.var3 > 0) { ChangeLight(missile._mlid, position, missile.var3); } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessChainLightning(Missile &missile) { int id = missile._misource; Point position = missile.position.tile; const Point dst { missile.var1, missile.var2 }; Direction dir = GetDirection(position, dst); AddMissile(position, dst, dir, MissileID::LightningControl, TARGET_MONSTERS, id, 1, missile._mispllvl); const int rad = std::min(missile._mispllvl + 3, MaxCrawlRadius); Crawl(1, rad, [&](Displacement displacement) { const Point target = position + displacement; if (InDungeonBounds(target) && dMonster[target.x][target.y] > 0) { dir = GetDirection(position, target); AddMissile(position, target, dir, MissileID::LightningControl, TARGET_MONSTERS, id, 1, missile._mispllvl); } return false; }); missile.duration--; if (missile.duration == 0) missile._miDelFlag = true; } void ProcessWeaponExplosion(Missile &missile) { constexpr int ExpLight[10] = { 9, 10, 11, 12, 11, 10, 8, 6, 4, 2 }; missile.duration--; const Player &player = Players[missile._misource]; int mind; int maxd; DamageType damageType; if (missile.var2 == 1) { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. mind = player._pIFMinDam; maxd = player._pIFMaxDam; damageType = DamageType::Fire; } else { // BUGFIX: damage of missile should be encoded in missile struct; player can be dead/have left the game before missile arrives. mind = player._pILMinDam; maxd = player._pILMaxDam; damageType = DamageType::Lightning; } CheckMissileCol(missile, damageType, mind, maxd, false, missile.position.tile, false); if (missile.var1 == 0) { missile._mlid = AddLight(missile.position.tile, 9); } else { if (missile.duration != 0) ChangeLight(missile._mlid, missile.position.tile, ExpLight[missile.var1]); } missile.var1++; if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } else { PutMissile(missile); } } void ProcessMissileExplosion(Missile &missile) { constexpr int ExpLight[] = { 9, 10, 11, 12, 11, 10, 8, 6, 4, 2, 1, 0, 0, 0, 0 }; missile.duration--; if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } else { if (missile.var1 == 0) missile._mlid = AddLight(missile.position.tile, 9); else ChangeLight(missile._mlid, missile.position.tile, ExpLight[missile.var1]); missile.var1++; PutMissile(missile); } } void ProcessAcidSplate(Missile &missile) { if (missile.duration == missile._miAnimLen) { missile.position.tile += Displacement { 1, 1 }; missile.position.offset.deltaY -= 32; } missile.duration--; if (missile.duration == 0) { missile._miDelFlag = true; const int monst = missile._misource; const int dam = (Monsters[monst].data().level >= 2 ? 2 : 1); AddMissile(missile.position.tile, { 0, 0 }, Direction::South, MissileID::AcidPuddle, TARGET_PLAYERS, monst, dam, missile._mispllvl); } else { PutMissile(missile); } } void ProcessTeleport(Missile &missile) { missile.duration--; if (missile.duration <= 0) { missile._miDelFlag = true; return; } const int id = missile._misource; Player &player = Players[id]; std::optional teleportDestination = FindClosestValidPosition( [&player](Point target) { return PosOkPlayer(player, target); }, missile.position.tile, 0, 5); if (!teleportDestination) return; dPlayer[player.position.tile.x][player.position.tile.y] = 0; PlrClrTrans(player.position.tile); player.position.tile = *teleportDestination; player.position.future = player.position.tile; player.position.old = player.position.tile; PlrDoTrans(player.position.tile); missile.var1 = 1; player.occupyTile(player.position.tile, false); if (leveltype != DTYPE_TOWN) { ChangeLightXY(player.lightId, player.position.tile); ChangeVisionXY(player.getId(), player.position.tile); } if (&player == MyPlayer) { ViewPosition = player.position.tile; } } void ProcessStoneCurse(Missile &missile) { missile.duration--; Monster &monster = Monsters[missile.var2]; if (monster.hitPoints == 0 && missile._miAnimType != MissileGraphicID::StoneCurseShatter) { missile.setDefaultFrameGroup(); missile._miDrawFlag = true; missile.setAnimation(MissileGraphicID::StoneCurseShatter); missile.duration = 11; } if (monster.mode != MonsterMode::Petrified) { missile._miDelFlag = true; return; } if (missile.duration == 0) { missile._miDelFlag = true; if (monster.hitPoints > 0) { monster.mode = static_cast(missile.var1); monster.animInfo.isPetrified = false; } else { AddCorpse(monster.position.tile, stonendx, monster.direction); } } if (missile._miAnimType == MissileGraphicID::StoneCurseShatter) PutMissile(missile); } void ProcessApocalypseBoom(Missile &missile) { missile.duration--; if (missile.var1 == 0) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, missile.position.tile, true); if (missile._miHitFlag) missile.var1 = 1; if (missile.duration == 0) missile._miDelFlag = true; PutMissile(missile); } void ProcessRhino(Missile &missile) { const int monst = missile._misource; Monster &monster = Monsters[monst]; if (monster.mode != MonsterMode::Charge) { missile._miDelFlag = true; return; } UpdateMissilePos(missile); const Point prevPos = missile.position.tile; Point newPosSnake; dMonster[prevPos.x][prevPos.y] = 0; if (monster.ai == MonsterAIID::Snake) { missile.position.traveled += missile.position.velocity * 2; UpdateMissilePos(missile); newPosSnake = missile.position.tile; missile.position.traveled -= missile.position.velocity; } else { missile.position.traveled += missile.position.velocity; } UpdateMissilePos(missile); const Point newPos = missile.position.tile; if (!IsTileAvailable(monster, newPos) || (monster.ai == MonsterAIID::Snake && (!IsTileAvailable(monster, newPosSnake) || missile._miAnimFrame >= missile._miAnimLen))) { MissToMonst(missile, prevPos); missile._miDelFlag = true; return; } monster.position.future = newPos; monster.position.old = newPos; monster.position.tile = newPos; monster.occupyTile(newPos, true); if (monster.isUnique()) ChangeLightXY(missile._mlid, newPos); MoveMissilePos(missile); PutMissile(missile); } void ProcessWallControl(Missile &missile) { missile.duration--; if (missile.duration == 0) { missile._miDelFlag = true; return; } MissileID type; const int sourceIdx = missile._misource; int lvl = 0; int dmg = 0; switch (missile._mitype) { case MissileID::FireWallControl: type = MissileID::FireWall; break; case MissileID::LightningWallControl: type = MissileID::LightningWall; lvl = !missile.IsTrap() ? Players[sourceIdx].getCharacterLevel() : 0; dmg = 16 * (GenerateRndSum(10, 2) + lvl + 2); break; default: app_fatal("ProcessWallControl: Invalid missile type for control missile"); } const Point leftPosition = { missile.var1, missile.var2 }; const Point rightPosition = { missile.var5, missile.var6 }; const Direction leftDirection = static_cast(missile.var3); const Direction rightDirection = static_cast(missile.var4); bool isStart = leftPosition == rightPosition; std::optional nextLeftPosition = std::nullopt; std::optional nextRightPosition = std::nullopt; if (isStart) { if (!CanPlaceWall(leftPosition)) { missile._miDelFlag = true; return; } PlaceWall(missile._misource, type, leftPosition, leftDirection, missile._mispllvl, dmg); nextLeftPosition = MoveWallToNextPosition(leftPosition, leftDirection); nextRightPosition = MoveWallToNextPosition(rightPosition, rightDirection); } else { if (!missile.limitReached && TryGrowWall(missile._misource, type, leftPosition, leftDirection, Direction::South, missile._mispllvl, dmg)) { nextLeftPosition = MoveWallToNextPosition(leftPosition, leftDirection); } if (missile.var7 == 0 && TryGrowWall(missile._misource, type, rightPosition, rightDirection, Direction::South, missile._mispllvl, dmg)) { nextRightPosition = MoveWallToNextPosition(rightPosition, rightDirection); } } if (nextLeftPosition) { missile.var1 = nextLeftPosition->x; missile.var2 = nextLeftPosition->y; } else { missile.limitReached = true; } if (nextRightPosition) { missile.var5 = nextRightPosition->x; missile.var6 = nextRightPosition->y; } else { missile.var7 = 1; } } void ProcessInfravision(Missile &missile) { Player &player = Players[missile._misource]; missile.duration--; player._pInfraFlag = true; if (missile.duration == 0) { missile._miDelFlag = true; CalcPlrItemVals(player, true); } } void ProcessApocalypse(Missile &missile) { for (int j = missile.var2; j < missile.var3; j++) { for (int k = missile.var4; k < missile.var5; k++) { const int mid = dMonster[k][j] - 1; if (mid < 0) continue; if (Monsters[mid].isPlayerMinion()) continue; if (TileHasAny(PointOf { k, j }, TileProperties::Solid)) continue; if (gbIsHellfire && !LineClearMissile(missile.position.tile, { k, j })) continue; const int id = missile._misource; AddMissile(WorldTilePosition(k, j), WorldTilePosition(k, j), Players[id]._pdir, MissileID::ApocalypseBoom, TARGET_MONSTERS, id, missile._midam, 0); missile.var2 = j; missile.var4 = k + 1; return; } missile.var4 = missile.var6; } missile._miDelFlag = true; } void ProcessFlameWaveControl(Missile &missile) { const int id = missile._misource; const Direction pdir = Players[id]._pdir; const Point src = missile.position.tile; const Direction sd = GetDirection(src, { missile.var1, missile.var2 }); const Point start = src + sd; if (CanPlaceWall(start)) { PlaceWall(id, MissileID::FlameWave, start, pdir, missile._mispllvl, 0); int segmentsToAdd = (missile._mispllvl / 2) + 2; Point left = start; const Direction dirLeft = Left(Left(sd)); for (int j = 0; j < segmentsToAdd; j++) { left += dirLeft; if (!TryGrowWall(id, MissileID::FlameWave, left, dirLeft, pdir, missile._mispllvl, 0)) break; } Point right = start; const Direction dirRight = Right(Right(sd)); for (int j = 0; j < segmentsToAdd; j++) { right += dirRight; if (!TryGrowWall(id, MissileID::FlameWave, right, dirRight, pdir, missile._mispllvl, 0)) break; } } missile.duration--; if (missile.duration == 0) missile._miDelFlag = true; } void ProcessRage(Missile &missile) { missile.duration--; if (missile.duration != 0) return; Player &player = Players[missile._misource]; if (HasAnyOf(player._pSpellFlags, SpellFlag::RageActive)) { player._pSpellFlags &= ~SpellFlag::RageActive; player._pSpellFlags |= SpellFlag::RageCooldown; missile.duration = missile.var1; // Start timer over } else if (HasAnyOf(player._pSpellFlags, SpellFlag::RageCooldown)) { player._pSpellFlags &= ~SpellFlag::RageCooldown; missile._miDelFlag = true; } CalcPlrItemVals(player, true); // Prevent the player from dying as a result of recalculating their current life if (player.hasNoLife()) SetPlayerHitPoints(player, 64); RedrawEverything(); player.Say(HeroSpeech::HeavyBreathing); if (missile._miDelFlag) ApplyPlrDamage(DamageType::Physical, player, missile._midam, 1); // Prevent penalty from killing the player } void ProcessInferno(Missile &missile) { missile.duration--; missile.var2--; int k = missile.duration; CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, true, missile.position.tile, false); if (missile.duration == 0 && missile._miHitFlag) missile.duration = k; if (missile.var2 == 0) missile._miAnimFrame = 20; if (missile.var2 <= 0) { k = missile._miAnimFrame; if (k > 11) k = 24 - k; ChangeLight(missile._mlid, missile.position.tile, k); } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } if (missile.var2 <= 0) PutMissile(missile); } void ProcessInfernoControl(Missile &missile) { missile.duration--; missile.position.traveled += missile.position.velocity; UpdateMissilePos(missile); if (missile.position.tile != Point { missile.var1, missile.var2 }) { if (!TileHasAny(missile.position.tile, TileProperties::BlockMissile)) { AddMissile( missile.position.tile, missile.position.start, Direction::South, MissileID::Inferno, missile._micaster, missile._misource, missile.var3, missile._mispllvl, &missile); } else { missile.duration = 0; } missile.var1 = missile.position.tile.x; missile.var2 = missile.position.tile.y; missile.var3++; } if (missile.duration == 0 || missile.var3 == 3) missile._miDelFlag = true; } void ProcessChargedBolt(Missile &missile) { missile.duration--; if (missile._miAnimType != MissileGraphicID::Lightning) { if (missile.var3 == 0) { constexpr int BPath[16] = { -1, 0, 1, -1, 0, 1, -1, -1, 0, 0, 1, 1, 0, 1, -1, 0 }; auto md = static_cast(missile.var2); switch (BPath[missile._mirnd]) { case -1: md = Left(md); break; case 1: md = Right(md); break; } missile._mirnd = (missile._mirnd + 1) & 0xF; UpdateMissileVelocity(missile, missile.position.tile + md, 8); missile.var3 = 16; } else { missile.var3--; } MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), missile._midam, missile._midam, false, false); if (missile._miHitFlag) { missile.var1 = 8; missile.setDefaultFrameGroup(); missile.position.offset = { 0, 0 }; missile.position.velocity = {}; missile.setAnimation(MissileGraphicID::Lightning); missile.duration = missile._miAnimLen; } ChangeLight(missile._mlid, missile.position.tile, missile.var1); } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } void ProcessHolyBolt(Missile &missile) { missile.duration--; if (missile._miAnimType != MissileGraphicID::HolyBoltExplosion) { const int dam = missile._midam; MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, true); if (missile.duration == 0) { missile.setDefaultFrameGroup(); missile.setAnimation(MissileGraphicID::HolyBoltExplosion); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); } else { if (missile.position.tile != Point { missile.var1, missile.var2 }) { missile.var1 = missile.position.tile.x; missile.var2 = missile.position.tile.y; ChangeLight(missile._mlid, missile.position.tile, 8); } } } else { ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame + 7); if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } } PutMissile(missile); } void ProcessElemental(Missile &missile) { missile.duration--; const int dam = missile._midam; const Point missilePosition = missile.position.tile; if (missile._miAnimType == MissileGraphicID::BigExplosion) { ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame); const Point startPoint = missile.var3 == 2 ? Point { missile.var4, missile.var5 } : Point(missile.position.start); constexpr Direction Offsets[] = { Direction::NoDirection, Direction::SouthWest, Direction::NorthEast, Direction::SouthEast, Direction::East, Direction::South, Direction::NorthWest, Direction::West, Direction::North }; for (const Direction offset : Offsets) { if (!CheckBlock(startPoint, missilePosition + offset)) CheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, true, missilePosition + offset, true); } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } } else { MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, false, false); if (missile.var3 == 0 && missilePosition == Point { missile.var4, missile.var5 }) missile.var3 = 1; if (missile.var3 == 1) { missile.var3 = 2; missile.duration = 255; auto *nextMonster = FindClosest(missilePosition, 19); if (nextMonster != nullptr) { const Direction sd = GetDirection(missilePosition, nextMonster->position.tile); missile.setDirection(sd); UpdateMissileVelocity(missile, nextMonster->position.tile, 16); } else { const Direction sd = Players[missile._misource]._pdir; missile.setDirection(sd); UpdateMissileVelocity(missile, missilePosition + sd, 16); } } if (missilePosition != Point { missile.var1, missile.var2 }) { missile.var1 = missilePosition.x; missile.var2 = missilePosition.y; ChangeLight(missile._mlid, missilePosition, 8); } if (missile.duration == 0) { missile.setDefaultFrameGroup(); missile.setAnimation(MissileGraphicID::BigExplosion); missile.duration = missile._miAnimLen - 1; missile.position.StopMissile(); } } PutMissile(missile); } void ProcessBoneSpirit(Missile &missile) { missile.duration--; const int dam = missile._midam; if (missile.getDirection() == Direction::NoDirection) { ChangeLight(missile._mlid, missile.position.tile, missile._miAnimFrame); if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } else { MoveMissileAndCheckMissileCol(missile, GetMissileData(missile._mitype).damageType(), dam, dam, false, false); const Point c = missile.position.tile; if (missile.var3 == 0 && c == Point { missile.var4, missile.var5 }) missile.var3 = 1; if (missile.var3 == 1) { missile.var3 = 2; missile.duration = 255; auto *monster = FindClosest(c, 19); if (monster != nullptr) { missile._midam = monster->hitPoints >> 7; missile.setDirection(GetDirection(c, monster->position.tile)); UpdateMissileVelocity(missile, monster->position.tile, 16); } else { const Direction sd = Players[missile._misource]._pdir; missile.setDirection(sd); UpdateMissileVelocity(missile, c + sd, 16); } } if (c != Point { missile.var1, missile.var2 }) { missile.var1 = c.x; missile.var2 = c.y; ChangeLight(missile._mlid, c, 8); } if (missile.duration == 0) { missile.setDirection(Direction::NoDirection); missile.position.velocity = {}; missile.duration = 7; } PutMissile(missile); } } void ProcessResurrectBeam(Missile &missile) { missile.duration--; if (missile.duration == 0) missile._miDelFlag = true; PutMissile(missile); } void ProcessRedPortal(Missile &missile) { const int expLight[17] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15 }; if (missile.duration > 1) missile.duration--; if (missile.duration == missile.var1) missile.setFrameGroup(RedPortalFrame::Idle); if (leveltype != DTYPE_TOWN && missile.getFrameGroup() != RedPortalFrame::Idle && missile.duration != 0) { if (missile.var2 == 0) missile._mlid = AddLight(missile.position.tile, 1); ChangeLight(missile._mlid, missile.position.tile, expLight[missile.var2]); missile.var2++; } if (missile.duration == 0) { missile._miDelFlag = true; AddUnLight(missile._mlid); } PutMissile(missile); } static void DeleteMissiles() { Missiles.remove_if([](Missile &missile) { return missile._miDelFlag; }); } void ProcessManaShield() { Player &myPlayer = *MyPlayer; if (myPlayer.pManaShield && myPlayer.hasNoMana()) { myPlayer.pManaShield = false; NetSendCmd(true, CMD_REMSHIELD); } } void ProcessMissiles() { for (auto &missile : Missiles) { const auto &position = missile.position.tile; if (InDungeonBounds(position)) { dFlags[position.x][position.y] &= ~(DungeonFlag::Missile | DungeonFlag::MissileFireWall | DungeonFlag::MissileLightningWall); } else { missile._miDelFlag = true; } } DeleteMissiles(); MissilePreFlag = false; for (auto &missile : Missiles) { const MissileData &missileData = GetMissileData(missile._mitype); if (missileData.processFn != nullptr) missileData.processFn(missile); if (missile._miAnimFlags == MissileGraphicsFlags::NotAnimated) continue; missile._miAnimCnt++; if (missile._miAnimCnt < missile._miAnimDelay) continue; missile._miAnimCnt = 0; missile._miAnimFrame += missile._miAnimAdd; if (missile._miAnimFrame > missile._miAnimLen) missile._miAnimFrame = 1; else if (missile._miAnimFrame < 1) missile._miAnimFrame = missile._miAnimLen; } ProcessManaShield(); DeleteMissiles(); } void SetUpMissileAnimationData() { for (auto &missile : Missiles) { if (missile._miAnimType == MissileGraphicID::None) continue; if (missile._mitype != MissileID::Rhino) { missile._miAnimData = GetMissileSpriteData(missile._miAnimType).spritesForDirection(missile.getDirection16()); continue; } const CMonster &mon = Monsters[missile._misource].type(); MonsterGraphic graphic; if (IsAnyOf(mon.type, MT_HORNED, MT_MUDRUN, MT_FROSTC, MT_OBLORD)) { graphic = MonsterGraphic::Special; } else if (IsAnyOf(mon.type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) { graphic = MonsterGraphic::Attack; } else { graphic = MonsterGraphic::Walk; } missile._miAnimData = mon.getAnimData(graphic).spritesForDirection(missile.getDirection()); } } void RedoMissileFlags() { for (auto &missile : Missiles) { PutMissile(missile); } } } // namespace devilution ================================================ FILE: Source/missiles.h ================================================ /** * @file missiles.h * * Interface of missile functionality. */ #pragma once #include #include #include #include "engine/displacement.hpp" #include "engine/point.hpp" #include "engine/world_tile.hpp" #include "monster.h" #include "player.h" #include "tables/misdat.h" #include "tables/spelldat.h" #include "utils/is_of.hpp" namespace devilution { constexpr WorldTilePosition GolemHoldingCell = Point { 1, 0 }; struct MissilePosition { /** Sprite's pixel offset from tile. */ Displacement offset; /** Pixel velocity while moving */ Displacement velocity; /** Pixels traveled as a numerator of 65,536. */ Displacement traveled; WorldTilePosition tile; /** Start position */ WorldTilePosition start; /** * @brief Specifies the location (tile) while rendering */ WorldTilePosition tileForRendering; /** * @brief Specifies the location (offset) while rendering */ Displacement offsetForRendering; /** * @brief Stops the missile (set velocity to zero and set offset to last renderer location; shouldn't matter because the missile doesn't move anymore) */ void StopMissile() { velocity = {}; if (tileForRendering == tile) offset = offsetForRendering; } }; enum class MissileSource : uint8_t { Player, Monster, Trap, }; enum class GuardianFrame : uint8_t { Start = 0, Idle = 1, Attack = 2, }; enum class AcidPuddleFrame : uint8_t { Idle = 0, End = 1, }; enum class FireWallFrame : uint8_t { Start = 0, Idle = 1, }; enum class PortalFrame : uint8_t { Start = 0, Idle = 1, }; enum class RedPortalFrame : uint8_t { Start = 0, Idle = 1, }; struct Missile { /** Type of projectile */ MissileID _mitype; MissilePosition position; private: int _mimfnum; // The direction of the missile (direction enum) public: int _mispllvl; bool _miDelFlag; // Indicate whether the missile should be deleted MissileGraphicID _miAnimType; MissileGraphicsFlags _miAnimFlags; OptionalClxSpriteList _miAnimData; int _miAnimDelay; // Tick length of each frame in the current animation int _miAnimLen; // Number of frames in current animation // TODO: This field is no longer used and is always equal to // (*_miAnimData)[0].width() uint16_t _miAnimWidth; int16_t _miAnimWidth2; int _miAnimCnt; // Increases by one each game tick, counting how close we are to _pAnimDelay int _miAnimAdd; int _miAnimFrame; // Current frame of animation + 1. bool _miDrawFlag; bool _miLightFlag; bool _miPreFlag; uint32_t _miUniqTrans; /** @brief Time to live for the missile in game ticks; once 0, the missile will be marked for deletion via _miDelFlag */ int duration; int _misource; mienemy_type _micaster; int _midam; bool _miHitFlag; int _midist; // Used for arrows to measure distance travelled (increases by 1 each game tick). Higher value is a penalty for accuracy calculation when hitting enemy int _mlid; int _mirnd; int var1; int var2; int var3; int var4; int var5; int var6; int var7; bool limitReached; /** * @brief For moving missiles lastCollisionTargetHash contains the last entity (player or monster) that was checked in CheckMissileCol (needed to avoid multiple hits for a entity at the same tile). */ int16_t lastCollisionTargetHash; /** @brief Was the missile generated by a trap? */ [[nodiscard]] bool IsTrap() const { return _misource == -1; } [[nodiscard]] Player *sourcePlayer() { if (IsNoneOf(_micaster, TARGET_BOTH, TARGET_MONSTERS) || _misource == -1) return nullptr; return &Players[_misource]; } [[nodiscard]] Monster *sourceMonster() { if (_micaster != TARGET_PLAYERS || _misource == -1) return nullptr; return &Monsters[_misource]; } [[nodiscard]] bool isSameSource(Missile &missile) { return sourceType() == missile.sourceType() && _misource == missile._misource; } MissileSource sourceType() { if (_misource == -1) return MissileSource::Trap; if (_micaster == TARGET_PLAYERS) return MissileSource::Monster; return MissileSource::Player; } void setAnimation(MissileGraphicID animtype); /** * @brief Sets the missile sprite to the given sheet frame * @param dir Sprite frame */ void setFrameGroupRaw(int frameGroup) { _mimfnum = frameGroup; setAnimation(_miAnimType); } void setDefaultFrameGroup() { setFrameGroupRaw(0); } template void setFrameGroup(FrameEnum frameGroup) { setFrameGroupRaw(static_cast(frameGroup)); } /** * @brief Sets the sprite for this missile so it matches the given Direction * @param dir Desired facing */ void setDirection(Direction dir) { setFrameGroupRaw(static_cast(dir)); } /** * @brief Sets the sprite for this missile so it matches the given Direction16 * @param dir Desired facing at a 22.8125 degree resolution */ void setDirection(Direction16 dir) { setFrameGroupRaw(static_cast(dir)); } int getFrameGroupRaw() const { return _mimfnum; } template FrameEnum getFrameGroup() const { static_assert(std::is_enum_v, "Frame group must be an enum"); return static_cast(_mimfnum); } [[nodiscard]] Direction getDirection() const { return static_cast(_mimfnum); } [[nodiscard]] Direction16 getDirection16() const { return static_cast(_mimfnum); } }; extern std::list Missiles; extern bool MissilePreFlag; struct DamageRange { int min; int max; }; DamageRange GetDamageAmt(SpellID spell, int spellLevel); /** * @brief Returns the direction a vector from p1(x1, y1) to p2(x2, y2) is pointing to. * * @code{.unparsed} * W sW SW Sw S * ^ * nW | Se * | * NW ------+-----> SE * | * Nw | sE * | * N Ne NE nE E * @endcode * * @param p1 The point from which the vector starts. * @param p2 The point from which the vector ends. * @return the direction of the p1->p2 vector */ Direction16 GetDirection16(Point p1, Point p2); bool MonsterTrapHit(Monster &monster, int mindam, int maxdam, int dist, MissileID t, DamageType damageType, bool shift); bool PlayerMHit(Player &player, Monster *monster, int dist, int mind, int maxd, MissileID mtype, DamageType damageType, bool shift, DeathReason deathReason, bool *blocked); /** * @brief Could the missile collide with solid objects? (like walls or closed doors) */ bool IsMissileBlockedByTile(Point tile); void InitMissiles(); struct AddMissileParameter { WorldTilePosition dst; Direction midir; Missile *pParent; bool spellFizzled; }; void AddOpenNest(Missile &missile, AddMissileParameter ¶meter); void AddRuneOfFire(Missile &missile, AddMissileParameter ¶meter); void AddRuneOfLight(Missile &missile, AddMissileParameter ¶meter); void AddRuneOfNova(Missile &missile, AddMissileParameter ¶meter); void AddRuneOfImmolation(Missile &missile, AddMissileParameter ¶meter); void AddRuneOfStone(Missile &missile, AddMissileParameter ¶meter); void AddReflect(Missile &missile, AddMissileParameter ¶meter); void AddBerserk(Missile &missile, AddMissileParameter ¶meter); /** * var1: Direction to place the spawn */ void AddHorkSpawn(Missile &missile, AddMissileParameter ¶meter); void AddJester(Missile &missile, AddMissileParameter ¶meter); void AddStealPotions(Missile &missile, AddMissileParameter ¶meter); void AddStealMana(Missile &missile, AddMissileParameter ¶meter); void AddSpectralArrow(Missile &missile, AddMissileParameter ¶meter); void AddWarp(Missile &missile, AddMissileParameter ¶meter); void AddLightningWall(Missile &missile, AddMissileParameter ¶meter); void AddBigExplosion(Missile &missile, AddMissileParameter ¶meter); void AddImmolation(Missile &missile, AddMissileParameter ¶meter); void AddLightningBow(Missile &missile, AddMissileParameter ¶meter); void AddMana(Missile &missile, AddMissileParameter ¶meter); void AddMagi(Missile &missile, AddMissileParameter ¶meter); void AddRingOfFire(Missile &missile, AddMissileParameter ¶meter); void AddSearch(Missile &missile, AddMissileParameter ¶meter); void AddChargedBoltBow(Missile &missile, AddMissileParameter ¶meter); void AddElementalArrow(Missile &missile, AddMissileParameter ¶meter); void AddArrow(Missile &missile, AddMissileParameter ¶meter); void AddPhasing(Missile &missile, AddMissileParameter ¶meter); void AddFirebolt(Missile &missile, AddMissileParameter ¶meter); void AddMagmaBall(Missile &missile, AddMissileParameter ¶meter); void AddTeleport(Missile &missile, AddMissileParameter ¶meter); void AddNovaBall(Missile &missile, AddMissileParameter ¶meter); void AddFireWall(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the missile-light * var2: Y coordinate of the missile-light * var4: X coordinate of the missile-light * var5: Y coordinate of the missile-light */ void AddFireball(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the missile * var2: Y coordinate of the missile */ void AddLightningControl(Missile &missile, AddMissileParameter ¶meter); void AddLightning(Missile &missile, AddMissileParameter ¶meter); void AddMissileExplosion(Missile &missile, AddMissileParameter ¶meter); void AddWeaponExplosion(Missile &missile, AddMissileParameter ¶meter); /** * var1: Animation */ void AddTownPortal(Missile &missile, AddMissileParameter ¶meter); void AddFlashBottom(Missile &missile, AddMissileParameter ¶meter); void AddFlashTop(Missile &missile, AddMissileParameter ¶meter); void AddManaShield(Missile &missile, AddMissileParameter ¶meter); void AddFlameWave(Missile &missile, AddMissileParameter ¶meter); /** * var1: Animation * var3: Light strength */ void AddGuardian(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the destination * var2: Y coordinate of the destination */ void AddChainLightning(Missile &missile, AddMissileParameter ¶meter); void AddRhino(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the missile-light * var2: Y coordinate of the missile-light */ void AddGenericMagicMissile(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the missile-light * var2: Y coordinate of the missile-light */ void AddAcid(Missile &missile, AddMissileParameter ¶meter); void AddAcidPuddle(Missile &missile, AddMissileParameter ¶meter); /** * var1: mmode of the monster * var2: mnum of the monster */ void AddStoneCurse(Missile &missile, AddMissileParameter ¶meter); void AddGolem(Missile &missile, AddMissileParameter ¶meter); void AddApocalypseBoom(Missile &missile, AddMissileParameter ¶meter); void AddHealing(Missile &missile, AddMissileParameter ¶meter); void AddHealOther(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the missile-light * var2: Y coordinate of the missile-light * var4: X coordinate of the destination * var5: Y coordinate of the destination */ void AddElemental(Missile &missile, AddMissileParameter ¶meter); void AddIdentify(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the first wave * var2: Y coordinate of the first wave * var3: Direction of the first wave * var4: Direction of the second wave * var5: X coordinate of the second wave * var6: Y coordinate of the second wave */ void AddWallControl(Missile &missile, AddMissileParameter ¶meter); void AddInfravision(Missile &missile, AddMissileParameter ¶meter); /** * var1: X coordinate of the destination * var2: Y coordinate of the destination */ void AddFlameWaveControl(Missile &missile, AddMissileParameter ¶meter); void AddNova(Missile &missile, AddMissileParameter ¶meter); void AddRage(Missile &missile, AddMissileParameter ¶meter); void AddItemRepair(Missile &missile, AddMissileParameter ¶meter); void AddStaffRecharge(Missile &missile, AddMissileParameter ¶meter); void AddTrapDisarm(Missile &missile, AddMissileParameter ¶meter); void AddApocalypse(Missile &missile, AddMissileParameter ¶meter); void AddInferno(Missile &missile, AddMissileParameter ¶meter); void AddInfernoControl(Missile &missile, AddMissileParameter ¶meter); /** * var1: Light strength * var2: Base direction */ void AddChargedBolt(Missile &missile, AddMissileParameter ¶meter); void AddHolyBolt(Missile &missile, AddMissileParameter ¶meter); void AddResurrect(Missile &missile, AddMissileParameter ¶meter); void AddResurrectBeam(Missile &missile, AddMissileParameter ¶meter); void AddTelekinesis(Missile &missile, AddMissileParameter ¶meter); void AddBoneSpirit(Missile &missile, AddMissileParameter ¶meter); void AddRedPortal(Missile &missile, AddMissileParameter ¶meter); void AddDiabloApocalypse(Missile &missile, AddMissileParameter ¶meter); Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midir, MissileID mitype, mienemy_type micaster, int id, int midam, int spllvl, Missile *parent = nullptr, std::optional lSFX = std::nullopt); inline Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midir, MissileID mitype, mienemy_type micaster, const Player &player, int midam, int spllvl, Missile *parent = nullptr, std::optional lSFX = std::nullopt) { return AddMissile(src, dst, midir, mitype, micaster, player.getId(), midam, spllvl, parent, lSFX); } inline Missile *AddMissile(WorldTilePosition src, WorldTilePosition dst, Direction midir, MissileID mitype, mienemy_type micaster, const Monster &monster, int midam, int spllvl, Missile *parent = nullptr, std::optional lSFX = std::nullopt) { return AddMissile(src, dst, midir, mitype, micaster, static_cast(monster.getId()), midam, spllvl, parent, lSFX); } void ProcessElementalArrow(Missile &missile); void ProcessArrow(Missile &missile); void ProcessGenericProjectile(Missile &missile); void ProcessNovaBall(Missile &missilei); void ProcessAcidPuddle(Missile &missile); void ProcessFireWall(Missile &missile); void ProcessFireball(Missile &missile); void ProcessHorkSpawn(Missile &missile); void ProcessRune(Missile &missile); void ProcessLightningWall(Missile &missile); void ProcessBigExplosion(Missile &missile); void ProcessLightningBow(Missile &missile); void ProcessRingOfFire(Missile &missile); void ProcessSearch(Missile &missile); void ProcessImmolation(Missile &missile); void ProcessSpectralArrow(Missile &missile); void ProcessLightningControl(Missile &missile); void ProcessLightning(Missile &missile); void ProcessTownPortal(Missile &missile); void ProcessFlashBottom(Missile &missile); void ProcessFlashTop(Missile &missile); void ProcessFlameWave(Missile &missile); void ProcessGuardian(Missile &missile); void ProcessChainLightning(Missile &missile); void ProcessWeaponExplosion(Missile &missile); void ProcessMissileExplosion(Missile &missile); void ProcessAcidSplate(Missile &missile); void ProcessTeleport(Missile &missile); void ProcessStoneCurse(Missile &missile); void ProcessApocalypseBoom(Missile &missile); void ProcessRhino(Missile &missile); void ProcessWallControl(Missile &missile); void ProcessInfravision(Missile &missile); void ProcessApocalypse(Missile &missile); void ProcessFlameWaveControl(Missile &missile); void ProcessNova(Missile &missile); void ProcessRage(Missile &missile); void ProcessInferno(Missile &missile); void ProcessInfernoControl(Missile &missile); void ProcessChargedBolt(Missile &missile); void ProcessHolyBolt(Missile &missile); void ProcessElemental(Missile &missile); void ProcessBoneSpirit(Missile &missile); void ProcessResurrectBeam(Missile &missile); void ProcessRedPortal(Missile &missile); void ProcessMissiles(); void SetUpMissileAnimationData(); void RedoMissileFlags(); #ifdef BUILD_TESTING void TestRotateBlockedMissile(Missile &missile); #endif } // namespace devilution ================================================ FILE: Source/monster.cpp ================================================ /** * @file monster.cpp * * Implementation of monster functionality, AI, actions, spawning, loading, etc. */ #include "monster.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include #include "automap.h" #include "control/control.hpp" #include "crawl.hpp" #include "cursor.h" #include "dead.h" #include "diablo.h" #include "dvlnet/leaveinfo.hpp" #include "effects.h" #include "engine/animationinfo.h" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/direction.hpp" #include "engine/lighting_defs.hpp" #include "engine/load_cl2.hpp" #include "engine/load_file.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/sound.h" #include "engine/sound_position.hpp" #include "engine/world_tile.hpp" #include "function_ref.hpp" #include "game_mode.hpp" #include "headless_mode.hpp" #include "inv.h" #include "items.h" #include "levels/crypt.h" #include "levels/drlg_l4.h" #include "levels/dun_tile.hpp" #include "levels/gendung.h" #include "levels/gendung_defs.hpp" #include "levels/themes.h" #include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "lua/lua_event.hpp" #include "minitext.h" #include "missiles.h" #include "movie.h" #include "msg.h" #include "multi.h" #include "objects.h" #include "options.h" #include "player.h" #include "quests.h" #include "sound_effect_enums.h" #include "storm/storm_net.hpp" #include "tables/itemdat.h" #include "tables/misdat.h" #include "tables/monstdat.h" #include "tables/objdat.h" #include "tables/playerdat.hpp" #include "tables/spelldat.h" #include "tables/textdat.h" #include "utils/algorithm/container.hpp" #include "utils/attributes.h" #include "utils/cl2_to_clx.hpp" #include "utils/endian_swap.hpp" #include "utils/enum_traits.h" #include "utils/file_name_generator.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/pointer_value_union.hpp" #include "utils/static_vector.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef _DEBUG #include "debug.h" #endif namespace devilution { CMonster LevelMonsterTypes[MaxLvlMTypes]; size_t LevelMonsterTypeCount; Monster Monsters[MaxMonsters]; unsigned ActiveMonsters[MaxMonsters]; size_t ActiveMonsterCount; /** Tracks the total number of monsters killed per monster_id. */ int MonsterKillCounts[NUM_MAX_MTYPES]; bool sgbSaveSoundOn; namespace { constexpr int NightmareToHitBonus = 85; constexpr int HellToHitBonus = 120; constexpr int NightmareAcBonus = 50; constexpr int HellAcBonus = 80; /** @brief Reserved some entries in @Monster for golems. For vanilla compatibility, this must remain 4. */ constexpr int ReservedMonsterSlotsForGolems = 4; /** Tracks which missile files are already loaded */ size_t totalmonsters; int monstimgtot; int uniquetrans; constexpr const std::array<_monster_id, 12> SkeletonTypes { MT_WSKELAX, MT_TSKELAX, MT_RSKELAX, MT_XSKELAX, MT_WSKELBW, MT_TSKELBW, MT_RSKELBW, MT_XSKELBW, MT_WSKELSD, MT_TSKELSD, MT_RSKELSD, MT_XSKELSD, }; /** Maps from monster action to monster animation letter. */ constexpr char Animletter[7] = "nwahds"; size_t GetNumAnims(const MonsterData &monsterData) { return monsterData.hasSpecial ? 6 : 5; } size_t GetNumAnimsWithGraphics(const MonsterData &monsterData) { // Monster graphics can be missing for certain actions, // e.g. Golem has no standing graphics. const size_t numAnims = GetNumAnims(monsterData); size_t result = 0; for (size_t i = 0; i < numAnims; ++i) { if (monsterData.hasAnim(i)) ++result; } return result; } void InitMonsterTRN(CMonster &monst) { char path[64]; *BufCopy(path, "monsters\\", monst.data().trnFile, ".trn") = '\0'; std::array colorTranslations; LoadFileInMem(path, colorTranslations); std::replace(colorTranslations.begin(), colorTranslations.end(), 255, 0); const size_t numAnims = GetNumAnims(monst.data()); for (size_t i = 0; i < numAnims; i++) { if (i == 1 && IsAnyOf(monst.type, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE)) { continue; } AnimStruct &anim = monst.anims[i]; if (anim.sprites->isSheet()) { ClxApplyTrans(ClxSpriteSheet { anim.sprites->sheet() }, colorTranslations.data()); } else { ClxApplyTrans(ClxSpriteList { anim.sprites->list() }, colorTranslations.data()); } } } void InitMonster(Monster &monster, Direction rd, size_t typeIndex, Point position) { monster.direction = rd; monster.position.tile = position; monster.position.future = position; monster.position.old = position; monster.levelType = static_cast(typeIndex); monster.mode = MonsterMode::Stand; monster.animInfo = {}; monster.changeAnimationData(MonsterGraphic::Stand); monster.animInfo.tickCounterOfCurrentFrame = GenerateRnd(monster.animInfo.ticksPerFrame - 1); monster.animInfo.currentFrame = GenerateRnd(monster.animInfo.numberOfFrames - 1); const int maxhp = RandomIntBetween(monster.data().hitPointsMinimum, monster.data().hitPointsMaximum); monster.maxHitPoints = maxhp << 6; if (!gbIsMultiplayer) monster.maxHitPoints = std::max(monster.maxHitPoints / 2, 64); monster.hitPoints = monster.maxHitPoints; monster.ai = monster.data().ai; monster.intelligence = monster.data().intelligence; monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; monster.goalVar3 = 0; monster.pathCount = 0; monster.enemy = 0; monster.isInvalid = false; monster.uniqueType = UniqueMonsterType::None; monster.activeForTicks = 0; monster.lightId = NO_LIGHT; monster.rndItemSeed = AdvanceRndSeed(); monster.aiSeed = AdvanceRndSeed(); monster.whoHit = 0; monster.minDamage = monster.data().minDamage; monster.maxDamage = monster.data().maxDamage; monster.minDamageSpecial = monster.data().minDamageSpecial; monster.maxDamageSpecial = monster.data().maxDamageSpecial; monster.armorClass = monster.data().armorClass; monster.reducePlayerStrength = monster.data().reducePlayerStrength; monster.reducePlayerMagic = monster.data().reducePlayerMagic; monster.reducePlayerDexterity = monster.data().reducePlayerDexterity; monster.reducePlayerVitality = monster.data().reducePlayerVitality; monster.reducePlayerMaxHP = monster.data().reducePlayerMaxHP; monster.reducePlayerMaxMana = monster.data().reducePlayerMaxMana; monster.resistance = monster.data().resistance; monster.leader = Monster::NoLeader; monster.leaderRelation = LeaderRelation::None; monster.flags = monster.data().abilityFlags; monster.talkMsg = TEXT_NONE; if (monster.ai == MonsterAIID::Gargoyle) { monster.changeAnimationData(MonsterGraphic::Special); monster.animInfo.currentFrame = 0; monster.flags |= MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.maxHitPoints = 3 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 100 : 50) << 6; else monster.maxHitPoints += 100 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 2 * (monster.minDamage + 2); monster.maxDamage = 2 * (monster.maxDamage + 2); monster.minDamageSpecial = 2 * (monster.minDamageSpecial + 2); monster.maxDamageSpecial = 2 * (monster.maxDamageSpecial + 2); monster.armorClass += NightmareAcBonus; } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.maxHitPoints = 4 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 200 : 100) << 6; else monster.maxHitPoints += 200 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 4 * monster.minDamage + 6; monster.maxDamage = 4 * monster.maxDamage + 6; monster.minDamageSpecial = 4 * monster.minDamageSpecial + 6; monster.maxDamageSpecial = 4 * monster.maxDamageSpecial + 6; monster.armorClass += HellAcBonus; monster.resistance = monster.data().resistanceHell; } } bool CanPlaceMonster(Point position) { return InDungeonBounds(position) && dMonster[position.x][position.y] == 0 && dPlayer[position.x][position.y] == 0 && !IsTileVisible(position) && !TileContainsSetPiece(position) && !IsTileOccupied(position); } void PlaceMonster(size_t i, size_t typeIndex, Point position) { if (LevelMonsterTypes[typeIndex].type == MT_NAKRUL) { for (size_t j = 0; j < ActiveMonsterCount; j++) { if (Monsters[j].levelType == typeIndex) { return; } } } Monster &monster = Monsters[i]; monster.occupyTile(position, false); auto rd = static_cast(GenerateRnd(8)); InitMonster(monster, rd, typeIndex, position); } void PlaceGroup(size_t typeIndex, size_t num, Monster *leader = nullptr, bool leashed = false) { uint8_t placed = 0; for (int try1 = 0; try1 < 10; try1++) { while (placed != 0) { ActiveMonsterCount--; placed--; const Point &position = Monsters[ActiveMonsterCount].position.tile; dMonster[position.x][position.y] = 0; } int xp; int yp; if (leader != nullptr) { const int offset = GenerateRnd(8); auto position = leader->position.tile + static_cast(offset); xp = position.x; yp = position.y; } else { do { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; } while (!CanPlaceMonster({ xp, yp })); } const int x1 = xp; const int y1 = yp; if (num + ActiveMonsterCount > totalmonsters) { num = totalmonsters - ActiveMonsterCount; } unsigned j = 0; for (unsigned try2 = 0; j < num && try2 < 100; xp += Displacement(static_cast(GenerateRnd(8))).deltaX, yp += Displacement(static_cast(GenerateRnd(8))).deltaX) { /// BUGFIX: `yp += Point.y` if (!CanPlaceMonster({ xp, yp }) || (dTransVal[xp][yp] != dTransVal[x1][y1]) || (leashed && (std::abs(xp - x1) >= 4 || std::abs(yp - y1) >= 4))) { try2++; continue; } PlaceMonster(ActiveMonsterCount, typeIndex, { xp, yp }); if (leader != nullptr) { Monster &minion = Monsters[ActiveMonsterCount]; minion.maxHitPoints *= 2; minion.hitPoints = minion.maxHitPoints; minion.intelligence = leader->intelligence; if (leashed) { minion.setLeader(leader); } if (minion.ai != MonsterAIID::Gargoyle) { minion.changeAnimationData(MonsterGraphic::Stand); minion.animInfo.currentFrame = GenerateRnd(minion.animInfo.numberOfFrames - 1); minion.flags &= ~MFLAG_ALLOW_SPECIAL; minion.mode = MonsterMode::Stand; } } ActiveMonsterCount++; placed++; j++; } if (placed >= num) { break; } } if (leashed) { leader->packSize = placed; } } size_t GetMonsterTypeIndex(_monster_id type) { for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (LevelMonsterTypes[i].type == type) return i; } return LevelMonsterTypeCount; } Point GetUniqueMonstPosition(UniqueMonsterType uniqindex) { if (setlevel) { switch (uniqindex) { case UniqueMonsterType::Lazarus: return { 32, 46 }; case UniqueMonsterType::RedVex: return { 40, 45 }; case UniqueMonsterType::BlackJade: return { 38, 49 }; case UniqueMonsterType::SkeletonKing: return { 35, 47 }; default: break; } } switch (uniqindex) { case UniqueMonsterType::SnotSpill: return SetPiece.position.megaToWorld() + Displacement { 8, 12 }; case UniqueMonsterType::WarlordOfBlood: return SetPiece.position.megaToWorld() + Displacement { 6, 7 }; case UniqueMonsterType::Zhar: for (int i = 0; i < themeCount; i++) { if (i == zharlib) { return themeLoc[i].room.position.megaToWorld() + Displacement { 4, 4 }; } } break; case UniqueMonsterType::Lazarus: return SetPiece.position.megaToWorld() + Displacement { 3, 6 }; case UniqueMonsterType::RedVex: return SetPiece.position.megaToWorld() + Displacement { 5, 3 }; case UniqueMonsterType::BlackJade: return SetPiece.position.megaToWorld() + Displacement { 5, 9 }; case UniqueMonsterType::Butcher: return SetPiece.position.megaToWorld() + Displacement { 4, 4 }; case UniqueMonsterType::NaKrul: if (UberRow == 0 || UberCol == 0) { UberDiabloMonsterIndex = -1; break; } UberDiabloMonsterIndex = static_cast(ActiveMonsterCount); return { UberRow - 2, UberCol }; default: break; } Point position; int count = 0; do { position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 }; int count2 = 0; for (int x = position.x - 3; x < position.x + 3; x++) { for (int y = position.y - 3; y < position.y + 3; y++) { if (InDungeonBounds({ x, y }) && CanPlaceMonster({ x, y })) { count2++; } } } if (count2 < 9) { count++; if (count < 1000) { continue; } } } while (!CanPlaceMonster(position)); return position; } tl::expected PlaceUniqueMonst(UniqueMonsterType uniqindex, size_t minionType, int bosspacksize) { const auto &uniqueMonsterData = UniqueMonstersData[static_cast(uniqindex)]; const size_t typeIndex = GetMonsterTypeIndex(uniqueMonsterData.mtype); const Point position = GetUniqueMonstPosition(uniqindex); PlaceMonster(ActiveMonsterCount, typeIndex, position); Monster &monster = Monsters[ActiveMonsterCount]; ActiveMonsterCount++; return PrepareUniqueMonst(monster, uniqindex, minionType, bosspacksize, uniqueMonsterData); } void ClearMVars(Monster &monster) { monster.var1 = 0; monster.var2 = 0; monster.var3 = 0; monster.position.temp = { 0, 0 }; } void ClrAllMonsters() { for (auto &monster : Monsters) { ClearMVars(monster); monster.goal = MonsterGoal::None; monster.mode = MonsterMode::Stand; monster.var1 = 0; monster.var2 = 0; monster.position.tile = { 0, 0 }; monster.position.future = { 0, 0 }; monster.position.old = { 0, 0 }; monster.direction = static_cast(GenerateRnd(8)); monster.animInfo = {}; monster.flags = MFLAG_NO_ENEMY; monster.isInvalid = false; monster.enemy = 0; monster.enemyPosition = {}; DiscardRandomValues(1); } } tl::expected PlaceUniqueMonsters() { for (size_t u = 0; u < UniqueMonstersData.size(); ++u) { if (UniqueMonstersData[u].mlevel != currlevel) continue; const size_t minionType = GetMonsterTypeIndex(UniqueMonstersData[u].mtype); if (minionType == LevelMonsterTypeCount) continue; const auto uniqueType = static_cast(u); if (uniqueType == UniqueMonsterType::Garbud && Quests[Q_GARBUD]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::Zhar && Quests[Q_ZHAR]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::SnotSpill && Quests[Q_LTBANNER]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::Lachdan && Quests[Q_VEIL]._qactive == QUEST_NOTAVAIL) continue; if (uniqueType == UniqueMonsterType::WarlordOfBlood && Quests[Q_WARLORD]._qactive == QUEST_NOTAVAIL) continue; RETURN_IF_ERROR(PlaceUniqueMonst(uniqueType, minionType, 8)); } return {}; } tl::expected PlaceQuestMonsters() { if (!setlevel) { if (Quests[Q_BUTCHER].IsAvailable()) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Butcher, 0, 0)); } if (currlevel == Quests[Q_SKELKING]._qlevel && UseMultiplayerQuests()) { for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsSkel(LevelMonsterTypes[i].type)) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::SkeletonKing, i, 30)); break; } } } if (Quests[Q_LTBANNER].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l1data\\banner1.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_BLOOD].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l2data\\blood2.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_BLIND].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l2data\\blind2.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (Quests[Q_ANVIL].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l3data\\anvil.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld() + Displacement { 2, 2 })); } if (Quests[Q_WARLORD].IsAvailable()) { auto dunData = LoadFileInMem("levels\\l4data\\warlord.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::WarlordOfBlood, PLACE_SCATTER)); } if (Quests[Q_VEIL].IsAvailable()) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lachdan, PLACE_SCATTER)); } if (Quests[Q_ZHAR].IsAvailable() && zharlib == -1) { Quests[Q_ZHAR]._qactive = QUEST_NOTAVAIL; } if (currlevel == Quests[Q_BETRAYER]._qlevel && UseMultiplayerQuests()) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lazarus, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::RedVex, PLACE_UNIQUE)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Lazarus, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::RedVex, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::BlackJade, 0, 0)); auto dunData = LoadFileInMem("levels\\l4data\\vile1.dun"); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), SetPiece.position.megaToWorld())); } if (currlevel == 24) { UberDiabloMonsterIndex = -1; const size_t typeIndex = GetMonsterTypeIndex(MT_NAKRUL); if (typeIndex < LevelMonsterTypeCount) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const Monster &monster = Monsters[i]; if (monster.isUnique() || monster.levelType == typeIndex) { UberDiabloMonsterIndex = static_cast(i); break; } } } if (UberDiabloMonsterIndex == -1) RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::NaKrul, 0, 0)); } } else if (setlvlnum == SL_SKELKING) { RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::SkeletonKing, 0, 0)); } else if (setlvlnum == SL_VILEBETRAYER) { RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lazarus, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::RedVex, PLACE_UNIQUE)); RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::BlackJade, PLACE_UNIQUE)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::Lazarus, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::RedVex, 0, 0)); RETURN_IF_ERROR(PlaceUniqueMonst(UniqueMonsterType::BlackJade, 0, 0)); } return {}; } tl::expected LoadDiabMonsts() { { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab1.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad1.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab2a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad2.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab3a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad3.megaToWorld())); } { ASSIGN_OR_RETURN(auto dunData, LoadFileInMemWithStatus("levels\\l4data\\diab4a.dun")); RETURN_IF_ERROR(SetMapMonsters(dunData.get(), DiabloQuad4.megaToWorld())); } return {}; } void DeleteMonster(size_t activeIndex) { const unsigned monsterId = ActiveMonsters[activeIndex]; const Monster &monster = Monsters[monsterId]; if ((monster.flags & MFLAG_BERSERK) != 0) { AddUnLight(monster.lightId); } ActiveMonsterCount--; std::swap(ActiveMonsters[activeIndex], ActiveMonsters[ActiveMonsterCount]); // This ensures alive monsters are before ActiveMonsterCount in the array and any deleted monster after for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &activeMonster = Monsters[ActiveMonsters[i]]; if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) != 0 && activeMonster.enemy == monsterId) { activeMonster.flags |= MFLAG_NO_ENEMY; } } } void NewMonsterAnim(Monster &monster, MonsterGraphic graphic, Direction md, AnimationDistributionFlags flags = AnimationDistributionFlags::None, int8_t numSkippedFrames = 0, int8_t distributeFramesBeforeFrame = 0) { const auto &animData = monster.type().getAnimData(graphic); monster.animInfo.setNewAnimation(animData.spritesForDirection(md), animData.frames, animData.rate, flags, numSkippedFrames, distributeFramesBeforeFrame); monster.flags &= ~(MFLAG_LOCK_ANIMATION | MFLAG_ALLOW_SPECIAL); monster.direction = md; } void StartMonsterGotHit(Monster &monster) { if (monster.type().type != MT_GOLEM) { auto animationFlags = gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None; NewMonsterAnim(monster, MonsterGraphic::GotHit, monster.direction, animationFlags); monster.mode = MonsterMode::HitRecovery; } monster.position.tile = monster.position.old; monster.position.future = monster.position.old; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); } DVL_ALWAYS_INLINE bool IsRanged(Monster &monster) { return IsAnyOf(monster.ai, MonsterAIID::SkeletonRanged, MonsterAIID::GoatRanged, MonsterAIID::Succubus, MonsterAIID::LazarusSuccubus); } void UpdateEnemy(Monster &monster) { WorldTilePosition target; int menemy = -1; int bestDist = -1; bool bestsameroom = false; const WorldTilePosition position = monster.position.tile; const bool isPlayerMinion = monster.isPlayerMinion(); if (!isPlayerMinion) { for (size_t pnum = 0; pnum < Players.size(); pnum++) { const Player &player = Players[pnum]; if (!player.plractive || !player.isOnActiveLevel() || player._pLvlChanging || (player.hasNoLife() && gbIsMultiplayer)) continue; const bool sameroom = (dTransVal[position.x][position.y] == dTransVal[player.position.tile.x][player.position.tile.y]); const int dist = position.WalkingDistance(player.position.tile); if ((sameroom && !bestsameroom) || ((sameroom || !bestsameroom) && dist < bestDist) || (menemy == -1)) { monster.flags &= ~MFLAG_TARGETS_MONSTER; menemy = static_cast(pnum); target = player.position.future; bestDist = dist; bestsameroom = sameroom; } } } for (size_t i = 0; i < ActiveMonsterCount; i++) { const unsigned monsterId = ActiveMonsters[i]; Monster &otherMonster = Monsters[monsterId]; if (&otherMonster == &monster) continue; if (otherMonster.hasNoLife()) continue; if (otherMonster.position.tile == GolemHoldingCell) continue; if (otherMonster.talkMsg != TEXT_NONE && M_Talker(otherMonster)) continue; if (isPlayerMinion && otherMonster.isPlayerMinion()) // prevent golems from fighting each other continue; const int dist = otherMonster.position.tile.WalkingDistance(position); if (((monster.flags & MFLAG_GOLEM) == 0 && (monster.flags & MFLAG_BERSERK) == 0 && dist >= 2 && !IsRanged(monster)) || ((monster.flags & MFLAG_GOLEM) == 0 && (monster.flags & MFLAG_BERSERK) == 0 && (otherMonster.flags & MFLAG_GOLEM) == 0)) { continue; } const bool sameroom = dTransVal[position.x][position.y] == dTransVal[otherMonster.position.tile.x][otherMonster.position.tile.y]; if ((sameroom && !bestsameroom) || ((sameroom || !bestsameroom) && dist < bestDist) || (menemy == -1)) { monster.flags |= MFLAG_TARGETS_MONSTER; menemy = static_cast(monsterId); target = otherMonster.position.future; bestDist = dist; bestsameroom = sameroom; } } if (menemy != -1) { monster.flags &= ~MFLAG_NO_ENEMY; monster.enemy = menemy; monster.enemyPosition = target; } else { monster.flags |= MFLAG_NO_ENEMY; } } /** * @brief Make the AI wait a bit before thinking again * @param monster The monster that will wait * @param len */ void AiDelay(Monster &monster, int len) { if (len <= 0) { return; } if (monster.ai == MonsterAIID::Lazarus) { return; } monster.var2 = len; monster.mode = MonsterMode::Delay; } /** * @brief Get the direction from the monster to its current enemy */ Direction GetMonsterDirection(Monster &monster) { return GetDirection(monster.position.tile, monster.enemyPosition); } void StartSpecialStand(Monster &monster, Direction md) { NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::SpecialStand; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void WalkInDirection(Monster &monster, Direction endDir) { Point dir = { 0, 0 }; dir += endDir; const auto fx = static_cast(monster.position.tile.x + dir.x); const auto fy = static_cast(monster.position.tile.y + dir.y); MonsterMode mode; switch (endDir) { case Direction::NorthWest: case Direction::North: case Direction::NorthEast: mode = MonsterMode::MoveNorthwards; break; case Direction::West: case Direction::East: mode = MonsterMode::MoveSideways; break; case Direction::SouthWest: case Direction::South: case Direction::SouthEast: mode = MonsterMode::MoveSouthwards; break; case Direction::NoDirection: DVL_UNREACHABLE(); break; } monster.mode = mode; monster.position.old = monster.position.tile; monster.position.future = { fx, fy }; monster.occupyTile(monster.position.future, true); monster.var1 = dir.x; monster.var2 = dir.y; monster.var3 = static_cast(endDir); NewMonsterAnim(monster, MonsterGraphic::Walk, endDir, AnimationDistributionFlags::ProcessAnimationPending, -1); } void StartAttack(Monster &monster) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending); monster.mode = MonsterMode::MeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartRangedAttack(Monster &monster, MissileID missileType, int dam) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Attack, md, AnimationDistributionFlags::ProcessAnimationPending); monster.mode = MonsterMode::RangedAttack; monster.var1 = static_cast(missileType); monster.var2 = dam; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartRangedSpecialAttack(Monster &monster, MissileID missileType, int dam) { const Direction md = GetMonsterDirection(monster); int8_t distributeFramesBeforeFrame = 0; if (monster.ai == MonsterAIID::Mega) distributeFramesBeforeFrame = monster.data().animFrameNumSpecial; NewMonsterAnim(monster, MonsterGraphic::Special, md, AnimationDistributionFlags::ProcessAnimationPending, 0, distributeFramesBeforeFrame); monster.mode = MonsterMode::SpecialRangedAttack; monster.var1 = static_cast(missileType); monster.var2 = 0; monster.var3 = dam; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartSpecialAttack(Monster &monster) { const Direction md = GetMonsterDirection(monster); NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::SpecialMeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void StartEating(Monster &monster) { NewMonsterAnim(monster, MonsterGraphic::Special, monster.direction); monster.mode = MonsterMode::SpecialMeleeAttack; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; } void DiabloDeath(Monster &diablo, bool sendmsg) { PlaySFX(SfxID::DiabloDeath); auto &quest = Quests[Q_DIABLO]; quest._qactive = QUEST_DONE; if (sendmsg) NetSendCmdQuest(true, quest); sgbSaveSoundOn = gbSoundOn; gbProcessPlayers = false; for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.type().type == MT_DIABLO || diablo.activeForTicks == 0) continue; NewMonsterAnim(monster, MonsterGraphic::Death, monster.direction); monster.mode = MonsterMode::Death; monster.var1 = 0; monster.position.tile = monster.position.old; monster.position.future = monster.position.tile; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); } AddLight(diablo.position.tile, 8); DoVision(diablo.position.tile, 8, MAP_EXP_NONE, true); int dist = diablo.position.tile.WalkingDistance(ViewPosition); dist = std::min(dist, 20); diablo.var3 = ViewPosition.x << 16; diablo.position.temp.x = ViewPosition.y << 16; diablo.position.temp.y = (int)((diablo.var3 - (diablo.position.tile.x << 16)) / (float)dist); if (!gbIsMultiplayer) { Player &myPlayer = *MyPlayer; myPlayer.pDiabloKillLevel = std::max(myPlayer.pDiabloKillLevel, static_cast(sgGameInitInfo.nDifficulty + 1)); } } void SpawnLoot(Monster &monster, bool sendmsg) { if (monster.type().type == MT_HORKSPWN) { return; } if (Quests[Q_GARBUD].IsAvailable() && monster.uniqueType == UniqueMonsterType::Garbud) { CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, true, ItemType::Mace, IMISC_NONE, sendmsg, false); } else if (monster.uniqueType == UniqueMonsterType::Defiler) { if (effect_is_playing(SfxID::Defiler8)) stream_stop(); SpawnMapOfDoom(monster.position.tile, sendmsg); Quests[Q_DEFILER]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_DEFILER]); } else if (monster.uniqueType == UniqueMonsterType::HorkDemon) { if (sgGameInitInfo.bTheoQuest != 0) { SpawnTheodore(monster.position.tile, sendmsg); } else { CreateAmulet(monster.position.tile, 13, sendmsg, false); } } else if (monster.type().type == MT_NAKRUL) { SfxID nSFX = IsUberRoomOpened ? SfxID::NaKrul4 : SfxID::NaKrul5; if (sgGameInitInfo.bCowQuest != 0) nSFX = SfxID::NaKrul6; if (effect_is_playing(nSFX)) stream_stop(); UberDiabloMonsterIndex = -2; CreateMagicWeapon(monster.position.tile, ItemType::Sword, ICURS_GREAT_SWORD, sendmsg, false); CreateMagicWeapon(monster.position.tile, ItemType::Staff, ICURS_WAR_STAFF, sendmsg, false); CreateMagicWeapon(monster.position.tile, ItemType::Bow, ICURS_LONG_WAR_BOW, sendmsg, false); CreateSpellBook(monster.position.tile, SpellID::Apocalypse, sendmsg, false); } else if (!monster.isPlayerMinion()) { SpawnItem(monster, monster.position.tile, sendmsg); } } std::optional GetTeleportTile(const Monster &monster) { const int mx = monster.enemyPosition.x; const int my = monster.enemyPosition.y; const int rx = PickRandomlyAmong({ -1, 1 }); const int ry = PickRandomlyAmong({ -1, 1 }); for (int j = -1; j <= 1; j++) { for (int k = -1; k < 1; k++) { if (j == 0 && k == 0) continue; const int x = mx + (rx * j); const int y = my + (ry * k); if (!InDungeonBounds({ x, y }) || x == monster.position.tile.x || y == monster.position.tile.y) continue; if (IsTileAvailable(monster, { x, y })) return Point { x, y }; } } return {}; } void Teleport(Monster &monster) { if (monster.mode == MonsterMode::Petrified) return; std::optional position = GetTeleportTile(monster); if (!position) return; M_ClearSquares(monster); dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.occupyTile(*position, false); monster.position.old = *position; monster.direction = GetMonsterDirection(monster); ChangeLightXY(monster.lightId, *position); } bool IsHardHit(Monster &target, unsigned dam) { switch (target.type().type) { case MT_SNEAK: case MT_STALKER: case MT_UNSEEN: case MT_ILLWEAV: return true; default: return (dam >> 6) >= target.level(sgGameInitInfo.nDifficulty) + 3; } } void MonsterHitMonster(Monster &attacker, Monster &target, int dam) { if (IsHardHit(target, dam)) { target.direction = Opposite(attacker.direction); } M_StartHit(target, dam); } void StartDeathFromMonster(Monster &attacker, Monster &target) { const Direction md = GetDirection(target.position.tile, attacker.position.tile); MonsterDeath(target, md, true); if (gbIsHellfire) M_StartStand(attacker, attacker.direction); } void StartFadein(Monster &monster, Direction md, bool backwards) { NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::FadeIn; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; monster.flags &= ~MFLAG_HIDDEN; if (backwards) { monster.flags |= MFLAG_LOCK_ANIMATION; monster.animInfo.currentFrame = monster.animInfo.numberOfFrames - 1; } } void StartFadeout(Monster &monster, Direction md, bool backwards) { NewMonsterAnim(monster, MonsterGraphic::Special, md); monster.mode = MonsterMode::FadeOut; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; if (backwards) { monster.flags |= MFLAG_LOCK_ANIMATION; monster.animInfo.currentFrame = monster.animInfo.numberOfFrames - 1; } } /** * @brief Starts the monster healing procedure. * * The monster will be healed between 1.47% and 25% of its max HP. The healing amount is stored in _mVar1. * * This is only used by Gargoyles. * * @param monster The monster that will be healed. */ void StartHeal(Monster &monster) { monster.changeAnimationData(MonsterGraphic::Special); monster.animInfo.currentFrame = monster.type().getAnimData(MonsterGraphic::Special).frames - 1; monster.flags |= MFLAG_LOCK_ANIMATION; monster.mode = MonsterMode::Heal; monster.var1 = monster.maxHitPoints / (16 * (GenerateRnd(5) + 4)); } void SyncLightPosition(Monster &monster) { if (monster.lightId == NO_LIGHT) return; const WorldTileDisplacement offset = monster.isWalking() ? monster.position.CalculateWalkingOffset(monster.direction, monster.animInfo) : WorldTileDisplacement {}; ChangeLightOffset(monster.lightId, offset.screenToLight()); } void MonsterIdle(Monster &monster) { if (monster.type().type == MT_GOLEM) monster.changeAnimationData(MonsterGraphic::Walk); else monster.changeAnimationData(MonsterGraphic::Stand); if (monster.animInfo.isLastFrame()) UpdateEnemy(monster); if (monster.var2 < std::numeric_limits::max()) monster.var2++; } /** * @brief Continue movement towards new tile */ bool MonsterWalk(Monster &monster) { // Check if we reached new tile const bool isAnimationEnd = monster.animInfo.isLastFrame(); if (isAnimationEnd) { dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.position.tile.x += monster.var1; monster.position.tile.y += monster.var2; // dMonster is set here for backwards compatibility; without it, the monster would be invisible if loaded from a vanilla save. monster.occupyTile(monster.position.tile, false); ChangeLightXY(monster.lightId, monster.position.tile); M_StartStand(monster, monster.direction); } else { // We didn't reach new tile so update monster's "sub-tile" position if (monster.animInfo.tickCounterOfCurrentFrame == 0) { if (monster.animInfo.currentFrame == 0 && monster.type().type == MT_FLESTHNG) PlayEffect(monster, MonsterSound::Special); } } SyncLightPosition(monster); return isAnimationEnd; } void MonsterAttackMonster(Monster &attacker, Monster &target, int hper, int mind, int maxd) { if (!target.isPossibleToHit()) return; int hit = GenerateRnd(100); if (target.mode == MonsterMode::Petrified) hit = 0; if (target.tryLiftGargoyle()) return; if (hit >= hper) return; const int dam = RandomIntBetween(mind, maxd) << 6; ApplyMonsterDamage(DamageType::Physical, target, dam); if (attacker.isPlayerMinion()) { const auto playerId = static_cast(attacker.goalVar3); const Player &player = Players[playerId]; target.tag(player); } if (target.hasNoLife()) { StartDeathFromMonster(attacker, target); } else { MonsterHitMonster(attacker, target, dam); } if (target.activeForTicks == 0) { target.activeForTicks = UINT8_MAX; target.position.last = attacker.position.tile; } } int CheckReflect(Monster &monster, Player &player, int dam) { player.wReflections--; if (player.wReflections <= 0) NetSendCmdParam1(true, CMD_SETREFLECT, 0); // reflects 20-30% damage const int mdam = dam * RandomIntBetween(20, 30, true) / 100; ApplyMonsterDamage(DamageType::Physical, monster, mdam); if (monster.hasNoLife()) M_StartKill(monster, player); else M_StartHit(monster, player, mdam); return mdam; } int GetMinHit() { switch (currlevel) { case 16: return 30; case 15: return 25; case 14: return 20; default: return 15; } } void MonsterAttackPlayer(Monster &monster, Player &player, int hit, int minDam, int maxDam) { if (player.hasNoLife() || player._pInvincible || HasAnyOf(player._pSpellFlags, SpellFlag::Etherealize)) return; if (monster.position.tile.WalkingDistance(player.position.tile) >= 2) return; int hper = GenerateRnd(100); #ifdef _DEBUG if (DebugGodMode) hper = 1000; #endif int ac = player.GetArmor(); if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstDemons) && monster.data().monsterClass == MonsterClass::Demon) ac += 40; if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::ACAgainstUndead) && monster.data().monsterClass == MonsterClass::Undead) ac += 20; hit += 2 * (monster.level(sgGameInitInfo.nDifficulty) - player.getCharacterLevel()) + 30 - ac; const int minhit = GetMinHit(); hit = std::max(hit, minhit); int blkper = 100; if ((player._pmode == PM_STAND || player._pmode == PM_ATTACK) && player._pBlockFlag) { blkper = GenerateRnd(100); } int blk = player.GetBlockChance() - (monster.level(sgGameInitInfo.nDifficulty) * 2); blk = std::clamp(blk, 0, 100); if (hper >= hit) return; if (blkper < blk) { const Direction dir = GetDirection(player.position.tile, monster.position.tile); StartPlrBlock(player, dir); if (&player == MyPlayer && player.wReflections > 0) { int dam = GenerateRnd(((maxDam - minDam) << 6) + 1) + (minDam << 6); dam = std::max(dam + (player._pIGetHit << 6), 64); CheckReflect(monster, player, dam); } return; } MonsterReducePlayerAttribute(monster, player); // New method fixes a bug which caused the maximum possible damage value to be 63/64ths too low. int dam = RandomIntBetween(minDam << 6, maxDam << 6); dam = std::max(dam + (player._pIGetHit << 6), 64); if (&player == MyPlayer) { if (player.wReflections > 0) { const int reflectedDamage = CheckReflect(monster, player, dam); dam = std::max(dam - reflectedDamage, 0); } ApplyPlrDamage(DamageType::Physical, player, 0, 0, dam); } // Reflect can also kill a monster, so make sure the monster is still alive if (HasAnyOf(player._pIFlags, ItemSpecialEffect::Thorns) && monster.mode != MonsterMode::Death) { const int mdam = (GenerateRnd(3) + 1) << 6; ApplyMonsterDamage(DamageType::Physical, monster, mdam); if (monster.hasNoLife()) M_StartKill(monster, player); else M_StartHit(monster, player, mdam); } if ((monster.flags & MFLAG_NOLIFESTEAL) == 0 && monster.type().type == MT_SKING && gbIsMultiplayer) monster.hitPoints += dam; if (player.hasNoLife()) { if (gbIsHellfire) M_StartStand(monster, monster.direction); return; } StartPlrHit(player, dam, false); if ((monster.flags & MFLAG_KNOCKBACK) != 0) { if (player._pmode != PM_GOTHIT) StartPlrHit(player, 0, true); const Point newPosition = player.position.tile + monster.direction; if (PosOkPlayer(player, newPosition)) { player.position.tile = newPosition; FixPlayerLocation(player, player._pdir); FixPlrWalkTags(player); player.occupyTile(newPosition, false); SetPlayerOld(player); } } } void MonsterAttackEnemy(Monster &monster, int hit, int minDam, int maxDam) { if ((monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) MonsterAttackMonster(monster, Monsters[monster.enemy], hit, minDam, maxDam); else MonsterAttackPlayer(monster, Players[monster.enemy], hit, minDam, maxDam); } } bool MonsterAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNum - 1) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty), monster.minDamage, monster.maxDamage); if (monster.ai != MonsterAIID::Snake) PlayEffect(monster, MonsterSound::Attack); } if (IsAnyOf(monster.type().type, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA) && monster.animInfo.currentFrame == 8) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty) + 10, monster.minDamage - 2, monster.maxDamage - 2); PlayEffect(monster, MonsterSound::Attack); } if (IsAnyOf(monster.type().type, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL) && monster.animInfo.currentFrame == 12) { MonsterAttackEnemy(monster, monster.toHit(sgGameInitInfo.nDifficulty) - 20, monster.minDamage + 4, monster.maxDamage + 4); PlayEffect(monster, MonsterSound::Attack); } if (monster.ai == MonsterAIID::Snake && monster.animInfo.currentFrame == 0) PlayEffect(monster, MonsterSound::Attack); if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterRangedAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNum - 1) { const auto &missileType = static_cast(monster.var1); if (missileType != MissileID::Null) { int multimissiles = 1; if (missileType == MissileID::ChargedBolt) multimissiles = 3; for (int mi = 0; mi < multimissiles; mi++) { AddMissile( monster.position.tile, monster.enemyPosition, monster.direction, missileType, TARGET_PLAYERS, monster, monster.var2, 0); } } PlayEffect(monster, MonsterSound::Attack); } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterRangedSpecialAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1 && monster.animInfo.tickCounterOfCurrentFrame == 0 && (monster.ai != MonsterAIID::Mega || monster.var2 == 0)) { if (AddMissile( monster.position.tile, monster.enemyPosition, monster.direction, static_cast(monster.var1), TARGET_PLAYERS, monster, monster.var3, 0) != nullptr) { PlayEffect(monster, MonsterSound::Special); } } if (monster.ai == MonsterAIID::Mega && monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) { if (monster.var2++ == 0) { monster.flags |= MFLAG_ALLOW_SPECIAL; } else if (monster.var2 == 15) { monster.flags &= ~MFLAG_ALLOW_SPECIAL; } } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterSpecialAttack(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) { MonsterAttackEnemy(monster, monster.toHitSpecial(sgGameInitInfo.nDifficulty), monster.minDamageSpecial, monster.maxDamageSpecial); } if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterFadein(Monster &monster) { if (((monster.flags & MFLAG_LOCK_ANIMATION) == 0 || monster.animInfo.currentFrame != 0) && ((monster.flags & MFLAG_LOCK_ANIMATION) != 0 || monster.animInfo.currentFrame != monster.animInfo.numberOfFrames - 1)) { return false; } M_StartStand(monster, monster.direction); monster.flags &= ~MFLAG_LOCK_ANIMATION; return true; } bool MonsterFadeout(Monster &monster) { if (((monster.flags & MFLAG_LOCK_ANIMATION) == 0 || monster.animInfo.currentFrame != 0) && ((monster.flags & MFLAG_LOCK_ANIMATION) != 0 || monster.animInfo.currentFrame != monster.animInfo.numberOfFrames - 1)) { return false; } monster.flags &= ~MFLAG_LOCK_ANIMATION; monster.flags |= MFLAG_HIDDEN; M_StartStand(monster, monster.direction); return true; } /** * @brief Applies the healing effect on the monster. * * This is triggered by StartHeal() * * @param monster The monster that will be healed. * @return */ void MonsterHeal(Monster &monster) { if (monster.animInfo.currentFrame == 0) { monster.flags &= ~MFLAG_LOCK_ANIMATION; monster.flags |= MFLAG_ALLOW_SPECIAL; if (monster.var1 + monster.hitPoints < monster.maxHitPoints) { monster.hitPoints = monster.var1 + monster.hitPoints; } else { monster.hitPoints = monster.maxHitPoints; monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } } } void MonsterTalk(Monster &monster) { M_StartStand(monster, monster.direction); monster.goal = MonsterGoal::Talking; if (effect_is_playing(Speeches[monster.talkMsg].sfxnr)) return; InitQTextMsg(monster.talkMsg); if (monster.uniqueType == UniqueMonsterType::SnotSpill) { if (monster.talkMsg == TEXT_BANNER10 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { ObjChangeMap(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + (SetPiece.size.width / 2) + 2, SetPiece.position.y + (SetPiece.size.height / 2) - 2); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans({ SetPiece.position, WorldTileSize((SetPiece.size.width / 2) + 4, SetPiece.size.height / 2) }); TransVal = tren; Quests[Q_LTBANNER]._qvar1 = 2; if (Quests[Q_LTBANNER]._qactive == QUEST_INIT) Quests[Q_LTBANNER]._qactive = QUEST_ACTIVE; monster.flags |= MFLAG_QUEST_COMPLETE; NetSendCmdQuest(true, Quests[Q_LTBANNER]); } if (Quests[Q_LTBANNER]._qvar1 < 2) { app_fatal(StrCat("SS Talk = ", monster.talkMsg, ", Flags = ", monster.flags)); } } if (monster.uniqueType == UniqueMonsterType::Lachdan) { if (monster.talkMsg == TEXT_VEIL9) { Quests[Q_VEIL]._qactive = QUEST_ACTIVE; Quests[Q_VEIL]._qlog = true; NetSendCmdQuest(true, Quests[Q_VEIL]); } } if (monster.uniqueType == UniqueMonsterType::WarlordOfBlood) { Quests[Q_WARLORD]._qvar1 = QS_WARLORD_TALKING; NetSendCmdQuest(true, Quests[Q_WARLORD]); } if (monster.uniqueType == UniqueMonsterType::Lazarus && UseMultiplayerQuests()) { Quests[Q_BETRAYER]._qvar1 = 6; monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; } } bool MonsterGotHit(Monster &monster) { if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } void ReleaseMinions(const Monster &leader) { for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &minion = Monsters[ActiveMonsters[i]]; if (minion.leaderRelation == LeaderRelation::Leashed && minion.getLeader() == &leader) { minion.setLeader(nullptr); } } } void ShrinkLeaderPacksize(const Monster &monster) { if (monster.leaderRelation == LeaderRelation::Leashed) { monster.getLeader()->packSize--; } } void MonsterDeath(Monster &monster) { monster.var1++; if (monster.type().type == MT_DIABLO) { if (monster.position.tile.x < ViewPosition.x) { ViewPosition.x--; } else if (monster.position.tile.x > ViewPosition.x) { ViewPosition.x++; } if (monster.position.tile.y < ViewPosition.y) { ViewPosition.y--; } else if (monster.position.tile.y > ViewPosition.y) { ViewPosition.y++; } if (monster.var1 == 140) PrepDoEnding(); } else if (monster.animInfo.isLastFrame()) { if (monster.isUnique()) AddCorpse(monster.position.tile, monster.corpseId, monster.direction); else AddCorpse(monster.position.tile, monster.type().corpseId, monster.direction); dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.isInvalid = true; M_UpdateRelations(monster); } } bool MonsterSpecialStand(Monster &monster) { if (monster.animInfo.currentFrame == monster.data().animFrameNumSpecial - 1) PlayEffect(monster, MonsterSound::Special); if (monster.animInfo.isLastFrame()) { M_StartStand(monster, monster.direction); return true; } return false; } bool MonsterDelay(Monster &monster) { monster.changeAnimationData(MonsterGraphic::Stand, GetMonsterDirection(monster)); if (monster.ai == MonsterAIID::Lazarus) { if (monster.var2 > 8 || monster.var2 < 0) monster.var2 = 8; } if (monster.var2-- == 0) { const int oFrame = monster.animInfo.currentFrame; M_StartStand(monster, monster.direction); monster.animInfo.currentFrame = oFrame; return true; } return false; } void MonsterPetrified(Monster &monster) { if (monster.hitPoints <= 0) { dMonster[monster.position.tile.x][monster.position.tile.y] = 0; monster.isInvalid = true; } } std::optional GetRandomSkeletonTypeIndex() { int32_t typeCount = 0; size_t skeletonIndexes[SkeletonTypes.size()]; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if (IsSkel(LevelMonsterTypes[i].type)) { skeletonIndexes[typeCount++] = i; } } if (typeCount == 0) { return {}; } const size_t typeIndex = skeletonIndexes[GenerateRnd(typeCount)]; return typeIndex; } Monster *AddSkeleton(Point position, Direction dir, bool inMap) { auto typeIndex = GetRandomSkeletonTypeIndex(); if (!typeIndex) return nullptr; return AddMonster(position, dir, *typeIndex, inMap); } bool LineClear(tl::function_ref clear, Point startPoint, Point endPoint) { Point position = startPoint; int dx = endPoint.x - position.x; int dy = endPoint.y - position.y; if (std::abs(dx) > std::abs(dy)) { if (dx < 0) { std::swap(position, endPoint); dx = -dx; dy = -dy; } int d; int yincD; int dincD; int dincH; if (dy > 0) { d = 2 * dy - dx; dincD = 2 * dy; dincH = 2 * (dy - dx); yincD = 1; } else { d = 2 * dy + dx; dincD = 2 * dy; dincH = 2 * (dx + dy); yincD = -1; } bool done = false; while (!done && position != endPoint) { if ((d <= 0) ^ (yincD < 0)) { d += dincD; } else { d += dincH; position.y += yincD; } position.x++; done = position != startPoint && !clear(position); } } else { if (dy < 0) { std::swap(position, endPoint); dy = -dy; dx = -dx; } int d; int xincD; int dincD; int dincH; if (dx > 0) { d = 2 * dx - dy; dincD = 2 * dx; dincH = 2 * (dx - dy); xincD = 1; } else { d = 2 * dx + dy; dincD = 2 * dx; dincH = 2 * (dy + dx); xincD = -1; } bool done = false; while (!done && position != endPoint) { if ((d <= 0) ^ (xincD < 0)) { d += dincD; } else { d += dincH; position.x += xincD; } position.y++; done = position != startPoint && !clear(position); } } return position == endPoint; } bool IsLineNotSolid(Point startPoint, Point endPoint) { return LineClear(IsTileNotSolid, startPoint, endPoint); } void FollowTheLeader(Monster &monster) { if (monster.leaderRelation != LeaderRelation::Leashed) return; Monster *leader = monster.getLeader(); if (leader == nullptr) return; if (leader->activeForTicks > monster.activeForTicks) { monster.position.last = leader->position.tile; monster.activeForTicks = leader->activeForTicks - 1; } if (monster.ai != MonsterAIID::Gargoyle || (monster.flags & MFLAG_ALLOW_SPECIAL) == 0) return; if (leader->mode == MonsterMode::SpecialMeleeAttack) return; monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::SpecialMeleeAttack; } void GroupUnity(Monster &monster) { if (monster.leaderRelation == LeaderRelation::None) return; // No unique monster would be a minion of someone else! assert(!monster.isUnique()); // Someone with a leaderRelation should have a leader, if we end up trying to access a nullptr then the relation was already broken... auto &leader = *monster.getLeader(); if (IsLineNotSolid(monster.position.tile, leader.position.future)) { if (monster.leaderRelation == LeaderRelation::Separated && monster.position.tile.WalkingDistance(leader.position.future) < 4) { // Reunite the separated monster with the pack leader.packSize++; monster.leaderRelation = LeaderRelation::Leashed; } } else if (monster.leaderRelation == LeaderRelation::Leashed) { leader.packSize--; monster.leaderRelation = LeaderRelation::Separated; } if (monster.leaderRelation == LeaderRelation::Leashed) { if (monster.activeForTicks > leader.activeForTicks) { leader.position.last = monster.position.tile; leader.activeForTicks = monster.activeForTicks - 1; } if (leader.ai == MonsterAIID::Gargoyle && (leader.flags & MFLAG_ALLOW_SPECIAL) != 0) { leader.flags &= ~MFLAG_ALLOW_SPECIAL; leader.mode = MonsterMode::SpecialMeleeAttack; } } } bool RandomWalk(Monster &monster, Direction md) { const Direction mdtemp = md; bool ok = DirOK(monster, md); if (FlipCoin()) ok = ok || (md = Right(mdtemp), DirOK(monster, md)) || (md = Left(mdtemp), DirOK(monster, md)); else ok = ok || (md = Left(mdtemp), DirOK(monster, md)) || (md = Right(mdtemp), DirOK(monster, md)); if (FlipCoin()) { ok = ok || (md = Left(Left(mdtemp)), DirOK(monster, md)) || (md = Right(Right(mdtemp)), DirOK(monster, md)); } else { ok = ok || (md = Right(Right(mdtemp)), DirOK(monster, md)) || (md = Left(Left(mdtemp)), DirOK(monster, md)); } if (ok) Walk(monster, md); return ok; } bool RandomWalk2(Monster &monster, Direction md) { Direction mdtemp = md; bool ok = DirOK(monster, md); // Can we continue in the same direction // Randomly go left or right if (FlipCoin()) { ok = ok || (mdtemp = Right(md), DirOK(monster, Right(md))) || (mdtemp = Left(md), DirOK(monster, Left(md))); } else { ok = ok || (mdtemp = Left(md), DirOK(monster, Left(md))) || (mdtemp = Right(md), DirOK(monster, Right(md))); } if (ok) Walk(monster, mdtemp); return ok; } /** * @brief Check if a tile is affected by a spell we are vulnerable to */ bool IsTileSafe(const Monster &monster, Point position) { if (!InDungeonBounds(position)) return false; const bool fearsFire = (monster.resistance & IMMUNE_FIRE) == 0 || monster.type().type == MT_DIABLO; const bool fearsLightning = (monster.resistance & IMMUNE_LIGHTNING) == 0 || monster.type().type == MT_DIABLO; return !(fearsFire && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::MissileFireWall)) && !(fearsLightning && HasAnyOf(dFlags[position.x][position.y], DungeonFlag::MissileLightningWall)); } /** * @brief Check that the given tile is not currently blocked */ bool IsTileAvailable(Point position) { if (dPlayer[position.x][position.y] != 0 || dMonster[position.x][position.y] != 0) return false; if (!IsTileWalkable(position)) return false; return true; } /** * @brief If a monster can access the given tile (possibly by opening a door) */ bool IsTileAccessible(const Monster &monster, Point position) { if (dPlayer[position.x][position.y] != 0 || dMonster[position.x][position.y] != 0) return false; if (!IsTileWalkable(position, (monster.flags & MFLAG_CAN_OPEN_DOOR) != 0)) return false; return IsTileSafe(monster, position); } bool AiPlanWalk(Monster &monster) { int8_t path[MaxPathLengthMonsters]; /** Maps from walking path step to facing direction. */ const Direction plr2monst[9] = { Direction::South, Direction::NorthEast, Direction::NorthWest, Direction::SouthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West }; if (FindPath(CanStep, [&monster](Point position) { return IsTileAccessible(monster, position); }, monster.position.tile, monster.enemyPosition, path, MaxPathLengthMonsters) == 0) { return false; } RandomWalk(monster, plr2monst[path[0]]); return true; } Direction Turn(Direction direction, bool turnLeft) { return turnLeft ? Left(direction) : Right(direction); } bool RoundWalk(Monster &monster, Direction direction, int8_t *dir) { const Direction turn45deg = Turn(direction, *dir != 0); const Direction turn90deg = Turn(turn45deg, *dir != 0); // Turn 90 degrees if (Walk(monster, turn90deg)) { return true; } // Only do a small turn if (Walk(monster, turn45deg)) { return true; } // Continue straight if (Walk(monster, direction)) { return true; } // Try 90 degrees in the opposite than desired direction *dir = (*dir == 0) ? 1 : 0; return RandomWalk(monster, Opposite(turn90deg)); } bool AiPlanPath(Monster &monster) { if (monster.type().type != MT_GOLEM) { if (monster.activeForTicks == 0) return false; if (monster.mode != MonsterMode::Stand) return false; if (IsNoneOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Move, MonsterGoal::Attack)) return false; if (monster.position.tile == GolemHoldingCell) return false; } const bool clear = LineClear( [&monster](Point position) { return (IsTileWalkable(position) && IsTileSafe(monster, position)); }, monster.position.tile, monster.enemyPosition); if (!clear || (monster.pathCount >= 5 && monster.pathCount < 8)) { if ((monster.flags & MFLAG_CAN_OPEN_DOOR) != 0) MonstCheckDoors(monster); monster.pathCount++; if (monster.pathCount < 5) return false; if (AiPlanWalk(monster)) return true; } if (monster.type().type != MT_GOLEM) monster.pathCount = 0; return false; } void AiAvoidance(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 4 && FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if ((monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 2) { if ((monster.var2 > 20 && v < 2 * monster.intelligence + 28) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 78)) { RandomWalk(monster, md); } } else if (v < 2 * monster.intelligence + 23) { monster.direction = md; if (IsAnyOf(monster.ai, MonsterAIID::GoatMelee, MonsterAIID::Gharbad) && monster.hitPoints < (monster.maxHitPoints / 2) && !FlipCoin()) StartSpecialAttack(monster); else StartAttack(monster); } } monster.checkStandAnimationIsLoaded(md); } MissileID GetMissileType(MonsterAIID ai) { switch (ai) { case MonsterAIID::GoatRanged: return MissileID::Arrow; case MonsterAIID::Succubus: case MonsterAIID::LazarusSuccubus: return MissileID::BloodStar; case MonsterAIID::Acid: case MonsterAIID::AcidUnique: return MissileID::Acid; case MonsterAIID::FireBat: return MissileID::Firebolt; case MonsterAIID::Torchant: return MissileID::Fireball; case MonsterAIID::Lich: return MissileID::OrangeFlare; case MonsterAIID::ArchLich: return MissileID::YellowFlare; case MonsterAIID::Psychorb: return MissileID::BlueFlare; case MonsterAIID::Necromorb: return MissileID::RedFlare; case MonsterAIID::Magma: return MissileID::MagmaBall; case MonsterAIID::Storm: return MissileID::ThinLightningControl; case MonsterAIID::Diablo: return MissileID::DiabloApocalypse; case MonsterAIID::BoneDemon: return MissileID::BlueFlare2; default: return MissileID::Arrow; } } void AiRanged(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } if (monster.activeForTicks == UINT8_MAX || (monster.flags & MFLAG_TARGETS_MONSTER) != 0) { const Direction md = GetMonsterDirection(monster); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); monster.direction = md; if (static_cast(monster.var1) == MonsterMode::RangedAttack) { AiDelay(monster, GenerateRnd(20)); } else if (monster.distanceToEnemy() < 4) { if (GenerateRnd(100) < 10 * (monster.intelligence + 7)) RandomWalk(monster, Opposite(md)); } if (monster.mode == MonsterMode::Stand) { if (LineClearMovingMissile(monster.position.tile, monster.enemyPosition)) { const MissileID missileType = GetMissileType(monster.ai); if (monster.ai == MonsterAIID::AcidUnique) StartRangedSpecialAttack(monster, missileType, 0); else StartRangedAttack(monster, missileType, 0); } else { monster.checkStandAnimationIsLoaded(md); } } return; } if (monster.activeForTicks != 0) { const Direction md = GetDirection(monster.position.tile, monster.position.last); RandomWalk(monster, md); } } void AiRangedAvoidance(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (IsAnyOf(monster.ai, MonsterAIID::Magma, MonsterAIID::Storm, MonsterAIID::BoneDemon) && monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int lessmissiles = (monster.ai == MonsterAIID::Acid) ? 1 : 0; const int dam = (monster.ai == MonsterAIID::Diablo) ? 40 : 0; const MissileID missileType = GetMissileType(monster.ai); int v = GenerateRnd(10000); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 3 && FlipCoin(4 << lessmissiles))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) { monster.goal = MonsterGoal::Normal; } else if (v < (500 * (monster.intelligence + 1) >> lessmissiles) && (LineClearMovingMissile(monster.position.tile, monster.enemyPosition))) { StartRangedSpecialAttack(monster, missileType, dam); } else { RoundWalk(monster, md, &monster.goalVar2); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (((distanceToEnemy >= 3 && v < ((500 * (monster.intelligence + 2)) >> lessmissiles)) || v < ((500 * (monster.intelligence + 1)) >> lessmissiles)) && LineClearMovingMissile(monster.position.tile, monster.enemyPosition)) { StartRangedSpecialAttack(monster, missileType, dam); } else if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v < 1000 * (monster.intelligence + 5) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 1000 * (monster.intelligence + 8))) { RandomWalk(monster, md); } } else if (v < 1000 * (monster.intelligence + 6)) { monster.direction = md; StartAttack(monster); } } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void ZombieAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } if (!IsTileVisible(monster.position.tile)) { return; } if (GenerateRnd(100) < 2 * monster.intelligence + 10) { const int dist = monster.enemyPosition.WalkingDistance(monster.position.tile); if (dist >= 2) { if (dist >= 2 * monster.intelligence + 4) { Direction md = monster.direction; if (GenerateRnd(100) < 2 * monster.intelligence + 20) { md = static_cast(GenerateRnd(8)); } Walk(monster, md); } else { RandomWalk(monster, GetMonsterDirection(monster)); } } else { StartAttack(monster); } } monster.checkStandAnimationIsLoaded(monster.direction); } void OverlordAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetMonsterDirection(monster); monster.direction = md; const int v = GenerateRnd(100); if (monster.distanceToEnemy() >= 2) { if ((monster.var2 > 20 && v < 4 * monster.intelligence + 20) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 4 * monster.intelligence + 70)) { RandomWalk(monster, md); } } else if (v < 4 * monster.intelligence + 15) { StartAttack(monster); } else if (v < 4 * monster.intelligence + 20) { StartSpecialAttack(monster); } monster.checkStandAnimationIsLoaded(md); } void SkeletonAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; if (monster.distanceToEnemy() >= 2) { if (static_cast(monster.var1) == MonsterMode::Delay || (GenerateRnd(100) >= 35 - 4 * monster.intelligence)) { RandomWalk(monster, md); } else { AiDelay(monster, 15 - (2 * monster.intelligence) + GenerateRnd(10)); } } else { if (static_cast(monster.var1) == MonsterMode::Delay || (GenerateRnd(100) < 2 * monster.intelligence + 20)) { StartAttack(monster); } else { AiDelay(monster, (2 * (5 - monster.intelligence)) + GenerateRnd(10)); } } monster.checkStandAnimationIsLoaded(md); } void SkeletonBowAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetMonsterDirection(monster); monster.direction = md; const int v = GenerateRnd(100); bool walking = false; if (monster.distanceToEnemy() < 4) { if ((monster.var2 > 20 && v < 2 * monster.intelligence + 13) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 63)) { walking = Walk(monster, Opposite(md)); } } if (!walking) { if (GenerateRnd(100) < 2 * monster.intelligence + 3) { if (LineClearMovingMissile(monster.position.tile, monster.enemyPosition)) StartRangedAttack(monster, MissileID::Arrow, 4); } } monster.checkStandAnimationIsLoaded(md); } std::optional ScavengerFindCorpse(const Monster &scavenger) { const bool reverseSearch = FlipCoin(); const int first = reverseSearch ? 4 : -4; const int last = reverseSearch ? -4 : 4; const int increment = reverseSearch ? -1 : 1; for (int y = first; y <= last; y += increment) { for (int x = first; x <= last; x += increment) { Point position = scavenger.position.tile + Displacement { x, y }; if (!InDungeonBounds(position)) continue; if (dCorpse[position.x][position.y] == 0) continue; if (!IsLineNotSolid(scavenger.position.tile, position)) continue; return position; } } return {}; } void ScavengerAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) return; if (monster.hitPoints < (monster.maxHitPoints / 2) && monster.goal != MonsterGoal::Healing) { if (monster.leaderRelation != LeaderRelation::None) { ShrinkLeaderPacksize(monster); monster.leaderRelation = LeaderRelation::None; } monster.goal = MonsterGoal::Healing; monster.goalVar3 = 10; } if (monster.goal == MonsterGoal::Healing && monster.goalVar3 != 0) { monster.goalVar3--; if (dCorpse[monster.position.tile.x][monster.position.tile.y] != 0) { StartEating(monster); if (gbIsHellfire) { const int mMaxHP = monster.maxHitPoints; monster.hitPoints += mMaxHP / 8; monster.hitPoints = std::min(monster.hitPoints, monster.maxHitPoints); if (monster.goalVar3 <= 0 || monster.hitPoints == monster.maxHitPoints) dCorpse[monster.position.tile.x][monster.position.tile.y] = 0; } else { monster.hitPoints += 64; } int targetHealth = monster.maxHitPoints; if (!gbIsHellfire) targetHealth = (monster.maxHitPoints / 2) + (monster.maxHitPoints / 4); if (monster.hitPoints >= targetHealth) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; } } else { if (monster.goalVar1 == 0) { std::optional position = ScavengerFindCorpse(monster); if (position) { monster.goalVar1 = position->x + 1; monster.goalVar2 = position->y + 1; } } if (monster.goalVar1 != 0) { const int x = monster.goalVar1 - 1; const int y = monster.goalVar2 - 1; monster.direction = GetDirection(monster.position.tile, { x, y }); RandomWalk(monster, monster.direction); } } } if (monster.mode == MonsterMode::Stand) SkeletonAi(monster); } void RhinoAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 5 && !FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 5 && v < 2 * monster.intelligence + 43 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition)) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { if (monster.data().hasSpecialSound) PlayEffect(monster, MonsterSound::Special); monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else { if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v >= 2 * monster.intelligence + 33 && (IsNoneOf(static_cast(monster.var1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) || monster.var2 != 0 || v >= 2 * monster.intelligence + 83)) { AiDelay(monster, GenerateRnd(10) + 10); } else { RandomWalk(monster, md); } } else if (v < 2 * monster.intelligence + 28) { monster.direction = md; StartAttack(monster); } } } monster.checkStandAnimationIsLoaded(monster.direction); } void FallenAi(Monster &monster) { if (monster.goal == MonsterGoal::Attack) { if (monster.goalVar1 != 0) monster.goalVar1--; else monster.goal = MonsterGoal::Normal; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1-- == 0) { monster.goal = MonsterGoal::Normal; M_StartStand(monster, Opposite(static_cast(monster.goalVar2))); } } if (monster.animInfo.isLastFrame()) { if (!FlipCoin(4)) { return; } StartSpecialStand(monster, monster.direction); if (monster.maxHitPoints - (2 * monster.intelligence + 2) >= monster.hitPoints) monster.hitPoints += 2 * monster.intelligence + 2; else monster.hitPoints = monster.maxHitPoints; const int rad = (2 * monster.intelligence) + 4; for (int y = -rad; y <= rad; y++) { for (int x = -rad; x <= rad; x++) { const int xpos = monster.position.tile.x + x; const int ypos = monster.position.tile.y + y; if (InDungeonBounds({ xpos, ypos })) { const int m = dMonster[xpos][ypos]; if (m <= 0) continue; Monster &otherMonster = Monsters[m - 1]; if (otherMonster.ai != MonsterAIID::Fallen) continue; otherMonster.goal = MonsterGoal::Attack; otherMonster.goalVar1 = 30 * monster.intelligence + 105; } } } } else if (monster.goal == MonsterGoal::Retreat) { monster.direction = static_cast(monster.goalVar2); RandomWalk(monster, monster.direction); } else if (monster.goal == MonsterGoal::Attack) { if (monster.distanceToEnemy() < 2) StartAttack(monster); else RandomWalk(monster, GetMonsterDirection(monster)); } else { SkeletonAi(monster); } } void LeoricAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 3 && FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if ((monster.goalVar1++ >= static_cast(2 * distanceToEnemy) && DirOK(monster, md)) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (!UseMultiplayerQuests() && ((distanceToEnemy >= 3 && v < 4 * monster.intelligence + 35) || v < 6) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { const Point newPosition = monster.position.tile + md; if (IsTileAvailable(monster, newPosition) && ActiveMonsterCount < MaxMonsters) { auto typeIndex = GetRandomSkeletonTypeIndex(); if (typeIndex) { SpawnMonster(newPosition, md, *typeIndex); } StartSpecialStand(monster, md); } } else { if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v >= monster.intelligence + 25 && (IsNoneOf(static_cast(monster.var1), MonsterMode::MoveNorthwards, MonsterMode::MoveSouthwards, MonsterMode::MoveSideways) || monster.var2 != 0 || (v >= monster.intelligence + 75))) { AiDelay(monster, GenerateRnd(10) + 10); } else { RandomWalk(monster, md); } } else if (v < monster.intelligence + 20) { monster.direction = md; StartAttack(monster); } } } monster.checkStandAnimationIsLoaded(md); } void BatAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; const int v = GenerateRnd(100); if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1 == 0) { RandomWalk(monster, Opposite(md)); monster.goalVar1++; } else { RandomWalk(monster, PickRandomlyAmong({ Right(md), Left(md) })); monster.goal = MonsterGoal::Normal; } return; } const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.type().type == MT_GLOOM && distanceToEnemy >= 5 && v < 4 * monster.intelligence + 33 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition)) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else if (distanceToEnemy >= 2) { if ((monster.var2 > 20 && v < monster.intelligence + 13) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < monster.intelligence + 63)) { RandomWalk(monster, md); } } else if (v < 4 * monster.intelligence + 8) { StartAttack(monster); monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; if (monster.type().type == MT_FAMILIAR) { AddMissile(monster.enemyPosition, monster.enemyPosition + Direction::SouthEast, Direction::South, MissileID::Lightning, TARGET_PLAYERS, monster, GenerateRnd(10) + 1, 0); } } monster.checkStandAnimationIsLoaded(md); } void GargoyleAi(Monster &monster) { const Direction md = GetMonsterDirection(monster); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.activeForTicks != 0 && (monster.flags & MFLAG_ALLOW_SPECIAL) != 0) { UpdateEnemy(monster); if (distanceToEnemy < monster.intelligence + 2U) { monster.flags &= ~MFLAG_ALLOW_SPECIAL; } return; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } if (monster.hitPoints < (monster.maxHitPoints / 2)) monster.goal = MonsterGoal::Retreat; if (monster.goal == MonsterGoal::Retreat) { if (distanceToEnemy >= monster.intelligence + 2U) { monster.goal = MonsterGoal::Normal; StartHeal(monster); } else if (!RandomWalk(monster, Opposite(md))) { monster.goal = MonsterGoal::Normal; } } AiAvoidance(monster); } void ButcherAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; if (monster.distanceToEnemy() >= 2) RandomWalk(monster, md); else StartAttack(monster); monster.checkStandAnimationIsLoaded(md); } void SneakAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const unsigned dist = 5 - monster.intelligence; const unsigned distanceToEnemy = monster.distanceToEnemy(); if (static_cast(monster.var1) == MonsterMode::HitRecovery) { monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; } else if (distanceToEnemy >= dist + 3 || monster.goalVar1 > 8) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; } Direction md = GetMonsterDirection(monster); if (monster.goal == MonsterGoal::Retreat && (monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) md = GetDirection(monster.position.tile, Monsters[monster.enemy].position.tile); else md = GetDirection(monster.position.tile, Players[monster.enemy].position.last); md = Opposite(md); if (monster.type().type == MT_UNSEEN) { md = PickRandomlyAmong({ Right(md), Left(md) }); } } monster.direction = md; const int v = GenerateRnd(100); if (distanceToEnemy < dist && (monster.flags & MFLAG_HIDDEN) != 0) { StartFadein(monster, md, false); } else { if ((distanceToEnemy >= dist + 1) && (monster.flags & MFLAG_HIDDEN) == 0) { StartFadeout(monster, md, true); } else { if (monster.goal == MonsterGoal::Retreat || (distanceToEnemy >= 2 && ((monster.var2 > 20 && v < 4 * monster.intelligence + 14) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 4 * monster.intelligence + 64)))) { monster.goalVar1++; RandomWalk(monster, md); } } } if (monster.mode == MonsterMode::Stand) { if (distanceToEnemy >= 2 || v >= 4 * monster.intelligence + 10) monster.changeAnimationData(MonsterGraphic::Stand); else StartAttack(monster); } } void GharbadAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg >= TEXT_GARBUD1 && monster.talkMsg <= TEXT_GARBUD3 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.goal = MonsterGoal::Inquiring; switch (monster.talkMsg) { case TEXT_GARBUD1: monster.talkMsg = TEXT_GARBUD2; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_FIRST_ITEM_READY; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; case TEXT_GARBUD2: monster.talkMsg = TEXT_GARBUD3; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_SECOND_ITEM_NEARLY_DONE; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; case TEXT_GARBUD3: monster.talkMsg = TEXT_GARBUD4; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_SECOND_ITEM_READY; NetSendCmdQuest(true, Quests[Q_GARBUD]); break; default: break; } } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_GARBUD4) { if (!effect_is_playing(SfxID::Gharbad4) && monster.goal == MonsterGoal::Talking) { monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_ATTACKING; NetSendCmdQuest(true, Quests[Q_GARBUD]); } } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Move)) AiAvoidance(monster); monster.checkStandAnimationIsLoaded(md); } void SnotSpilAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_BANNER10 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_BANNER11; monster.goal = MonsterGoal::Inquiring; } if (monster.talkMsg == TEXT_BANNER11 && Quests[Q_LTBANNER]._qvar1 == 3) { monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_BANNER12) { if (!effect_is_playing(SfxID::Snotspill3) && monster.goal == MonsterGoal::Talking) { ObjChangeMap(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + SetPiece.size.width + 1, SetPiece.position.y + SetPiece.size.height + 1); Quests[Q_LTBANNER]._qvar1 = 3; NetSendCmdQuest(true, Quests[Q_LTBANNER]); RedoPlayerVision(); monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; } } if (Quests[Q_LTBANNER]._qvar1 == 3) { if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Attack)) FallenAi(monster); } } monster.checkStandAnimationIsLoaded(md); } void SnakeAi(Monster &monster) { const int8_t pattern[6] = { 1, 1, 0, -1, -1, 0 }; if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) return; Direction md = GetDirection(monster.position.tile, monster.position.last); monster.direction = md; const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 2) { if (distanceToEnemy < 3 && LineClear([&monster](Point position) { return IsTileAvailable(monster, position); }, monster.position.tile, monster.enemyPosition) && static_cast(monster.var1) != MonsterMode::Charge) { if (AddMissile(monster.position.tile, monster.enemyPosition, md, MissileID::Rhino, TARGET_PLAYERS, monster, 0, 0) != nullptr) { PlayEffect(monster, MonsterSound::Attack); monster.occupyTile(monster.position.tile, true); monster.mode = MonsterMode::Charge; } } else if (static_cast(monster.var1) == MonsterMode::Delay || GenerateRnd(100) >= 35 - 2 * monster.intelligence) { if (pattern[monster.goalVar1] == -1) md = Left(md); else if (pattern[monster.goalVar1] == 1) md = Right(md); monster.goalVar1++; if (monster.goalVar1 > 5) monster.goalVar1 = 0; const auto targetDirection = static_cast(monster.goalVar2); if (md != targetDirection) { int drift = static_cast(md) - monster.goalVar2; if (drift < 0) drift += 8; if (drift < 4) md = Right(targetDirection); else if (drift > 4) md = Left(targetDirection); monster.goalVar2 = static_cast(md); } if (!Walk(monster, md)) RandomWalk2(monster, monster.direction); } else { AiDelay(monster, 15 - monster.intelligence + GenerateRnd(10)); } } else { if (IsAnyOf(static_cast(monster.var1), MonsterMode::Delay, MonsterMode::Charge) || (GenerateRnd(100) < monster.intelligence + 20)) { StartAttack(monster); } else { AiDelay(monster, 10 - monster.intelligence + GenerateRnd(10)); } } monster.checkStandAnimationIsLoaded(monster.direction); } void CounselorAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); const int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (monster.goal == MonsterGoal::Retreat) { if (monster.goalVar1++ <= 3) RandomWalk(monster, Opposite(md)); else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else if (monster.goal == MonsterGoal::Move) { if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goalVar1++ < static_cast(2 * distanceToEnemy) || !DirOK(monster, md)) { RoundWalk(monster, md, &monster.goalVar2); } else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else { monster.goal = MonsterGoal::Normal; StartFadein(monster, md, true); } } else if (monster.goal == MonsterGoal::Normal) { if (distanceToEnemy >= 2) { if (v < 5 * (monster.intelligence + 10) && LineClearMovingMissile(monster.position.tile, monster.enemyPosition)) { constexpr MissileID MissileTypes[4] = { MissileID::Firebolt, MissileID::ChargedBolt, MissileID::LightningControl, MissileID::Fireball }; StartRangedAttack(monster, MissileTypes[monster.intelligence], RandomIntBetween(monster.minDamage, monster.maxDamage)); } else if (GenerateRnd(100) < 30) { monster.goal = MonsterGoal::Move; monster.goalVar1 = 0; StartFadeout(monster, md, false); } else { AiDelay(monster, GenerateRnd(10) + (2 * (5 - monster.intelligence))); } } else { monster.direction = md; if (monster.hitPoints < (monster.maxHitPoints / 2)) { monster.goal = MonsterGoal::Retreat; monster.goalVar1 = 0; StartFadeout(monster, md, false); } else if (static_cast(monster.var1) == MonsterMode::Delay || GenerateRnd(100) < 2 * monster.intelligence + 20) { StartRangedAttack(monster, MissileID::Null, 0); AddMissile(monster.position.tile, { 0, 0 }, monster.direction, MissileID::FlashBottom, TARGET_PLAYERS, monster, 4, 0); AddMissile(monster.position.tile, { 0, 0 }, monster.direction, MissileID::FlashTop, TARGET_PLAYERS, monster, 4, 0); } else { AiDelay(monster, GenerateRnd(10) + (2 * (5 - monster.intelligence))); } } } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void ZharAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_ZHAR1 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_ZHAR2; monster.goal = MonsterGoal::Inquiring; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ANGRY; NetSendCmdQuest(true, Quests[Q_ZHAR]); } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_ZHAR2) { if (!effect_is_playing(SfxID::Zhar2) && monster.goal == MonsterGoal::Talking) { monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ATTACKING; NetSendCmdQuest(true, Quests[Q_ZHAR]); } } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Retreat, MonsterGoal::Move)) CounselorAi(monster); monster.checkStandAnimationIsLoaded(md); } void MegaAi(Monster &monster) { const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy >= 5) { SkeletonAi(monster); return; } if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < UINT8_MAX) MonstCheckDoors(monster); int v = GenerateRnd(100); if (distanceToEnemy >= 2 && monster.activeForTicks == UINT8_MAX && dTransVal[monster.position.tile.x][monster.position.tile.y] == dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { if (monster.goal == MonsterGoal::Move || distanceToEnemy >= 3) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; monster.goalVar3 = 4; if (monster.goalVar1++ < static_cast(2 * distanceToEnemy) || !DirOK(monster, md)) { if (v < 5 * (monster.intelligence + 16)) RoundWalk(monster, md, &monster.goalVar2); } else { monster.goal = MonsterGoal::Normal; } } } else { monster.goal = MonsterGoal::Normal; } if (monster.goal == MonsterGoal::Normal) { if (((distanceToEnemy >= 3 && v < 5 * (monster.intelligence + 2)) || v < 5 * (monster.intelligence + 1) || monster.goalVar3 == 4) && LineClearMissile(monster.position.tile, monster.enemyPosition)) { StartRangedSpecialAttack(monster, MissileID::InfernoControl, 0); } else if (distanceToEnemy >= 2) { v = GenerateRnd(100); if (v < 2 * (5 * monster.intelligence + 25) || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * (5 * monster.intelligence + 40))) { RandomWalk(monster, md); } } else { if (GenerateRnd(100) < 10 * (monster.intelligence + 4)) { monster.direction = md; if (FlipCoin()) StartRangedSpecialAttack(monster, MissileID::InfernoControl, 0); else StartAttack(monster); } } monster.goalVar3 = 1; } if (monster.mode == MonsterMode::Stand) { AiDelay(monster, GenerateRnd(10) + 5); } } void LazarusAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (!UseMultiplayerQuests()) { const Player &myPlayer = *MyPlayer; if (monster.talkMsg == TEXT_VILE13 && monster.goal == MonsterGoal::Inquiring && myPlayer.position.tile == Point { 35, 46 }) { if (!gbIsMultiplayer) { // Playing ingame movies is currently not supported in multiplayer PlayInGameMovie("gendata\\fprst3.smk"); } monster.mode = MonsterMode::Talk; Quests[Q_BETRAYER]._qvar1 = 5; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } if (monster.talkMsg == TEXT_VILE13 && !effect_is_playing(SfxID::LazarusGreeting) && monster.goal == MonsterGoal::Talking) { ObjChangeMap(1, 18, 20, 24); RedoPlayerVision(); Quests[Q_BETRAYER]._qvar1 = 6; monster.goal = MonsterGoal::Normal; monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } } if (UseMultiplayerQuests() && monster.talkMsg == TEXT_VILE13 && monster.goal == MonsterGoal::Inquiring && Quests[Q_BETRAYER]._qvar1 <= 3) { monster.mode = MonsterMode::Talk; } } if (IsAnyOf(monster.goal, MonsterGoal::Normal, MonsterGoal::Retreat, MonsterGoal::Move)) { if (!UseMultiplayerQuests() && Quests[Q_BETRAYER]._qvar1 == 4 && monster.talkMsg == TEXT_NONE) { // Fix save games affected by teleport bug ObjChangeMapResync(1, 18, 20, 24); RedoPlayerVision(); Quests[Q_BETRAYER]._qvar1 = 6; } monster.talkMsg = TEXT_NONE; CounselorAi(monster); } monster.checkStandAnimationIsLoaded(md); } void LazarusMinionAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) return; const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (!UseMultiplayerQuests()) { if (Quests[Q_BETRAYER]._qvar1 <= 5) { monster.goal = MonsterGoal::Inquiring; } else { monster.goal = MonsterGoal::Normal; monster.talkMsg = TEXT_NONE; } } else { monster.goal = MonsterGoal::Normal; } } if (monster.goal == MonsterGoal::Normal) AiRanged(monster); monster.checkStandAnimationIsLoaded(md); } void LachdananAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (monster.talkMsg == TEXT_VEIL9 && !IsTileVisible(monster.position.tile) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_VEIL10; monster.goal = MonsterGoal::Inquiring; Quests[Q_VEIL]._qvar2 = QS_VEIL_EARLY_RETURN; NetSendCmdQuest(true, Quests[Q_VEIL]); } if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_VEIL11) { if (!effect_is_playing(SfxID::Lachdanan3) && monster.goal == MonsterGoal::Talking) { monster.talkMsg = TEXT_NONE; Quests[Q_VEIL]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_VEIL]); MonsterDeath(monster, monster.direction, true); delta_kill_monster(monster, monster.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, static_cast(monster.getId())); } } } monster.checkStandAnimationIsLoaded(md); } void WarlordAi(Monster &monster) { if (monster.mode != MonsterMode::Stand) { return; } const Direction md = GetMonsterDirection(monster); if (IsTileVisible(monster.position.tile)) { if (monster.talkMsg == TEXT_WARLRD9 && monster.goal == MonsterGoal::Inquiring) monster.mode = MonsterMode::Talk; if (monster.talkMsg == TEXT_WARLRD9 && !effect_is_playing(SfxID::Warlord) && monster.goal == MonsterGoal::Talking) { monster.activeForTicks = UINT8_MAX; monster.talkMsg = TEXT_NONE; monster.goal = MonsterGoal::Normal; Quests[Q_WARLORD]._qvar1 = QS_WARLORD_ATTACKING; NetSendCmdQuest(true, Quests[Q_WARLORD]); } } if (monster.goal == MonsterGoal::Normal) SkeletonAi(monster); monster.checkStandAnimationIsLoaded(md); } void HorkDemonAi(Monster &monster) { if (monster.mode != MonsterMode::Stand || monster.activeForTicks == 0) { return; } const Direction md = GetDirection(monster.position.tile, monster.position.last); if (monster.activeForTicks < 255) { MonstCheckDoors(monster); } int v = GenerateRnd(100); const unsigned distanceToEnemy = monster.distanceToEnemy(); if (distanceToEnemy < 2) { monster.goal = MonsterGoal::Normal; } else if (monster.goal == MonsterGoal::Move || (distanceToEnemy >= 5 && !FlipCoin(4))) { if (monster.goal != MonsterGoal::Move) { monster.goalVar1 = 0; monster.goalVar2 = GenerateRnd(2); } monster.goal = MonsterGoal::Move; if (monster.goalVar1++ >= static_cast(2 * distanceToEnemy) || dTransVal[monster.position.tile.x][monster.position.tile.y] != dTransVal[monster.enemyPosition.x][monster.enemyPosition.y]) { monster.goal = MonsterGoal::Normal; } else if (!RoundWalk(monster, md, &monster.goalVar2)) { AiDelay(monster, GenerateRnd(10) + 10); } } if (monster.goal == MonsterGoal::Normal) { if ((distanceToEnemy >= 3) && v < 2 * monster.intelligence + 43) { const Point position = monster.position.tile + monster.direction; if (IsTileAvailable(monster, position) && ActiveMonsterCount < MaxMonsters) { StartRangedSpecialAttack(monster, MissileID::HorkSpawn, 0); } } else if (distanceToEnemy < 2) { if (v < 2 * monster.intelligence + 28) { monster.direction = md; StartAttack(monster); } } else { v = GenerateRnd(100); if (v < 2 * monster.intelligence + 33 || (IsMonsterModeMove(static_cast(monster.var1)) && monster.var2 == 0 && v < 2 * monster.intelligence + 83)) { RandomWalk(monster, md); } else { AiDelay(monster, GenerateRnd(10) + 10); } } } monster.checkStandAnimationIsLoaded(monster.direction); } std::string_view GetMonsterTypeText(const MonsterData &monsterData) { switch (monsterData.monsterClass) { case MonsterClass::Animal: return _("Animal"); case MonsterClass::Demon: return _("Demon"); case MonsterClass::Undead: return _("Undead"); } app_fatal(StrCat("Unknown monsterClass ", static_cast(monsterData.monsterClass))); } void ActivateSpawn(Monster &monster, Point position, Direction dir) { monster.occupyTile(position, false); monster.position.tile = position; monster.position.future = position; monster.position.old = position; StartSpecialStand(monster, dir); } /** Maps from monster AI ID to monster AI function. */ void (*AiProc[])(Monster &monster) = { /*MonsterAIID::Zombie */ &ZombieAi, /*MonsterAIID::Fat */ &OverlordAi, /*MonsterAIID::SkeletonMelee */ &SkeletonAi, /*MonsterAIID::SkeletonRanged */ &SkeletonBowAi, /*MonsterAIID::Scavenger */ &ScavengerAi, /*MonsterAIID::Rhino */ &RhinoAi, /*MonsterAIID::GoatMelee */ &AiAvoidance, /*MonsterAIID::GoatRanged */ &AiRanged, /*MonsterAIID::Fallen */ &FallenAi, /*MonsterAIID::Magma */ &AiRangedAvoidance, /*MonsterAIID::SkeletonKing */ &LeoricAi, /*MonsterAIID::Bat */ &BatAi, /*MonsterAIID::Gargoyle */ &GargoyleAi, /*MonsterAIID::Butcher */ &ButcherAi, /*MonsterAIID::Succubus */ &AiRanged, /*MonsterAIID::Sneak */ &SneakAi, /*MonsterAIID::Storm */ &AiRangedAvoidance, /*MonsterAIID::FireMan */ nullptr, /*MonsterAIID::Gharbad */ &GharbadAi, /*MonsterAIID::Acid */ &AiRangedAvoidance, /*MonsterAIID::AcidUnique */ &AiRanged, /*MonsterAIID::Golem */ &GolumAi, /*MonsterAIID::Zhar */ &ZharAi, /*MonsterAIID::Snotspill */ &SnotSpilAi, /*MonsterAIID::Snake */ &SnakeAi, /*MonsterAIID::Counselor */ &CounselorAi, /*MonsterAIID::Mega */ &MegaAi, /*MonsterAIID::Diablo */ &AiRangedAvoidance, /*MonsterAIID::Lazarus */ &LazarusAi, /*MonsterAIID::LazarusSuccubus*/ &LazarusMinionAi, /*MonsterAIID::Lachdanan */ &LachdananAi, /*MonsterAIID::Warlord */ &WarlordAi, /*MonsterAIID::FireBat */ &AiRanged, /*MonsterAIID::Torchant */ &AiRanged, /*MonsterAIID::HorkDemon */ &HorkDemonAi, /*MonsterAIID::Lich */ &AiRanged, /*MonsterAIID::ArchLich */ &AiRanged, /*MonsterAIID::Psychorb */ &AiRanged, /*MonsterAIID::Necromorb */ &AiRanged, /*MonsterAIID::BoneDemon */ &AiRangedAvoidance }; bool IsRelativeMoveOK(const Monster &monster, Point position, Direction mdir) { const Point futurePosition = position + mdir; if (!InDungeonBounds(futurePosition) || !IsTileAvailable(monster, futurePosition)) return false; if (mdir == Direction::East) { if (IsTileSolid(position + Direction::SouthEast)) return false; } else if (mdir == Direction::West) { if (IsTileSolid(position + Direction::SouthWest)) return false; } else if (mdir == Direction::North) { if (IsTileSolid(position + Direction::NorthEast) || IsTileSolid(position + Direction::NorthWest)) return false; } else if (mdir == Direction::South) { if (IsTileSolid(position + Direction::SouthWest) || IsTileSolid(position + Direction::SouthEast)) return false; } return true; } bool IsMonsterAvailable(const MonsterData &monsterData) { if (monsterData.availability == MonsterAvailability::Never) return false; if (gbIsSpawn && monsterData.availability == MonsterAvailability::Retail) return false; return currlevel >= monsterData.minDunLvl && currlevel <= monsterData.maxDunLvl; } bool UpdateModeStance(Monster &monster) { switch (monster.mode) { case MonsterMode::Stand: MonsterIdle(monster); return false; case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: return MonsterWalk(monster); case MonsterMode::MeleeAttack: return MonsterAttack(monster); case MonsterMode::HitRecovery: return MonsterGotHit(monster); case MonsterMode::Death: MonsterDeath(monster); return false; case MonsterMode::SpecialMeleeAttack: return MonsterSpecialAttack(monster); case MonsterMode::FadeIn: return MonsterFadein(monster); case MonsterMode::FadeOut: return MonsterFadeout(monster); case MonsterMode::RangedAttack: return MonsterRangedAttack(monster); case MonsterMode::SpecialStand: return MonsterSpecialStand(monster); case MonsterMode::SpecialRangedAttack: return MonsterRangedSpecialAttack(monster); case MonsterMode::Delay: return MonsterDelay(monster); case MonsterMode::Petrified: MonsterPetrified(monster); return false; case MonsterMode::Heal: MonsterHeal(monster); return false; case MonsterMode::Talk: MonsterTalk(monster); return false; default: return false; } } MonsterSpritesData LoadMonsterSpritesData(const MonsterData &monsterData) { const size_t numAnims = GetNumAnims(monsterData); MonsterSpritesData result; result.data = MultiFileLoader {}( numAnims, FileNameWithCharAffixGenerator({ "monsters\\", monsterData.spritePath() }, DEVILUTIONX_CL2_EXT, Animletter), result.offsets.data(), [&monsterData](size_t index) { return monsterData.hasAnim(index); }); #ifndef UNPACKED_MPQS // Convert CL2 to CLX: std::vector> clxData; size_t accumulatedSize = 0; for (size_t i = 0, j = 0; i < numAnims; ++i) { if (!monsterData.hasAnim(i)) continue; const uint32_t begin = result.offsets[j]; const uint32_t end = result.offsets[j + 1]; clxData.emplace_back(); Cl2ToClx(reinterpret_cast(&result.data[begin]), end - begin, PointerOrValue { monsterData.width }, clxData.back()); result.offsets[j] = static_cast(accumulatedSize); accumulatedSize += clxData.back().size(); ++j; } result.offsets[clxData.size()] = static_cast(accumulatedSize); result.data = nullptr; result.data = std::unique_ptr(new std::byte[accumulatedSize]); for (size_t i = 0; i < clxData.size(); ++i) { memcpy(&result.data[result.offsets[i]], clxData[i].data(), clxData[i].size()); } #endif return result; } void EnsureMonsterIndexIsActive(size_t monsterId) { assert(monsterId < MaxMonsters); for (size_t index = 0; index < MaxMonsters; index++) { if (ActiveMonsters[index] != monsterId) continue; if (index < ActiveMonsterCount) return; // monster is already active const unsigned oldId = ActiveMonsters[ActiveMonsterCount]; ActiveMonsters[ActiveMonsterCount] = static_cast(monsterId); ActiveMonsters[index] = oldId; ActiveMonsterCount += 1; } } void InitGolem(devilution::Monster &monster, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { monster.flags |= MFLAG_GOLEM; monster.goalVar3 = static_cast(golemOwnerPlayerId); const Player &player = Players[golemOwnerPlayerId]; monster.maxHitPoints = 2 * (320 * golemSpellLevel + player._pMaxMana / 3); monster.hitPoints = monster.maxHitPoints; monster.armorClass = 25; monster.golemToHit = 5 * (golemSpellLevel + 8) + 2 * player.getCharacterLevel(); monster.minDamage = 2 * (golemSpellLevel + 4); monster.maxDamage = 2 * (golemSpellLevel + 8); UpdateEnemy(monster); } bool PosOkMissile(Point position) { return !TileHasAny(position, TileProperties::BlockMissile); } bool PosOkMovingMissile(Point position) { return !IsMissileBlockedByTile(position); } } // namespace tl::expected AddMonsterType(_monster_id type, placeflag placeflag) { const size_t typeIndex = GetMonsterTypeIndex(type); CMonster &monsterType = LevelMonsterTypes[typeIndex]; if (typeIndex == LevelMonsterTypeCount) { LevelMonsterTypeCount++; monsterType.type = type; const MonsterData &monsterData = MonstersData[type]; monstimgtot += monsterData.image; const size_t numAnims = GetNumAnims(monsterData); for (size_t i = 0; i < numAnims; ++i) { AnimStruct &anim = monsterType.anims[i]; anim.frames = monsterData.frames[i]; if (monsterData.hasAnim(i)) { anim.rate = monsterData.rate[i]; anim.width = monsterData.width; } } RETURN_IF_ERROR(InitMonsterSND(monsterType)); } monsterType.placeFlags |= placeflag; return typeIndex; } tl::expected InitTRNForUniqueMonster(Monster &monster) { char filestr[64]; *BufCopy(filestr, R"(monsters\monsters\)", UniqueMonstersData[static_cast(monster.uniqueType)].mTrnName, ".trn") = '\0'; ASSIGN_OR_RETURN(monster.uniqueMonsterTRN, LoadFileInMemWithStatus(filestr)); return {}; } tl::expected PrepareUniqueMonst(Monster &monster, UniqueMonsterType monsterType, size_t minionType, int bosspacksize, const UniqueMonsterData &uniqueMonsterData) { monster.uniqueType = monsterType; monster.maxHitPoints = uniqueMonsterData.mmaxhp << 6; if (!gbIsMultiplayer) monster.maxHitPoints = std::max(monster.maxHitPoints / 2, 64); monster.hitPoints = monster.maxHitPoints; monster.ai = uniqueMonsterData.mAi; monster.intelligence = uniqueMonsterData.mint; monster.minDamage = uniqueMonsterData.mMinDamage; monster.maxDamage = uniqueMonsterData.mMaxDamage; monster.minDamageSpecial = uniqueMonsterData.mMinDamage; monster.maxDamageSpecial = uniqueMonsterData.mMaxDamage; monster.reducePlayerStrength = uniqueMonsterData.reducePlayerStrength; monster.reducePlayerMagic = uniqueMonsterData.reducePlayerMagic; monster.reducePlayerDexterity = uniqueMonsterData.reducePlayerDexterity; monster.reducePlayerVitality = uniqueMonsterData.reducePlayerVitality; monster.reducePlayerMaxHP = uniqueMonsterData.reducePlayerMaxHP; monster.reducePlayerMaxMana = uniqueMonsterData.reducePlayerMaxMana; monster.resistance = uniqueMonsterData.mMagicRes; monster.talkMsg = uniqueMonsterData.mtalkmsg; if (monsterType == UniqueMonsterType::HorkDemon) monster.lightId = NO_LIGHT; else monster.lightId = AddLight(monster.position.tile, 3); if (UseMultiplayerQuests()) { if (monster.ai == MonsterAIID::LazarusSuccubus) monster.talkMsg = TEXT_NONE; if (monster.ai == MonsterAIID::Lazarus && Quests[Q_BETRAYER]._qvar1 > 3) { monster.goal = MonsterGoal::Normal; } else if (monster.talkMsg != TEXT_NONE) { monster.goal = MonsterGoal::Inquiring; } } else if (monster.talkMsg != TEXT_NONE) { monster.goal = MonsterGoal::Inquiring; } if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.maxHitPoints = 3 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 100 : 50) << 6; else monster.maxHitPoints += 100 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 2 * (monster.minDamage + 2); monster.maxDamage = 2 * (monster.maxDamage + 2); monster.minDamageSpecial = 2 * (monster.minDamageSpecial + 2); monster.maxDamageSpecial = 2 * (monster.maxDamageSpecial + 2); } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.maxHitPoints = 4 * monster.maxHitPoints; if (gbIsHellfire) monster.maxHitPoints += (gbIsMultiplayer ? 200 : 100) << 6; else monster.maxHitPoints += 200 << 6; monster.hitPoints = monster.maxHitPoints; monster.minDamage = 4 * monster.minDamage + 6; monster.maxDamage = 4 * monster.maxDamage + 6; monster.minDamageSpecial = 4 * monster.minDamageSpecial + 6; monster.maxDamageSpecial = 4 * monster.maxDamageSpecial + 6; } RETURN_IF_ERROR(InitTRNForUniqueMonster(monster)); monster.uniqTrans = uniquetrans++; if (uniqueMonsterData.customArmorClass != 0) { monster.armorClass = uniqueMonsterData.customArmorClass; if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { monster.armorClass += NightmareAcBonus; } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { monster.armorClass += HellAcBonus; } } if (uniqueMonsterData.monsterPack != UniqueMonsterPack::None) { PlaceGroup(minionType, bosspacksize, &monster, uniqueMonsterData.monsterPack == UniqueMonsterPack::Leashed); } if (monster.ai != MonsterAIID::Gargoyle) { monster.changeAnimationData(MonsterGraphic::Stand); monster.animInfo.currentFrame = GenerateRnd(monster.animInfo.numberOfFrames - 1); monster.flags &= ~MFLAG_ALLOW_SPECIAL; monster.mode = MonsterMode::Stand; } return {}; } void InitLevelMonsters() { LevelMonsterTypeCount = 0; monstimgtot = 0; for (CMonster &levelMonsterType : LevelMonsterTypes) { levelMonsterType.placeFlags = 0; } ClrAllMonsters(); ActiveMonsterCount = 0; totalmonsters = MaxMonsters; std::iota(std::begin(ActiveMonsters), std::end(ActiveMonsters), 0U); uniquetrans = 0; } tl::expected GetLevelMTypes() { RETURN_IF_ERROR(AddMonsterType(MT_GOLEM, PLACE_SPECIAL)); if (currlevel == 16) { RETURN_IF_ERROR(AddMonsterType(MT_ADVOCATE, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_RBLACK, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_DIABLO, PLACE_SPECIAL)); return {}; } if (currlevel == 18) RETURN_IF_ERROR(AddMonsterType(MT_HORKSPWN, PLACE_SCATTER)); if (currlevel == 19) { RETURN_IF_ERROR(AddMonsterType(MT_HORKSPWN, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_HORKDMN, PLACE_UNIQUE)); } if (currlevel == 20) RETURN_IF_ERROR(AddMonsterType(MT_DEFILER, PLACE_UNIQUE)); if (currlevel == 24) { RETURN_IF_ERROR(AddMonsterType(MT_ARCHLICH, PLACE_SCATTER)); RETURN_IF_ERROR(AddMonsterType(MT_NAKRUL, PLACE_SPECIAL)); } if (!setlevel) { if (Quests[Q_BUTCHER].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(MT_CLEAVER, PLACE_SPECIAL)); if (Quests[Q_GARBUD].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Garbud, PLACE_UNIQUE)); if (Quests[Q_ZHAR].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Zhar, PLACE_UNIQUE)); if (Quests[Q_LTBANNER].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::SnotSpill, PLACE_UNIQUE)); if (Quests[Q_VEIL].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::Lachdan, PLACE_UNIQUE)); if (Quests[Q_WARLORD].IsAvailable()) RETURN_IF_ERROR(AddMonsterType(UniqueMonsterType::WarlordOfBlood, PLACE_UNIQUE)); if (UseMultiplayerQuests() && currlevel == Quests[Q_SKELKING]._qlevel) { RETURN_IF_ERROR(AddMonsterType(MT_SKING, PLACE_UNIQUE)); int skeletonTypeCount = 0; _monster_id skeltypes[NUM_MAX_MTYPES]; for (const _monster_id skeletonType : SkeletonTypes) { if (!IsMonsterAvailable(MonstersData[skeletonType])) continue; skeltypes[skeletonTypeCount++] = skeletonType; } RETURN_IF_ERROR(AddMonsterType(skeltypes[GenerateRnd(skeletonTypeCount)], PLACE_SCATTER)); } _monster_id typelist[MaxMonsters]; int nt = 0; for (size_t i = 0; i < MonstersData.size(); i++) { if (!IsMonsterAvailable(MonstersData[i])) continue; typelist[nt++] = (_monster_id)i; } while (nt > 0 && LevelMonsterTypeCount < MaxLvlMTypes && monstimgtot < 4000) { for (int i = 0; i < nt;) { if (MonstersData[typelist[i]].image > 4000 - monstimgtot) { typelist[i] = typelist[--nt]; continue; } i++; } if (nt != 0) { const int i = GenerateRnd(nt); RETURN_IF_ERROR(AddMonsterType(typelist[i], PLACE_SCATTER)); typelist[i] = typelist[--nt]; } } } else { if (setlvlnum == SL_SKELKING) { RETURN_IF_ERROR(AddMonsterType(MT_SKING, PLACE_UNIQUE)); } } return {}; } tl::expected InitMonsterSND(CMonster &monsterType) { if (!gbSndInited) return {}; const char *prefixes[] { "a", // Attack "h", // Hit "d", // Death "s", // Special }; const MonsterData &data = MonstersData[monsterType.type]; const std::string_view soundSuffix = data.soundPath(); for (int i = 0; i < 4; i++) { const std::string_view prefix = prefixes[i]; if (prefix == "s" && !data.hasSpecialSound) continue; for (int j = 0; j < 2; j++) { char path[64]; *BufCopy(path, "monsters\\", soundSuffix, prefix, j + 1, ".wav") = '\0'; ASSIGN_OR_RETURN(monsterType.sounds[i][j], SoundFileLoadWithStatus(path)); } } return {}; } tl::expected InitMonsterGFX(CMonster &monsterType, MonsterSpritesData &&spritesData) { if (HeadlessMode) return {}; const _monster_id mtype = monsterType.type; const MonsterData &monsterData = MonstersData[mtype]; if (spritesData.data == nullptr) spritesData = LoadMonsterSpritesData(monsterData); monsterType.animData = std::move(spritesData.data); const size_t numAnims = GetNumAnims(monsterData); for (size_t i = 0, j = 0; i < numAnims; ++i) { if (!monsterData.hasAnim(i)) { monsterType.anims[i].sprites = std::nullopt; continue; } const uint32_t begin = spritesData.offsets[j]; const uint32_t end = spritesData.offsets[j + 1]; auto *animSpritesData = reinterpret_cast(&monsterType.animData[begin]); const uint16_t numLists = GetNumListsFromClxListOrSheetBuffer(animSpritesData, end - begin); monsterType.anims[i].sprites = ClxSpriteListOrSheet { animSpritesData, numLists }; ++j; } if (!monsterData.trnFile.empty()) { InitMonsterTRN(monsterType); } if (IsAnyOf(mtype, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::MagmaBall).LoadGFX()); if (IsAnyOf(mtype, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::ThinLightning).LoadGFX()); if (mtype == MT_SNOWWICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarBlue).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarBlueExplosion).LoadGFX()); } if (mtype == MT_HLSPWN) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarRed).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarRedExplosion).LoadGFX()); } if (mtype == MT_SOLBRNR) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarYellow).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BloodStarYellowExplosion).LoadGFX()); } if (IsAnyOf(mtype, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD)) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::Acid).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::AcidSplat).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::AcidPuddle).LoadGFX()); } if (mtype == MT_LICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::OrangeFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::OrangeFlareExplosion).LoadGFX()); } if (mtype == MT_ARCHLICH) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::YellowFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::YellowFlareExplosion).LoadGFX()); } if (IsAnyOf(mtype, MT_PSYCHORB, MT_BONEDEMN)) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlare2).LoadGFX()); if (mtype == MT_NECRMORB) { RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::RedFlare).LoadGFX()); RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::RedFlareExplosion).LoadGFX()); } if (mtype == MT_PSYCHORB) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlareExplosion).LoadGFX()); if (mtype == MT_BONEDEMN) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::BlueFlareExplosion2).LoadGFX()); if (mtype == MT_DIABLO) RETURN_IF_ERROR(GetMissileSpriteData(MissileGraphicID::DiabloApocalypseBoom).LoadGFX()); return {}; } tl::expected InitAllMonsterGFX() { if (HeadlessMode) return {}; using LevelMonsterTypeIndices = StaticVector; std::vector monstersBySprite(GetNumMonsterSprites()); for (size_t i = 0; i < LevelMonsterTypeCount; ++i) { monstersBySprite[static_cast(LevelMonsterTypes[i].data().spriteId)].emplace_back(i); } size_t totalUniqueBytes = 0; size_t totalBytes = 0; for (const LevelMonsterTypeIndices &monsterTypes : monstersBySprite) { if (monsterTypes.empty()) continue; CMonster &firstMonster = LevelMonsterTypes[monsterTypes[0]]; if (firstMonster.animData != nullptr) continue; MonsterSpritesData spritesData = LoadMonsterSpritesData(firstMonster.data()); const size_t spritesDataSize = spritesData.offsets[GetNumAnimsWithGraphics(firstMonster.data())]; for (size_t i = 1; i < monsterTypes.size(); ++i) { MonsterSpritesData spritesDataCopy { std::unique_ptr { new std::byte[spritesDataSize] }, spritesData.offsets }; memcpy(spritesDataCopy.data.get(), spritesData.data.get(), spritesDataSize); RETURN_IF_ERROR(InitMonsterGFX(LevelMonsterTypes[monsterTypes[i]], std::move(spritesDataCopy))); } LogVerbose("Loaded monster graphics: {:15s} {:>4d} KiB x{:d}", firstMonster.data().spritePath(), spritesDataSize / 1024, monsterTypes.size()); totalUniqueBytes += spritesDataSize; totalBytes += spritesDataSize * monsterTypes.size(); RETURN_IF_ERROR(InitMonsterGFX(firstMonster, std::move(spritesData))); } LogVerbose(" Total monster graphics: {:>4d} KiB {:>4d} KiB", totalUniqueBytes / 1024, totalBytes / 1024); if (totalUniqueBytes > 0) { // we loaded new sprites, check if we need to update existing monsters for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; if (!monster.animInfo.sprites) RETURN_IF_ERROR(SyncMonsterAnim(monster)); } } return {}; } void WeakenNaKrul() { if (currlevel != 24 || static_cast(UberDiabloMonsterIndex) >= ActiveMonsterCount) return; Monster &monster = Monsters[UberDiabloMonsterIndex]; PlayEffect(monster, MonsterSound::Death); monster.armorClass -= 50; const int hp = monster.maxHitPoints / 2; monster.resistance = 0; monster.hitPoints = hp; monster.maxHitPoints = hp; } void InitGolems() { if (!setlevel) { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) AddMonster(GolemHoldingCell, Direction::South, 0, false); } } tl::expected InitMonsters() { if (!gbIsSpawn && !setlevel && currlevel == 16) LoadDiabMonsts(); int nt = numtrigs; if (currlevel == 15) nt = 1; for (int i = 0; i < nt; i++) { for (int s = -2; s < 2; s++) { for (int t = -2; t < 2; t++) DoVision(trigs[i].position + Displacement { s, t }, 15, MAP_EXP_NONE, false); } } if (!gbIsSpawn) RETURN_IF_ERROR(PlaceQuestMonsters()); if (!setlevel) { if (!gbIsSpawn) RETURN_IF_ERROR(PlaceUniqueMonsters()); size_t na = 0; for (int s = 16; s < 96; s++) { for (int t = 16; t < 96; t++) { if (!IsTileSolid({ s, t })) na++; } } size_t numplacemonsters = na / 30; if (gbIsMultiplayer) numplacemonsters += numplacemonsters / 2; if (ActiveMonsterCount + numplacemonsters > MaxMonsters - 10) numplacemonsters = MaxMonsters - 10 - ActiveMonsterCount; totalmonsters = ActiveMonsterCount + numplacemonsters; int numscattypes = 0; size_t scattertypes[NUM_MAX_MTYPES]; for (size_t i = 0; i < LevelMonsterTypeCount; i++) { if ((LevelMonsterTypes[i].placeFlags & PLACE_SCATTER) != 0) { scattertypes[numscattypes] = i; numscattypes++; } } if (numscattypes > 0) { while (ActiveMonsterCount < totalmonsters) { const size_t typeIndex = scattertypes[GenerateRnd(numscattypes)]; if (currlevel == 1 || FlipCoin()) na = 1; else if (currlevel == 2 || leveltype == DTYPE_CRYPT) na = GenerateRnd(2) + 2; else na = GenerateRnd(3) + 3; PlaceGroup(typeIndex, na); } } } for (int i = 0; i < nt; i++) { for (int s = -2; s < 2; s++) { for (int t = -2; t < 2; t++) DoUnVision(trigs[i].position + Displacement { s, t }, 15); } } return InitAllMonsterGFX(); } tl::expected SetMapMonsters(const uint16_t *dunData, Point startPosition) { RETURN_IF_ERROR(AddMonsterType(MT_GOLEM, PLACE_SPECIAL)); if (setlevel) for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) AddMonster(GolemHoldingCell, Direction::South, 0, false); WorldTileSize size = GetDunSize(dunData); const int layer2Offset = 2 + (size.width * size.height); // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *monsterLayer = &dunData[layer2Offset + (size.width * size.height)]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto monsterId = static_cast(Swap16LE(monsterLayer[j * size.width + i])); if (monsterId != 0) { ASSIGN_OR_RETURN(const size_t typeIndex, AddMonsterType(MonstConvTbl[monsterId - 1], PLACE_SPECIAL)); PlaceMonster(ActiveMonsterCount++, typeIndex, startPosition + Displacement { i, j }); } } } return {}; } Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap) { if (ActiveMonsterCount < MaxMonsters) { Monster &monster = Monsters[ActiveMonsters[ActiveMonsterCount++]]; if (inMap) monster.occupyTile(position, false); InitMonster(monster, dir, typeIndex, position); return &monster; } return nullptr; } void SpawnMonster(Point position, Direction dir, size_t typeIndex) { if (ActiveMonsterCount >= MaxMonsters) return; // The command is only executed for the level owner, to prevent desyncs in multiplayer. if (!MyPlayer->isLevelOwnedByLocalClient()) return; const size_t monsterIndex = ActiveMonsters[ActiveMonsterCount]; ActiveMonsterCount += 1; const uint32_t seed = GetLCGEngineState(); // Update local state immediately to increase ActiveMonsterCount instantly (this allows multiple monsters to be spawned in one game tick) InitializeSpawnedMonster(position, dir, typeIndex, monsterIndex, seed, 0, 0); NetSendCmdSpawnMonster(position, dir, static_cast(typeIndex), static_cast(monsterIndex), seed, 0, 0); } void LoadDeltaSpawnedMonster(size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { SetRndSeed(seed); EnsureMonsterIndexIsActive(monsterId); const WorldTilePosition position = GolemHoldingCell; Monster &monster = Monsters[monsterId]; M_ClearSquares(monster); InitMonster(monster, Direction::South, typeIndex, position); if (monster.type().type == MT_GOLEM) { InitGolem(monster, golemOwnerPlayerId, golemSpellLevel); } } void InitializeSpawnedMonster(Point position, Direction dir, size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel) { SetRndSeed(seed); EnsureMonsterIndexIsActive(monsterId); Monster &monster = Monsters[monsterId]; M_ClearSquares(monster); // When we receive a network message, the position we got for the new monster may already be occupied. // That's why we check for the next free tile for the monster. auto freePosition = Crawl(0, MaxCrawlRadius, [&](Displacement displacement) -> std::optional { Point posToCheck = position + displacement; if (IsTileAvailable(posToCheck)) return posToCheck; return {}; }); assert(freePosition); assert(!MyPlayer->isLevelOwnedByLocalClient() || (freePosition && position == *freePosition)); position = freePosition.value_or(position); monster.occupyTile(position, false); InitMonster(monster, dir, typeIndex, position); if (monster.type().type == MT_GOLEM) { InitGolem(monster, golemOwnerPlayerId, golemSpellLevel); StartSpecialStand(monster, dir); } else if (IsSkel(monster.type().type)) { StartSpecialStand(monster, dir); } else { M_StartStand(monster, dir); } } void AddDoppelganger(Monster &monster) { Point target = { 0, 0 }; for (int d = 0; d < 8; d++) { const Point position = monster.position.tile + static_cast(d); if (!IsTileAvailable(position)) continue; target = position; } if (target != Point { 0, 0 }) { const size_t typeIndex = GetMonsterTypeIndex(monster.type().type); SpawnMonster(target, monster.direction, typeIndex); } } void ApplyMonsterDamage(DamageType damageType, Monster &monster, int damage) { lua::OnMonsterTakeDamage(&monster, damage, static_cast(damageType)); monster.hitPoints -= damage; if (monster.hasNoLife()) { delta_kill_monster(monster, monster.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, monster.position.tile, static_cast(monster.getId())); return; } delta_monster_hp(monster, *MyPlayer); NetSendCmdMonDmg(false, static_cast(monster.getId()), damage); } void MonsterReducePlayerAttribute(Monster &monster, Player &player) { if (&player != MyPlayer) return; if (monster.reducePlayerStrength > 0) { ModifyPlrStr(player, -static_cast(monster.reducePlayerStrength)); } if (monster.reducePlayerMagic > 0) { ModifyPlrMag(player, -static_cast(monster.reducePlayerMagic)); } if (monster.reducePlayerDexterity > 0) { ModifyPlrDex(player, -static_cast(monster.reducePlayerDexterity)); } if (monster.reducePlayerVitality > 0) { ModifyPlrVit(player, -static_cast(monster.reducePlayerVitality)); } if (monster.reducePlayerMaxHP > 0) { const int reduceAmount = std::min(player._pMaxHPBase - 64, monster.reducePlayerMaxHP * 64); player._pMaxHP = std::max(64, player._pMaxHP - reduceAmount); player._pHitPoints = std::min(player._pHitPoints, player._pMaxHP); player._pMaxHPBase -= reduceAmount; player._pHPBase = std::min(player._pHPBase, player._pMaxHPBase); RedrawComponent(PanelDrawComponent::Health); } if (monster.reducePlayerMaxMana > 0) { const int reduceAmount = std::min(player._pMaxManaBase, monster.reducePlayerMaxMana * 64); player._pMaxMana = std::max(0, player._pMaxMana - reduceAmount); player._pMana = std::min(player._pMana, player._pMaxMana); player._pMaxManaBase -= reduceAmount; player._pManaBase = std::min(player._pManaBase, player._pMaxManaBase); RedrawComponent(PanelDrawComponent::Mana); } } bool M_Talker(const Monster &monster) { return IsAnyOf(monster.ai, MonsterAIID::Lazarus, MonsterAIID::Warlord, MonsterAIID::Gharbad, MonsterAIID::Zhar, MonsterAIID::Snotspill, MonsterAIID::Lachdanan, MonsterAIID::LazarusSuccubus); } void M_StartStand(Monster &monster, Direction md) { ClearMVars(monster); if (monster.type().type == MT_GOLEM) NewMonsterAnim(monster, MonsterGraphic::Walk, md); else NewMonsterAnim(monster, MonsterGraphic::Stand, md); monster.var1 = static_cast(monster.mode); monster.var2 = 0; monster.mode = MonsterMode::Stand; monster.position.future = monster.position.tile; monster.position.old = monster.position.tile; UpdateEnemy(monster); } void M_ClearSquares(const Monster &monster) { for (const Point searchTile : PointsInRectangle(Rectangle { monster.position.old, 1 })) { if (FindMonsterAtPosition(searchTile) == &monster) dMonster[searchTile.x][searchTile.y] = 0; } } void M_GetKnockback(Monster &monster, WorldTilePosition attackerStartPos) { const Direction dir = GetDirection(attackerStartPos, monster.position.tile); if (!IsRelativeMoveOK(monster, monster.position.old, dir)) { return; } M_ClearSquares(monster); monster.position.old += dir; StartMonsterGotHit(monster); ChangeLightXY(monster.lightId, monster.position.tile); } void M_StartHit(Monster &monster, int dam) { PlayEffect(monster, MonsterSound::Hit); if (IsHardHit(monster, dam)) { if (monster.type().type == MT_BLINK) { Teleport(monster); } else if (IsAnyOf(monster.type().type, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_GRAVEDIG)) { monster.goal = MonsterGoal::Normal; monster.goalVar1 = 0; monster.goalVar2 = 0; } if (monster.mode != MonsterMode::Petrified) { StartMonsterGotHit(monster); } } } void M_StartHit(Monster &monster, const Player &player, int dam) { monster.tag(player); if (IsHardHit(monster, dam)) { monster.enemy = player.getId(); monster.enemyPosition = player.position.future; monster.flags &= ~MFLAG_TARGETS_MONSTER; if (monster.mode != MonsterMode::Petrified) { monster.direction = GetMonsterDirection(monster); } } M_StartHit(monster, dam); } void MonsterDeath(Monster &monster, Direction md, bool sendmsg) { if (!monster.isPlayerMinion()) AddPlrMonstExper(monster.level(sgGameInitInfo.nDifficulty), monster.exp(sgGameInitInfo.nDifficulty), monster.whoHit); MonsterKillCounts[monster.type().type]++; monster.hitPoints = 0; monster.flags &= ~MFLAG_HIDDEN; SetRndSeed(monster.rndItemSeed); SpawnLoot(monster, sendmsg); if (monster.type().type == MT_DIABLO) DiabloDeath(monster, true); else PlayEffect(monster, MonsterSound::Death); if (monster.mode != MonsterMode::Petrified) { if (monster.type().type == MT_GOLEM) md = Direction::South; NewMonsterAnim(monster, MonsterGraphic::Death, md, gGameLogicStep < GameLogicStep::ProcessMonsters ? AnimationDistributionFlags::ProcessAnimationPending : AnimationDistributionFlags::None); monster.mode = MonsterMode::Death; } else if (monster.isUnique()) { AddUnLight(monster.lightId); } monster.goal = MonsterGoal::None; monster.var1 = 0; monster.position.tile = monster.position.old; monster.position.future = monster.position.old; M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); CheckQuestKill(monster, sendmsg); M_FallenFear(monster.position.tile); if (IsAnyOf(monster.type().type, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SPIDLORD)) AddMissile(monster.position.tile, { 0, 0 }, Direction::South, MissileID::AcidPuddle, TARGET_PLAYERS, monster, monster.intelligence + 1, 0); } void StartMonsterDeath(Monster &monster, const Player &player, bool sendmsg) { monster.tag(player); const Direction md = GetDirection(monster.position.tile, player.position.tile); MonsterDeath(monster, md, sendmsg); } void KillGolem(Monster &golem) { delta_kill_monster(golem, golem.position.tile, *MyPlayer); NetSendCmdLocParam1(false, CMD_MONSTDEATH, golem.position.tile, static_cast(golem.getId())); M_StartKill(golem, *MyPlayer); } void M_StartKill(Monster &monster, const Player &player) { StartMonsterDeath(monster, player, true); } void M_SyncStartKill(Monster &monster, Point position, const Player &player) { if (monster.hitPoints == 0 || monster.mode == MonsterMode::Death) { return; } if (dMonster[position.x][position.y] == 0) { M_ClearSquares(monster); monster.position.tile = position; monster.position.old = position; } StartMonsterDeath(monster, player, false); } void M_UpdateRelations(const Monster &monster) { if (monster.hasLeashedMinions()) ReleaseMinions(monster); ShrinkLeaderPacksize(monster); } void DoEnding() { if (gbIsMultiplayer) { SNetLeaveGame(leaveinfo_t::LEAVE_ENDING); } music_stop(); if (gbIsMultiplayer) { SDL_Delay(1000); } if (gbIsSpawn) return; switch (MyPlayer->_pClass) { case HeroClass::Sorcerer: case HeroClass::Monk: play_movie("gendata\\diabvic1.smk", false); break; case HeroClass::Warrior: case HeroClass::Barbarian: play_movie("gendata\\diabvic2.smk", false); break; default: play_movie("gendata\\diabvic3.smk", false); break; } play_movie("gendata\\diabend.smk", false); const bool bMusicOn = gbMusicOn; gbMusicOn = true; const int musicVolume = sound_get_or_set_music_volume(1); sound_get_or_set_music_volume(0); music_start(TMUSIC_CATACOMBS); loop_movie = true; play_movie("gendata\\loopdend.smk", true); loop_movie = false; music_stop(); sound_get_or_set_music_volume(musicVolume); gbMusicOn = bMusicOn; } void PrepDoEnding() { gbSoundOn = sgbSaveSoundOn; gbRunGame = false; MyPlayerIsDead = false; cineflag = true; Player &myPlayer = *MyPlayer; myPlayer.pDiabloKillLevel = std::max(myPlayer.pDiabloKillLevel, static_cast(sgGameInitInfo.nDifficulty + 1)); for (Player &player : Players) { player._pmode = PM_QUIT; player._pInvincible = true; if (gbIsMultiplayer) { if (player.hasNoLife()) player._pHitPoints = 64; if (player.hasNoMana()) player._pMana = 64; } } } bool Walk(Monster &monster, Direction md) { if (!DirOK(monster, md)) { return false; } if (md == Direction::NoDirection) return true; WalkInDirection(monster, md); return true; } void GolumAi(Monster &golem) { if (golem.position.tile.x == 1 && golem.position.tile.y == 0) { return; } if (IsAnyOf(golem.mode, MonsterMode::Death, MonsterMode::SpecialStand) || golem.isWalking()) { return; } if ((golem.flags & MFLAG_TARGETS_MONSTER) == 0) UpdateEnemy(golem); if (golem.mode == MonsterMode::MeleeAttack) { return; } if ((golem.flags & MFLAG_NO_ENEMY) == 0) { Monster &enemy = Monsters[golem.enemy]; const int mex = golem.position.tile.x - enemy.position.future.x; const int mey = golem.position.tile.y - enemy.position.future.y; golem.direction = GetDirection(golem.position.tile, enemy.position.tile); if (std::abs(mex) < 2 && std::abs(mey) < 2) { golem.enemyPosition = enemy.position.tile; if (enemy.activeForTicks == 0) { enemy.activeForTicks = UINT8_MAX; enemy.position.last = golem.position.tile; for (int j = 0; j < 5; j++) { for (int k = 0; k < 5; k++) { const int mx = golem.position.tile.x + k - 2; const int my = golem.position.tile.y + j - 2; if (!InDungeonBounds({ mx, my })) continue; const int enemyId = dMonster[mx][my]; if (enemyId > 0) Monsters[enemyId - 1].activeForTicks = UINT8_MAX; } } } StartAttack(golem); return; } if (AiPlanPath(golem)) return; } golem.pathCount++; if (golem.pathCount > 8) golem.pathCount = 5; if (RandomWalk(golem, Players[golem.goalVar3]._pdir)) return; Direction md = Left(golem.direction); for (int j = 0; j < 8; j++) { md = Right(md); if (Walk(golem, md)) { break; } } } void DeleteMonsterList() { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) { Monster &golem = Monsters[i]; if (!golem.isInvalid) continue; golem.position.tile = GolemHoldingCell; golem.position.future = { 0, 0 }; golem.position.old = { 0, 0 }; golem.isInvalid = false; } for (size_t i = ReservedMonsterSlotsForGolems; i < ActiveMonsterCount;) { if (Monsters[ActiveMonsters[i]].isInvalid) { if (pcursmonst == static_cast(ActiveMonsters[i])) // Unselect monster if player highlighted it pcursmonst = -1; DeleteMonster(i); } else { i++; } } } void RemoveEnemyReferences(const Player &player) { if (&player == MyPlayer || !player.isOnActiveLevel()) return; const size_t playerId = player.getId(); for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &activeMonster = Monsters[ActiveMonsters[i]]; if ((activeMonster.flags & MFLAG_TARGETS_MONSTER) == 0 && activeMonster.enemy == playerId) { activeMonster.flags |= MFLAG_NO_ENEMY; } } } void ProcessMonsters() { DeleteMonsterList(); assert(ActiveMonsterCount <= MaxMonsters); for (size_t i = 0; i < ActiveMonsterCount; i++) { Monster &monster = Monsters[ActiveMonsters[i]]; FollowTheLeader(monster); if (gbIsMultiplayer) { SetRndSeed(monster.aiSeed); monster.aiSeed = AdvanceRndSeed(); } if (monster.hitPoints < monster.maxHitPoints && !monster.hasNoLife()) { if (monster.level(sgGameInitInfo.nDifficulty) > 1) { monster.hitPoints += monster.level(sgGameInitInfo.nDifficulty) / 2; } else { monster.hitPoints += monster.level(sgGameInitInfo.nDifficulty); } monster.hitPoints = std::min(monster.hitPoints, monster.maxHitPoints); // prevent going over max HP with part of a single regen tick } const bool isMonsterVisible = IsTileVisible(monster.position.tile); if (isMonsterVisible && monster.activeForTicks == 0) { if (monster.type().type == MT_CLEAVER) { PlaySFX(SfxID::ButcherGreeting); } if (monster.type().type == MT_NAKRUL) { if (sgGameInitInfo.bCowQuest != 0) { PlaySFX(SfxID::NaKrul6); } else { if (IsUberRoomOpened) PlaySFX(SfxID::NaKrul4); else PlaySFX(SfxID::NaKrul5); } } if (monster.type().type == MT_DEFILER) PlaySFX(SfxID::Defiler8); UpdateEnemy(monster); } if ((monster.flags & MFLAG_NO_ENEMY) == 0) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) { assert(monster.enemy >= 0 && monster.enemy < MaxMonsters); monster.position.last = Monsters[monster.enemy].position.future; monster.enemyPosition = monster.position.last; } else { assert(monster.enemy >= 0 && monster.enemy < MAX_PLRS); const Player &player = Players[monster.enemy]; monster.enemyPosition = player.position.future; if (isMonsterVisible) { monster.position.last = player.position.future; } } } if ((monster.flags & MFLAG_TARGETS_MONSTER) == 0) { if (isMonsterVisible) { monster.activeForTicks = UINT8_MAX; } else if (monster.activeForTicks != 0 && monster.type().type != MT_DIABLO) { monster.activeForTicks--; } } while (true) { if ((monster.flags & MFLAG_SEARCH) == 0 || !AiPlanPath(monster)) { AiProc[static_cast(monster.ai)](monster); } if (!UpdateModeStance(monster)) break; GroupUnity(monster); } if (monster.mode != MonsterMode::Petrified && (monster.flags & MFLAG_ALLOW_SPECIAL) == 0) { monster.animInfo.processAnimation((monster.flags & MFLAG_LOCK_ANIMATION) != 0); } } DeleteMonsterList(); } void FreeMonsters() { for (CMonster &monsterType : LevelMonsterTypes) { monsterType.animData = nullptr; monsterType.corpseId = 0; for (AnimStruct &animData : monsterType.anims) { animData.sprites = std::nullopt; } for (auto &variants : monsterType.sounds) { for (auto &sound : variants) { sound = nullptr; } } } } bool DirOK(const Monster &monster, Direction mdir) { const Point position = monster.position.tile; const Point futurePosition = position + mdir; if (!IsRelativeMoveOK(monster, position, mdir)) return false; if (monster.leaderRelation == LeaderRelation::Leashed) { return futurePosition.WalkingDistance(monster.getLeader()->position.future) < 4; } if (!monster.hasLeashedMinions()) return true; int mcount = 0; for (int x = futurePosition.x - 3; x <= futurePosition.x + 3; x++) { for (int y = futurePosition.y - 3; y <= futurePosition.y + 3; y++) { if (!InDungeonBounds({ x, y })) continue; Monster *minion = FindMonsterAtPosition({ x, y }, true); if (minion == nullptr) continue; if (minion->leaderRelation == LeaderRelation::Leashed && minion->getLeader() == &monster) { mcount++; } } } return mcount == monster.packSize; } bool LineClearMissile(Point startPoint, Point endPoint) { return LineClear(PosOkMissile, startPoint, endPoint); } bool LineClearMovingMissile(Point startPoint, Point endPoint) { return LineClear(PosOkMovingMissile, startPoint, endPoint); } tl::expected SyncMonsterAnim(Monster &monster) { #ifdef _DEBUG // fix for saves with debug monsters having type originally not on the level CMonster &monsterType = LevelMonsterTypes[monster.levelType]; if (monsterType.corpseId == 0) { RETURN_IF_ERROR(InitMonsterGFX(monsterType)); monsterType.corpseId = 1; } #endif if (monster.isUnique()) { RETURN_IF_ERROR(InitTRNForUniqueMonster(monster)); } MonsterGraphic graphic = MonsterGraphic::Stand; switch (monster.getVisualMonsterMode()) { case MonsterMode::Stand: case MonsterMode::Delay: case MonsterMode::Talk: break; case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: graphic = MonsterGraphic::Walk; break; case MonsterMode::MeleeAttack: case MonsterMode::RangedAttack: graphic = MonsterGraphic::Attack; break; case MonsterMode::HitRecovery: graphic = MonsterGraphic::GotHit; break; case MonsterMode::Death: graphic = MonsterGraphic::Death; break; case MonsterMode::SpecialMeleeAttack: case MonsterMode::FadeIn: case MonsterMode::FadeOut: case MonsterMode::SpecialStand: case MonsterMode::SpecialRangedAttack: case MonsterMode::Heal: graphic = MonsterGraphic::Special; break; case MonsterMode::Charge: graphic = MonsterGraphic::Attack; monster.animInfo.currentFrame = 0; break; default: monster.animInfo.currentFrame = 0; break; } monster.changeAnimationData(graphic); return {}; } void M_FallenFear(Point position) { const Rectangle fearArea = Rectangle { position, 4 }; for (const Point tile : PointsInRectangle(fearArea)) { if (!InDungeonBounds(tile)) continue; const int m = dMonster[tile.x][tile.y]; if (m == 0) continue; Monster &monster = Monsters[std::abs(m) - 1]; if (monster.ai != MonsterAIID::Fallen || monster.hasNoLife()) continue; const int runDistance = std::max((8 - monster.data().level), 2); monster.goal = MonsterGoal::Retreat; monster.goalVar1 = runDistance; monster.goalVar2 = static_cast(GetDirection(position, monster.position.tile)); } } void PrintMonstHistory(int mt) { if (*GetOptions().Gameplay.showMonsterType) { AddInfoBoxString(fmt::format(fmt::runtime(_("Type: {:s} Kills: {:d}")), GetMonsterTypeText(MonstersData[mt]), MonsterKillCounts[mt])); } else { AddInfoBoxString(fmt::format(fmt::runtime(_("Total kills: {:d}")), MonsterKillCounts[mt])); } if (MonsterKillCounts[mt] >= 30) { int minHP = MonstersData[mt].hitPointsMinimum; int maxHP = MonstersData[mt].hitPointsMaximum; if (!gbIsMultiplayer) { minHP /= 2; maxHP /= 2; } minHP = std::max(minHP, 1); maxHP = std::max(maxHP, 1); int hpBonusNightmare = 100; int hpBonusHell = 200; if (gbIsHellfire) { hpBonusNightmare = (!gbIsMultiplayer ? 50 : 100); hpBonusHell = (!gbIsMultiplayer ? 100 : 200); } if (sgGameInitInfo.nDifficulty == DIFF_NIGHTMARE) { minHP = 3 * minHP + hpBonusNightmare; maxHP = 3 * maxHP + hpBonusNightmare; } else if (sgGameInitInfo.nDifficulty == DIFF_HELL) { minHP = 4 * minHP + hpBonusHell; maxHP = 4 * maxHP + hpBonusHell; } AddInfoBoxString(fmt::format(fmt::runtime(_("Hit Points: {:d}-{:d}")), minHP, maxHP)); } if (MonsterKillCounts[mt] >= 15) { const int res = (sgGameInitInfo.nDifficulty != DIFF_HELL) ? MonstersData[mt].resistance : MonstersData[mt].resistanceHell; if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) == 0) { AddInfoBoxString(_("No magic resistance")); } else { if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0) { std::string resists = std::string(_("Resists:")); if ((res & RESIST_MAGIC) != 0) resists.append(_(" Magic")); if ((res & RESIST_FIRE) != 0) resists.append(_(" Fire")); if ((res & RESIST_LIGHTNING) != 0) resists.append(_(" Lightning")); AddInfoBoxString(resists); } if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) { std::string immune = std::string(_("Immune:")); if ((res & IMMUNE_MAGIC) != 0) immune.append(_(" Magic")); if ((res & IMMUNE_FIRE) != 0) immune.append(_(" Fire")); if ((res & IMMUNE_LIGHTNING) != 0) immune.append(_(" Lightning")); AddInfoBoxString(immune); } } } } void PrintUniqueHistory() { const Monster &monster = Monsters[pcursmonst]; if (*GetOptions().Gameplay.showMonsterType) { AddInfoBoxString(fmt::format(fmt::runtime(_("Type: {:s}")), GetMonsterTypeText(monster.data()))); } const int res = monster.resistance & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING | IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING); if (res == 0) { AddInfoBoxString(_("No resistances")); AddInfoBoxString(_("No Immunities")); } else { if ((res & (RESIST_MAGIC | RESIST_FIRE | RESIST_LIGHTNING)) != 0) AddInfoBoxString(_("Some Magic Resistances")); else AddInfoBoxString(_("No resistances")); if ((res & (IMMUNE_MAGIC | IMMUNE_FIRE | IMMUNE_LIGHTNING)) != 0) { AddInfoBoxString(_("Some Magic Immunities")); } else { AddInfoBoxString(_("No Immunities")); } } } void PlayEffect(Monster &monster, MonsterSound mode) { if (MyPlayer->pLvlLoad != 0) { return; } const int sndIdx = GenerateRnd(2); if (!gbSndInited || !gbSoundOn || gbBufferMsgs != 0) { return; } TSnd *snd = monster.type().sounds[static_cast(mode)][sndIdx].get(); if (snd == nullptr || snd->isPlaying()) { return; } int lVolume = 0; int lPan = 0; if (!CalculateSoundPosition(monster.position.tile, &lVolume, &lPan)) return; snd_play_snd(snd, lVolume, lPan, *GetOptions().Audio.soundVolume); } void MissToMonst(Missile &missile, Point position) { assert(static_cast(missile._misource) < MaxMonsters); Monster &monster = Monsters[missile._misource]; const Point oldPosition = missile.position.tile; monster.occupyTile(position, false); monster.direction = missile.getDirection(); monster.position.tile = position; M_StartStand(monster, monster.direction); M_StartHit(monster, 0); if (monster.type().type == MT_GLOOM) return; if ((monster.flags & MFLAG_TARGETS_MONSTER) == 0) { Player *player = PlayerAtPosition(oldPosition, true); if (player == nullptr) return; MonsterAttackPlayer(monster, *player, 500, monster.minDamageSpecial, monster.maxDamageSpecial); if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) return; if (player->_pmode != PM_GOTHIT && player->_pmode != PM_DEATH) StartPlrHit(*player, 0, true); const Point newPosition = oldPosition + GetDirection(missile.position.start, oldPosition); if (PosOkPlayer(*player, newPosition)) { player->position.tile = newPosition; FixPlayerLocation(*player, player->_pdir); FixPlrWalkTags(*player); player->occupyTile(newPosition, false); SetPlayerOld(*player); } return; } Monster *target = FindMonsterAtPosition(oldPosition, true); if (target == nullptr) return; MonsterAttackMonster(monster, *target, 500, monster.minDamageSpecial, monster.maxDamageSpecial); if (IsAnyOf(monster.type().type, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE)) return; const Point newPosition = oldPosition + GetDirection(missile.position.start, oldPosition); if (IsTileAvailable(*target, newPosition)) { monster.occupyTile(newPosition, false); dMonster[oldPosition.x][oldPosition.y] = 0; monster.position.tile = newPosition; monster.position.future = newPosition; } } Monster *FindMonsterAtPosition(Point position, bool ignoreMovingMonsters) { if (!InDungeonBounds(position)) { return nullptr; } auto monsterId = dMonster[position.x][position.y]; if (monsterId == 0 || (ignoreMovingMonsters && monsterId < 0)) { // nothing at this position, return a nullptr return nullptr; } return &Monsters[std::abs(monsterId) - 1]; } Monster *FindUniqueMonster(UniqueMonsterType monsterType) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.uniqueType == monsterType) return &monster; } return nullptr; } Monster *FindGolemForPlayer(const Player &player) { for (size_t i = 0; i < ActiveMonsterCount; i++) { const int monsterId = ActiveMonsters[i]; Monster &monster = Monsters[monsterId]; if (monster.type().type != MT_GOLEM) continue; if (monster.position.tile == GolemHoldingCell) continue; if (monster.goalVar3 != player.getId()) continue; if (monster.hitPoints == 0) continue; return &monster; } return nullptr; } bool IsTileAvailable(const Monster &monster, Point position) { if (!IsTileAvailable(position)) return false; return IsTileSafe(monster, position); } bool IsSkel(_monster_id mt) { return c_find(SkeletonTypes, mt) != SkeletonTypes.end(); } bool IsGoat(_monster_id mt) { return IsAnyOf(mt, MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC, MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW); } void ActivateSkeleton(Monster &monster, Point position) { if (IsTileAvailable(position)) { ActivateSpawn(monster, position, Direction::SouthWest); return; } constexpr std::array spawnDirections { Direction::North, Direction::NorthEast, Direction::East, Direction::NorthWest, Direction::SouthEast, Direction::West, Direction::SouthWest, Direction::South }; std::bitset<8> spawnOk; for (size_t i = 0; i < spawnDirections.size(); i++) { if (IsTileAvailable(position + spawnDirections[i])) spawnOk.set(i); } if (spawnOk.none()) return; // this is used in the following loop to find the nth set bit. int spawnChoice = GenerateRnd(15) % spawnOk.count(); for (size_t i = 0; i < spawnOk.size(); i++) { if (!spawnOk.test(i)) continue; if (spawnChoice > 0) { spawnChoice--; continue; } ActivateSpawn(monster, position + spawnDirections[i], Opposite(spawnDirections[i])); return; } } Monster *PreSpawnSkeleton() { Monster *skeleton = AddSkeleton({ 0, 0 }, Direction::South, false); if (skeleton != nullptr) M_StartStand(*skeleton, Direction::South); return skeleton; } void TalktoMonster(Player &player, Monster &monster) { if (&player == MyPlayer) monster.mode = MonsterMode::Talk; if (monster.uniqueType == UniqueMonsterType::SnotSpill && Quests[Q_LTBANNER].IsAvailable() && Quests[Q_LTBANNER]._qvar1 == 2) { if (RemoveInventoryItemById(player, IDI_BANNER)) { Quests[Q_LTBANNER]._qactive = QUEST_DONE; monster.talkMsg = TEXT_BANNER12; monster.goal = MonsterGoal::Inquiring; NetSendCmdQuest(true, Quests[Q_LTBANNER]); } } if (monster.uniqueType == UniqueMonsterType::Lachdan && Quests[Q_VEIL].IsAvailable() && monster.talkMsg >= TEXT_VEIL9) { if (RemoveInventoryItemById(player, IDI_GLDNELIX) && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { monster.talkMsg = TEXT_VEIL11; monster.goal = MonsterGoal::Inquiring; monster.flags |= MFLAG_QUEST_COMPLETE; if (MyPlayer == &player) { SpawnUnique(UITEM_STEELVEIL, monster.position.tile + Direction::South); Quests[Q_VEIL]._qvar2 = QS_VEIL_ITEM_SPAWNED; NetSendCmdQuest(true, Quests[Q_VEIL]); } } } if (monster.uniqueType == UniqueMonsterType::Zhar && monster.talkMsg == TEXT_ZHAR1 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { if (MyPlayer == &player) { Quests[Q_ZHAR]._qactive = QUEST_ACTIVE; Quests[Q_ZHAR]._qlog = true; Quests[Q_ZHAR]._qvar1 = QS_ZHAR_ITEM_SPAWNED; SetRndSeed(monster.rndItemSeed); DiscardRandomValues(10); CreateTypeItem(monster.position.tile + Displacement { 1, 1 }, false, ItemType::Misc, IMISC_BOOK, false, false, true); monster.flags |= MFLAG_QUEST_COMPLETE; NetSendCmdQuest(true, Quests[Q_ZHAR]); } } if (monster.uniqueType == UniqueMonsterType::Garbud && MyPlayer == &player) { if (monster.talkMsg == TEXT_GARBUD1) { Quests[Q_GARBUD]._qactive = QUEST_ACTIVE; Quests[Q_GARBUD]._qlog = true; NetSendCmdQuest(true, Quests[Q_GARBUD]); } if (monster.talkMsg == TEXT_GARBUD2 && (monster.flags & MFLAG_QUEST_COMPLETE) == 0) { SetRndSeed(monster.rndItemSeed); DiscardRandomValues(10); SpawnItem(monster, monster.position.tile + Displacement { 1, 1 }, false, true); monster.flags |= MFLAG_QUEST_COMPLETE; Quests[Q_GARBUD]._qvar1 = QS_GHARBAD_FIRST_ITEM_SPAWNED; NetSendCmdQuest(true, Quests[Q_GARBUD]); } } } void SpawnGolem(const Player &player, Point position, uint8_t spellLevel) { // Search monster index to use for the new golem Monster *golem = nullptr; // 1. Prefer MonsterIndex = PlayerIndex for vanilla compatibility if (player.getId() < ReservedMonsterSlotsForGolems) { Monster &reservedGolem = Monsters[player.getId()]; if (reservedGolem.position.tile == GolemHoldingCell || reservedGolem.hitPoints == 0) golem = &reservedGolem; } // 2. Use reserved slots, so additional Monsters can spawn if (golem == nullptr) { for (int i = 0; i < ReservedMonsterSlotsForGolems; i++) { Monster &reservedGolem = Monsters[i]; if (reservedGolem.position.tile == GolemHoldingCell || reservedGolem.hitPoints == 0) { golem = &reservedGolem; break; } } } // 3. Use normal monster slot if (golem == nullptr) { if (ActiveMonsterCount >= MaxMonsters) return; const size_t monsterIndex = ActiveMonsters[ActiveMonsterCount]; ActiveMonsterCount += 1; golem = &Monsters[monsterIndex]; } if (golem == nullptr) return; const size_t monsterIndex = golem->getId(); const uint32_t seed = GetLCGEngineState(); // Update local state immediately to increase ActiveMonsterCount instantly (this allows multiple monsters to be spawned in one game tick) InitializeSpawnedMonster(position, Direction::South, 0, monsterIndex, seed, player.getId(), spellLevel); NetSendCmdSpawnMonster(position, Direction::South, 0, static_cast(monsterIndex), seed, player.getId(), spellLevel); } bool CanTalkToMonst(const Monster &monster) { return IsAnyOf(monster.goal, MonsterGoal::Inquiring, MonsterGoal::Talking); } uint8_t encode_enemy(Monster &monster) { if ((monster.flags & MFLAG_TARGETS_MONSTER) != 0) return monster.enemy; return monster.enemy + MaxMonsters; } void decode_enemy(Monster &monster, uint8_t enemyId) { if (enemyId >= MaxMonsters) { enemyId -= MaxMonsters; monster.flags &= ~MFLAG_TARGETS_MONSTER; monster.enemy = enemyId; monster.enemyPosition = Players[enemyId].position.future; } else { monster.flags |= MFLAG_TARGETS_MONSTER; monster.enemy = enemyId; monster.enemyPosition = Monsters[enemyId].position.future; } } [[nodiscard]] size_t Monster::getId() const { return std::distance(&Monsters[0], this); } Monster *Monster::getLeader() const { if (leader == Monster::NoLeader) return nullptr; return &Monsters[leader]; } void Monster::setLeader(const Monster *newLeader) { if (newLeader == nullptr) { // really we should update this->leader to NoLeader to avoid leaving a dangling reference to a dead monster // when passed nullptr. So that buffed minions are drawn with a distinct colour in monhealthbar we leave the // reference and hope that no code tries to modify the leader through this instance later. leaderRelation = LeaderRelation::None; return; } this->leader = static_cast(newLeader->getId()); leaderRelation = LeaderRelation::Leashed; ai = newLeader->ai; } [[nodiscard]] unsigned Monster::distanceToEnemy() const { const int mx = position.tile.x - enemyPosition.x; const int my = position.tile.y - enemyPosition.y; return std::max(std::abs(mx), std::abs(my)); } void Monster::checkStandAnimationIsLoaded(Direction mdir) { if (IsAnyOf(mode, MonsterMode::Stand, MonsterMode::Talk)) { direction = mdir; changeAnimationData(MonsterGraphic::Stand); } } void Monster::petrify() { mode = MonsterMode::Petrified; animInfo.isPetrified = true; } bool Monster::isWalking() const { switch (getVisualMonsterMode()) { case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: return true; default: return false; } } bool Monster::isImmune(MissileID missileType, DamageType missileElement) const { if (((resistance & IMMUNE_MAGIC) != 0 && missileElement == DamageType::Magic) || ((resistance & IMMUNE_FIRE) != 0 && missileElement == DamageType::Fire) || ((resistance & IMMUNE_LIGHTNING) != 0 && missileElement == DamageType::Lightning) || ((resistance & IMMUNE_ACID) != 0 && missileElement == DamageType::Acid)) return true; if (missileType == MissileID::HolyBolt && type().type != MT_DIABLO && data().monsterClass != MonsterClass::Undead) return true; return false; } bool Monster::isResistant(MissileID missileType, DamageType missileElement) const { if (((resistance & RESIST_MAGIC) != 0 && missileElement == DamageType::Magic) || ((resistance & RESIST_FIRE) != 0 && missileElement == DamageType::Fire) || ((resistance & RESIST_LIGHTNING) != 0 && missileElement == DamageType::Lightning)) return true; if (gbIsHellfire && missileType == MissileID::HolyBolt && IsAnyOf(type().type, MT_DIABLO, MT_BONEDEMN)) return true; return false; } bool Monster::isPlayerMinion() const { return (flags & MFLAG_GOLEM) != 0 && (flags & MFLAG_BERSERK) == 0; } bool Monster::isPossibleToHit() const { return !hasNoLife() && talkMsg == TEXT_NONE && (type().type != MT_ILLWEAV || goal != MonsterGoal::Retreat) && !(IsAnyOf(mode, MonsterMode::Charge, MonsterMode::Death)) && (!IsAnyOf(type().type, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE) || goal == MonsterGoal::Normal); } void Monster::tag(const Player &tagger) { whoHit |= 1 << tagger.getId(); } bool Monster::tryLiftGargoyle() { if (ai == MonsterAIID::Gargoyle && (flags & MFLAG_ALLOW_SPECIAL) != 0) { flags &= ~MFLAG_ALLOW_SPECIAL; mode = MonsterMode::SpecialMeleeAttack; return true; } return false; } MonsterMode Monster::getVisualMonsterMode() const { if (mode != MonsterMode::Petrified) return mode; const size_t monsterId = this->getId(); for (auto &missile : Missiles) { // Search the missile that will restore the original monster mode and use the saved/original monster mode from it if (missile._mitype == MissileID::StoneCurse && static_cast(missile.var2) == monsterId) { return static_cast(missile.var1); } } return MonsterMode::Petrified; } unsigned int Monster::toHit(_difficulty difficulty) const { if (isPlayerMinion()) return golemToHit; unsigned int baseToHit = data().toHit; if (isUnique() && UniqueMonstersData[static_cast(uniqueType)].customToHit != 0) { baseToHit = UniqueMonstersData[static_cast(uniqueType)].customToHit; } if (difficulty == DIFF_NIGHTMARE) { baseToHit += NightmareToHitBonus; } else if (difficulty == DIFF_HELL) { baseToHit += HellToHitBonus; } return baseToHit; } unsigned int Monster::toHitSpecial(_difficulty difficulty) const { unsigned int baseToHitSpecial = data().toHitSpecial; if (isUnique() && UniqueMonstersData[static_cast(uniqueType)].customToHit != 0) { baseToHitSpecial = UniqueMonstersData[static_cast(uniqueType)].customToHit; } if (difficulty == DIFF_NIGHTMARE) { baseToHitSpecial += NightmareToHitBonus; } else if (difficulty == DIFF_HELL) { baseToHitSpecial += HellToHitBonus; } return baseToHitSpecial; } void Monster::occupyTile(Point tile, bool isMoving) const { const auto id = static_cast(this->getId() + 1); dMonster[tile.x][tile.y] = isMoving ? -id : id; } } // namespace devilution ================================================ FILE: Source/monster.h ================================================ /** * @file monster.h * * Interface of monster functionality, AI, actions, spawning, loading, etc. */ #pragma once #include #include #include #include #include #include #include #include "engine/actor_position.hpp" #include "engine/animationinfo.h" #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/sound.h" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "levels/dun_tile.hpp" #include "tables/misdat.h" #include "tables/monstdat.h" #include "tables/spelldat.h" #include "tables/textdat.h" #include "utils/language.h" namespace devilution { struct Missile; struct Player; constexpr size_t MaxMonsters = 200; constexpr size_t MaxLvlMTypes = 24; enum monster_flag : uint16_t { // clang-format off MFLAG_HIDDEN = 1 << 0, MFLAG_LOCK_ANIMATION = 1 << 1, MFLAG_ALLOW_SPECIAL = 1 << 2, MFLAG_TARGETS_MONSTER = 1 << 4, MFLAG_GOLEM = 1 << 5, MFLAG_QUEST_COMPLETE = 1 << 6, MFLAG_KNOCKBACK = 1 << 7, MFLAG_SEARCH = 1 << 8, MFLAG_CAN_OPEN_DOOR = 1 << 9, MFLAG_NO_ENEMY = 1 << 10, MFLAG_BERSERK = 1 << 11, MFLAG_NOLIFESTEAL = 1 << 12, // clang-format on }; /** Indexes from UniqueMonstersData array for special unique monsters (usually quest related) */ enum class UniqueMonsterType : uint8_t { Garbud, SkeletonKing, Zhar, SnotSpill, Lazarus, RedVex, BlackJade, Lachdan, WarlordOfBlood, Butcher, HorkDemon, Defiler, NaKrul, None = static_cast(-1), }; enum class MonsterMode : uint8_t { Stand, /** Movement towards N, NW, or NE */ MoveNorthwards, /** Movement towards S, SW, or SE */ MoveSouthwards, /** Movement towards W or E */ MoveSideways, MeleeAttack, HitRecovery, Death, SpecialMeleeAttack, FadeIn, FadeOut, RangedAttack, SpecialStand, SpecialRangedAttack, Delay, Charge, Petrified, Heal, Talk, }; inline bool IsMonsterModeMove(MonsterMode mode) { switch (mode) { case MonsterMode::MoveNorthwards: case MonsterMode::MoveSouthwards: case MonsterMode::MoveSideways: return true; default: return false; } } enum class MonsterGraphic : uint8_t { Stand, Walk, Attack, GotHit, Death, Special, }; enum class MonsterGoal : uint8_t { None, Normal, Retreat, Healing, Move, Attack, Inquiring, Talking, }; enum placeflag : uint8_t { // clang-format off PLACE_SCATTER = 1 << 0, PLACE_SPECIAL = 1 << 1, PLACE_UNIQUE = 1 << 2, // clang-format on }; /** * @brief Defines the relation of the monster to a monster pack. * If value is different from Individual Monster, the leader must also be set */ enum class LeaderRelation : uint8_t { None, /** * @brief Minion that sticks to the leader */ Leashed, /** * @brief Minion that was separated from the leader and acts individually until it reaches the leader again */ Separated, }; struct AnimStruct { /** * @brief Sprite lists for each of the 8 directions. */ OptionalClxSpriteListOrSheet sprites; [[nodiscard]] OptionalClxSpriteList spritesForDirection(Direction direction) const { if (!sprites) return std::nullopt; return sprites->isSheet() ? (*sprites).sheet()[static_cast(direction)] : (*sprites).list(); } uint16_t width; int8_t frames; int8_t rate; }; enum class MonsterSound : uint8_t { Attack, Hit, Death, Special }; struct MonsterSpritesData { static constexpr size_t MaxAnims = 6; std::unique_ptr data; std::array offsets; }; struct CMonster { std::unique_ptr animData; AnimStruct anims[6]; std::unique_ptr sounds[4][2]; _monster_id type; /** placeflag enum as a flags*/ uint8_t placeFlags; int8_t corpseId = 0; const MonsterData &data() const { return MonstersData[type]; } /** * @brief Returns AnimStruct for specified graphic */ [[nodiscard]] const AnimStruct &getAnimData(MonsterGraphic graphic) const { return anims[static_cast(graphic)]; } }; extern CMonster LevelMonsterTypes[MaxLvlMTypes]; struct Monster { // note: missing field _mAFNum std::unique_ptr uniqueMonsterTRN; /** * @brief Contains information for current animation */ AnimationInfo animInfo; int maxHitPoints; int hitPoints; uint32_t flags; /** Seed used to determine item drops on death */ uint32_t rndItemSeed; /** Seed used to determine AI behaviour/sync sounds in multiplayer games? */ uint32_t aiSeed; uint16_t golemToHit; uint16_t resistance; _speech_id talkMsg; /** @brief Specifies monster's behaviour regarding moving and changing goals. */ int16_t goalVar1; /** * @brief Specifies turning direction for @p RoundWalk in most cases. * Used in custom way by @p FallenAi, @p SnakeAi, @p M_FallenFear and @p FallenAi. */ int8_t goalVar2; /** * @brief Controls monster's behaviour regarding special actions. * Used only by @p ScavengerAi, @p MegaAi and @p GolemAi. */ int8_t goalVar3; int16_t var1; int16_t var2; int8_t var3; ActorPosition position; /** Specifies current goal of the monster */ MonsterGoal goal; /** Usually corresponds to the enemy's future position */ WorldTilePosition enemyPosition; uint8_t levelType; MonsterMode mode; uint8_t pathCount; /** Direction faced by monster (direction enum) */ Direction direction; /** The current target of the monster. An index in to either the player or monster array based on the _meflag value. */ uint8_t enemy; bool isInvalid; MonsterAIID ai; /** * @brief Specifies monster's behaviour across various actions. * Generally, when monster thinks it decides what to do based on this value, among other things. * Higher values should result in more aggressive behaviour (e.g. some monsters use this to calculate the @p AiDelay). */ uint8_t intelligence; /** Stores information for how many ticks the monster will remain active */ uint8_t activeForTicks; UniqueMonsterType uniqueType; uint8_t uniqTrans; int8_t corpseId; int8_t whoHit; uint8_t minDamage; uint8_t maxDamage; uint8_t minDamageSpecial; uint8_t maxDamageSpecial; uint8_t armorClass; uint8_t reducePlayerStrength; uint8_t reducePlayerMagic; uint8_t reducePlayerDexterity; uint8_t reducePlayerVitality; uint8_t reducePlayerMaxHP; uint8_t reducePlayerMaxMana; uint8_t leader; LeaderRelation leaderRelation; uint8_t packSize; int8_t lightId; static constexpr uint8_t NoLeader = -1; /** * @brief Sets the current cell sprite to match the desired desiredDirection and animation sequence * @param graphic Animation sequence of interest * @param desiredDirection Desired desiredDirection the monster should be visually facing */ void changeAnimationData(MonsterGraphic graphic, Direction desiredDirection) { const AnimStruct &animationData = type().getAnimData(graphic); // Passing the frames and rate properties here is only relevant when initialising a monster, but doesn't cause any harm when switching animations. this->animInfo.changeAnimationData(animationData.spritesForDirection(desiredDirection), animationData.frames, animationData.rate); } /** * @brief Sets the current cell sprite to match the desired animation sequence using the direction the monster is currently facing * @param graphic Animation sequence of interest */ void changeAnimationData(MonsterGraphic graphic) { this->changeAnimationData(graphic, this->direction); } /** * @brief Check if the correct stand Animation is loaded. This is needed if direction is changed (monster stands and looks at the player). * @param dir direction of the monster */ void checkStandAnimationIsLoaded(Direction dir); /** * @brief Sets mode to MonsterMode::Petrified */ void petrify(); const CMonster &type() const { return LevelMonsterTypes[levelType]; } const MonsterData &data() const { return type().data(); } /** * @brief Returns monster's name * Internally it returns a name stored in global array of monsters' data. * @return Monster's name */ std::string_view name() const { if (uniqueType != UniqueMonsterType::None) return pgettext("monster", UniqueMonstersData[static_cast(uniqueType)].mName); return pgettext("monster", data().name); } /** * @brief Calculates monster's experience points. * Fetches base exp value from @p MonstersData array. * @param difficulty - difficulty on which calculation is performed * @return Monster's experience points, including bonuses from difficulty and monster being unique */ unsigned int exp(_difficulty difficulty) const { unsigned int monsterExp = data().exp; if (difficulty == DIFF_NIGHTMARE) { monsterExp = 2 * (monsterExp + 1000); } else if (difficulty == DIFF_HELL) { monsterExp = 4 * (monsterExp + 1000); } if (isUnique()) { monsterExp *= 2; } return monsterExp; } /** * @brief Calculates monster's chance to hit with normal attack. * Fetches base value from @p MonstersData array or @p UniqueMonstersData. * @param difficulty - difficulty on which calculation is performed * @return Monster's chance to hit with normal attack, including bonuses from difficulty and monster being unique */ unsigned int toHit(_difficulty difficulty) const; /** * @brief Calculates monster's chance to hit with special attack. * Fetches base value from @p MonstersData array or @p UniqueMonstersData. * @param difficulty - difficulty on which calculation is performed * @return Monster's chance to hit with special attack, including bonuses from difficulty and monster being unique */ unsigned int toHitSpecial(_difficulty difficulty) const; /** * @brief Calculates monster's level. * Fetches base level value from @p MonstersData array or @p UniqueMonstersData. * @param difficulty - difficulty on which calculation is performed * @return Monster's level, including bonuses from difficulty and monster being unique */ unsigned int level(_difficulty difficulty) const { unsigned int baseLevel = data().level; if (isUnique()) { baseLevel = UniqueMonstersData[static_cast(uniqueType)].mlevel; if (baseLevel != 0) { baseLevel *= 2; } else { baseLevel = data().level + 5; } } if (difficulty == DIFF_NIGHTMARE) { baseLevel += 15; } else if (difficulty == DIFF_HELL) { baseLevel += 30; } return baseLevel; } /** * @brief Returns the network identifier for this monster * * This is currently the index into the Monsters array, but may change in the future. */ [[nodiscard]] size_t getId() const; [[nodiscard]] Monster *getLeader() const; void setLeader(const Monster *leader); [[nodiscard]] bool hasLeashedMinions() const { return isUnique() && UniqueMonstersData[static_cast(uniqueType)].monsterPack == UniqueMonsterPack::Leashed; } /** * @brief Calculates the distance in tiles between this monster and its current target * * The distance is not calculated as the euclidean distance, but rather as * the longest number of tiles in the coordinate system. * * @return The distance in tiles */ [[nodiscard]] unsigned distanceToEnemy() const; /** * @brief Is the monster currently walking? */ [[nodiscard]] bool isWalking() const; [[nodiscard]] bool isImmune(MissileID mitype, DamageType missileElement) const; [[nodiscard]] bool isResistant(MissileID mitype, DamageType missileElement) const; /** * Is this a player's golem? */ [[nodiscard]] bool isPlayerMinion() const; bool isPossibleToHit() const; void tag(const Player &tagger); [[nodiscard]] bool isUnique() const { return uniqueType != UniqueMonsterType::None; } bool tryLiftGargoyle(); /** * @brief Gets the visual/shown monster mode. * * When a monster is petrified it's monster mode is changed to MonsterMode::Petrified. * But for graphics and rendering we show the old/real mode. */ [[nodiscard]] MonsterMode getVisualMonsterMode() const; [[nodiscard]] Displacement getRenderingOffset(const ClxSprite sprite) const { Displacement offset = { -CalculateSpriteTileCenterX(sprite.width()), 0 }; if (isWalking()) offset += GetOffsetForWalking(animInfo, direction); return offset; } /** * @brief Sets a tile/dMonster to be occupied by the monster * @param position tile to update * @param isMoving specifies whether the monster is moving or not (true/moving results in a negative index in dMonster) */ void occupyTile(Point tile, bool isMoving) const; bool hasNoLife() const { return hitPoints >> 6 <= 0; } }; extern size_t LevelMonsterTypeCount; extern Monster Monsters[MaxMonsters]; extern unsigned ActiveMonsters[MaxMonsters]; extern size_t ActiveMonsterCount; extern int MonsterKillCounts[NUM_MAX_MTYPES]; extern bool sgbSaveSoundOn; tl::expected PrepareUniqueMonst(Monster &monster, UniqueMonsterType monsterType, size_t miniontype, int bosspacksize, const UniqueMonsterData &uniqueMonsterData); void InitLevelMonsters(); tl::expected GetLevelMTypes(); tl::expected AddMonsterType(_monster_id type, placeflag placeflag); inline tl::expected AddMonsterType(UniqueMonsterType uniqueType, placeflag placeflag) { return AddMonsterType(UniqueMonstersData[static_cast(uniqueType)].mtype, placeflag); } tl::expected InitMonsterSND(CMonster &monsterType); tl::expected InitMonsterGFX(CMonster &monsterType, MonsterSpritesData &&spritesData = {}); tl::expected InitAllMonsterGFX(); void WeakenNaKrul(); void InitGolems(); tl::expected InitMonsters(); tl::expected SetMapMonsters(const uint16_t *dunData, Point startPosition); Monster *AddMonster(Point position, Direction dir, size_t typeIndex, bool inMap); /** * @brief Spawns a new monsters (dynamically/not on level load). * The command is only executed for the level owner, to prevent desyncs in multiplayer. * The level owner sends a CMD_SPAWNMONSTER-message to the other players. */ void SpawnMonster(Point position, Direction dir, size_t typeIndex); /** * @brief Loads data for a dynamically spawned monster when entering a level in multiplayer. */ void LoadDeltaSpawnedMonster(size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel); /** * @brief Initialize a spanwed monster (from a network message or from SpawnMonster-function). */ void InitializeSpawnedMonster(Point position, Direction dir, size_t typeIndex, size_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, int16_t golemSpellLevel); void AddDoppelganger(Monster &monster); void ApplyMonsterDamage(DamageType damageType, Monster &monster, int damage); void MonsterReducePlayerAttribute(Monster &monster, Player &player); bool M_Talker(const Monster &monster); void M_StartStand(Monster &monster, Direction md); void M_ClearSquares(const Monster &monster); void M_GetKnockback(Monster &monster, WorldTilePosition attackerStartPos); void M_StartHit(Monster &monster, int dam); void M_StartHit(Monster &monster, const Player &player, int dam); void StartMonsterDeath(Monster &monster, const Player &player, bool sendmsg); void MonsterDeath(Monster &monster, Direction md, bool sendmsg); void KillGolem(Monster &golem); void M_StartKill(Monster &monster, const Player &player); void M_SyncStartKill(Monster &monster, Point position, const Player &player); void M_UpdateRelations(const Monster &monster); void DoEnding(); void PrepDoEnding(); bool Walk(Monster &monster, Direction md); void GolumAi(Monster &golem); void DeleteMonsterList(); void RemoveEnemyReferences(const Player &player); void ProcessMonsters(); void FreeMonsters(); bool DirOK(const Monster &monster, Direction mdir); bool LineClearMissile(Point startPoint, Point endPoint); /** * @brief Checks for same missile obstructions as CheckMissileCol() for missiles that move along a path between two points */ bool LineClearMovingMissile(Point startPoint, Point endPoint); tl::expected SyncMonsterAnim(Monster &monster); void M_FallenFear(Point position); void PrintMonstHistory(int mt); void PrintUniqueHistory(); void PlayEffect(Monster &monster, MonsterSound mode); void MissToMonst(Missile &missile, Point position); Monster *FindMonsterAtPosition(Point position, bool ignoreMovingMonsters = false); Monster *FindUniqueMonster(UniqueMonsterType monsterType); Monster *FindGolemForPlayer(const Player &player); /** * @brief Check that the given tile is available to the monster */ bool IsTileAvailable(const Monster &monster, Point position); bool IsSkel(_monster_id mt); bool IsGoat(_monster_id mt); /** * @brief Reveals a monster that was hiding in a container * @param monster instance returned from a previous call to PreSpawnSkeleton * @param position tile to try spawn the monster at, neighboring tiles will be used as a fallback */ void ActivateSkeleton(Monster &monster, Point position); Monster *PreSpawnSkeleton(); void TalktoMonster(Player &player, Monster &monster); void SpawnGolem(const Player &player, Point position, uint8_t spellLevel); bool CanTalkToMonst(const Monster &monster); uint8_t encode_enemy(Monster &monster); void decode_enemy(Monster &monster, uint8_t enemyId); } // namespace devilution ================================================ FILE: Source/monsters/validation.cpp ================================================ /** * @file monsters/validation.cpp * * Implementation of functions for validation of monster data. */ #include "monsters/validation.hpp" #include #include "monster.h" #include "player.h" namespace devilution { namespace { bool IsEnemyValid(size_t enemyId, bool checkMonsterTable) { if (enemyId < MaxMonsters) return !checkMonsterTable || Monsters[enemyId].hitPoints > 0; const size_t playerId = enemyId - MaxMonsters; return playerId < Players.size() && Players[playerId].plractive; } } // namespace bool IsEnemyIdValid(size_t enemyId) { return IsEnemyValid(enemyId, false); } bool IsEnemyValid(size_t monsterId, size_t enemyId) { if (monsterId >= MaxMonsters) return false; if (monsterId == enemyId) return false; return IsEnemyValid(enemyId, true); } bool IsMonsterValid(const Monster &monster) { const CMonster &monsterType = LevelMonsterTypes[monster.levelType]; const _monster_id monsterId = monsterType.type; const size_t monsterIndex = static_cast(monsterId); if (monsterIndex >= MonstersData.size()) { return false; } if (monster.isUnique() && !IsUniqueMonsterValid(monster)) { return false; } return true; } bool IsUniqueMonsterValid(const Monster &monster) { assert(monster.isUnique()); const size_t uniqueMonsterIndex = static_cast(monster.uniqueType); if (uniqueMonsterIndex >= UniqueMonstersData.size()) { return false; } const CMonster &monsterType = LevelMonsterTypes[monster.levelType]; const _monster_id monsterId = monsterType.type; const UniqueMonsterData &uniqueMonsterData = UniqueMonstersData.at(uniqueMonsterIndex); if (monsterId != uniqueMonsterData.mtype) { return false; } return true; } } // namespace devilution ================================================ FILE: Source/monsters/validation.hpp ================================================ /** * @file monsters/validation.hpp * * Interface of functions for validation of monster data. */ #pragma once #include namespace devilution { struct Monster; bool IsEnemyIdValid(size_t enemyId); bool IsEnemyValid(size_t monsterId, size_t enemyId); bool IsMonsterValid(const Monster &monster); bool IsUniqueMonsterValid(const Monster &monster); } // namespace devilution ================================================ FILE: Source/movie.cpp ================================================ /** * @file movie.cpp * * Implementation of video playback. */ #include #ifdef USE_SDL3 #include #include #else #include #endif #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "diablo.h" #include "effects.h" #include "engine/backbuffer_state.hpp" #include "engine/demomode.h" #include "engine/events.hpp" #include "engine/sound.h" #include "hwcursor.hpp" #include "storm/storm_svid.h" #include "utils/display.h" #include "utils/sdl_compat.h" namespace devilution { /** Should the movie continue playing. */ bool movie_playing; /** Should the movie play in a loop. */ bool loop_movie; void play_movie(const char *pszMovie, bool userCanClose) { if (demo::IsRunning()) return; movie_playing = true; sound_disable_music(true); stream_stop(); if (IsHardwareCursorEnabled() && ControlDevice == ControlTypes::KeyboardAndMouse) { SetHardwareCursorVisible(false); } if (SVidPlayBegin(pszMovie, loop_movie ? 0x100C0808 : 0x10280808)) { SDL_Event event; uint16_t modState; while (movie_playing) { while (movie_playing && FetchMessage(&event, &modState)) { if (userCanClose) { for (const ControllerButtonEvent ctrlEvent : ToControllerButtonEvents(event)) { if (!SkipsMovie(ctrlEvent)) continue; movie_playing = false; break; } } switch (event.type) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_MOUSE_BUTTON_UP: if (userCanClose || (event.type == SDL_EVENT_KEY_DOWN && SDLC_EventKey(event) == SDLK_ESCAPE)) movie_playing = false; break; #ifdef USE_SDL3 case SDL_EVENT_WINDOW_FOCUS_LOST: if (*GetOptions().Gameplay.pauseOnFocusLoss) diablo_focus_pause(); break; case SDL_EVENT_WINDOW_FOCUS_GAINED: if (*GetOptions().Gameplay.pauseOnFocusLoss) diablo_focus_unpause(); break; #elif !defined(USE_SDL1) case SDL_WINDOWEVENT: if (*GetOptions().Gameplay.pauseOnFocusLoss) { if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) diablo_focus_pause(); else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) diablo_focus_unpause(); } break; #else case SDL_ACTIVEEVENT: if ((event.active.state & SDL_APPINPUTFOCUS) != 0) { if (event.active.gain == 0) diablo_focus_pause(); else diablo_focus_unpause(); } break; #endif case SDL_EVENT_QUIT: SVidPlayEnd(); diablo_quit(0); default: break; } } if (!SVidPlayContinue()) break; } SVidPlayEnd(); } sound_disable_music(false); movie_playing = false; #ifdef USE_SDL3 float x, y; SDL_GetMouseState(&x, &y); MousePosition.x = static_cast(x); MousePosition.y = static_cast(y); #else SDL_GetMouseState(&MousePosition.x, &MousePosition.y); #endif OutputToLogical(&MousePosition.x, &MousePosition.y); InitBackbufferState(); } void PlayInGameMovie(const char *pszMovie) { PaletteFadeOut(8); play_movie(pszMovie, false); ClearScreenBuffer(); RedrawEverything(); scrollrt_draw_game_screen(); PaletteFadeIn(8); RedrawEverything(); } } // namespace devilution ================================================ FILE: Source/movie.h ================================================ /** * @file movie.h * * Interface of video playback. */ #pragma once namespace devilution { extern bool movie_playing; extern bool loop_movie; /** * @brief Start playback of a given video. * @param pszMovie The file name of the video * @param user_can_close Set to false to make the video unskippable. */ void play_movie(const char *pszMovie, bool user_can_close); /** * @brief Fade to black and play a video * @param pszMovie file path of movie */ void PlayInGameMovie(const char *pszMovie); } // namespace devilution ================================================ FILE: Source/mpq/mpq_common.cpp ================================================ #include "mpq/mpq_common.hpp" #include #include namespace devilution { #if !defined(UNPACKED_MPQS) || !defined(UNPACKED_SAVES) MpqFileHash CalculateMpqFileHash(std::string_view filename) { MpqFileHash fileHash; libmpq__file_hash_s(filename.data(), filename.size(), &fileHash[0], &fileHash[1], &fileHash[2]); return fileHash; } #endif } // namespace devilution ================================================ FILE: Source/mpq/mpq_common.hpp ================================================ #pragma once #include #include #include #include #include "utils/endian_read.hpp" namespace devilution { constexpr size_t MaxMpqPathSize = 256; #pragma pack(push, 1) struct MpqFileHeader { static constexpr uint32_t DiabloSignature = LoadLE32("MPQ\x1A"); static constexpr uint32_t DiabloSize = 32; // The signature, always 0x1A51504D ('MPQ\x1A') for Diablo MPQs. uint32_t signature; // The size of this header in bytes, always 32 for Diablo MPQs. uint32_t headerSize; // The size of the MPQ file in bytes. uint32_t fileSize; // Version, always 0 for Diablo MPQs. uint16_t version; // Block size is `512 * 2 ^ blockSizeFactor`. // e.g. if `blockSizeFactor` is 3, the block size is 4096 (512 << 3). uint16_t blockSizeFactor; // Location of the hash entries table. uint32_t hashEntriesOffset; // Location of the block entries table. uint32_t blockEntriesOffset; // Size of the hash entries table (number of entries). uint32_t hashEntriesCount; // Size of the block entries table (number of entries). uint32_t blockEntriesCount; // Empty space after the header. Not included into `headerSize`. uint8_t pad[72]; }; struct MpqHashEntry { // Special values for the `block` field. // Does not point to a block (unassigned hash entry) static constexpr uint32_t NullBlock = -1; // Used to point to a block but is now deleted (can be reclaimed) static constexpr uint32_t DeletedBlock = -2; // `hashA` and `hashB` are used for resolving hash index collisions. uint32_t hashA; uint32_t hashB; // Always `0` in Diablo. uint16_t locale; // Always `0` in Diablo. uint16_t platform; // Index of the first block in the block entries table, or // -1 for an unused entry, -2 for a deleted entry. uint32_t block; }; struct MpqBlockEntry { static constexpr uint32_t FlagExists = 0x80000000; static constexpr uint32_t CompressPkZip = 0x00000100; // Offset to the start of this block. uint32_t offset; // Size in the MPQ. uint32_t packedSize; // Uncompressed size. uint32_t unpackedSize; // Flags indicating compression type, encryption, etc. uint32_t flags; }; #pragma pack(pop) using MpqFileHash = std::array; #if !defined(UNPACKED_MPQS) || !defined(UNPACKED_SAVES) MpqFileHash CalculateMpqFileHash(std::string_view filename); #endif } // namespace devilution ================================================ FILE: Source/mpq/mpq_reader.cpp ================================================ #include "mpq/mpq_reader.hpp" #include #include #include #include #include namespace devilution { std::optional MpqArchive::Open(const char *path, int32_t &error) { mpq_archive_s *archive; error = libmpq__archive_open(&archive, path, -1); if (error != 0) { if (error == LIBMPQ_ERROR_EXIST) error = 0; return std::nullopt; } return MpqArchive { std::string(path), archive }; } std::optional MpqArchive::Clone(int32_t &error) { mpq_archive_s *copy; error = libmpq__archive_dup(archive_, path_.c_str(), ©); if (error != 0) return std::nullopt; return MpqArchive { path_, copy }; } const char *MpqArchive::ErrorMessage(int32_t errorCode) { return libmpq__strerror(errorCode); } MpqArchive &MpqArchive::operator=(MpqArchive &&other) noexcept { path_ = std::move(other.path_); if (archive_ != nullptr) libmpq__archive_close(archive_); archive_ = other.archive_; other.archive_ = nullptr; tmp_buf_ = std::move(other.tmp_buf_); return *this; } MpqArchive::~MpqArchive() { if (archive_ != nullptr) libmpq__archive_close(archive_); } bool MpqArchive::GetFileNumber(MpqFileHash fileHash, uint32_t &fileNumber) { return libmpq__file_number_from_hash(archive_, fileHash[0], fileHash[1], fileHash[2], &fileNumber) == 0; } std::unique_ptr MpqArchive::ReadFile(std::string_view filename, std::size_t &fileSize, int32_t &error) { std::unique_ptr result; std::uint32_t fileNumber; error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); if (error != 0) return result; libmpq__off_t unpackedSize; error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); if (error != 0) return result; error = OpenBlockOffsetTable(fileNumber, filename); if (error != 0) return result; result = std::make_unique(static_cast(unpackedSize)); const std::size_t blockSize = GetBlockSize(fileNumber, 0, error); if (error != 0) return result; std::vector &tmp = GetTemporaryBuffer(blockSize); if (error != 0) return result; error = libmpq__file_read_with_filename_and_temporary_buffer_s( archive_, fileNumber, filename.data(), filename.size(), reinterpret_cast(result.get()), unpackedSize, tmp.data(), static_cast(blockSize), nullptr); if (error != 0) { result = nullptr; CloseBlockOffsetTable(fileNumber); return result; } CloseBlockOffsetTable(fileNumber); fileSize = static_cast(unpackedSize); return result; } int32_t MpqArchive::ReadBlock(uint32_t fileNumber, uint32_t blockNumber, uint8_t *out, size_t outSize) { std::vector &tmpBuf = GetTemporaryBuffer(outSize); return libmpq__block_read_with_temporary_buffer( archive_, fileNumber, blockNumber, out, static_cast(outSize), tmpBuf.data(), outSize, /*transferred=*/nullptr); } std::size_t MpqArchive::GetUnpackedFileSize(uint32_t fileNumber, int32_t &error) { libmpq__off_t unpackedSize; error = libmpq__file_size_unpacked(archive_, fileNumber, &unpackedSize); return static_cast(unpackedSize); } uint32_t MpqArchive::GetNumBlocks(uint32_t fileNumber, int32_t &error) { uint32_t numBlocks; error = libmpq__file_blocks(archive_, fileNumber, &numBlocks); return numBlocks; } int32_t MpqArchive::OpenBlockOffsetTable(uint32_t fileNumber, std::string_view filename) { return libmpq__block_open_offset_with_filename_s(archive_, fileNumber, filename.data(), filename.size()); } int32_t MpqArchive::CloseBlockOffsetTable(uint32_t fileNumber) { return libmpq__block_close_offset(archive_, fileNumber); } // Requires the block offset table to be open std::size_t MpqArchive::GetBlockSize(uint32_t fileNumber, uint32_t blockNumber, int32_t &error) { libmpq__off_t blockSize; error = libmpq__block_size_unpacked(archive_, fileNumber, blockNumber, &blockSize); return static_cast(blockSize); } bool MpqArchive::HasFile(std::string_view filename) const { std::uint32_t fileNumber; const int32_t error = libmpq__file_number_s(archive_, filename.data(), filename.size(), &fileNumber); return error == 0; } } // namespace devilution ================================================ FILE: Source/mpq/mpq_reader.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #include "mpq/mpq_common.hpp" // Forward-declare so that we can avoid exposing libmpq. struct mpq_archive; using mpq_archive_s = struct mpq_archive; namespace devilution { class MpqArchive { public: // If the file does not exist, returns nullopt without an error. static std::optional Open(const char *path, int32_t &error); std::optional Clone(int32_t &error); static const char *ErrorMessage(int32_t errorCode); MpqArchive(MpqArchive &&other) noexcept : path_(std::move(other.path_)) , archive_(other.archive_) , tmp_buf_(std::move(other.tmp_buf_)) { other.archive_ = nullptr; } MpqArchive &operator=(MpqArchive &&other) noexcept; ~MpqArchive(); // Returns false if the file does not exit. bool GetFileNumber(MpqFileHash fileHash, uint32_t &fileNumber); std::unique_ptr ReadFile(std::string_view filename, std::size_t &fileSize, int32_t &error); // Returns error code. int32_t ReadBlock(uint32_t fileNumber, uint32_t blockNumber, uint8_t *out, size_t outSize); std::size_t GetUnpackedFileSize(uint32_t fileNumber, int32_t &error); uint32_t GetNumBlocks(uint32_t fileNumber, int32_t &error); int32_t OpenBlockOffsetTable(uint32_t fileNumber, std::string_view filename); int32_t CloseBlockOffsetTable(uint32_t fileNumber); // Requires the block offset table to be open std::size_t GetBlockSize(uint32_t fileNumber, uint32_t blockNumber, int32_t &error); bool HasFile(std::string_view filename) const; private: MpqArchive(std::string path, mpq_archive_s *archive) : path_(std::move(path)) , archive_(archive) { } std::vector &GetTemporaryBuffer(std::size_t size) { if (tmp_buf_.size() < size) tmp_buf_.resize(size); return tmp_buf_; } std::string path_; mpq_archive_s *archive_; std::vector tmp_buf_; }; } // namespace devilution ================================================ FILE: Source/mpq/mpq_sdl_rwops.cpp ================================================ #include "mpq/mpq_sdl_rwops.hpp" #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #include "utils/sdl_compat.h" #endif namespace devilution { namespace { struct Data { // File information: std::optional ownedArchive; MpqArchive *mpqArchive; uint32_t fileNumber; size_t blockSize; size_t lastBlockSize; uint32_t numBlocks; size_t size; // State: size_t position; bool blockRead; std::unique_ptr blockData; }; #ifdef USE_SDL3 Data *GetData(void *userdata) { return reinterpret_cast(userdata); } #else Data *GetData(struct SDL_RWops *context) { return reinterpret_cast(context->hidden.unknown.data1); } void SetData(struct SDL_RWops *context, Data *data) { context->hidden.unknown.data1 = data; } #endif #ifndef USE_SDL1 using OffsetType = Sint64; using SizeType = size_t; #else using OffsetType = int; using SizeType = int; #endif extern "C" { #ifndef USE_SDL1 static Sint64 MpqFileRwSize( #ifdef USE_SDL3 void * #else struct SDL_RWops * #endif context) { return static_cast(GetData(context)->size); } #endif #ifdef USE_SDL3 static Sint64 MpqFileRwSeek(void *context, Sint64 offset, SDL_IOWhence whence) #else static OffsetType MpqFileRwSeek(struct SDL_RWops *context, OffsetType offset, int whence) #endif { Data &data = *GetData(context); OffsetType newPosition; switch (whence) { case SDL_IO_SEEK_SET: newPosition = offset; break; case SDL_IO_SEEK_CUR: newPosition = static_cast(data.position + offset); break; case SDL_IO_SEEK_END: newPosition = static_cast(data.size + offset); break; default: return -1; } if (newPosition == static_cast(data.position)) return newPosition; if (newPosition > static_cast(data.size)) { SDL_SetError("MpqFileRwSeek beyond EOF (%d > %u)", static_cast(newPosition), static_cast(data.size)); return -1; } if (newPosition < 0) { SDL_SetError("MpqFileRwSeek beyond BOF (%d < 0)", static_cast(newPosition)); return -1; } if (data.position / data.blockSize != static_cast(newPosition) / data.blockSize) data.blockRead = false; data.position = static_cast(newPosition); return newPosition; } #ifdef USE_SDL3 static SizeType MpqFileRwRead(void *context, void *ptr, size_t size, SDL_IOStatus *status) #else static SizeType MpqFileRwRead(struct SDL_RWops *context, void *ptr, SizeType size, SizeType maxnum) #endif { #ifdef USE_SDL3 const size_t maxnum = 1; #endif Data &data = *GetData(context); const size_t totalSize = size * maxnum; size_t remainingSize = totalSize; auto *out = static_cast(ptr); if (data.blockData == nullptr) { data.blockData = std::unique_ptr { new uint8_t[data.blockSize] }; } auto blockNumber = static_cast(data.position / data.blockSize); while (remainingSize > 0) { if (data.position == data.size) { #ifdef USE_SDL3 *status = SDL_IO_STATUS_EOF; #endif break; } const size_t currentBlockSize = blockNumber + 1 == data.numBlocks ? data.lastBlockSize : data.blockSize; if (!data.blockRead) { const int32_t error = data.mpqArchive->ReadBlock(data.fileNumber, blockNumber, data.blockData.get(), currentBlockSize); if (error != 0) { SDL_SetError("MpqFileRwRead ReadBlock: %s", MpqArchive::ErrorMessage(error)); return 0; } data.blockRead = true; } const size_t blockPosition = data.position - (blockNumber * data.blockSize); const size_t remainingBlockSize = currentBlockSize - blockPosition; if (remainingSize < remainingBlockSize) { std::memcpy(out, data.blockData.get() + blockPosition, remainingSize); data.position += remainingSize; #ifdef USE_SDL3 return size; #else return maxnum; #endif } std::memcpy(out, data.blockData.get() + blockPosition, remainingBlockSize); out += remainingBlockSize; data.position += remainingBlockSize; remainingSize -= remainingBlockSize; ++blockNumber; data.blockRead = false; } #ifdef USE_SDL3 return static_cast(totalSize - remainingSize); #else return static_cast((totalSize - remainingSize) / size); #endif } #ifdef USE_SDL3 static bool MpqFileRwClose(void *context) #else static int MpqFileRwClose(struct SDL_RWops *context) #endif { Data *data = GetData(context); data->mpqArchive->CloseBlockOffsetTable(data->fileNumber); delete data; #ifdef USE_SDL3 return true; #else delete context; return 0; #endif } } // extern "C" } // namespace SDL_IOStream *SDL_RWops_FromMpqFile(MpqArchive &mpqArchive, uint32_t fileNumber, std::string_view filename, bool threadsafe) { #ifdef USE_SDL3 SDL_IOStreamInterface interface; SDL_INIT_INTERFACE(&interface); SDL_IOStreamInterface *result = &interface; #else auto result = std::make_unique(); std::memset(result.get(), 0, sizeof(*result)); #endif #ifndef USE_SDL1 result->size = &MpqFileRwSize; #ifndef USE_SDL3 result->type = SDL_RWOPS_UNKNOWN; #endif #else result->type = 0; #endif result->seek = &MpqFileRwSeek; result->read = &MpqFileRwRead; result->write = nullptr; result->close = &MpqFileRwClose; #ifdef USE_SDL3 result->flush = nullptr; #endif auto data = std::make_unique(); int32_t error = 0; if (threadsafe) { data->ownedArchive = mpqArchive.Clone(error); if (error != 0) { SDL_SetError("MpqFileRwRead Clone: %s", MpqArchive::ErrorMessage(error)); return nullptr; } data->mpqArchive = &*data->ownedArchive; } else { data->mpqArchive = &mpqArchive; } data->fileNumber = fileNumber; MpqArchive &archive = *data->mpqArchive; error = archive.OpenBlockOffsetTable(fileNumber, filename); if (error != 0) { SDL_SetError("MpqFileRwRead OpenBlockOffsetTable: %s", MpqArchive::ErrorMessage(error)); return nullptr; } data->size = archive.GetUnpackedFileSize(fileNumber, error); if (error != 0) { SDL_SetError("MpqFileRwRead GetUnpackedFileSize: %s", MpqArchive::ErrorMessage(error)); return nullptr; } const std::uint32_t numBlocks = archive.GetNumBlocks(fileNumber, error); if (error != 0) { SDL_SetError("MpqFileRwRead GetNumBlocks: %s", MpqArchive::ErrorMessage(error)); return nullptr; } data->numBlocks = numBlocks; const size_t blockSize = archive.GetBlockSize(fileNumber, 0, error); if (error != 0) { SDL_SetError("MpqFileRwRead GetBlockSize: %s", MpqArchive::ErrorMessage(error)); return nullptr; } data->blockSize = blockSize; if (numBlocks > 1) { data->lastBlockSize = archive.GetBlockSize(fileNumber, numBlocks - 1, error); if (error != 0) { SDL_SetError("MpqFileRwRead GetBlockSize: %s", MpqArchive::ErrorMessage(error)); return nullptr; } } else { data->lastBlockSize = blockSize; } data->position = 0; data->blockRead = false; #ifdef USE_SDL3 return SDL_OpenIO(&interface, data.release()); #else SetData(result.get(), data.release()); return result.release(); #endif } } // namespace devilution ================================================ FILE: Source/mpq/mpq_sdl_rwops.hpp ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #else #include #include "utils/sdl_compat.h" #endif #include "mpq/mpq_reader.hpp" namespace devilution { SDL_IOStream *SDL_RWops_FromMpqFile(MpqArchive &mpqArchive, uint32_t fileNumber, std::string_view filename, bool threadsafe); } // namespace devilution ================================================ FILE: Source/mpq/mpq_writer.cpp ================================================ #include "mpq/mpq_writer.hpp" #include #include #include #include #include #include #include "appfat.h" #include "encrypt.h" #include "utils/endian_swap.hpp" #include "utils/file_util.h" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { // Validates that a Type is of a particular size and that its alignment is <= the size of the type. // Done with templates so that error messages include actual size. template struct AssertEq : std::true_type { static_assert(A == B, "A == B not satisfied"); }; template struct AssertLte : std::true_type { static_assert(A <= B, "A <= B not satisfied"); }; template struct CheckSize : AssertEq, AssertLte { }; // Check sizes and alignments of the structs that we decrypt and encrypt. // The decryption algorithm treats them as a stream of 32-bit uints, so the // sizes must be exact as there cannot be any padding. static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqHashEntry) == 4 * 4 && alignof(MpqHashEntry) <= 4 * 4 not satisfied"); static_assert(CheckSize(4 * 4)>::value, "sizeof(MpqBlockEntry) == 4 * 4 && alignof(MpqBlockEntry) <= 4 * 4 not satisfied"); // We use fixed size block and hash entry tables. constexpr uint32_t HashEntriesCount = 2048; constexpr uint32_t BlockEntriesCount = 2048; constexpr uint32_t BlockEntrySize = HashEntriesCount * sizeof(MpqBlockEntry); constexpr uint32_t HashEntrySize = BlockEntriesCount * sizeof(MpqHashEntry); // We store the block and the hash entry tables immediately after the header. // This is unlike most other MPQ archives, that store these at the end of the file. constexpr long MpqBlockEntryOffset = sizeof(MpqFileHeader); constexpr long MpqHashEntryOffset = MpqBlockEntryOffset + BlockEntrySize; // Special return value for `GetHashIndex` and `GetHandle`. constexpr uint32_t HashEntryNotFound = -1; // We use 4096-byte blocks, generally. constexpr uint16_t BlockSizeFactor = 3; constexpr uint32_t BlockSize = 512 << BlockSizeFactor; // 4096 // Sometimes we can end up with smaller blocks. constexpr uint32_t MinBlockSize = 1024; void ByteSwapHdr(MpqFileHeader *hdr) { hdr->signature = Swap32LE(hdr->signature); hdr->headerSize = Swap32LE(hdr->headerSize); hdr->fileSize = Swap32LE(hdr->fileSize); hdr->version = Swap16LE(hdr->version); hdr->blockSizeFactor = Swap16LE(hdr->blockSizeFactor); hdr->hashEntriesOffset = Swap32LE(hdr->hashEntriesOffset); hdr->blockEntriesOffset = Swap32LE(hdr->blockEntriesOffset); hdr->hashEntriesCount = Swap32LE(hdr->hashEntriesCount); hdr->blockEntriesCount = Swap32LE(hdr->blockEntriesCount); } bool IsAllocatedUnusedBlock(const MpqBlockEntry *block) { return block->offset != 0 && block->flags == 0 && block->unpackedSize == 0; } bool IsUnallocatedBlock(const MpqBlockEntry *block) { return block->offset == 0 && block->packedSize == 0 && block->unpackedSize == 0 && block->flags == 0; } } // namespace MpqWriter::MpqWriter(const char *path) { const std::string dir = std::string(Dirname(path)); if (!dir.empty()) { RecursivelyCreateDir(dir.c_str()); } LogVerbose("Opening {}", path); bool isNewFile = false; std::string error; if (!FileExists(path)) { // FileExists() may return false in the case of an error // so we use "ab" instead of "wb" to avoid accidentally // truncating an existing file stream_.Open(path, "ab"); // However, we cannot actually use a file handle that was // opened in "ab" mode because we need to be able to seek // and write to the middle of the file stream_.Close(); } std::uintmax_t fileSize; if (!GetFileSize(path, &fileSize)) { error = R"(GetFileSize failed: "{}")"; LogError(error, path, std::strerror(errno)); goto on_error; } size_ = static_cast(fileSize); isNewFile = size_ == 0; LogVerbose("GetFileSize(\"{}\") = {}", path, size_); if (!stream_.Open(path, "r+b")) { stream_.Close(); error = "Failed to open file"; goto on_error; } name_ = path; if (blockTable_ == nullptr || hashTable_ == nullptr) { MpqFileHeader fhdr; if (isNewFile) { InitDefaultMpqHeader(&fhdr); } else if (!ReadMPQHeader(&fhdr)) { error = "Failed to read MPQ header"; goto on_error; } blockTable_ = std::make_unique(BlockEntriesCount); std::memset(blockTable_.get(), 0, BlockEntriesCount * sizeof(MpqBlockEntry)); if (fhdr.blockEntriesCount > 0) { if (!stream_.Read(reinterpret_cast(blockTable_.get()), static_cast(fhdr.blockEntriesCount * sizeof(MpqBlockEntry)))) { error = "Failed to read block table"; goto on_error; } libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), fhdr.blockEntriesCount * sizeof(MpqBlockEntry), LIBMPQ_BLOCK_TABLE_HASH_KEY); } hashTable_ = std::make_unique(HashEntriesCount); // We fill with 0xFF so that the `block` field defaults to -1 (a null block pointer). std::memset(hashTable_.get(), 0xFF, HashEntriesCount * sizeof(MpqHashEntry)); if (fhdr.hashEntriesCount > 0) { if (!stream_.Read(reinterpret_cast(hashTable_.get()), static_cast(fhdr.hashEntriesCount * sizeof(MpqHashEntry)))) { error = "Failed to read hash entries"; goto on_error; } libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), fhdr.hashEntriesCount * sizeof(MpqHashEntry), LIBMPQ_HASH_TABLE_HASH_KEY); } #ifndef CAN_SEEKP_BEYOND_EOF if (!stream_.Seekp(0, SEEK_SET)) goto on_error; // Memorize stream begin, we'll need it for calculations later. if (!stream_.Tellp(&streamBegin_)) goto on_error; // Write garbage header and tables because some platforms cannot `Seekp` beyond EOF. // The data is incorrect at this point, it will be overwritten on Close. if (isNewFile) WriteHeaderAndTables(); #endif } return; on_error: app_fatal(StrCat(_("Failed to open archive for writing."), "\n", path, "\n", error)); } MpqWriter::~MpqWriter() { if (!stream_.IsOpen()) return; LogVerbose("Closing {}", name_); bool result = true; if (!(stream_.Seekp(0, SEEK_SET) && WriteHeaderAndTables())) result = false; stream_.Close(); if (result && size_ != 0) { LogVerbose("ResizeFile(\"{}\", {})", name_, size_); result = ResizeFile(name_.c_str(), size_); } if (!result) LogVerbose("Closing failed {}", name_); } uint32_t MpqWriter::FetchHandle(std::string_view filename) const { return GetHashIndex(CalculateMpqFileHash(filename)); } void MpqWriter::InitDefaultMpqHeader(MpqFileHeader *hdr) { std::memset(hdr, 0, sizeof(*hdr)); hdr->signature = MpqFileHeader::DiabloSignature; hdr->headerSize = MpqFileHeader::DiabloSize; hdr->blockSizeFactor = BlockSizeFactor; hdr->version = 0; size_ = MpqHashEntryOffset + HashEntrySize; } bool MpqWriter::IsValidMpqHeader(MpqFileHeader *hdr) const { return hdr->signature == MpqFileHeader::DiabloSignature && hdr->headerSize == MpqFileHeader::DiabloSize && hdr->version <= 0 && hdr->blockSizeFactor == BlockSizeFactor && hdr->fileSize == size_ && hdr->hashEntriesOffset == MpqHashEntryOffset && hdr->blockEntriesOffset == sizeof(MpqFileHeader) && hdr->hashEntriesCount == HashEntriesCount && hdr->blockEntriesCount == BlockEntriesCount; } bool MpqWriter::ReadMPQHeader(MpqFileHeader *hdr) { const bool hasHdr = size_ >= sizeof(*hdr); if (hasHdr) { if (!stream_.Read(reinterpret_cast(hdr), sizeof(*hdr))) return false; ByteSwapHdr(hdr); } if (!hasHdr || !IsValidMpqHeader(hdr)) { InitDefaultMpqHeader(hdr); } return true; } MpqBlockEntry *MpqWriter::NewBlock(uint32_t *blockIndex) { MpqBlockEntry *blockEntry = blockTable_.get(); for (unsigned i = 0; i < BlockEntriesCount; ++i, ++blockEntry) { if (!IsUnallocatedBlock(blockEntry)) continue; if (blockIndex != nullptr) *blockIndex = i; return blockEntry; } app_fatal("Out of free block entries"); } void MpqWriter::AllocBlock(uint32_t blockOffset, uint32_t blockSize) { MpqBlockEntry *block; bool expand; do { block = blockTable_.get(); expand = false; for (unsigned i = BlockEntriesCount; i-- != 0; ++block) { // Expand to adjacent blocks. if (!IsAllocatedUnusedBlock(block)) continue; if (block->offset + block->packedSize == blockOffset) { blockOffset = block->offset; blockSize += block->packedSize; memset(block, 0, sizeof(MpqBlockEntry)); expand = true; break; } if (blockOffset + blockSize == block->offset) { blockSize += block->packedSize; memset(block, 0, sizeof(MpqBlockEntry)); expand = true; break; } } } while (expand); if (blockOffset + blockSize > size_) { // Expanded beyond EOF, this should never happen. app_fatal("MPQ free list error"); } if (blockOffset + blockSize == size_) { size_ = blockOffset; } else { block = NewBlock(); block->offset = blockOffset; block->packedSize = blockSize; block->unpackedSize = 0; block->flags = 0; } } uint32_t MpqWriter::FindFreeBlock(uint32_t size) { uint32_t result; MpqBlockEntry *block = blockTable_.get(); for (unsigned i = 0; i < BlockEntriesCount; ++i, ++block) { // Find a block entry to use space from. if (!IsAllocatedUnusedBlock(block) || block->packedSize < size) continue; result = block->offset; block->offset += size; block->packedSize -= size; // Clear the block entry if we used its entire capacity. if (block->packedSize == 0) memset(block, 0, sizeof(*block)); return result; } result = size_; size_ += size; return result; } uint32_t MpqWriter::GetHashIndex(MpqFileHash fileHash) const // NOLINT(bugprone-easily-swappable-parameters) { uint32_t i = HashEntriesCount; for (unsigned idx = fileHash[0] & 0x7FF; hashTable_[idx].block != MpqHashEntry::NullBlock; idx = (idx + 1) & 0x7FF) { if (i-- == 0) break; if (hashTable_[idx].hashA != fileHash[1]) continue; if (hashTable_[idx].hashB != fileHash[2]) continue; if (hashTable_[idx].block == MpqHashEntry::DeletedBlock) continue; return idx; } return HashEntryNotFound; } bool MpqWriter::WriteHeaderAndTables() { return WriteHeader() && WriteBlockTable() && WriteHashTable(); } MpqBlockEntry *MpqWriter::AddFile(std::string_view filename, MpqBlockEntry *block, uint32_t blockIndex) { const MpqFileHash fileHash = CalculateMpqFileHash(filename); if (GetHashIndex(fileHash) != HashEntryNotFound) app_fatal(StrCat("Hash collision between \"", filename, "\" and existing file\n")); unsigned int hIdx = fileHash[0] & 0x7FF; bool hasSpace = false; for (unsigned i = 0; i < HashEntriesCount; ++i) { if (hashTable_[hIdx].block == MpqHashEntry::NullBlock || hashTable_[hIdx].block == MpqHashEntry::DeletedBlock) { hasSpace = true; break; } hIdx = (hIdx + 1) & 0x7FF; } if (!hasSpace) app_fatal("Out of hash space"); if (block == nullptr) block = NewBlock(&blockIndex); MpqHashEntry &entry = hashTable_[hIdx]; entry.hashA = fileHash[1]; entry.hashB = fileHash[2]; entry.locale = 0; entry.platform = 0; entry.block = blockIndex; return block; } bool MpqWriter::WriteFileContents(const std::byte *fileData, uint32_t fileSize, MpqBlockEntry *block) { const uint32_t numSectors = (fileSize + (BlockSize - 1)) / BlockSize; const uint32_t offsetTableByteSize = sizeof(uint32_t) * (numSectors + 1); block->offset = FindFreeBlock(fileSize + offsetTableByteSize); // `packedSize` is reduced at the end of the function if it turns out to be smaller. block->packedSize = fileSize + offsetTableByteSize; block->unpackedSize = fileSize; block->flags = MpqBlockEntry::FlagExists | MpqBlockEntry::CompressPkZip; // We populate the table of sector offsets while we write the data. // We can't pre-populate it because we don't know the compressed sector sizes yet. // First offset is the start of the first sector, last offset is the end of the last sector. const std::unique_ptr offsetTable { new uint32_t[numSectors + 1] }; #ifdef CAN_SEEKP_BEYOND_EOF if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) return false; #else // Ensure we do not Seekp beyond EOF by filling the missing space. long stream_end; if (!stream_.Seekp(0, SEEK_END) || !stream_.Tellp(&stream_end)) return false; const std::uintmax_t cur_size = stream_end - streamBegin_; if (cur_size < block->offset + offsetTableByteSize) { if (cur_size < block->offset) { std::unique_ptr filler { new char[block->offset - cur_size] }; if (!stream_.Write(filler.get(), block->offset - cur_size)) return false; } if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) return false; } else { if (!stream_.Seekp(block->offset + offsetTableByteSize, SEEK_SET)) return false; } #endif uint32_t destSize = offsetTableByteSize; std::byte mpqBuf[BlockSize]; size_t curSector = 0; while (true) { uint32_t len = std::min(fileSize, BlockSize); memcpy(mpqBuf, fileData, len); fileData += len; len = PkwareCompress(mpqBuf, len); if (!stream_.Write(reinterpret_cast(&mpqBuf[0]), len)) return false; offsetTable[curSector++] = Swap32LE(destSize); destSize += len; // compressed length if (fileSize <= BlockSize) break; fileSize -= BlockSize; } offsetTable[numSectors] = Swap32LE(destSize); if (!stream_.Seekp(block->offset, SEEK_SET)) return false; if (!stream_.Write(reinterpret_cast(offsetTable.get()), offsetTableByteSize)) return false; if (!stream_.Seekp(destSize - offsetTableByteSize, SEEK_CUR)) return false; if (destSize < block->packedSize) { const uint32_t remainingBlockSize = block->packedSize - destSize; if (remainingBlockSize >= MinBlockSize) { // Allocate another block if we didn't use all of this one. block->packedSize = destSize; AllocBlock(block->packedSize + block->offset, remainingBlockSize); } } return true; } bool MpqWriter::WriteHeader() { MpqFileHeader fhdr; memset(&fhdr, 0, sizeof(fhdr)); fhdr.signature = MpqFileHeader::DiabloSignature; fhdr.headerSize = MpqFileHeader::DiabloSize; fhdr.fileSize = size_; fhdr.version = 0; fhdr.blockSizeFactor = BlockSizeFactor; fhdr.hashEntriesOffset = MpqHashEntryOffset; fhdr.blockEntriesOffset = MpqBlockEntryOffset; fhdr.hashEntriesCount = HashEntriesCount; fhdr.blockEntriesCount = BlockEntriesCount; ByteSwapHdr(&fhdr); return stream_.Write(reinterpret_cast(&fhdr), sizeof(fhdr)); } bool MpqWriter::WriteBlockTable() { libmpq__encrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); const bool success = stream_.Write(reinterpret_cast(blockTable_.get()), BlockEntrySize); libmpq__decrypt_block(reinterpret_cast(blockTable_.get()), BlockEntrySize, LIBMPQ_BLOCK_TABLE_HASH_KEY); return success; } bool MpqWriter::WriteHashTable() { libmpq__encrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); const bool success = stream_.Write(reinterpret_cast(hashTable_.get()), HashEntrySize); libmpq__decrypt_block(reinterpret_cast(hashTable_.get()), HashEntrySize, LIBMPQ_HASH_TABLE_HASH_KEY); return success; } void MpqWriter::RemoveHashEntry(std::string_view filename) { const uint32_t hIdx = FetchHandle(filename); if (hIdx == HashEntryNotFound) { return; } MpqHashEntry *hashEntry = &hashTable_[hIdx]; MpqBlockEntry *block = &blockTable_[hashEntry->block]; hashEntry->block = MpqHashEntry::DeletedBlock; const uint32_t blockOffset = block->offset; const uint32_t blockSize = block->packedSize; memset(block, 0, sizeof(*block)); AllocBlock(blockOffset, blockSize); } void MpqWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) { char pszFileName[MaxMpqPathSize]; for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { RemoveHashEntry(pszFileName); } } bool MpqWriter::WriteFile(std::string_view filename, const std::byte *data, size_t size) { MpqBlockEntry *blockEntry; RemoveHashEntry(filename); blockEntry = AddFile(filename, nullptr, 0); if (!WriteFileContents(data, static_cast(size), blockEntry)) { RemoveHashEntry(filename); return false; } return true; } void MpqWriter::RenameFile(std::string_view name, std::string_view newName) // NOLINT(bugprone-easily-swappable-parameters) { const uint32_t index = FetchHandle(name); if (index == HashEntryNotFound) { return; } MpqHashEntry *hashEntry = &hashTable_[index]; const uint32_t block = hashEntry->block; MpqBlockEntry *blockEntry = &blockTable_[block]; hashEntry->block = MpqHashEntry::DeletedBlock; AddFile(newName, blockEntry, block); } bool MpqWriter::HasFile(std::string_view name) const { return FetchHandle(name) != HashEntryNotFound; } } // namespace devilution ================================================ FILE: Source/mpq/mpq_writer.hpp ================================================ /** * @file mpq/mpq_writer.hpp * * Interface of functions for creating and editing MPQ files. */ #pragma once #include #include #include #include "mpq/mpq_common.hpp" #include "utils/logged_fstream.hpp" namespace devilution { class MpqWriter { public: explicit MpqWriter(const char *path); explicit MpqWriter(const std::string &path) : MpqWriter(path.c_str()) { } MpqWriter(MpqWriter &&other) = default; MpqWriter &operator=(MpqWriter &&other) = default; ~MpqWriter(); bool HasFile(std::string_view name) const; void RemoveHashEntry(std::string_view filename); void RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)); bool WriteFile(std::string_view filename, const std::byte *data, size_t size); void RenameFile(std::string_view name, std::string_view newName); private: bool IsValidMpqHeader(MpqFileHeader *hdr) const; uint32_t GetHashIndex(MpqFileHash fileHash) const; uint32_t FetchHandle(std::string_view filename) const; bool ReadMPQHeader(MpqFileHeader *hdr); MpqBlockEntry *AddFile(std::string_view filename, MpqBlockEntry *block, uint32_t blockIndex); bool WriteFileContents(const std::byte *fileData, uint32_t fileSize, MpqBlockEntry *block); // Returns an unused entry in the block entry table. MpqBlockEntry *NewBlock(uint32_t *blockIndex = nullptr); // Marks space at `blockOffset` of size `blockSize` as free (unused) space. void AllocBlock(uint32_t blockOffset, uint32_t blockSize); // Returns the file offset that is followed by empty space of at least the given size. uint32_t FindFreeBlock(uint32_t size); bool WriteHeaderAndTables(); bool WriteHeader(); bool WriteBlockTable(); bool WriteHashTable(); void InitDefaultMpqHeader(MpqFileHeader *hdr); LoggedFStream stream_; std::string name_; uint32_t size_ {}; std::unique_ptr hashTable_; std::unique_ptr blockTable_; // Amiga cannot Seekp beyond EOF. // See https://github.com/bebbo/libnix/issues/30 #ifndef __AMIGA__ #define CAN_SEEKP_BEYOND_EOF #endif #ifndef CAN_SEEKP_BEYOND_EOF long streamBegin_; #endif }; } // namespace devilution ================================================ FILE: Source/msg.cpp ================================================ /** * @file msg.cpp * * Implementation of function for sending and receiving network messages. */ #include "msg.h" #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include #if !defined(UNPACKED_MPQS) || !defined(UNPACKED_SAVES) || !defined(NONET) #define USE_PKWARE #include "encrypt.h" #endif #include "DiabloUI/diabloui.h" #include "automap.h" #include "config.h" #include "control/control.hpp" #include "dead.h" #include "engine/backbuffer_state.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "gamemenu.h" #include "items/validation.h" #include "levels/crypt.h" #include "levels/town.h" #include "levels/trigs.h" #include "lighting.h" #include "missiles.h" #include "monster.h" #include "monsters/validation.hpp" #include "nthread.h" #include "objects.h" #include "options.h" #include "pack.h" #include "pfile.h" #include "player.h" #include "plrmsg.h" #include "portals/validation.hpp" #include "quests/validation.hpp" #include "spells.h" #include "storm/storm_net.hpp" #include "sync.h" #include "tmsg.h" #include "towners.h" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #include "utils/utf8.hpp" #define ValidateField(logValue, condition) \ do { \ if (!(condition)) { \ LogFailedPacket(#condition, #logValue, logValue); \ EventFailedPacket(player._pName); \ return false; \ } \ } while (0) #define ValidateFields(logValue1, logValue2, condition) \ do { \ if (!(condition)) { \ LogFailedPacket(#condition, #logValue1, logValue1, #logValue2, logValue2); \ EventFailedPacket(player._pName); \ return false; \ } \ } while (0) namespace devilution { void EventFailedPacket(const char *playerName) { const std::string message = fmt::format("Player '{}' sent an invalid packet.", playerName); EventPlrMsg(message); } template void LogFailedPacket(const char *condition, const char *name, T value) { LogDebug("Remote player packet validation failed: ValidateField({}: {}, {})", name, value, condition); } template void LogFailedPacket(const char *condition, const char *name1, T1 value1, const char *name2, T2 value2) { LogDebug("Remote player packet validation failed: ValidateFields({}: {}, {}: {}, {})", name1, value1, name2, value2, condition); } // #define LOG_RECEIVED_MESSAGES uint8_t gbBufferMsgs; int dwRecCount; namespace { #ifdef LOG_RECEIVED_MESSAGES std::string_view CmdIdString(_cmd_id cmd) { // clang-format off switch (cmd) { case CMD_STAND: return "CMD_STAND"; case CMD_WALKXY: return "CMD_WALKXY"; case CMD_ACK_PLRINFO: return "CMD_ACK_PLRINFO"; case CMD_ADDSTR: return "CMD_ADDSTR"; case CMD_ADDMAG: return "CMD_ADDMAG"; case CMD_ADDDEX: return "CMD_ADDDEX"; case CMD_ADDVIT: return "CMD_ADDVIT"; case CMD_GETITEM: return "CMD_GETITEM"; case CMD_AGETITEM: return "CMD_AGETITEM"; case CMD_PUTITEM: return "CMD_PUTITEM"; case CMD_SPAWNITEM: return "CMD_SPAWNITEM"; case CMD_ATTACKXY: return "CMD_ATTACKXY"; case CMD_RATTACKXY: return "CMD_RATTACKXY"; case CMD_SPELLXY: return "CMD_SPELLXY"; case CMD_OPOBJXY: return "CMD_OPOBJXY"; case CMD_DISARMXY: return "CMD_DISARMXY"; case CMD_ATTACKID: return "CMD_ATTACKID"; case CMD_ATTACKPID: return "CMD_ATTACKPID"; case CMD_RATTACKID: return "CMD_RATTACKID"; case CMD_RATTACKPID: return "CMD_RATTACKPID"; case CMD_SPELLID: return "CMD_SPELLID"; case CMD_SPELLPID: return "CMD_SPELLPID"; case CMD_RESURRECT: return "CMD_RESURRECT"; case CMD_OPOBJT: return "CMD_OPOBJT"; case CMD_KNOCKBACK: return "CMD_KNOCKBACK"; case CMD_TALKXY: return "CMD_TALKXY"; case CMD_NEWLVL: return "CMD_NEWLVL"; case CMD_WARP: return "CMD_WARP"; case CMD_CHEAT_EXPERIENCE: return "CMD_CHEAT_EXPERIENCE"; case CMD_CHANGE_SPELL_LEVEL: return "CMD_CHANGE_SPELL_LEVEL"; case CMD_DEBUG: return "CMD_DEBUG"; case CMD_SYNCDATA: return "CMD_SYNCDATA"; case CMD_MONSTDEATH: return "CMD_MONSTDEATH"; case CMD_MONSTDAMAGE: return "CMD_MONSTDAMAGE"; case CMD_PLRDEAD: return "CMD_PLRDEAD"; case CMD_REQUESTGITEM: return "CMD_REQUESTGITEM"; case CMD_REQUESTAGITEM: return "CMD_REQUESTAGITEM"; case CMD_GOTOGETITEM: return "CMD_GOTOGETITEM"; case CMD_GOTOAGETITEM: return "CMD_GOTOAGETITEM"; case CMD_OPENDOOR: return "CMD_OPENDOOR"; case CMD_CLOSEDOOR: return "CMD_CLOSEDOOR"; case CMD_OPERATEOBJ: return "CMD_OPERATEOBJ"; case CMD_BREAKOBJ: return "CMD_BREAKOBJ"; case CMD_CHANGEPLRITEMS: return "CMD_CHANGEPLRITEMS"; case CMD_DELPLRITEMS: return "CMD_DELPLRITEMS"; case CMD_CHANGEINVITEMS: return "CMD_CHANGEINVITEMS"; case CMD_DELINVITEMS: return "CMD_DELINVITEMS"; case CMD_CHANGEBELTITEMS: return "CMD_CHANGEBELTITEMS"; case CMD_DELBELTITEMS: return "CMD_DELBELTITEMS"; case CMD_PLRDAMAGE: return "CMD_PLRDAMAGE"; case CMD_PLRLEVEL: return "CMD_PLRLEVEL"; case CMD_DROPITEM: return "CMD_DROPITEM"; case CMD_PLAYER_JOINLEVEL: return "CMD_PLAYER_JOINLEVEL"; case CMD_SEND_PLRINFO: return "CMD_SEND_PLRINFO"; case CMD_SATTACKXY: return "CMD_SATTACKXY"; case CMD_ACTIVATEPORTAL: return "CMD_ACTIVATEPORTAL"; case CMD_DEACTIVATEPORTAL: return "CMD_DEACTIVATEPORTAL"; case CMD_DLEVEL: return "CMD_DLEVEL"; case CMD_DLEVEL_JUNK: return "CMD_DLEVEL_JUNK"; case CMD_DLEVEL_END: return "CMD_DLEVEL_END"; case CMD_HEALOTHER: return "CMD_HEALOTHER"; case CMD_STRING: return "CMD_STRING"; case CMD_FRIENDLYMODE: return "CMD_FRIENDLYMODE"; case CMD_SETSTR: return "CMD_SETSTR"; case CMD_SETMAG: return "CMD_SETMAG"; case CMD_SETDEX: return "CMD_SETDEX"; case CMD_SETVIT: return "CMD_SETVIT"; case CMD_RETOWN: return "CMD_RETOWN"; case CMD_SPELLXYD: return "CMD_SPELLXYD"; case CMD_ITEMEXTRA: return "CMD_ITEMEXTRA"; case CMD_SYNCPUTITEM: return "CMD_SYNCPUTITEM"; case CMD_SYNCQUEST: return "CMD_SYNCQUEST"; case CMD_REQUESTSPAWNGOLEM: return "CMD_REQUESTSPAWNGOLEM"; case CMD_SETSHIELD: return "CMD_SETSHIELD"; case CMD_REMSHIELD: return "CMD_REMSHIELD"; case CMD_SETREFLECT: return "CMD_SETREFLECT"; case CMD_NAKRUL: return "CMD_NAKRUL"; case CMD_OPENHIVE: return "CMD_OPENHIVE"; case CMD_OPENGRAVE: return "CMD_OPENGRAVE"; case CMD_SPAWNMONSTER: return "CMD_SPAWNMONSTER"; case FAKE_CMD_SETID: return "FAKE_CMD_SETID"; case FAKE_CMD_DROPID: return "FAKE_CMD_DROPID"; case CMD_INVALID: return "CMD_INVALID"; default: return ""; } // clang-format on } #endif // LOG_RECEIVED_MESSAGES struct TMegaPkt { size_t spaceLeft; std::byte data[32000]; TMegaPkt() : spaceLeft(sizeof(data)) { } }; #pragma pack(push, 1) struct DMonsterStr { WorldTilePosition position; uint8_t menemy; uint8_t mactive; int32_t hitPoints; int8_t mWhoHit; }; #pragma pack(pop) struct DObjectStr { _cmd_id bCmd; }; struct DSpawnedMonster { size_t typeIndex; uint32_t seed; uint8_t golemOwnerPlayerId; int16_t golemSpellLevel; }; struct DLevel { TCmdPItem item[MAXITEMS]; ankerl::unordered_dense::map object; ankerl::unordered_dense::map spawnedMonsters; DMonsterStr monster[MaxMonsters]; }; #pragma pack(push, 1) struct LocalLevel { LocalLevel(const uint8_t (&other)[DMAXX][DMAXY]) { memcpy(&automapsv, &other, sizeof(automapsv)); } uint8_t automapsv[DMAXX][DMAXY]; }; struct DPortal { uint8_t x; uint8_t y; uint8_t level; uint8_t ltype; uint8_t setlvl; }; struct MultiQuests { quest_state qstate; uint8_t qlog; uint8_t qvar1; uint8_t qvar2; int16_t qmsg; }; struct DJunk { DPortal portal[MAXPORTAL]; MultiQuests quests[MAXQUESTS]; }; #pragma pack(pop) constexpr size_t MaxMultiplayerLevels = NUMLEVELS + SL_LAST; constexpr size_t MaxChunks = MaxMultiplayerLevels + 4; uint32_t sgdwOwnerWait; uint32_t sgdwRecvOffset; int sgnCurrMegaPlayer; ankerl::unordered_dense::map DeltaLevels; uint8_t sbLastCmd; /** * @brief buffer used to receive level deltas, size is the worst expected case assuming every object on a level was touched */ std::byte sgRecvBuf[1U /* marker byte, always 0 */ + sizeof(uint8_t) /* level id */ + sizeof(DLevel::item) /* items spawned during dungeon generation which have been picked up, and items dropped by a player during a game */ + sizeof(uint8_t) /* count of object interactions which caused a state change since dungeon generation */ + (sizeof(WorldTilePosition) + sizeof(_cmd_id)) * MAXOBJECTS /* location/action pairs for the object interactions */ + sizeof(DLevel::monster) /* latest monster state */ + sizeof(uint16_t) /* spawned monster count */ + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * MaxMonsters]; /* spawned monsters */ _cmd_id sgbRecvCmd; ankerl::unordered_dense::map LocalLevels; DJunk sgJunk; uint8_t sgbDeltaChunks; std::list MegaPktList; Item ItemLimbo; /** @brief Last sent player command for the local player. */ TCmdLocParam5 lastSentPlayerCmd; void RecreateItem(const Player &player, const TCmdPItem &message, Item &item); bool IsMonsterDeltaValid(const DMonsterStr &monster) { return InDungeonBounds(monster.position) && monster.hitPoints >= 0; } bool IsPortalDeltaValid(const DPortal &portal) { const WorldTilePosition position { portal.x, portal.y }; return IsPortalDeltaValid(position, portal.level, portal.ltype, portal.setlvl != 0); } bool IsQuestDeltaValid(quest_id qidx, const MultiQuests &quest) { return IsQuestDeltaValid(qidx, quest.qstate, quest.qlog, quest.qmsg); } uint8_t GetLevelForMultiplayer(uint8_t level, bool isSetLevel) { if (isSetLevel) return level + NUMLEVELS; return level; } /** @brief Gets a delta level. */ DLevel &GetDeltaLevel(uint8_t level) { auto keyIt = DeltaLevels.find(level); if (keyIt != DeltaLevels.end()) return keyIt->second; DLevel &deltaLevel = DeltaLevels[level]; memset(&deltaLevel.item, 0xFF, sizeof(deltaLevel.item)); memset(&deltaLevel.monster, 0xFF, sizeof(deltaLevel.monster)); return deltaLevel; } /** @brief Gets a delta level. */ DLevel &GetDeltaLevel(const Player &player) { const uint8_t level = GetLevelForMultiplayer(player); return GetDeltaLevel(level); } Point GetItemPosition(Point position) { if (CanPut(position)) return position; for (int k = 1; k < 50; k++) { for (int j = -k; j <= k; j++) { const int yy = position.y + j; for (int l = -k; l <= k; l++) { const int xx = position.x + l; if (CanPut({ xx, yy })) return { xx, yy }; } } } return position; } /** * @brief Throttles that a player command is only sent once per game tick. * This is a workaround for a desync that happens when a command is processed in different game ticks for different clients. See https://github.com/diasurgical/devilutionX/issues/2681 for details. * When a proper fix is implemented this workaround can be removed. */ bool WasPlayerCmdAlreadyRequested(_cmd_id bCmd, Point position = {}, uint16_t wParam1 = 0, uint16_t wParam2 = 0, uint16_t wParam3 = 0, uint16_t wParam4 = 0, uint16_t wParam5 = 0) { switch (bCmd) { // All known commands that result in a changed player action (player.destAction) case _cmd_id::CMD_RATTACKID: case _cmd_id::CMD_SPELLID: case _cmd_id::CMD_ATTACKID: case _cmd_id::CMD_RATTACKPID: case _cmd_id::CMD_SPELLPID: case _cmd_id::CMD_ATTACKPID: case _cmd_id::CMD_SATTACKXY: case _cmd_id::CMD_RATTACKXY: case _cmd_id::CMD_SPELLXY: case _cmd_id::CMD_SPELLXYD: case _cmd_id::CMD_WALKXY: case _cmd_id::CMD_TALKXY: case _cmd_id::CMD_DISARMXY: case _cmd_id::CMD_OPOBJXY: case _cmd_id::CMD_GOTOGETITEM: case _cmd_id::CMD_GOTOAGETITEM: break; default: // None player actions should work normally return false; } const TCmdLocParam5 newSendParam = { bCmd, static_cast(position.x), static_cast(position.y), Swap16LE(wParam1), Swap16LE(wParam2), Swap16LE(wParam3), Swap16LE(wParam4), Swap16LE(wParam5) }; if (lastSentPlayerCmd.bCmd == newSendParam.bCmd && lastSentPlayerCmd.x == newSendParam.x && lastSentPlayerCmd.y == newSendParam.y && lastSentPlayerCmd.wParam1 == newSendParam.wParam1 && lastSentPlayerCmd.wParam2 == newSendParam.wParam2 && lastSentPlayerCmd.wParam3 == newSendParam.wParam3 && lastSentPlayerCmd.wParam4 == newSendParam.wParam4 && lastSentPlayerCmd.wParam5 == newSendParam.wParam5) { // Command already send in this game tick => don't send again / throttle return true; } lastSentPlayerCmd = newSendParam; return false; } void GetNextPacket() { MegaPktList.emplace_back(); } void FreePackets() { MegaPktList.clear(); } void PrePacket() { uint8_t playerId = std::numeric_limits::max(); for (TMegaPkt &pkt : MegaPktList) { std::byte *data = pkt.data; size_t remainingBytes = sizeof(pkt.data) - pkt.spaceLeft; while (remainingBytes > 0) { auto cmdId = static_cast<_cmd_id>(*data); if (cmdId == FAKE_CMD_SETID) { auto *cmd = reinterpret_cast(data); data += sizeof(*cmd); remainingBytes -= sizeof(*cmd); playerId = cmd->bPlr; continue; } if (cmdId == FAKE_CMD_DROPID) { auto *cmd = reinterpret_cast(data); data += sizeof(*cmd); remainingBytes -= sizeof(*cmd); multi_player_left(cmd->bPlr, static_cast(Swap32LE(cmd->dwReason))); continue; } if (playerId >= Players.size()) { Log("Missing source of network message"); return; } const size_t size = ParseCmd(playerId, reinterpret_cast(data), remainingBytes); if (size == 0) { Log("Discarding bad network message"); return; } data += size; remainingBytes -= size; } } } void BufferMessage(const void *message, size_t messageSize) { if (MegaPktList.back().spaceLeft < messageSize) GetNextPacket(); TMegaPkt &currMegaPkt = MegaPktList.back(); memcpy(currMegaPkt.data + sizeof(currMegaPkt.data) - currMegaPkt.spaceLeft, message, messageSize); currMegaPkt.spaceLeft -= messageSize; } void BufferMessage(uint8_t pnum, const void *message, size_t messageSize) { if (messageSize > sizeof(TMegaPkt::data)) { Log("Discarding enormous network message"); return; } if (pnum != sgnCurrMegaPlayer) { sgnCurrMegaPlayer = pnum; TFakeCmdPlr cmd; cmd.bCmd = FAKE_CMD_SETID; cmd.bPlr = pnum; BufferMessage(&cmd, sizeof(cmd)); } BufferMessage(message, messageSize); } void BufferMessage(const Player &player, const void *message, size_t messageSize) { BufferMessage(player.getId(), message, messageSize); } int WaitForTurns() { uint32_t turns; if (sgbDeltaChunks == 0) { nthread_send_and_recv_turn(0, 0); SNetGetOwnerTurnsWaiting(&turns); if (SDL_GetTicks() - sgdwOwnerWait <= 2000 && turns < gdwTurnsInTransit) return 0; sgbDeltaChunks++; } ProcessGameMessagePackets(); nthread_send_and_recv_turn(0, 0); if (nthread_has_500ms_passed()) { nthread_recv_turns(); } if (gbGameDestroyed) return 100; if (gbDeltaSender >= Players.size()) { sgbDeltaChunks = 0; sgbRecvCmd = CMD_DLEVEL_END; gbDeltaSender = MyPlayerId; nthread_set_turn_upper_bit(); } if (sgbDeltaChunks == MaxChunks - 1) { sgbDeltaChunks = MaxChunks; return 99; } return 100 * sgbDeltaChunks / static_cast(MaxChunks); } std::byte *DeltaExportItem(std::byte *dst, const TCmdPItem *src) { for (int i = 0; i < MAXITEMS; i++, src++) { if (src->bCmd == CMD_INVALID) { *dst++ = std::byte { 0xFF }; } else { memcpy(dst, src, sizeof(TCmdPItem)); dst += sizeof(TCmdPItem); } } return dst; } const std::byte *DeltaImportItem(const std::byte *src, const std::byte *end, TCmdPItem *dst) { size_t size = 0; for (int i = 0; i < MAXITEMS; i++, dst++) { if (&src[size] >= end) return nullptr; if (src[size] == std::byte { 0xFF }) { memset(dst, 0xFF, sizeof(TCmdPItem)); size++; } else { if (&src[size] + sizeof(TCmdPItem) > end) return nullptr; memcpy(dst, &src[size], sizeof(TCmdPItem)); if (!IsItemDeltaValid(*dst)) memset(dst, 0xFF, sizeof(TCmdPItem)); size += sizeof(TCmdPItem); } } return src + size; } std::byte *DeltaExportObject(std::byte *dst, const ankerl::unordered_dense::map &src) { *dst++ = static_cast(src.size()); for (const auto &[position, obj] : src) { *dst++ = static_cast(position.x); *dst++ = static_cast(position.y); *dst++ = static_cast(obj.bCmd); } return dst; } const std::byte *DeltaImportObjects(const std::byte *src, const std::byte *end, ankerl::unordered_dense::map &dst) { dst.clear(); if (src == nullptr || src == end) return nullptr; auto numDeltas = static_cast(*src++); if (numDeltas > MAXOBJECTS) return nullptr; const size_t numBytes = (sizeof(WorldTilePosition) + sizeof(_cmd_id)) * numDeltas; if (src + numBytes > end) return nullptr; dst.reserve(numDeltas); for (unsigned i = 0; i < numDeltas; i++) { const WorldTilePosition objectPosition { static_cast(src[0]), static_cast(src[1]) }; src += 2; dst[objectPosition] = DObjectStr { static_cast<_cmd_id>(*src++) }; } return src; } std::byte *DeltaExportMonster(std::byte *dst, const DMonsterStr *src) { for (size_t i = 0; i < MaxMonsters; i++, src++) { if (src->position.x == 0xFF) { *dst++ = std::byte { 0xFF }; } else { memcpy(dst, src, sizeof(DMonsterStr)); dst += sizeof(DMonsterStr); } } return dst; } const std::byte *DeltaImportMonster(const std::byte *src, const std::byte *end, DMonsterStr *dst) { if (src == nullptr) return nullptr; size_t size = 0; for (size_t i = 0; i < MaxMonsters; i++, dst++) { if (&src[size] >= end) return nullptr; if (src[size] == std::byte { 0xFF }) { memset(dst, 0xFF, sizeof(DMonsterStr)); size++; } else { if (&src[size] + sizeof(DMonsterStr) > end) return nullptr; memcpy(dst, &src[size], sizeof(DMonsterStr)); size += sizeof(DMonsterStr); } } return src + size; } std::byte *DeltaExportSpawnedMonsters(std::byte *dst, const ankerl::unordered_dense::map &spawnedMonsters) { uint16_t size = Swap16LE(static_cast(spawnedMonsters.size())); memcpy(dst, &size, sizeof(uint16_t)); dst += sizeof(uint16_t); for (const auto &deltaSpawnedMonster : spawnedMonsters) { uint16_t monsterId = Swap16LE(static_cast(deltaSpawnedMonster.first)); memcpy(dst, &monsterId, sizeof(uint16_t)); dst += sizeof(uint16_t); memcpy(dst, &deltaSpawnedMonster.second, sizeof(DSpawnedMonster)); dst += sizeof(DSpawnedMonster); } return dst; } const std::byte *DeltaImportSpawnedMonsters(const std::byte *src, const std::byte *end, ankerl::unordered_dense::map &spawnedMonsters) { if (src == nullptr || src + sizeof(uint16_t) > end) return nullptr; uint16_t size; memcpy(&size, src, sizeof(uint16_t)); size = Swap16LE(size); src += sizeof(uint16_t); if (size > MaxMonsters) return nullptr; const size_t requiredBytes = (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * size; if (src + requiredBytes > end) return nullptr; for (size_t i = 0; i < size; i++) { uint16_t monsterId; memcpy(&monsterId, src, sizeof(uint16_t)); monsterId = Swap16LE(monsterId); src += sizeof(uint16_t); DSpawnedMonster spawnedMonster; memcpy(&spawnedMonster, src, sizeof(DSpawnedMonster)); src += sizeof(DSpawnedMonster); spawnedMonsters.emplace(monsterId, spawnedMonster); } return src; } std::byte *DeltaExportJunk(std::byte *dst) { for (auto &portal : sgJunk.portal) { if (portal.x == 0xFF) { *dst++ = std::byte { 0xFF }; } else { memcpy(dst, &portal, sizeof(DPortal)); dst += sizeof(DPortal); } } int q = 0; for (auto &quest : Quests) { if (QuestsData[quest._qidx].isSinglePlayerOnly && UseMultiplayerQuests()) { continue; } sgJunk.quests[q].qlog = quest._qlog ? 1 : 0; sgJunk.quests[q].qstate = quest._qactive; sgJunk.quests[q].qvar1 = quest._qvar1; sgJunk.quests[q].qvar2 = quest._qvar2; sgJunk.quests[q].qmsg = Swap16LE(static_cast(quest._qmsg)); memcpy(dst, &sgJunk.quests[q], sizeof(MultiQuests)); dst += sizeof(MultiQuests); q++; } return dst; } const std::byte *DeltaImportJunk(const std::byte *src, const std::byte *end) { for (DPortal &portal : sgJunk.portal) { if (src >= end) return nullptr; if (*src == std::byte { 0xFF }) { memset(&portal, 0xFF, sizeof(DPortal)); src++; } else { if (src + sizeof(DPortal) > end) return nullptr; memcpy(&portal, src, sizeof(DPortal)); if (!IsPortalDeltaValid(portal)) memset(&portal, 0xFF, sizeof(DPortal)); src += sizeof(DPortal); } } int q = 0; for (int qidx = 0; qidx < MAXQUESTS; qidx++) { if (QuestsData[qidx].isSinglePlayerOnly && UseMultiplayerQuests()) { continue; } if (src + sizeof(MultiQuests) > end) { return nullptr; } memcpy(&sgJunk.quests[q], src, sizeof(MultiQuests)); if (!IsQuestDeltaValid(static_cast(qidx), sgJunk.quests[q])) { sgJunk.quests[q].qstate = QUEST_INVALID; } src += sizeof(MultiQuests); q++; } return src; } uint32_t CompressData(std::byte *buffer, std::byte *end) { #ifdef USE_PKWARE const auto size = static_cast(end - buffer - 1); const uint32_t pkSize = PkwareCompress(buffer + 1, size); *buffer = size != pkSize ? std::byte { 1 } : std::byte { 0 }; return pkSize + 1; #else *buffer = std::byte { 0 }; return end - buffer; #endif } void DeltaImportData(_cmd_id cmd, uint32_t recvOffset, int pnum) { size_t deltaSize = recvOffset; #ifdef USE_PKWARE if (sgRecvBuf[0] != std::byte { 0 }) { deltaSize = PkwareDecompress(&sgRecvBuf[1], static_cast(deltaSize), sizeof(sgRecvBuf) - 1); if (deltaSize == 0) { Log("PKWare decompression failure, dropping player {}", pnum); SNetDropPlayer(pnum, leaveinfo_t::LEAVE_DROP); return; } } #endif const std::byte *src = &sgRecvBuf[1]; const std::byte *end = src + deltaSize; if (cmd == CMD_DLEVEL_JUNK) { src = DeltaImportJunk(src, end); } else if (cmd == CMD_DLEVEL) { auto i = static_cast(src[0]); src += sizeof(uint8_t); DLevel &deltaLevel = GetDeltaLevel(i); src = DeltaImportItem(src, end, deltaLevel.item); src = DeltaImportObjects(src, end, deltaLevel.object); src = DeltaImportMonster(src, end, deltaLevel.monster); src = DeltaImportSpawnedMonsters(src, end, deltaLevel.spawnedMonsters); } else { Log("Received invalid deltas, dropping player {}", pnum); SNetDropPlayer(pnum, leaveinfo_t::LEAVE_DROP); return; } if (src == nullptr) { Log("Received invalid deltas, dropping player {}", pnum); SNetDropPlayer(pnum, leaveinfo_t::LEAVE_DROP); return; } sgbDeltaChunks++; } void DeltaLoadSpawnedMonsters(const DLevel &deltaLevel) { for (const auto &deltaSpawnedMonster : deltaLevel.spawnedMonsters) { const auto &monsterData = deltaSpawnedMonster.second; LoadDeltaSpawnedMonster(deltaSpawnedMonster.second.typeIndex, deltaSpawnedMonster.first, monsterData.seed, monsterData.golemOwnerPlayerId, monsterData.golemSpellLevel); assert(deltaLevel.monster[deltaSpawnedMonster.first].position.x != 0xFF); } } void DeltaLoadEnemies(const DLevel &deltaLevel) { for (size_t i = 0; i < MaxMonsters; i++) { const DMonsterStr &deltaMonster = deltaLevel.monster[i]; if (!IsMonsterDeltaValid(deltaMonster)) continue; if (deltaMonster.hitPoints == 0) continue; Monster &monster = Monsters[i]; if (IsEnemyValid(i, deltaMonster.menemy)) decode_enemy(monster, deltaMonster.menemy); if (monster.position.tile != Point { 0, 0 } && monster.position.tile != GolemHoldingCell) monster.occupyTile(monster.position.tile, false); if (monster.type().type == MT_GOLEM) { GolumAi(monster); monster.flags |= (MFLAG_TARGETS_MONSTER | MFLAG_GOLEM); } else { M_StartStand(monster, monster.direction); } monster.activeForTicks = deltaMonster.mactive; } } void DeltaLoadMonsters(const DLevel &deltaLevel) { for (size_t i = 0; i < MaxMonsters; i++) { const DMonsterStr &deltaMonster = deltaLevel.monster[i]; if (!IsMonsterDeltaValid(deltaMonster)) continue; Monster &monster = Monsters[i]; M_ClearSquares(monster); { const WorldTilePosition position = deltaMonster.position; monster.position.tile = position; monster.position.old = position; monster.position.future = position; if (monster.lightId != NO_LIGHT) ChangeLightXY(monster.lightId, position); } monster.hitPoints = Swap32LE(deltaMonster.hitPoints); monster.whoHit = deltaMonster.mWhoHit; if (deltaMonster.hitPoints != 0) continue; M_ClearSquares(monster); if (monster.ai != MonsterAIID::Diablo) { if (monster.isUnique()) { AddCorpse(monster.position.tile, monster.corpseId, monster.direction); } else { AddCorpse(monster.position.tile, monster.type().corpseId, monster.direction); } } monster.isInvalid = true; M_UpdateRelations(monster); } // Calling this here ensures that monster hitpoints // are synced before attempting to validate enemy IDs DeltaLoadEnemies(deltaLevel); } void DeltaLoadObjects(DLevel &deltaLevel) { for (auto it = deltaLevel.object.begin(); it != deltaLevel.object.end();) { Object *object = FindObjectAtPosition(it->first); if (object == nullptr) { it = deltaLevel.object.erase(it); continue; } switch (it->second.bCmd) { case CMD_OPENDOOR: case CMD_OPERATEOBJ: DeltaSyncOpObject(*object); it++; break; case CMD_CLOSEDOOR: DeltaSyncCloseObj(*object); it++; break; case CMD_BREAKOBJ: DeltaSyncBreakObj(*object); it++; break; default: it = deltaLevel.object.erase(it); // discard invalid commands break; } } for (int i = 0; i < ActiveObjectCount; i++) { Object &object = Objects[ActiveObjects[i]]; if (object.IsTrap()) { UpdateTrapState(object); } } } void DeltaLoadItems(const DLevel &deltaLevel) { for (const TCmdPItem &deltaItem : deltaLevel.item) { if (deltaItem.bCmd == CMD_INVALID) continue; if (deltaItem.bCmd == TCmdPItem::PickedUpItem) { const int activeItemIndex = FindGetItem( Swap32LE(deltaItem.def.dwSeed), static_cast<_item_indexes>(Swap16LE(deltaItem.def.wIndx)), Swap16LE(deltaItem.def.wCI)); if (activeItemIndex != -1) { const auto &position = Items[ActiveItems[activeItemIndex]].position; if (dItem[position.x][position.y] == ActiveItems[activeItemIndex] + 1) dItem[position.x][position.y] = 0; DeleteItem(activeItemIndex); } } if (deltaItem.bCmd == TCmdPItem::DroppedItem) { const int ii = AllocateItem(); auto &item = Items[ii]; RecreateItem(*MyPlayer, deltaItem, item); const int x = deltaItem.x; const int y = deltaItem.y; item.position = GetItemPosition({ x, y }); dItem[item.position.x][item.position.y] = static_cast(ii + 1); RespawnItem(Items[ii], false); } } } size_t OnLevelData(const TCmdPlrInfoHdr &message, size_t maxCmdSize, const Player &player) { const uint16_t wBytes = Swap16LE(message.wBytes); const uint16_t wOffset = Swap16LE(message.wOffset); if (!ValidateCmdSize(wBytes + sizeof(message), maxCmdSize, player.getId())) return maxCmdSize; if (gbDeltaSender != player.getId()) { if (message.bCmd != CMD_DLEVEL_END && (message.bCmd != CMD_DLEVEL || wOffset != 0)) { return wBytes + sizeof(message); } gbDeltaSender = player.getId(); sgbRecvCmd = CMD_DLEVEL_END; } if (sgbRecvCmd == CMD_DLEVEL_END) { if (message.bCmd == CMD_DLEVEL_END) { sgbDeltaChunks = MaxChunks - 1; return wBytes + sizeof(message); } if (message.bCmd != CMD_DLEVEL || wOffset != 0) { return wBytes + sizeof(message); } sgdwRecvOffset = 0; sgbRecvCmd = message.bCmd; } else if (sgbRecvCmd != message.bCmd || wOffset == 0) { DeltaImportData(sgbRecvCmd, sgdwRecvOffset, player.getId()); if (message.bCmd == CMD_DLEVEL_END) { sgbDeltaChunks = MaxChunks - 1; sgbRecvCmd = CMD_DLEVEL_END; return wBytes + sizeof(message); } sgdwRecvOffset = 0; sgbRecvCmd = message.bCmd; } if (sgdwRecvOffset + wBytes > sizeof(sgRecvBuf)) { Log("Received too many deltas, dropping player {}", player.getId()); SNetDropPlayer(player.getId(), leaveinfo_t::LEAVE_DROP); return wBytes + sizeof(message); } assert(wOffset == sgdwRecvOffset); memcpy(&sgRecvBuf[sgdwRecvOffset], &message + 1, wBytes); sgdwRecvOffset += wBytes; return wBytes + sizeof(message); } void DeltaLeaveSync(uint8_t bLevel) { if (!gbIsMultiplayer) return; if (leveltype == DTYPE_TOWN) { DungeonSeeds[0] = GenerateSeed(); return; } DLevel &deltaLevel = GetDeltaLevel(bLevel); for (size_t i = 0; i < ActiveMonsterCount; i++) { const unsigned ma = ActiveMonsters[i]; Monster &monster = Monsters[ma]; if (monster.hitPoints == 0) continue; DMonsterStr &delta = deltaLevel.monster[ma]; delta.position = monster.position.tile; delta.menemy = encode_enemy(monster); delta.hitPoints = monster.hitPoints; delta.mactive = monster.activeForTicks; delta.mWhoHit = monster.whoHit; } LocalLevels.insert_or_assign(bLevel, AutomapView); } void DeltaSyncObject(WorldTilePosition position, _cmd_id bCmd, const Player &player) { if (!gbIsMultiplayer) return; auto &objectDeltas = GetDeltaLevel(player).object; objectDeltas[position].bCmd = bCmd; } bool DeltaGetItem(const TCmdGItem &message, uint8_t bLevel) { if (!gbIsMultiplayer) return true; DLevel &deltaLevel = GetDeltaLevel(bLevel); for (TCmdPItem &item : deltaLevel.item) { if (item.bCmd == CMD_INVALID || item.def.wIndx != message.def.wIndx || item.def.wCI != message.def.wCI || item.def.dwSeed != message.def.dwSeed) { continue; } if (item.bCmd == TCmdPItem::PickedUpItem) { return true; } if (item.bCmd == TCmdPItem::FloorItem) { item.bCmd = TCmdPItem::PickedUpItem; return true; } if (item.bCmd == TCmdPItem::DroppedItem) { item.bCmd = CMD_INVALID; return true; } #ifdef _DEBUG app_fatal("delta:1"); #endif } if ((message.def.wCI & CF_PREGEN) == 0) return false; for (TCmdPItem &delta : deltaLevel.item) { if (delta.bCmd == CMD_INVALID) { delta.bCmd = TCmdPItem::PickedUpItem; delta.x = message.x; delta.y = message.y; delta.def.wIndx = message.def.wIndx; delta.def.wCI = message.def.wCI; delta.def.dwSeed = message.def.dwSeed; if (message.def.wIndx == IDI_EAR) { delta.ear.bCursval = message.ear.bCursval; CopyUtf8(delta.ear.heroname, message.ear.heroname, sizeof(delta.ear.heroname)); } else { delta.item.bId = message.item.bId; delta.item.bDur = message.item.bDur; delta.item.bMDur = message.item.bMDur; delta.item.bCh = message.item.bCh; delta.item.bMCh = message.item.bMCh; delta.item.wValue = message.item.wValue; delta.item.dwBuff = message.item.dwBuff; delta.item.wToHit = message.item.wToHit; } break; } } return true; } void DeltaPutItem(const TCmdPItem &message, Point position, const Player &player) { if (!gbIsMultiplayer) return; DLevel &deltaLevel = GetDeltaLevel(player); for (const TCmdPItem &item : deltaLevel.item) { if (item.bCmd != TCmdPItem::PickedUpItem && item.bCmd != CMD_INVALID && item.def.wIndx == message.def.wIndx && item.def.wCI == message.def.wCI && item.def.dwSeed == message.def.dwSeed) { if (item.bCmd != TCmdPItem::DroppedItem) { Log("Suspicious floor item duplication, dropping player {}", player.getId()); SNetDropPlayer(player.getId(), leaveinfo_t::LEAVE_DROP); } return; } } for (TCmdPItem &item : deltaLevel.item) { if (item.bCmd == CMD_INVALID) { memcpy(&item, &message, sizeof(TCmdPItem)); item.bCmd = TCmdPItem::DroppedItem; item.x = position.x; item.y = position.y; return; } } } void DeltaOpenPortal(size_t pnum, Point position, uint8_t bLevel, dungeon_type bLType, bool bSetLvl) { sgJunk.portal[pnum].x = position.x; sgJunk.portal[pnum].y = position.y; sgJunk.portal[pnum].level = bLevel; sgJunk.portal[pnum].ltype = bLType; sgJunk.portal[pnum].setlvl = bSetLvl ? 1 : 0; } void NetSendCmdGItem2(bool usonly, _cmd_id bCmd, uint8_t mast, uint8_t pnum, const TCmdGItem &item) { TCmdGItem cmd; memcpy(&cmd, &item, sizeof(cmd)); cmd.bPnum = pnum; cmd.bCmd = bCmd; cmd.bMaster = mast; if (!usonly) { cmd.dwTime = 0; NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); return; } auto ticks = static_cast(SDL_GetTicks()); if (cmd.dwTime == 0) { cmd.dwTime = Swap32LE(ticks); } else if (ticks - Swap32LE(cmd.dwTime) > 5000) { return; } tmsg_add(reinterpret_cast(&cmd), sizeof(cmd)); } bool NetSendCmdReq2(_cmd_id bCmd, const Player &player, uint8_t pnum, const TCmdGItem &item) { TCmdGItem cmd; memcpy(&cmd, &item, sizeof(cmd)); cmd.bCmd = bCmd; cmd.bPnum = pnum; cmd.bMaster = player.getId(); auto ticks = static_cast(SDL_GetTicks()); if (cmd.dwTime == 0) cmd.dwTime = Swap32LE(ticks); else if (ticks - Swap32LE(cmd.dwTime) > 5000) return false; tmsg_add(reinterpret_cast(&cmd), sizeof(cmd)); return true; } void NetSendCmdExtra(const TCmdGItem &item) { TCmdGItem cmd; memcpy(&cmd, &item, sizeof(cmd)); cmd.dwTime = 0; cmd.bCmd = CMD_ITEMEXTRA; NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } size_t OnWalk(const TCmdLoc &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position)) { ClrPlrPath(player); MakePlrPath(player, position, true); player.destAction = ACTION_NONE; } return sizeof(message); } size_t OnAddStrength(const TCmdParam1 &message, Player &player) { if (gbBufferMsgs == 1) BufferMessage(player, &message, sizeof(message)); else if (message.wParam1 <= 256) ModifyPlrStr(player, Swap16LE(message.wParam1)); return sizeof(message); } size_t OnAddMagic(const TCmdParam1 &message, Player &player) { if (gbBufferMsgs == 1) BufferMessage(player, &message, sizeof(message)); else if (message.wParam1 <= 256) ModifyPlrMag(player, Swap16LE(message.wParam1)); return sizeof(message); } size_t OnAddDexterity(const TCmdParam1 &message, Player &player) { if (gbBufferMsgs == 1) BufferMessage(player, &message, sizeof(message)); else if (message.wParam1 <= 256) ModifyPlrDex(player, Swap16LE(message.wParam1)); return sizeof(message); } size_t OnAddVitality(const TCmdParam1 &message, Player &player) { if (gbBufferMsgs == 1) BufferMessage(player, &message, sizeof(message)); else if (message.wParam1 <= 256) ModifyPlrVit(player, Swap16LE(message.wParam1)); return sizeof(message); } size_t OnGotoGetItem(const TCmdLocParam1 &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && Swap16LE(message.wParam1) < MAXITEMS + 1) { MakePlrPath(player, position, false); player.destAction = ACTION_PICKUPITEM; player.destParam1 = Swap16LE(message.wParam1); } return sizeof(message); } bool IsGItemValid(const TCmdGItem &message) { if (message.bMaster >= Players.size()) return false; if (message.bPnum >= Players.size()) return false; if (message.bCursitem >= MAXITEMS + 1) return false; if (!IsValidLevelForMultiplayer(message.bLevel)) return false; if (!InDungeonBounds({ message.x, message.y })) return false; return IsItemAvailable(static_cast<_item_indexes>(Swap16LE(message.def.wIndx))); } bool IsPItemValid(const TCmdPItem &message, const Player &player) { if (!gbIsMultiplayer) return true; const Point position { message.x, message.y }; if (!InDungeonBounds(position)) return false; auto idx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (idx != IDI_EAR) { const uint16_t creationFlags = Swap16LE(message.item.wCI); const uint32_t dwBuff = Swap16LE(message.item.dwBuff); if (idx != IDI_GOLD) ValidateField(creationFlags, IsCreationFlagComboValid(creationFlags)); if ((creationFlags & CF_TOWN) != 0) ValidateField(creationFlags, IsTownItemValid(creationFlags, player)); else if ((creationFlags & CF_USEFUL) == CF_UPER15) ValidateFields(creationFlags, dwBuff, IsUniqueMonsterItemValid(creationFlags, dwBuff)); else if ((dwBuff & CF_HELLFIRE) != 0 && AllItemsList[idx].iMiscId == IMISC_BOOK) return RecreateHellfireSpellBook(player, message.item); else ValidateFields(creationFlags, dwBuff, IsDungeonItemValid(creationFlags, dwBuff)); } return IsItemAvailable(idx); } void PrepareItemForNetwork(const Item &item, TCmdGItem &message) { message.def.wIndx = static_cast<_item_indexes>(Swap16LE(item.IDidx)); message.def.wCI = Swap16LE(item._iCreateInfo); message.def.dwSeed = Swap32LE(item._iSeed); if (item.IDidx == IDI_EAR) PrepareEarForNetwork(item, message.ear); else PrepareItemForNetwork(item, message.item); } void PrepareItemForNetwork(const Item &item, TCmdPItem &message) { message.def.wIndx = static_cast<_item_indexes>(Swap16LE(item.IDidx)); message.def.wCI = Swap16LE(item._iCreateInfo); message.def.dwSeed = Swap32LE(item._iSeed); if (item.IDidx == IDI_EAR) PrepareEarForNetwork(item, message.ear); else PrepareItemForNetwork(item, message.item); } void PrepareItemForNetwork(const Item &item, TCmdChItem &message) { message.def.wIndx = static_cast<_item_indexes>(Swap16LE(item.IDidx)); message.def.wCI = Swap16LE(item._iCreateInfo); message.def.dwSeed = Swap32LE(item._iSeed); if (item.IDidx == IDI_EAR) PrepareEarForNetwork(item, message.ear); else PrepareItemForNetwork(item, message.item); } void RecreateItem(const Player &player, const TCmdPItem &message, Item &item) { if (message.def.wIndx == Swap16LE(IDI_EAR)) RecreateEar(item, Swap16LE(message.ear.wCI), Swap32LE(message.ear.dwSeed), message.ear.bCursval, message.ear.heroname); else RecreateItem(player, message.item, item); } void RecreateItem(const Player &player, const TCmdChItem &message, Item &item) { if (message.def.wIndx == Swap16LE(IDI_EAR)) RecreateEar(item, Swap16LE(message.ear.wCI), Swap32LE(message.ear.dwSeed), message.ear.bCursval, message.ear.heroname); else RecreateItem(player, message.item, item); } int SyncDropItem(Point position, const TItem &item) { return SyncDropItem( position, static_cast<_item_indexes>(Swap16LE(item.wIndx)), Swap16LE(item.wCI), Swap32LE(item.dwSeed), item.bId, item.bDur, item.bMDur, item.bCh, item.bMCh, Swap16LE(item.wValue), Swap32LE(item.dwBuff), Swap16LE(item.wToHit), Swap16LE(item.wMaxDam)); } int SyncDropEar(Point position, const TEar &ear) { return SyncDropEar( position, Swap16LE(ear.wCI), Swap32LE(ear.dwSeed), ear.bCursval, ear.heroname); } int SyncDropItem(const TCmdGItem &message) { const Point position = GetItemPosition({ message.x, message.y }); if (message.def.wIndx == IDI_EAR) { return SyncDropEar( position, message.ear); } return SyncDropItem( position, message.item); } int SyncDropItem(const TCmdPItem &message) { const Point position = GetItemPosition({ message.x, message.y }); if (message.def.wIndx == IDI_EAR) { return SyncDropEar( position, message.ear); } return SyncDropItem( position, message.item); } size_t OnRequestGetItem(const TCmdGItem &message, Player &player) { if (gbBufferMsgs == 1 || !player.isLevelOwnedByLocalClient() || !IsGItemValid(message)) return sizeof(message); const Point position { message.x, message.y }; const uint32_t dwSeed = Swap32LE(message.def.dwSeed); const uint16_t wCI = Swap16LE(message.def.wCI); const auto wIndx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (!GetItemRecord(dwSeed, wCI, wIndx)) return sizeof(message); int ii = -1; if (InDungeonBounds(position)) { ii = std::abs(dItem[position.x][position.y]) - 1; if (ii >= 0 && !Items[ii].keyAttributesMatch(dwSeed, wIndx, wCI)) { ii = -1; } } if (ii == -1) { // No item at the target position or the key attributes don't match, so try find a matching item. const int activeItemIndex = FindGetItem(dwSeed, wIndx, wCI); if (activeItemIndex != -1) { ii = ActiveItems[activeItemIndex]; } } if (ii != -1) { NetSendCmdGItem2(false, CMD_GETITEM, MyPlayerId, message.bPnum, message); if (message.bPnum != MyPlayerId) SyncGetItem(position, dwSeed, wIndx, wCI); else InvGetItem(*MyPlayer, ii); SetItemRecord(dwSeed, wCI, wIndx); } else if (!NetSendCmdReq2(CMD_REQUESTGITEM, *MyPlayer, message.bPnum, message)) { NetSendCmdExtra(message); } return sizeof(message); } size_t OnGetItem(const TCmdGItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); return sizeof(message); } if (!IsGItemValid(message)) return sizeof(message); const Point position { message.x, message.y }; const uint32_t dwSeed = Swap32LE(message.def.dwSeed); const uint16_t wCI = Swap16LE(message.def.wCI); const auto wIndx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (!DeltaGetItem(message, message.bLevel)) { NetSendCmdGItem2(true, CMD_GETITEM, message.bMaster, message.bPnum, message); return sizeof(message); } const bool isOnActiveLevel = GetLevelForMultiplayer(*MyPlayer) == message.bLevel; if ((!isOnActiveLevel && message.bPnum != MyPlayerId) || message.bMaster == MyPlayerId) return sizeof(message); if (message.bPnum != MyPlayerId) { SyncGetItem(position, dwSeed, wIndx, wCI); return sizeof(message); } if (!isOnActiveLevel) { const int ii = SyncDropItem(message); if (ii != -1) InvGetItem(*MyPlayer, ii); } else { const int activeItemIndex = FindGetItem(dwSeed, wIndx, wCI); InvGetItem(*MyPlayer, ActiveItems[activeItemIndex]); } return sizeof(message); } size_t OnGotoAutoGetItem(const TCmdLocParam1 &message, Player &player) { const Point position { message.x, message.y }; const uint16_t itemIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && itemIdx < MAXITEMS + 1) { MakePlrPath(player, position, false); player.destAction = ACTION_PICKUPAITEM; player.destParam1 = itemIdx; } return sizeof(message); } size_t OnRequestAutoGetItem(const TCmdGItem &message, Player &player) { if (gbBufferMsgs != 1 && player.isLevelOwnedByLocalClient() && IsGItemValid(message)) { const Point position { message.x, message.y }; const uint32_t dwSeed = Swap32LE(message.def.dwSeed); const uint16_t wCI = Swap16LE(message.def.wCI); const auto wIndx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (GetItemRecord(dwSeed, wCI, wIndx)) { if (FindGetItem(dwSeed, wIndx, wCI) != -1) { NetSendCmdGItem2(false, CMD_AGETITEM, MyPlayerId, message.bPnum, message); if (message.bPnum != MyPlayerId) SyncGetItem(position, dwSeed, wIndx, wCI); else AutoGetItem(*MyPlayer, &Items[message.bCursitem], message.bCursitem); SetItemRecord(dwSeed, wCI, wIndx); } else if (!NetSendCmdReq2(CMD_REQUESTAGITEM, *MyPlayer, message.bPnum, message)) { NetSendCmdExtra(message); } } } return sizeof(message); } size_t OnAutoGetItem(const TCmdGItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); return sizeof(message); } if (!IsGItemValid(message)) return sizeof(message); const Point position { message.x, message.y }; if (!DeltaGetItem(message, message.bLevel)) { NetSendCmdGItem2(true, CMD_AGETITEM, message.bMaster, message.bPnum, message); return sizeof(message); } const bool isOnActiveLevel = GetLevelForMultiplayer(*MyPlayer) == message.bLevel; if ((!isOnActiveLevel && message.bPnum != MyPlayerId) || message.bMaster == MyPlayerId) return sizeof(message); if (message.bPnum != MyPlayerId) { SyncGetItem(position, Swap32LE(message.def.dwSeed), static_cast<_item_indexes>(Swap16LE(message.def.wIndx)), Swap16LE(message.def.wCI)); return sizeof(message); } if (!isOnActiveLevel) { const int ii = SyncDropItem(message); if (ii != -1) AutoGetItem(*MyPlayer, &Items[ii], ii); } else { AutoGetItem(*MyPlayer, &Items[message.bCursitem], message.bCursitem); } return sizeof(message); } size_t OnItemExtra(const TCmdGItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (IsGItemValid(message)) { DeltaGetItem(message, message.bLevel); if (player.isOnActiveLevel()) { const Point position { message.x, message.y }; SyncGetItem(position, Swap32LE(message.def.dwSeed), static_cast<_item_indexes>(Swap16LE(message.def.wIndx)), Swap16LE(message.def.wCI)); } } return sizeof(message); } size_t OnPutItem(const TCmdPItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { const Point position { message.x, message.y }; const bool isSelf = &player == MyPlayer; const int32_t dwSeed = Swap32LE(message.def.dwSeed); const uint16_t wCI = Swap16LE(message.def.wCI); const auto wIndx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (player.isOnActiveLevel()) { int ii; if (isSelf) { std::optional itemTile = FindAdjacentPositionForItem(player.position.tile, GetDirection(player.position.tile, position)); if (itemTile) ii = PlaceItemInWorld(std::move(ItemLimbo), *itemTile); else ii = -1; } else ii = SyncDropItem(message); if (ii != -1) { PutItemRecord(dwSeed, wCI, wIndx); DeltaPutItem(message, Items[ii].position, player); if (isSelf) pfile_update(true); } } else { PutItemRecord(dwSeed, wCI, wIndx); DeltaPutItem(message, position, player); if (isSelf) pfile_update(true); } } return sizeof(message); } size_t OnSyncPutItem(const TCmdPItem &message, Player &player) { if (gbBufferMsgs == 1) BufferMessage(player, &message, sizeof(message)); else if (IsPItemValid(message, player)) { const int32_t dwSeed = Swap32LE(message.def.dwSeed); const uint16_t wCI = Swap16LE(message.def.wCI); const auto wIndx = static_cast<_item_indexes>(Swap16LE(message.def.wIndx)); if (player.isOnActiveLevel()) { const int ii = SyncDropItem(message); if (ii != -1) { PutItemRecord(dwSeed, wCI, wIndx); DeltaPutItem(message, Items[ii].position, player); if (&player == MyPlayer) pfile_update(true); } } else { PutItemRecord(dwSeed, wCI, wIndx); DeltaPutItem(message, { message.x, message.y }, player); if (&player == MyPlayer) pfile_update(true); } } return sizeof(message); } size_t OnStandingAttackTile(const TCmdLoc &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && InDungeonBounds(position)) { ClrPlrPath(player); player.destAction = ACTION_ATTACK; player.destParam1 = position.x; player.destParam2 = position.y; } return sizeof(message); } size_t OnRangedAttackTile(const TCmdLoc &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && InDungeonBounds(position)) { ClrPlrPath(player); player.destAction = ACTION_RATTACK; player.destParam1 = position.x; player.destParam2 = position.y; } return sizeof(message); } bool InitNewSpell(Player &player, uint16_t wParamSpellID, uint16_t wParamSpellType, uint16_t wParamSpellFrom) { wParamSpellID = Swap16LE(wParamSpellID); wParamSpellType = Swap16LE(wParamSpellType); wParamSpellFrom = Swap16LE(wParamSpellFrom); if (wParamSpellID > static_cast(SpellID::LAST)) return false; auto spellID = static_cast(wParamSpellID); if (!IsValidSpell(spellID)) { LogError(_("{:s} has cast an invalid spell."), player._pName); return false; } if (leveltype == DTYPE_TOWN && !GetSpellData(spellID).isAllowedInTown()) { LogError(_("{:s} has cast an illegal spell."), player._pName); return false; } if (wParamSpellType > static_cast(SpellType::Invalid)) return false; if (wParamSpellFrom > INVITEM_BELT_LAST) return false; auto spellFrom = static_cast(wParamSpellFrom); if (!IsValidSpellFrom(spellFrom)) return false; player.queuedSpell.spellId = spellID; player.queuedSpell.spellType = static_cast(wParamSpellType); player.queuedSpell.spellFrom = spellFrom; return true; } size_t OnSpellWall(const TCmdLocParam4 &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) return sizeof(message); if (!InDungeonBounds(position)) return sizeof(message); const int16_t wParamDirection = Swap16LE(message.wParam3); if (wParamDirection > static_cast(Direction::SouthEast)) return sizeof(message); if (!InitNewSpell(player, message.wParam1, message.wParam2, message.wParam4)) return sizeof(message); ClrPlrPath(player); player.destAction = ACTION_SPELLWALL; player.destParam1 = position.x; player.destParam2 = position.y; player.destParam3 = wParamDirection; player.destParam4 = player.GetSpellLevel(player.queuedSpell.spellId); return sizeof(message); } size_t OnSpellTile(const TCmdLocParam3 &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) return sizeof(message); if (!InDungeonBounds(position)) return sizeof(message); if (!InitNewSpell(player, message.wParam1, message.wParam2, message.wParam3)) return sizeof(message); ClrPlrPath(player); player.destAction = ACTION_SPELL; player.destParam1 = position.x; player.destParam2 = position.y; player.destParam3 = player.GetSpellLevel(player.queuedSpell.spellId); return sizeof(message); } size_t OnObjectTileAction(const TCmdLoc &message, Player &player, action_id action, bool pathToObject = true) { const Point position { message.x, message.y }; const Object *object = FindObjectAtPosition(position); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && object != nullptr) { if (pathToObject) MakePlrPath(player, position, !object->_oSolidFlag && !object->_oDoorFlag); player.destAction = action; player.destParam1 = static_cast(object->GetId()); } return sizeof(message); } size_t OnObjectTileAction(const TCmdLoc &message, Player &player) { switch (message.bCmd) { case CMD_OPOBJXY: return OnObjectTileAction(message, player, ACTION_OPERATE); case CMD_DISARMXY: return OnObjectTileAction(message, player, ACTION_DISARM); case CMD_OPOBJT: return OnObjectTileAction(message, player, ACTION_OPERATETK, false); default: return sizeof(message); } } size_t OnAttackMonster(const TCmdParam1 &message, Player &player) { const uint16_t monsterIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && monsterIdx < MaxMonsters) { const Point position = Monsters[monsterIdx].position.future; if (player.position.tile.WalkingDistance(position) > 1) MakePlrPath(player, position, false); player.destAction = ACTION_ATTACKMON; player.destParam1 = monsterIdx; } return sizeof(message); } size_t OnAttackPlayer(const TCmdParam1 &message, Player &player) { const uint16_t playerIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && playerIdx < Players.size()) { MakePlrPath(player, Players[playerIdx].position.future, false); player.destAction = ACTION_ATTACKPLR; player.destParam1 = playerIdx; } return sizeof(message); } size_t OnRangedAttackMonster(const TCmdParam1 &message, Player &player) { const uint16_t monsterIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && monsterIdx < MaxMonsters) { ClrPlrPath(player); player.destAction = ACTION_RATTACKMON; player.destParam1 = monsterIdx; } return sizeof(message); } size_t OnRangedAttackPlayer(const TCmdParam1 &message, Player &player) { const uint16_t playerIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && playerIdx < Players.size()) { ClrPlrPath(player); player.destAction = ACTION_RATTACKPLR; player.destParam1 = playerIdx; } return sizeof(message); } size_t OnSpellMonster(const TCmdParam4 &message, Player &player) { if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) return sizeof(message); if (leveltype == DTYPE_TOWN) return sizeof(message); const uint16_t monsterIdx = Swap16LE(message.wParam1); if (monsterIdx >= MaxMonsters) return sizeof(message); if (!InitNewSpell(player, message.wParam2, message.wParam3, message.wParam4)) return sizeof(message); ClrPlrPath(player); player.destAction = ACTION_SPELLMON; player.destParam1 = monsterIdx; player.destParam2 = player.GetSpellLevel(player.queuedSpell.spellId); return sizeof(message); } size_t OnSpellPlayer(const TCmdParam4 &message, Player &player) { if (gbBufferMsgs == 1) return sizeof(message); if (!player.isOnActiveLevel()) return sizeof(message); const uint16_t playerIdx = Swap16LE(message.wParam1); if (playerIdx >= Players.size()) return sizeof(message); if (!InitNewSpell(player, message.wParam2, message.wParam3, message.wParam4)) return sizeof(message); ClrPlrPath(player); player.destAction = ACTION_SPELLPLR; player.destParam1 = playerIdx; player.destParam2 = player.GetSpellLevel(player.queuedSpell.spellId); return sizeof(message); } size_t OnKnockback(const TCmdParam1 &message, Player &player) { const uint16_t monsterIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && leveltype != DTYPE_TOWN && monsterIdx < MaxMonsters) { Monster &monster = Monsters[monsterIdx]; M_GetKnockback(monster, player.position.tile); M_StartHit(monster, player, 0); } return sizeof(message); } size_t OnResurrect(const TCmdParam1 &message, Player &caster) { const uint16_t playerIdx = Swap16LE(message.wParam1); if (gbBufferMsgs == 1) { BufferMessage(caster, &message, sizeof(message)); return sizeof(message); } if (playerIdx >= Players.size()) return sizeof(message); Player &target = Players[playerIdx]; SpawnResurrectBeam(caster, target); if (&target == MyPlayer && target._pHitPoints <= 0) { NetSendCmd(true, CMD_PLRALIVE); } return sizeof(message); } size_t OnPlayerAlive(const TCmd &message, Player &target) { if (gbBufferMsgs == 1) { BufferMessage(target, &message, sizeof(message)); return sizeof(message); } ApplyResurrect(target); return sizeof(message); } size_t OnHealOther(const TCmdParam1 &message, const Player &caster) { const uint16_t playerIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (caster.isOnActiveLevel() && playerIdx < Players.size()) { DoHealOther(caster, Players[playerIdx]); } } return sizeof(message); } size_t OnTalkXY(const TCmdLocParam1 &message, Player &player) { const Point position { message.x, message.y }; const uint16_t townerIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1 && player.isOnActiveLevel() && InDungeonBounds(position) && townerIdx < GetNumTowners()) { MakePlrPath(player, position, false); player.destAction = ACTION_TALK; player.destParam1 = townerIdx; } return sizeof(message); } size_t OnNewLevel(const TCmdParam2 &message, Player &player) { const uint16_t eventIdx = Swap16LE(message.wParam1); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer) { if (eventIdx < WM_FIRST || eventIdx > WM_LAST) return sizeof(message); auto mode = static_cast(eventIdx); const auto levelId = static_cast(Swap16LE(message.wParam2)); if (!IsValidLevel(levelId, mode == WM_DIABSETLVL)) { return sizeof(message); } StartNewLvl(player, mode, levelId); } return sizeof(message); } size_t OnWarp(const TCmdParam1 &message, Player &player) { const uint16_t portalIdx = Swap16LE(message.wParam1); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (portalIdx < MAXPORTAL) { StartWarpLvl(player, portalIdx); } return sizeof(message); } size_t OnMonstDeath(const TCmdLocParam1 &message, Player &player) { const Point position { message.x, message.y }; const uint16_t monsterIdx = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (&player != MyPlayer && player.plrlevel > 0 && InDungeonBounds(position) && monsterIdx < MaxMonsters) { Monster &monster = Monsters[monsterIdx]; if (player.isOnActiveLevel()) M_SyncStartKill(monster, position, player); delta_kill_monster(monster, position, player); } } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnRequestSpawnGolem(const TCmdLocParam1 &message, const Player &player) { if (gbBufferMsgs == 1) return sizeof(message); const WorldTilePosition position { message.x, message.y }; if (player.plrlevel > 0 && player.isLevelOwnedByLocalClient() && InDungeonBounds(position)) SpawnGolem(player, position, static_cast(message.wParam1)); return sizeof(message); } size_t OnMonstDamage(const TCmdMonDamage &message, Player &player) { const uint16_t monsterIdx = Swap16LE(message.wMon); if (gbBufferMsgs != 1) { if (&player != MyPlayer) { if (player.isOnActiveLevel() && leveltype != DTYPE_TOWN && monsterIdx < MaxMonsters) { Monster &monster = Monsters[monsterIdx]; monster.tag(player); if (monster.hitPoints > 0) { monster.hitPoints -= Swap32LE(message.dwDam); if ((monster.hitPoints >> 6) < 1) monster.hitPoints = 1 << 6; delta_monster_hp(monster, player); } } } } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnPlayerDeath(const TCmdParam1 &message, Player &player) { const auto deathReason = static_cast(Swap16LE(message.wParam1)); if (gbBufferMsgs != 1) { if (&player != MyPlayer) StartPlayerKill(player, deathReason); else pfile_update(true); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnPlayerDamage(const TCmdDamage &message, Player &player) { const uint32_t damage = Swap32LE(message.dwDam); Player &target = Players[message.bPlr]; if (&target == MyPlayer && leveltype != DTYPE_TOWN && gbBufferMsgs != 1) { if (player.isOnActiveLevel() && damage <= 192000 && !target.hasNoLife()) { ApplyPlrDamage(message.damageType, target, 0, 0, static_cast(damage), DeathReason::Player); } } return sizeof(message); } size_t OnOperateObject(const TCmdLoc &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else { const WorldTilePosition position { message.x, message.y }; assert(InDungeonBounds(position)); if (player.isOnActiveLevel()) { Object *object = FindObjectAtPosition(position); if (object != nullptr) SyncOpObject(player, message.bCmd, *object); } if (player.plrlevel > 0) { DeltaSyncObject(position, message.bCmd, player); } } return sizeof(message); } size_t OnBreakObject(const TCmdLoc &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else { const WorldTilePosition position { message.x, message.y }; assert(InDungeonBounds(position)); if (player.isOnActiveLevel()) { Object *object = FindObjectAtPosition(position); if (object != nullptr) SyncBreakObj(player, *object); } if (player.plrlevel > 0) { DeltaSyncObject(position, CMD_BREAKOBJ, player); } } return sizeof(message); } size_t OnChangePlayerItems(const TCmdChItem &message, Player &player) { if (message.bLoc >= NUM_INVLOC) return sizeof(message); auto bodyLocation = static_cast(message.bLoc); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer && IsItemAvailable(static_cast<_item_indexes>(Swap16LE(message.def.wIndx)))) { Item &item = player.InvBody[message.bLoc]; item = {}; RecreateItem(player, message, item); CheckInvSwap(player, bodyLocation); } player.ReadySpellFromEquipment(bodyLocation, message.forceSpell); return sizeof(message); } size_t OnDeletePlayerItems(const TCmdDelItem &message, Player &player) { if (gbBufferMsgs != 1) { if (&player != MyPlayer && message.bLoc < NUM_INVLOC) inv_update_rem_item(player, static_cast(message.bLoc)); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnChangeInventoryItems(const TCmdChItem &message, Player &player) { if (message.bLoc >= InventoryGridCells) return sizeof(message); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer && IsItemAvailable(static_cast<_item_indexes>(Swap16LE(message.def.wIndx)))) { Item item {}; RecreateItem(player, message, item); CheckInvSwap(player, item, message.bLoc); } return sizeof(message); } size_t OnDeleteInventoryItems(const TCmdParam1 &message, Player &player) { const uint16_t invGridIndex = Swap16LE(message.wParam1); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer && invGridIndex < InventoryGridCells) { CheckInvRemove(player, invGridIndex); } return sizeof(message); } size_t OnChangeBeltItems(const TCmdChItem &message, Player &player) { if (message.bLoc >= MaxBeltItems) return sizeof(message); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer && IsItemAvailable(static_cast<_item_indexes>(Swap16LE(message.def.wIndx)))) { Item &item = player.SpdList[message.bLoc]; item = {}; RecreateItem(player, message, item); } return sizeof(message); } size_t OnDeleteBeltItems(const TCmdParam1 &message, Player &player) { const uint16_t spdBarIndex = Swap16LE(message.wParam1); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (&player != MyPlayer && spdBarIndex < MaxBeltItems) { player.RemoveSpdBarItem(spdBarIndex); } return sizeof(message); } size_t OnPlayerLevel(const TCmdParam1 &message, Player &player) { const uint16_t playerLevel = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (playerLevel <= player.getMaxCharacterLevel() && &player != MyPlayer) player.setCharacterLevel(static_cast(playerLevel)); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnDropItem(const TCmdPItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { DeltaPutItem(message, { message.x, message.y }, player); } return sizeof(message); } size_t OnSpawnItem(const TCmdPItem &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (IsPItemValid(message, player)) { if (player.isOnActiveLevel() && &player != MyPlayer) { SyncDropItem(message); } PutItemRecord(Swap32LE(message.def.dwSeed), Swap16LE(message.def.wCI), static_cast<_item_indexes>(Swap16LE(message.def.wIndx))); DeltaPutItem(message, { message.x, message.y }, player); } return sizeof(message); } size_t OnSendPlayerInfo(const TCmdPlrInfoHdr &header, size_t maxCmdSize, Player &player) { const uint16_t wBytes = Swap16LE(header.wBytes); if (!ValidateCmdSize(wBytes + sizeof(header), maxCmdSize, player.getId())) return maxCmdSize; if (gbBufferMsgs == 1) BufferMessage(player, &header, wBytes + sizeof(header)); else recv_plrinfo(player, header, header.bCmd == CMD_ACK_PLRINFO); return wBytes + sizeof(header); } size_t OnPlayerJoinLevel(const TCmdLocParam2 &message, Player &player) { const Point position { message.x, message.y }; if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); return sizeof(message); } const auto playerLevel = static_cast(Swap16LE(message.wParam1)); const bool isSetLevel = message.wParam2 != 0; if (!IsValidLevel(playerLevel, isSetLevel) || !InDungeonBounds(position)) { return sizeof(message); } player._pLvlChanging = false; if (player._pName[0] != '\0' && !player.plractive) { ResetPlayerGFX(player); player.plractive = true; gbActivePlayers++; EventPlrMsg(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), player._pName, player.getCharacterLevel())); } if (player.plractive && &player != MyPlayer) { if (player.isOnActiveLevel()) { RemoveEnemyReferences(player); RemovePlrMissiles(player); FixPlrWalkTags(player); } player.position.tile = position; SetPlayerOld(player); if (isSetLevel) player.setLevel(static_cast<_setlevels>(playerLevel)); else player.setLevel(playerLevel); ResetPlayerGFX(player); if (player.isOnActiveLevel()) { SyncInitPlr(player); if (!player.hasNoLife()) { StartStand(player, Direction::South); } else { player._pgfxnum &= ~0xFU; player._pmode = PM_DEATH; NewPlrAnim(player, player_graphic::Death, Direction::South); player.AnimInfo.currentFrame = static_cast(player.AnimInfo.numberOfFrames - 2); dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; } ActivateVision(player.position.tile, player._pLightRad, player.getId()); } } return sizeof(message); } size_t OnActivatePortal(const TCmdLocParam3 &message, Player &player) { const Point position { message.x, message.y }; const auto level = static_cast(Swap16LE(message.wParam1)); const uint16_t dungeonTypeIdx = Swap16LE(message.wParam2); const bool isSetLevel = message.wParam3 != 0; if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else if (InDungeonBounds(position) && IsValidLevel(level, isSetLevel) && dungeonTypeIdx <= DTYPE_LAST) { auto dungeonType = static_cast(dungeonTypeIdx); ActivatePortal(player, position, level, dungeonType, isSetLevel); if (&player != MyPlayer) { if (leveltype == DTYPE_TOWN) { AddPortalInTown(player); } else if (player.isOnActiveLevel()) { bool addPortal = true; for (auto &missile : Missiles) { if (missile._mitype == MissileID::TownPortal && &Players[missile._misource] == &player) { addPortal = false; break; } } if (addPortal) { AddPortalMissile(player, position, false); } } else { RemovePortalMissile(player); } } DeltaOpenPortal(player.getId(), position, level, dungeonType, isSetLevel); } return sizeof(message); } size_t OnDeactivatePortal(const TCmd &cmd, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &cmd, sizeof(cmd)); } else { if (PortalOnLevel(player)) RemovePortalMissile(player); DeactivatePortal(player); delta_close_portal(player); } return sizeof(cmd); } size_t OnRestartTown(const TCmd &cmd, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &cmd, sizeof(cmd)); } else { if (&player == MyPlayer) { MyPlayerIsDead = false; gamemenu_off(); } RestartTownLvl(player); } return sizeof(cmd); } size_t OnSetStrength(const TCmdParam1 &message, Player &player) { const uint16_t value = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (value <= 750 && &player != MyPlayer) SetPlrStr(player, value); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnSetDexterity(const TCmdParam1 &message, Player &player) { const uint16_t value = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (value <= 750 && &player != MyPlayer) SetPlrDex(player, value); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnSetMagic(const TCmdParam1 &message, Player &player) { const uint16_t value = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (value <= 750 && &player != MyPlayer) SetPlrMag(player, value); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnSetVitality(const TCmdParam1 &message, Player &player) { const uint16_t value = Swap16LE(message.wParam1); if (gbBufferMsgs != 1) { if (value <= 750 && &player != MyPlayer) SetPlrVit(player, value); } else { BufferMessage(player, &message, sizeof(message)); } return sizeof(message); } size_t OnString(const TCmd &cmd, size_t maxCmdSize, Player &player) { const auto &message = reinterpret_cast(cmd); const size_t headerSize = sizeof(message) - sizeof(message.str); const size_t maxLength = std::min(MAX_SEND_STR_LEN, maxCmdSize - headerSize); const std::string_view str { message.str, maxLength }; const auto tokens = SplitByChar(str, '\0'); const std::string_view playerMessage = *tokens.begin(); if (gbBufferMsgs == 0) SendPlrMsg(player, playerMessage); const size_t nullSize = str.size() != playerMessage.size() ? 1 : 0; return headerSize + playerMessage.size() + nullSize; } size_t OnFriendlyMode(const TCmd &cmd, Player &player) // NOLINT(misc-unused-parameters) { player.friendlyMode = !player.friendlyMode; RedrawEverything(); return sizeof(cmd); } size_t OnSyncQuest(const TCmdQuest &message, Player &player) { if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else { if (&player != MyPlayer && message.q < MAXQUESTS && message.qstate <= QUEST_HIVE_DONE) SetMultiQuest(message.q, message.qstate, message.qlog != 0, message.qvar1, message.qvar2, Swap16LE(message.qmsg)); } return sizeof(message); } size_t OnCheatExperience(const TCmd &cmd, Player &player) // NOLINT(misc-unused-parameters) { #ifdef _DEBUG if (gbBufferMsgs == 1) BufferMessage(player, &cmd, sizeof(cmd)); else if (!player.isMaxCharacterLevel()) { player._pExperience = player.getNextExperienceThreshold(); if (*GetOptions().Gameplay.experienceBar) { RedrawEverything(); } NextPlrLevel(player); } #endif return sizeof(cmd); } size_t OnChangeSpellLevel(const TCmdParam2 &message, Player &player) // NOLINT(misc-unused-parameters) { const auto spellID = static_cast(Swap16LE(message.wParam1)); const uint8_t spellLevel = std::min(static_cast(Swap16LE(message.wParam2)), MaxSpellLevel); if (gbBufferMsgs == 1) { BufferMessage(player, &message, sizeof(message)); } else { player._pMemSpells |= GetSpellBitmask(spellID); player._pSplLvl[static_cast(spellID)] = spellLevel; } return sizeof(message); } size_t OnDebug(const TCmd &pCmd) { return sizeof(pCmd); } size_t OnSetShield(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) player.pManaShield = true; return sizeof(cmd); } size_t OnRemoveShield(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) player.pManaShield = false; return sizeof(cmd); } size_t OnSetReflect(const TCmdParam1 &message, Player &player) { if (gbBufferMsgs != 1) player.wReflections = Swap16LE(message.wParam1); return sizeof(message); } size_t OnNakrul(const TCmd &cmd) { if (gbBufferMsgs != 1) { if (currlevel == 24) { PlaySfxLoc(SfxID::CryptDoorOpen, { UberRow, UberCol }); SyncNakrulRoom(); } IsUberRoomOpened = true; Quests[Q_NAKRUL]._qactive = QUEST_DONE; WeakenNaKrul(); } return sizeof(cmd); } size_t OnOpenHive(const TCmd &cmd, Player &player) { if (gbBufferMsgs != 1) { AddMissile({ 0, 0 }, { 0, 0 }, Direction::South, MissileID::OpenNest, TARGET_MONSTERS, player, 0, 0); TownOpenHive(); InitTownTriggers(); } return sizeof(cmd); } size_t OnOpenGrave(const TCmd &cmd) { if (gbBufferMsgs != 1) { TownOpenGrave(); InitTownTriggers(); if (leveltype == DTYPE_TOWN) PlaySFX(SfxID::Sarcophagus); } return sizeof(cmd); } size_t OnSpawnMonster(const TCmdSpawnMonster &message, const Player &player) { if (gbBufferMsgs == 1) return sizeof(message); if (player.plrlevel == 0) return sizeof(message); const WorldTilePosition position { message.x, message.y }; auto typeIndex = static_cast(Swap16LE(message.typeIndex)); auto monsterId = static_cast(Swap16LE(message.monsterId)); const uint8_t golemOwnerPlayerId = message.golemOwnerPlayerId; if (golemOwnerPlayerId >= Players.size()) { return sizeof(message); } const uint8_t golemSpellLevel = std::min(message.golemSpellLevel, static_cast(MaxSpellLevel + Players[golemOwnerPlayerId]._pISplLvlAdd)); DLevel &deltaLevel = GetDeltaLevel(player); deltaLevel.spawnedMonsters[monsterId] = { typeIndex, message.seed, golemOwnerPlayerId, golemSpellLevel }; // Override old monster delta information auto &deltaMonster = deltaLevel.monster[monsterId]; deltaMonster.position = position; deltaMonster.hitPoints = -1; deltaMonster.menemy = 0; deltaMonster.mactive = 0; if (player.isOnActiveLevel() && &player != MyPlayer) InitializeSpawnedMonster(position, message.dir, typeIndex, monsterId, message.seed, golemOwnerPlayerId, golemSpellLevel); return sizeof(message); } template size_t HandleCmd(size_t (*handler)(const TCmdImpl &, size_t, Player &), Player &player, const TCmd *pCmd, size_t maxCmdSize) { if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) return maxCmdSize; const auto *message = reinterpret_cast(pCmd); return handler(*message, maxCmdSize, player); } template size_t HandleCmd(size_t (*handler)(const TCmdImpl &, size_t, const Player &), const Player &player, const TCmd *pCmd, size_t maxCmdSize) { if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) return maxCmdSize; const auto *message = reinterpret_cast(pCmd); return handler(*message, maxCmdSize, player); } template size_t HandleCmd(size_t (*handler)(const TCmdImpl &, Player &), Player &player, const TCmd *pCmd, size_t maxCmdSize) { if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) return maxCmdSize; const auto *message = reinterpret_cast(pCmd); return handler(*message, player); } template size_t HandleCmd(size_t (*handler)(const TCmdImpl &, const Player &), const Player &player, const TCmd *pCmd, size_t maxCmdSize) { if (!ValidateCmdSize(sizeof(TCmdImpl), maxCmdSize, player.getId())) return maxCmdSize; const auto *message = reinterpret_cast(pCmd); return handler(*message, player); } } // namespace void PrepareItemForNetwork(const Item &item, TItem &messageItem) { messageItem.bId = item._iIdentified ? 1 : 0; messageItem.bDur = item._iDurability; messageItem.bMDur = item._iMaxDur; messageItem.bCh = item._iCharges; messageItem.bMCh = item._iMaxCharges; messageItem.wValue = Swap16LE(item._ivalue); messageItem.wToHit = Swap16LE(item._iPLToHit); messageItem.wMaxDam = Swap16LE(item._iMaxDam); messageItem.dwBuff = Swap32LE(item.dwBuff); } void PrepareEarForNetwork(const Item &item, TEar &ear) { ear.bCursval = item._ivalue | ((item._iCurs - ICURS_EAR_SORCERER) << 6); CopyUtf8(ear.heroname, item._iIName, sizeof(ear.heroname)); } void RecreateItem(const Player &player, const TItem &messageItem, Item &item) { const uint32_t dwBuff = Swap32LE(messageItem.dwBuff); RecreateItem(player, item, static_cast<_item_indexes>(Swap16LE(messageItem.wIndx)), Swap16LE(messageItem.wCI), Swap32LE(messageItem.dwSeed), Swap16LE(messageItem.wValue), dwBuff); if (messageItem.bId != 0) item._iIdentified = true; item._iMaxDur = messageItem.bMDur; item._iDurability = ClampDurability(item, messageItem.bDur); item._iMaxCharges = std::clamp(messageItem.bMCh, 0, item._iMaxCharges); item._iCharges = std::clamp(messageItem.bCh, 0, item._iMaxCharges); if (gbIsHellfire) { item._iPLToHit = ClampToHit(item, static_cast(Swap16LE(messageItem.wToHit))); item._iMaxDam = ClampMaxDam(item, static_cast(Swap16LE(messageItem.wMaxDam))); } } void ClearLastSentPlayerCmd() { lastSentPlayerCmd = {}; } void msg_send_drop_pkt(uint8_t pnum, leaveinfo_t reason) { TFakeDropPlr cmd; cmd.dwReason = Swap32LE(static_cast(reason)); cmd.bCmd = FAKE_CMD_DROPID; cmd.bPlr = pnum; BufferMessage(pnum, &cmd, sizeof(cmd)); } bool msg_wait_resync() { bool success; GetNextPacket(); sgbDeltaChunks = 0; sgnCurrMegaPlayer = -1; sgbRecvCmd = CMD_DLEVEL_END; gbBufferMsgs = 1; sgdwOwnerWait = SDL_GetTicks(); success = UiProgressDialog(WaitForTurns); gbBufferMsgs = 0; if (!success) { FreePackets(); return false; } if (gbGameDestroyed) { UiErrorOkDialog(PROJECT_NAME, _("The game ended"), /*error=*/false); FreePackets(); return false; } if (sgbDeltaChunks != MaxChunks) { UiErrorOkDialog(PROJECT_NAME, _("Unable to get level data"), /*error=*/false); FreePackets(); return false; } return true; } void run_delta_info() { if (!gbIsMultiplayer) return; gbBufferMsgs = 2; PrePacket(); gbBufferMsgs = 0; FreePackets(); } void DeltaExportData(uint8_t pnum) { for (const auto &[levelNum, deltaLevel] : DeltaLevels) { const size_t bufferSize = 1U /* marker byte, always 0 */ + sizeof(uint8_t) /* level id */ + sizeof(deltaLevel.item) /* items spawned during dungeon generation which have been picked up, and items dropped by a player during a game */ + sizeof(uint8_t) /* count of object interactions which caused a state change since dungeon generation */ + (sizeof(WorldTilePosition) + sizeof(DObjectStr)) * deltaLevel.object.size() /* location/action pairs for the object interactions */ + sizeof(deltaLevel.monster) /* latest monster state */ + sizeof(uint16_t) /* spawned monster count */ + (sizeof(uint16_t) + sizeof(DSpawnedMonster)) * deltaLevel.spawnedMonsters.size(); /* spawned monsters */ const std::unique_ptr dst { new std::byte[bufferSize] }; std::byte *dstEnd = &dst.get()[1]; *dstEnd = static_cast(levelNum); dstEnd += sizeof(uint8_t); dstEnd = DeltaExportItem(dstEnd, deltaLevel.item); dstEnd = DeltaExportObject(dstEnd, deltaLevel.object); dstEnd = DeltaExportMonster(dstEnd, deltaLevel.monster); dstEnd = DeltaExportSpawnedMonsters(dstEnd, deltaLevel.spawnedMonsters); const uint32_t size = CompressData(dst.get(), dstEnd); multi_send_zero_packet(pnum, CMD_DLEVEL, dst.get(), size); } std::byte dst[sizeof(DJunk) + 1]; std::byte *dstEnd = &dst[1]; dstEnd = DeltaExportJunk(dstEnd); const uint32_t size = CompressData(dst, dstEnd); multi_send_zero_packet(pnum, CMD_DLEVEL_JUNK, dst, size); std::byte src[1] = { static_cast(0) }; multi_send_zero_packet(pnum, CMD_DLEVEL_END, src, 1); } void delta_init() { memset(&sgJunk, 0xFF, sizeof(sgJunk)); DeltaLevels.clear(); LocalLevels.clear(); } void DeltaClearLevel(uint8_t level) { DeltaLevels.erase(level); LocalLevels.erase(level); } void delta_kill_monster(const Monster &monster, Point position, const Player &player) { if (!gbIsMultiplayer) return; DMonsterStr *pD = &GetDeltaLevel(player).monster[monster.getId()]; pD->position = position; pD->hitPoints = 0; } void delta_monster_hp(const Monster &monster, const Player &player) { if (!gbIsMultiplayer) return; DMonsterStr *pD = &GetDeltaLevel(player).monster[monster.getId()]; if (SwapSigned32LE(pD->hitPoints) > monster.hitPoints) pD->hitPoints = SwapSigned32LE(monster.hitPoints); } void delta_sync_monster(const TSyncMonster &monsterSync, uint8_t level) { if (!gbIsMultiplayer) return; assert(level <= MaxMultiplayerLevels); DMonsterStr &monster = GetDeltaLevel(level).monster[monsterSync._mndx]; if (monster.hitPoints == 0) return; monster.position.x = monsterSync._mx; monster.position.y = monsterSync._my; monster.mactive = UINT8_MAX; monster.menemy = monsterSync._menemy; monster.hitPoints = monsterSync._mhitpoints; monster.mWhoHit = monsterSync.mWhoHit; } void DeltaSyncJunk() { for (int i = 0; i < MAXPORTAL; i++) { if (sgJunk.portal[i].x == 0xFF) { SetPortalStats(i, false, { 0, 0 }, 0, DTYPE_TOWN, false); } else { SetPortalStats( i, true, { sgJunk.portal[i].x, sgJunk.portal[i].y }, sgJunk.portal[i].level, (dungeon_type)sgJunk.portal[i].ltype, sgJunk.portal[i].setlvl != 0); } } int q = 0; for (auto &quest : Quests) { if (QuestsData[quest._qidx].isSinglePlayerOnly && UseMultiplayerQuests()) { continue; } if (sgJunk.quests[q].qstate != QUEST_INVALID) { quest._qlog = sgJunk.quests[q].qlog != 0; quest._qactive = sgJunk.quests[q].qstate; quest._qvar1 = sgJunk.quests[q].qvar1; quest._qvar2 = sgJunk.quests[q].qvar2; quest._qmsg = static_cast<_speech_id>(Swap16LE(sgJunk.quests[q].qmsg)); } q++; } } void DeltaAddItem(int ii) { if (!gbIsMultiplayer) return; const uint8_t localLevel = GetLevelForMultiplayer(*MyPlayer); DLevel &deltaLevel = GetDeltaLevel(localLevel); for (const TCmdPItem &item : deltaLevel.item) { if (item.bCmd != CMD_INVALID && static_cast<_item_indexes>(Swap16LE(item.def.wIndx)) == Items[ii].IDidx && Swap16LE(item.def.wCI) == Items[ii]._iCreateInfo && static_cast(Swap32LE(item.def.dwSeed)) == Items[ii]._iSeed && IsAnyOf(item.bCmd, TCmdPItem::PickedUpItem, TCmdPItem::FloorItem)) { return; } } for (TCmdPItem &delta : deltaLevel.item) { if (delta.bCmd != CMD_INVALID) continue; delta.bCmd = TCmdPItem::FloorItem; delta.x = Items[ii].position.x; delta.y = Items[ii].position.y; PrepareItemForNetwork(Items[ii], delta); return; } } void DeltaSaveLevel() { if (!gbIsMultiplayer) return; for (Player &player : Players) { if (&player != MyPlayer) ResetPlayerGFX(player); } uint8_t localLevel; if (setlevel) { localLevel = GetLevelForMultiplayer(static_cast(setlvlnum), setlevel); MyPlayer->_pSLvlVisited[static_cast(setlvlnum)] = true; } else { localLevel = GetLevelForMultiplayer(currlevel, setlevel); MyPlayer->_pLvlVisited[currlevel] = true; } DeltaLeaveSync(localLevel); } uint8_t GetLevelForMultiplayer(const Player &player) { return GetLevelForMultiplayer(player.plrlevel, player.plrIsOnSetLevel); } bool IsValidLevelForMultiplayer(uint8_t level) { return level <= MaxMultiplayerLevels; } bool IsValidLevel(uint8_t level, bool isSetLevel) { if (isSetLevel) return level <= SL_LAST; return level < NUMLEVELS; } void DeltaLoadLevel() { if (!gbIsMultiplayer) return; const uint8_t localLevel = GetLevelForMultiplayer(*MyPlayer); DLevel &deltaLevel = GetDeltaLevel(localLevel); if (leveltype != DTYPE_TOWN) { DeltaLoadSpawnedMonsters(deltaLevel); DeltaLoadMonsters(deltaLevel); auto localLevelIt = LocalLevels.find(localLevel); if (localLevelIt != LocalLevels.end()) memcpy(AutomapView, &localLevelIt->second, sizeof(AutomapView)); else memset(AutomapView, 0, sizeof(AutomapView)); DeltaLoadObjects(deltaLevel); } DeltaLoadItems(deltaLevel); } void NetSendCmd(bool bHiPri, _cmd_id bCmd) { TCmd cmd; cmd.bCmd = bCmd; if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdSpawnMonster(Point position, Direction dir, uint16_t typeIndex, uint16_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, uint8_t golemSpellLevel) { TCmdSpawnMonster cmd; cmd.bCmd = CMD_SPAWNMONSTER; cmd.x = position.x; cmd.y = position.y; cmd.dir = dir; cmd.typeIndex = Swap16LE(typeIndex); cmd.monsterId = Swap16LE(monsterId); cmd.seed = Swap32LE(seed); cmd.golemOwnerPlayerId = golemOwnerPlayerId; cmd.golemSpellLevel = golemSpellLevel; NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdLoc(uint8_t playerId, bool bHiPri, _cmd_id bCmd, Point position) { if (playerId == MyPlayerId && WasPlayerCmdAlreadyRequested(bCmd, position)) return; TCmdLoc cmd; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; if (bHiPri) NetSendHiPri(playerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(playerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, position, 0, 0); } void NetSendCmdLocParam1(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1) { if (WasPlayerCmdAlreadyRequested(bCmd, position, wParam1)) return; TCmdLocParam1 cmd; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; cmd.wParam1 = Swap16LE(wParam1); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, position, wParam1, 0); } void NetSendCmdLocParam2(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2) { if (WasPlayerCmdAlreadyRequested(bCmd, position, wParam1, wParam2)) return; TCmdLocParam2 cmd; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; cmd.wParam1 = Swap16LE(wParam1); cmd.wParam2 = Swap16LE(wParam2); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, position, wParam1, wParam2); } void NetSendCmdLocParam3(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3) { if (WasPlayerCmdAlreadyRequested(bCmd, position, wParam1, wParam2, wParam3)) return; TCmdLocParam3 cmd; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; cmd.wParam1 = Swap16LE(wParam1); cmd.wParam2 = Swap16LE(wParam2); cmd.wParam3 = Swap16LE(wParam3); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, position, wParam1, wParam2); } void NetSendCmdLocParam4(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3, uint16_t wParam4) { if (WasPlayerCmdAlreadyRequested(bCmd, position, wParam1, wParam2, wParam3, wParam4)) return; TCmdLocParam4 cmd; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; cmd.wParam1 = Swap16LE(wParam1); cmd.wParam2 = Swap16LE(wParam2); cmd.wParam3 = Swap16LE(wParam3); cmd.wParam4 = Swap16LE(wParam4); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, position, wParam1, wParam3); } void NetSendCmdParam1(bool bHiPri, _cmd_id bCmd, uint16_t wParam1) { if (WasPlayerCmdAlreadyRequested(bCmd, {}, wParam1)) return; TCmdParam1 cmd; cmd.bCmd = bCmd; cmd.wParam1 = Swap16LE(wParam1); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, {}, wParam1, 0); } void NetSendCmdParam2(bool bHiPri, _cmd_id bCmd, uint16_t wParam1, uint16_t wParam2) { TCmdParam2 cmd; cmd.bCmd = bCmd; cmd.wParam1 = Swap16LE(wParam1); cmd.wParam2 = Swap16LE(wParam2); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdParam4(bool bHiPri, _cmd_id bCmd, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3, uint16_t wParam4) { if (WasPlayerCmdAlreadyRequested(bCmd, {}, wParam1, wParam2, wParam3, wParam4)) return; TCmdParam4 cmd; cmd.bCmd = bCmd; cmd.wParam1 = Swap16LE(wParam1); cmd.wParam2 = Swap16LE(wParam2); cmd.wParam3 = Swap16LE(wParam3); cmd.wParam4 = Swap16LE(wParam4); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); MyPlayer->UpdatePreviewCelSprite(bCmd, {}, wParam1, wParam2); } void NetSendCmdQuest(bool bHiPri, const Quest &quest) { TCmdQuest cmd; cmd.bCmd = CMD_SYNCQUEST; cmd.q = quest._qidx, cmd.qstate = quest._qactive; cmd.qlog = quest._qlog ? 1 : 0; cmd.qvar1 = quest._qvar1; cmd.qvar2 = quest._qvar2; cmd.qmsg = Swap16LE(quest._qmsg); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdGItem(bool bHiPri, _cmd_id bCmd, const Player &player, uint8_t ii) { const uint8_t pnum = player.getId(); TCmdGItem cmd; cmd.bCmd = bCmd; cmd.bPnum = pnum; cmd.bMaster = pnum; cmd.bLevel = GetLevelForMultiplayer(*MyPlayer); cmd.bCursitem = ii; cmd.dwTime = 0; cmd.x = Items[ii].position.x; cmd.y = Items[ii].position.y; PrepareItemForNetwork(Items[ii], cmd); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdPItem(bool bHiPri, _cmd_id bCmd, Point position, const Item &item) { TCmdPItem cmd {}; cmd.bCmd = bCmd; cmd.x = position.x; cmd.y = position.y; PrepareItemForNetwork(item, cmd); ItemLimbo = item; if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdChItem(bool bHiPri, uint8_t bLoc, bool forceSpellChange) { TCmdChItem cmd {}; const Item &item = MyPlayer->InvBody[bLoc]; cmd.bCmd = CMD_CHANGEPLRITEMS; cmd.bLoc = bLoc; cmd.forceSpell = forceSpellChange; PrepareItemForNetwork(item, cmd); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdDelItem(bool bHiPri, uint8_t bLoc) { TCmdDelItem cmd; cmd.bLoc = bLoc; cmd.bCmd = CMD_DELPLRITEMS; if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSyncInvItem(const Player &player, int invListIndex) { if (&player != MyPlayer) return; for (int j = 0; j < InventoryGridCells; j++) { if (player.InvGrid[j] == invListIndex + 1) { NetSendCmdChInvItem(false, j); break; } } } void NetSendCmdChInvItem(bool bHiPri, int invGridIndex) { TCmdChItem cmd {}; const int invListIndex = std::abs(MyPlayer->InvGrid[invGridIndex]) - 1; const Item &item = MyPlayer->InvList[invListIndex]; cmd.bCmd = CMD_CHANGEINVITEMS; cmd.bLoc = invGridIndex; PrepareItemForNetwork(item, cmd); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdChBeltItem(bool bHiPri, int beltIndex) { TCmdChItem cmd {}; const Item &item = MyPlayer->SpdList[beltIndex]; cmd.bCmd = CMD_CHANGEBELTITEMS; cmd.bLoc = beltIndex; PrepareItemForNetwork(item, cmd); if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdDamage(bool bHiPri, const Player &player, uint32_t dwDam, DamageType damageType) { TCmdDamage cmd; cmd.bCmd = CMD_PLRDAMAGE; cmd.bPlr = player.getId(); cmd.dwDam = dwDam; cmd.damageType = damageType; if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdMonDmg(bool bHiPri, uint16_t wMon, uint32_t dwDam) { TCmdMonDamage cmd; cmd.bCmd = CMD_MONSTDAMAGE; cmd.wMon = wMon; cmd.dwDam = dwDam; if (bHiPri) NetSendHiPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); else NetSendLoPri(MyPlayerId, reinterpret_cast(&cmd), sizeof(cmd)); } void NetSendCmdString(uint32_t pmask, const char *pszStr) { TCmdString cmd; cmd.bCmd = CMD_STRING; CopyUtf8(cmd.str, pszStr, sizeof(cmd.str)); multi_send_msg_packet(pmask, reinterpret_cast(&cmd), strlen(cmd.str) + 2); } void delta_close_portal(const Player &player) { memset(&sgJunk.portal[player.getId()], 0xFF, sizeof(sgJunk.portal[player.getId()])); } bool ValidateCmdSize(size_t requiredCmdSize, size_t maxCmdSize, size_t playerId) { if (requiredCmdSize <= maxCmdSize) return true; Log("Suspiciously small packet size, dropping player {}", playerId); SNetDropPlayer(static_cast(playerId), leaveinfo_t::LEAVE_DROP); return false; } size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize) { sbLastCmd = pCmd->bCmd; if (sgwPackPlrOffsetTbl[pnum] != 0 && sbLastCmd != CMD_ACK_PLRINFO && sbLastCmd != CMD_SEND_PLRINFO) return 0; Player &player = Players[pnum]; #ifdef LOG_RECEIVED_MESSAGES Log("📥 {}", CmdIdString(pCmd->bCmd)); #endif switch (pCmd->bCmd) { case CMD_SYNCDATA: return HandleCmd(OnSyncData, player, pCmd, maxCmdSize); case CMD_WALKXY: return HandleCmd(OnWalk, player, pCmd, maxCmdSize); case CMD_ADDSTR: return HandleCmd(OnAddStrength, player, pCmd, maxCmdSize); case CMD_ADDDEX: return HandleCmd(OnAddDexterity, player, pCmd, maxCmdSize); case CMD_ADDMAG: return HandleCmd(OnAddMagic, player, pCmd, maxCmdSize); case CMD_ADDVIT: return HandleCmd(OnAddVitality, player, pCmd, maxCmdSize); case CMD_GOTOGETITEM: return HandleCmd(OnGotoGetItem, player, pCmd, maxCmdSize); case CMD_REQUESTGITEM: return HandleCmd(OnRequestGetItem, player, pCmd, maxCmdSize); case CMD_GETITEM: return HandleCmd(OnGetItem, player, pCmd, maxCmdSize); case CMD_GOTOAGETITEM: return HandleCmd(OnGotoAutoGetItem, player, pCmd, maxCmdSize); case CMD_REQUESTAGITEM: return HandleCmd(OnRequestAutoGetItem, player, pCmd, maxCmdSize); case CMD_AGETITEM: return HandleCmd(OnAutoGetItem, player, pCmd, maxCmdSize); case CMD_ITEMEXTRA: return HandleCmd(OnItemExtra, player, pCmd, maxCmdSize); case CMD_PUTITEM: return HandleCmd(OnPutItem, player, pCmd, maxCmdSize); case CMD_SYNCPUTITEM: return HandleCmd(OnSyncPutItem, player, pCmd, maxCmdSize); case CMD_SPAWNITEM: return HandleCmd(OnSpawnItem, player, pCmd, maxCmdSize); case CMD_SATTACKXY: return HandleCmd(OnStandingAttackTile, player, pCmd, maxCmdSize); case CMD_RATTACKXY: return HandleCmd(OnRangedAttackTile, player, pCmd, maxCmdSize); case CMD_SPELLXYD: return HandleCmd(OnSpellWall, player, pCmd, maxCmdSize); case CMD_SPELLXY: return HandleCmd(OnSpellTile, player, pCmd, maxCmdSize); case CMD_OPOBJXY: case CMD_DISARMXY: case CMD_OPOBJT: return HandleCmd(OnObjectTileAction, player, pCmd, maxCmdSize); case CMD_ATTACKID: return HandleCmd(OnAttackMonster, player, pCmd, maxCmdSize); case CMD_ATTACKPID: return HandleCmd(OnAttackPlayer, player, pCmd, maxCmdSize); case CMD_RATTACKID: return HandleCmd(OnRangedAttackMonster, player, pCmd, maxCmdSize); case CMD_RATTACKPID: return HandleCmd(OnRangedAttackPlayer, player, pCmd, maxCmdSize); case CMD_SPELLID: return HandleCmd(OnSpellMonster, player, pCmd, maxCmdSize); case CMD_SPELLPID: return HandleCmd(OnSpellPlayer, player, pCmd, maxCmdSize); case CMD_KNOCKBACK: return HandleCmd(OnKnockback, player, pCmd, maxCmdSize); case CMD_RESURRECT: return HandleCmd(OnResurrect, player, pCmd, maxCmdSize); case CMD_HEALOTHER: return HandleCmd(OnHealOther, player, pCmd, maxCmdSize); case CMD_TALKXY: return HandleCmd(OnTalkXY, player, pCmd, maxCmdSize); case CMD_DEBUG: return OnDebug(*pCmd); case CMD_NEWLVL: return HandleCmd(OnNewLevel, player, pCmd, maxCmdSize); case CMD_WARP: return HandleCmd(OnWarp, player, pCmd, maxCmdSize); case CMD_MONSTDEATH: return HandleCmd(OnMonstDeath, player, pCmd, maxCmdSize); case CMD_REQUESTSPAWNGOLEM: return HandleCmd(OnRequestSpawnGolem, player, pCmd, maxCmdSize); case CMD_MONSTDAMAGE: return HandleCmd(OnMonstDamage, player, pCmd, maxCmdSize); case CMD_PLRDEAD: return HandleCmd(OnPlayerDeath, player, pCmd, maxCmdSize); case CMD_PLRALIVE: return HandleCmd(OnPlayerAlive, player, pCmd, maxCmdSize); case CMD_PLRDAMAGE: return HandleCmd(OnPlayerDamage, player, pCmd, maxCmdSize); case CMD_OPENDOOR: case CMD_CLOSEDOOR: case CMD_OPERATEOBJ: return HandleCmd(OnOperateObject, player, pCmd, maxCmdSize); case CMD_BREAKOBJ: return HandleCmd(OnBreakObject, player, pCmd, maxCmdSize); case CMD_CHANGEPLRITEMS: return HandleCmd(OnChangePlayerItems, player, pCmd, maxCmdSize); case CMD_DELPLRITEMS: return HandleCmd(OnDeletePlayerItems, player, pCmd, maxCmdSize); case CMD_CHANGEINVITEMS: return HandleCmd(OnChangeInventoryItems, player, pCmd, maxCmdSize); case CMD_DELINVITEMS: return HandleCmd(OnDeleteInventoryItems, player, pCmd, maxCmdSize); case CMD_CHANGEBELTITEMS: return HandleCmd(OnChangeBeltItems, player, pCmd, maxCmdSize); case CMD_DELBELTITEMS: return HandleCmd(OnDeleteBeltItems, player, pCmd, maxCmdSize); case CMD_PLRLEVEL: return HandleCmd(OnPlayerLevel, player, pCmd, maxCmdSize); case CMD_DROPITEM: return HandleCmd(OnDropItem, player, pCmd, maxCmdSize); case CMD_ACK_PLRINFO: case CMD_SEND_PLRINFO: return HandleCmd(OnSendPlayerInfo, player, pCmd, maxCmdSize); case CMD_PLAYER_JOINLEVEL: return HandleCmd(OnPlayerJoinLevel, player, pCmd, maxCmdSize); case CMD_ACTIVATEPORTAL: return HandleCmd(OnActivatePortal, player, pCmd, maxCmdSize); case CMD_DEACTIVATEPORTAL: return OnDeactivatePortal(*pCmd, player); case CMD_RETOWN: return OnRestartTown(*pCmd, player); case CMD_SETSTR: return HandleCmd(OnSetStrength, player, pCmd, maxCmdSize); case CMD_SETMAG: return HandleCmd(OnSetMagic, player, pCmd, maxCmdSize); case CMD_SETDEX: return HandleCmd(OnSetDexterity, player, pCmd, maxCmdSize); case CMD_SETVIT: return HandleCmd(OnSetVitality, player, pCmd, maxCmdSize); case CMD_STRING: return OnString(*pCmd, maxCmdSize, player); case CMD_FRIENDLYMODE: return OnFriendlyMode(*pCmd, player); case CMD_SYNCQUEST: return HandleCmd(OnSyncQuest, player, pCmd, maxCmdSize); case CMD_CHEAT_EXPERIENCE: return OnCheatExperience(*pCmd, player); case CMD_CHANGE_SPELL_LEVEL: return HandleCmd(OnChangeSpellLevel, player, pCmd, maxCmdSize); case CMD_SETSHIELD: return OnSetShield(*pCmd, player); case CMD_REMSHIELD: return OnRemoveShield(*pCmd, player); case CMD_SETREFLECT: return HandleCmd(OnSetReflect, player, pCmd, maxCmdSize); case CMD_NAKRUL: return OnNakrul(*pCmd); case CMD_OPENHIVE: return OnOpenHive(*pCmd, player); case CMD_OPENGRAVE: return OnOpenGrave(*pCmd); case CMD_SPAWNMONSTER: return HandleCmd(OnSpawnMonster, player, pCmd, maxCmdSize); default: break; } if (pCmd->bCmd < CMD_DLEVEL || pCmd->bCmd > CMD_DLEVEL_END) { Log("Unrecognized network message {}, dropping player {}", static_cast(pCmd->bCmd), pnum); SNetDropPlayer(pnum, leaveinfo_t::LEAVE_DROP); return 0; } return HandleCmd(OnLevelData, player, pCmd, maxCmdSize); } } // namespace devilution ================================================ FILE: Source/msg.h ================================================ /** * @file msg.h * * Interface of function for sending and receiving network messages. */ #pragma once #include #include "dvlnet/leaveinfo.hpp" #include "engine/point.hpp" #include "items.h" #include "monster.h" #include "objects.h" #include "portal.h" #include "quests.h" namespace devilution { using net::leaveinfo_t; #define MAX_SEND_STR_LEN 80 enum _cmd_id : uint8_t { // Player mode standing. // // body (TCmd) CMD_STAND, // Walk to location. // // body (TCmdLoc) CMD_WALKXY, // Acknowledge receive of player info. // // body (TCmdPlrInfoHdr) CMD_ACK_PLRINFO, // Increment player strength. // // body (TCmdParam1): // int16_t delta CMD_ADDSTR, // Increment player magic. // // body (TCmdParam1): // int16_t delta CMD_ADDMAG, // Increment player dexterity. // // body (TCmdParam1): // int16_t delta CMD_ADDDEX, // Increment player vitality. // // body (TCmdParam1): // int16_t delta CMD_ADDVIT, // Lift item to hand. // // body (TCmdGItem) CMD_GETITEM, // Loot item to inventory. // // body (TCmdGItem) CMD_AGETITEM, // Drop item from hand on ground. // // body (TCmdPItem) CMD_PUTITEM, // Spawn item on ground (place quest items, drop dead player item, or drop // attempted loot item when inventory is full). // body (TCmdPItem) CMD_SPAWNITEM, // Range attack target location. // // body (TCmdLoc) CMD_RATTACKXY, // Cast spell at target location. // // body (TCmdLocParam4): // int8_t x // int8_t y // int16_t spellID // int16_t spellType // int16_t spellLevel // int16_t spellFrom CMD_SPELLXY, // Operate object at location. // // body (TCmdLoc): // int8_t x // int8_t y CMD_OPOBJXY, // Disarm trap at location. // // body (TCmdLoc): // int8_t x // int8_t y CMD_DISARMXY, // Attack target monster. // // body (TCmdParam1): // int16_t monster_num CMD_ATTACKID, // Attack target player. // // body (TCmdParam1): // int16_t player_num CMD_ATTACKPID, // Range attack target monster. // // body (TCmdParam1): // int16_t monster_num CMD_RATTACKID, // Range attack target player. // // body (TCmdParam1): // int16_t player_num CMD_RATTACKPID, // Cast spell on target monster. // // body (TCmdParam5): // int16_t monster_num // int16_t spellID // int16_t spellType // int16_t spellLevel // int16_t spellFrom CMD_SPELLID, // Cast spell on target player. // // body (TCmdParam5): // int16_t player_num // int16_t spellID // int16_t spellType // int16_t spellLevel // int16_t spellFrom CMD_SPELLPID, // Cast resurrect spell on target player. // // body (TCmdParam1): // int16_t player_num CMD_RESURRECT, // Operate object using telekinesis. // // body (TCmdLoc): // int8_t x // int8_t y CMD_OPOBJT, // Knockback target monster using telekinesis. // // body (TCmdParam1): // int16_t monster_num CMD_KNOCKBACK, // Talk with towner at location. // // body (TCmdLocParam1): // int8_t x // int8_t y // int16_t towner_num CMD_TALKXY, // Enter new dungeon level. // // body (TCmdParam2): // int16_t trig_msg // int16_t level CMD_NEWLVL, // Enter target portal. // // body (TCmdParam1): // int16_t portal_num CMD_WARP, // Cheat: give player level up. // // body (TCmd) CMD_CHEAT_EXPERIENCE, // Change spell level of player. // // body (TCmdParam2) // int16_t spellID // int16_t spellLevel CMD_CHANGE_SPELL_LEVEL, // Debug command (nop). // // body (TCmd) CMD_DEBUG, // Synchronize data of unvisited dungeon level (state of objects, items and // monsters). // // body (TSyncHeader, TSyncMonster+) CMD_SYNCDATA, // Monster death at location. // // body (TCmdLocParam1): // int8_t x // int8_t y // int16_t monster_num CMD_MONSTDEATH, // Damage target monster. // // body (TCmdParam2): // int16_t monster_num // int16_t damage CMD_MONSTDAMAGE, // Player death. // // body (TCmdParam1): // int16_t ear_flag CMD_PLRDEAD, // Player resurrection. // // body (TCmd) CMD_PLRALIVE, // Lift item to hand request. // // body (TCmdGItem) CMD_REQUESTGITEM, // Loot item to inventory request. // // body (TCmdGItem) CMD_REQUESTAGITEM, // Lift item to hand at location. // // body (TCmdLocParam1): // int8_t x // int8_t y // int16_t item_num CMD_GOTOGETITEM, // Loot item to inventory at location. // // body (TCmdLocParam1): // int8_t x // int8_t y // int16_t item_num CMD_GOTOAGETITEM, // Open target door. // // body (TCmdLoc): // int8_t x // int8_t y CMD_OPENDOOR, // Close target door. // // body (TCmdLoc): // int8_t x // int8_t y CMD_CLOSEDOOR, // Operate object. // // body (TCmdLoc): // int8_t x // int8_t y CMD_OPERATEOBJ, // Break object. // // body (TCmdLoc): // int8_t x // int8_t y CMD_BREAKOBJ, // Equip item for player. // // body (TCmdChItem) CMD_CHANGEPLRITEMS, // Remove equipped item (destroy equipped item, swap equipped item, unequip // equipped item). // // body (TCmdDelItem) CMD_DELPLRITEMS, // Put item into player's backpack. // // body (TCmdChItem) CMD_CHANGEINVITEMS, // Remove item from player's backpack. // // body (TCmdParam1) CMD_DELINVITEMS, // Put item into player's belt. // // body (TCmdChItem) CMD_CHANGEBELTITEMS, // Remove item from player's belt. // // body (TCmdParam1) CMD_DELBELTITEMS, // Damage target player. // // body (TCmdDamage) CMD_PLRDAMAGE, // Set player level. // // body (TCmdParam1): // int16_t clvl CMD_PLRLEVEL, // Place item on ground (e.g. monster item drop, chest/barrel/sarcophagus // item drop, etc). // // body (TCmdPItem) CMD_DROPITEM, // Player join dungeon level at location. // // body (TCmdLocParam1): // int8_t x // int8_t y // int16_t dlvl CMD_PLAYER_JOINLEVEL, // Acknowledge receive of player info. // // body (TCmdPlrInfoHdr) CMD_SEND_PLRINFO, // Shift attack target location. // // body (TCmdLoc) CMD_SATTACKXY, // Activate town portal at location. // // body (TCmdLocParam3): // int8_t x // int8_t y // int16_t level // int16_t dtype // int16_t is_setlevel CMD_ACTIVATEPORTAL, // Deactivate portal of player. // // body (TCmd) CMD_DEACTIVATEPORTAL, // Delta information for a dungeon level. // // body (TCmdPlrInfoHdr) CMD_DLEVEL, // Delta information of quest and portal states. // // body (TCmdPlrInfoHdr) CMD_DLEVEL_JUNK, // Delta information end marker. // // body (TCmdPlrInfoHdr) CMD_DLEVEL_END, // Cast heal other spell on target player. // // body (TCmdParam1): // int16_t player_num CMD_HEALOTHER, // Chat message. // // body (TCmdString) CMD_STRING, // Toggles friendly Mode // // body (TCmd) CMD_FRIENDLYMODE, // Set player strength. // // body (TCmdParam1): // int16_t str CMD_SETSTR, // Set player magic. // // body (TCmdParam1): // int16_t mag CMD_SETMAG, // Set player dexterity. // // body (TCmdParam1): // int16_t dex CMD_SETDEX, // Set player vitality. // // body (TCmdParam1): // int16_t vit CMD_SETVIT, // Restart in town. // // body (TCmd) CMD_RETOWN, // Cast spell with direction at target location (e.g. firewall). // // body (TCmdLocParam5): // int8_t x // int8_t y // int16_t spellID // int16_t spellType // int16_t direction // int16_t spellLevel // int16_t spellFrom CMD_SPELLXYD, // Track (dungeon generated) item looted by other player on dungeon level not // yet visited by player. The item is tracked as "already taken" in the delta // table, so it is not generated twice on the same dungeon level. // // body (TCmdGItem) CMD_ITEMEXTRA, // Synchronize item drop state. // // body (TCmdPItem) CMD_SYNCPUTITEM, // Synchronize quest state. // // body (TCmdQuest) CMD_SYNCQUEST, // Request to spawn a golem at target location. // // body (TCmdLocParam1) CMD_REQUESTSPAWNGOLEM, // Enable mana shield of player (render). // // body (TCmd) CMD_SETSHIELD, // Disable mana shield of player (don't render). // // body (TCmd) CMD_REMSHIELD, CMD_SETREFLECT, CMD_NAKRUL, CMD_OPENHIVE, CMD_OPENGRAVE, // Spawn a monster at target location. // // body (TCmdSpawnMonster) CMD_SPAWNMONSTER, // Fake command; set current player for succeeding mega pkt buffer messages. // // body (TFakeCmdPlr) FAKE_CMD_SETID, // Fake command; drop mega pkt buffer messages of specified player. // // body (TFakeDropPlr) FAKE_CMD_DROPID, NUM_CMDS, CMD_INVALID = 0xFF, }; #pragma pack(push, 1) struct TCmd { _cmd_id bCmd; }; struct TCmdLoc { _cmd_id bCmd; uint8_t x; uint8_t y; }; struct TCmdLocParam1 { _cmd_id bCmd; uint8_t x; uint8_t y; uint16_t wParam1; }; struct TCmdLocParam2 { _cmd_id bCmd; uint8_t x; uint8_t y; uint16_t wParam1; uint16_t wParam2; }; struct TCmdLocParam3 { _cmd_id bCmd; uint8_t x; uint8_t y; uint16_t wParam1; uint16_t wParam2; uint16_t wParam3; }; struct TCmdLocParam4 { _cmd_id bCmd; uint8_t x; uint8_t y; uint16_t wParam1; uint16_t wParam2; uint16_t wParam3; uint16_t wParam4; }; struct TCmdLocParam5 { _cmd_id bCmd; uint8_t x; uint8_t y; uint16_t wParam1; uint16_t wParam2; uint16_t wParam3; uint16_t wParam4; uint16_t wParam5; }; struct TCmdParam1 { _cmd_id bCmd; uint16_t wParam1; }; struct TCmdParam2 { _cmd_id bCmd; uint16_t wParam1; uint16_t wParam2; }; struct TCmdParam4 { _cmd_id bCmd; uint16_t wParam1; uint16_t wParam2; uint16_t wParam3; uint16_t wParam4; }; struct TCmdSpawnMonster { _cmd_id bCmd; uint8_t x; uint8_t y; Direction dir; uint16_t typeIndex; uint16_t monsterId; uint32_t seed; uint8_t golemOwnerPlayerId; uint8_t golemSpellLevel; }; struct TCmdQuest { _cmd_id bCmd; int8_t q; quest_state qstate; uint8_t qlog; uint8_t qvar1; uint8_t qvar2; int16_t qmsg; }; struct TItemDef { _item_indexes wIndx; uint16_t wCI; uint32_t dwSeed; }; struct TItem { _item_indexes wIndx; uint16_t wCI; uint32_t dwSeed; uint8_t bId; uint8_t bDur; uint8_t bMDur; uint8_t bCh; uint8_t bMCh; uint16_t wValue; uint32_t dwBuff; uint16_t wToHit; uint16_t wMaxDam; }; struct TEar { _item_indexes wIndx; uint16_t wCI; uint32_t dwSeed; uint8_t bCursval; char heroname[17]; }; /** * Represents an item being picked up from the ground */ struct TCmdGItem { _cmd_id bCmd; uint8_t x; uint8_t y; union { TItemDef def; TItem item; TEar ear; }; uint8_t bMaster; uint8_t bPnum; uint8_t bCursitem; uint8_t bLevel; int32_t dwTime; }; /** * Represents an item being dropped onto the ground */ struct TCmdPItem { _cmd_id bCmd; uint8_t x; uint8_t y; union { TItemDef def; TItem item; TEar ear; }; /** * Items placed during dungeon generation */ static constexpr _cmd_id FloorItem = CMD_STAND; /** * Floor items that have already been picked up */ static constexpr _cmd_id PickedUpItem = CMD_WALKXY; /** * Items dropped by players, monsters, or objects and left on the floor of the dungeon */ static constexpr _cmd_id DroppedItem = CMD_ACK_PLRINFO; }; struct TCmdChItem { _cmd_id bCmd; uint8_t bLoc; bool forceSpell; union { TItemDef def; TItem item; TEar ear; }; }; struct TCmdDelItem { _cmd_id bCmd; uint8_t bLoc; }; struct TCmdDamage { _cmd_id bCmd; uint8_t bPlr; uint32_t dwDam; DamageType damageType; }; struct TCmdMonDamage { _cmd_id bCmd; uint16_t wMon; uint32_t dwDam; }; struct TCmdPlrInfoHdr { _cmd_id bCmd; uint16_t wOffset; uint16_t wBytes; }; struct TCmdString { _cmd_id bCmd; char str[MAX_SEND_STR_LEN]; }; struct TFakeCmdPlr { _cmd_id bCmd; uint8_t bPlr; }; struct TFakeDropPlr { _cmd_id bCmd; uint8_t bPlr; uint32_t dwReason; }; struct TSyncHeader { _cmd_id bCmd; uint8_t bLevel; uint16_t wLen; uint8_t bItemI; uint8_t bItemX; uint8_t bItemY; uint16_t wItemIndx; uint16_t wItemCI; uint32_t dwItemSeed; uint8_t bItemId; uint8_t bItemDur; uint8_t bItemMDur; uint8_t bItemCh; uint8_t bItemMCh; uint16_t wItemVal; uint32_t dwItemBuff; uint8_t bPInvLoc; uint16_t wPInvIndx; uint16_t wPInvCI; uint32_t dwPInvSeed; uint8_t bPInvId; }; struct TSyncMonster { uint8_t _mndx; uint8_t _mx; uint8_t _my; uint8_t _menemy; uint8_t _mdelta; int32_t _mhitpoints; int8_t mWhoHit; }; struct TPktHdr { uint8_t px; uint8_t py; uint8_t targx; uint8_t targy; int32_t php; int32_t pmhp; int32_t mana; int32_t maxmana; uint8_t bstr; uint8_t bmag; uint8_t bdex; uint8_t pdir; uint16_t wCheck; uint16_t wLen; }; struct TPkt { TPktHdr hdr; std::byte body[493]; }; #pragma pack(pop) extern uint8_t gbBufferMsgs; extern int dwRecCount; void PrepareItemForNetwork(const Item &item, TItem &messageItem); void PrepareEarForNetwork(const Item &item, TEar &ear); void RecreateItem(const Player &player, const TItem &messageItem, Item &item); void msg_send_drop_pkt(uint8_t pnum, leaveinfo_t reason); bool msg_wait_resync(); void run_delta_info(); void DeltaExportData(uint8_t pnum); void DeltaSyncJunk(); void delta_init(); void DeltaClearLevel(uint8_t level); void delta_kill_monster(const Monster &monster, Point position, const Player &player); void delta_monster_hp(const Monster &monster, const Player &player); void delta_sync_monster(const TSyncMonster &monsterSync, uint8_t level); uint8_t GetLevelForMultiplayer(const Player &player); bool IsValidLevelForMultiplayer(uint8_t level); bool IsValidLevel(uint8_t level, bool isSetLevel); void DeltaAddItem(int ii); void DeltaSaveLevel(); void DeltaLoadLevel(); /** @brief Clears last sent player command for the local player. This is used when a game tick changes. */ void ClearLastSentPlayerCmd(); void NetSendCmd(bool bHiPri, _cmd_id bCmd); void NetSendCmdSpawnMonster(Point position, Direction dir, uint16_t typeIndex, uint16_t monsterId, uint32_t seed, uint8_t golemOwnerPlayerId, uint8_t golemSpellLevel); void NetSendCmdLoc(uint8_t playerId, bool bHiPri, _cmd_id bCmd, Point position); void NetSendCmdLocParam1(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1); void NetSendCmdLocParam2(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2); void NetSendCmdLocParam3(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3); void NetSendCmdLocParam4(bool bHiPri, _cmd_id bCmd, Point position, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3, uint16_t wParam4); void NetSendCmdParam1(bool bHiPri, _cmd_id bCmd, uint16_t wParam1); void NetSendCmdParam2(bool bHiPri, _cmd_id bCmd, uint16_t wParam1, uint16_t wParam2); void NetSendCmdParam4(bool bHiPri, _cmd_id bCmd, uint16_t wParam1, uint16_t wParam2, uint16_t wParam3, uint16_t wParam4); void NetSendCmdQuest(bool bHiPri, const Quest &quest); void NetSendCmdGItem(bool bHiPri, _cmd_id bCmd, const Player &player, uint8_t ii); void NetSendCmdPItem(bool bHiPri, _cmd_id bCmd, Point position, const Item &item); void NetSyncInvItem(const Player &player, int invListIndex); void NetSendCmdChItem(bool bHiPri, uint8_t bLoc, bool forceSpellChange = false); void NetSendCmdDelItem(bool bHiPri, uint8_t bLoc); void NetSendCmdChInvItem(bool bHiPri, int invGridIndex); void NetSendCmdChBeltItem(bool bHiPri, int beltIndex); void NetSendCmdDamage(bool bHiPri, const Player &player, uint32_t dwDam, DamageType damageType); void NetSendCmdMonDmg(bool bHiPri, uint16_t wMon, uint32_t dwDam); void NetSendCmdString(uint32_t pmask, const char *pszStr); void delta_close_portal(const Player &player); bool ValidateCmdSize(size_t requiredCmdSize, size_t maxCmdSize, size_t playerId); size_t ParseCmd(uint8_t pnum, const TCmd *pCmd, size_t maxCmdSize); } // namespace devilution ================================================ FILE: Source/multi.cpp ================================================ /** * @file multi.cpp * * Implementation of functions for keeping multiplaye games in sync. */ #include "multi.h" #include #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include #include #include "DiabloUI/diabloui.h" #include "diablo.h" #include "engine/demomode.h" #include "engine/point.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "menu.h" #include "monster.h" #include "msg.h" #include "nthread.h" #include "options.h" #include "pfile.h" #include "player.h" #include "plrmsg.h" #include "qol/chatlog.h" #include "storm/storm_net.hpp" #include "sync.h" #include "tmsg.h" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" namespace devilution { bool gbSomebodyWonGameKludge; uint16_t sgwPackPlrOffsetTbl[MAX_PLRS]; bool sgbPlayerTurnBitTbl[MAX_PLRS]; bool sgbPlayerLeftGameTbl[MAX_PLRS]; bool shareNextHighPriorityMessage; uint8_t gbActivePlayers; bool gbGameDestroyed; bool sgbSendDeltaTbl[MAX_PLRS]; GameData sgGameInitInfo; bool gbSelectProvider; int sglTimeoutStart; leaveinfo_t sgdwPlayerLeftReasonTbl[MAX_PLRS]; uint32_t sgdwGameLoops; /** * Specifies the maximum number of players in a game, where 1 * represents a single player game and 4 represents a multi player game. */ bool gbIsMultiplayer; bool sgbTimeout; std::string GameName; std::string GamePassword; bool PublicGame; uint8_t gbDeltaSender; bool sgbNetInited; uint32_t player_state[MAX_PLRS]; Uint32 playerInfoTimers[MAX_PLRS]; bool IsLoopback; /** * Contains the set of supported event types supported by the multiplayer * event handler. */ const event_type EventTypes[3] = { EVENT_TYPE_PLAYER_LEAVE_GAME, EVENT_TYPE_PLAYER_CREATE_GAME, EVENT_TYPE_PLAYER_MESSAGE }; void GameData::swapLE() { size = Swap32LE(size); programid = Swap32LE(programid); gameSeed[0] = Swap32LE(gameSeed[0]); gameSeed[1] = Swap32LE(gameSeed[1]); gameSeed[2] = Swap32LE(gameSeed[2]); gameSeed[3] = Swap32LE(gameSeed[3]); } namespace { struct TBuffer { size_t dwNextWriteOffset; std::byte bData[4096]; }; TBuffer highPriorityBuffer; TBuffer lowPriorityBuffer; constexpr uint16_t HeaderCheckVal = #if SDL_BYTEORDER == SDL_LIL_ENDIAN LoadBE16("ip"); #else LoadLE16("ip"); #endif uint32_t sgbSentThisCycle; void BufferInit(TBuffer *pBuf) { pBuf->dwNextWriteOffset = 0; pBuf->bData[0] = std::byte { 0 }; } void CopyPacket(TBuffer *buf, const std::byte *packet, size_t size) { if (buf->dwNextWriteOffset + size + 2 > 0x1000) { return; } std::byte *p = &buf->bData[buf->dwNextWriteOffset]; buf->dwNextWriteOffset += size + 1; *p = static_cast(size); p++; memcpy(p, packet, size); p[size] = std::byte { 0 }; } std::byte *CopyBufferedPackets(std::byte *destination, TBuffer *source, size_t *size) { if (source->dwNextWriteOffset != 0) { std::byte *srcPtr = source->bData; while (true) { auto chunkSize = static_cast(*srcPtr); if (chunkSize == 0) break; if (chunkSize > *size) break; srcPtr++; memcpy(destination, srcPtr, chunkSize); destination += chunkSize; srcPtr += chunkSize; *size -= chunkSize; } memmove(source->bData, srcPtr, (source->bData - srcPtr) + source->dwNextWriteOffset + 1); source->dwNextWriteOffset += source->bData - srcPtr; return destination; } return destination; } void NetReceivePlayerData(TPkt *pkt) { const Player &myPlayer = *MyPlayer; Point target = myPlayer.GetTargetPosition(); // Don't send desired target position when we will change our position soon. // This prevents a desync where the remote client starts a walking to the old target position when the teleport is finished but the the new position isn't received yet. if (myPlayer._pmode == PM_SPELL && IsAnyOf(myPlayer.executedSpell.spellId, SpellID::Teleport, SpellID::Phasing, SpellID::Warp)) target = {}; pkt->hdr.wCheck = HeaderCheckVal; pkt->hdr.px = myPlayer.position.tile.x; pkt->hdr.py = myPlayer.position.tile.y; pkt->hdr.targx = target.x; pkt->hdr.targy = target.y; pkt->hdr.php = Swap32LE(myPlayer._pHitPoints); pkt->hdr.pmhp = Swap32LE(myPlayer._pMaxHP); pkt->hdr.mana = Swap32LE(myPlayer._pMana); pkt->hdr.maxmana = Swap32LE(myPlayer._pMaxMana); pkt->hdr.bstr = myPlayer._pBaseStr; pkt->hdr.bmag = myPlayer._pBaseMag; pkt->hdr.bdex = myPlayer._pBaseDex; pkt->hdr.pdir = static_cast(myPlayer._pdir); } bool IsNetPlayerValid(const Player &player) { // we no longer check character level here, players with out-of-range clevels are not allowed to join the game and we don't observe change clevel messages that would set it out of range // (there's no code path that would result in _pLevel containing an out of range value in the DevilutionX code) return static_cast(player._pClass) < GetNumPlayerClasses() && player.plrlevel < NUMLEVELS && InDungeonBounds(player.position.tile) && !std::string_view(player._pName).empty(); } void CheckPlayerInfoTimeouts() { for (uint8_t i = 0; i < Players.size(); i++) { Player &player = Players[i]; if (&player == MyPlayer) { continue; } Uint32 &timerStart = playerInfoTimers[i]; const bool isPlayerConnected = (player_state[i] & PS_CONNECTED) != 0; const bool isPlayerValid = isPlayerConnected && IsNetPlayerValid(player); if (isPlayerConnected && !isPlayerValid && timerStart == 0) { timerStart = SDL_GetTicks(); } if (!isPlayerConnected || isPlayerValid) { timerStart = 0; } if (timerStart == 0) { continue; } // Time the player out after 15 seconds // if we do not receive valid player info if (SDL_GetTicks() - timerStart >= 15000) { SNetDropPlayer(i, leaveinfo_t::LEAVE_DROP); timerStart = 0; } } } void SendPacket(uint8_t playerId, const std::byte *packet, size_t size) { TPkt pkt; NetReceivePlayerData(&pkt); const size_t sizeWithheader = size + sizeof(pkt.hdr); pkt.hdr.wLen = Swap16LE(static_cast(sizeWithheader)); memcpy(pkt.body, packet, size); if (!SNetSendMessage(playerId, &pkt.hdr, sizeWithheader)) nthread_terminate_game("SNetSendMessage0"); } void MonsterSeeds() { sgdwGameLoops++; const uint32_t seed = (sgdwGameLoops >> 8) | (sgdwGameLoops << 24); for (uint32_t i = 0; i < MaxMonsters; i++) Monsters[i].aiSeed = seed + i; } void HandleTurnUpperBit(uint8_t pnum) { uint8_t i; for (i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_CONNECTED) != 0 && i != pnum) break; } if (MyPlayerId == i) { sgbSendDeltaTbl[pnum] = true; } else if (pnum == MyPlayerId) { gbDeltaSender = i; } } void ParseTurn(uint8_t pnum, uint32_t turn) { if ((turn & 0x80000000) != 0) HandleTurnUpperBit(pnum); uint32_t absTurns = turn & 0x7FFFFFFF; if (sgbSentThisCycle < gdwTurnsInTransit + absTurns) { if (absTurns >= 0x7FFFFFFF) absTurns &= 0xFFFF; sgbSentThisCycle = absTurns + gdwTurnsInTransit; sgdwGameLoops = 4 * absTurns * sgbNetUpdateRate; } } void PlayerLeftMsg(Player &player, bool left) { if (&player == InspectPlayer) InspectPlayer = MyPlayer; if (&player == MyPlayer) return; if (!player.plractive) return; FixPlrWalkTags(player); RemovePortalMissile(player); DeactivatePortal(player); delta_close_portal(player); RemoveEnemyReferences(player); RemovePlrMissiles(player); if (left) { const leaveinfo_t leaveReason = sgdwPlayerLeftReasonTbl[player.getId()]; const std::string reasonDescription = DescribeLeaveReason(leaveReason); std::string_view pszFmt = _("Player '{:s}' just left the game"); switch (leaveReason) { case leaveinfo_t::LEAVE_EXIT: break; case leaveinfo_t::LEAVE_ENDING: pszFmt = _("Player '{:s}' killed Diablo and left the game!"); gbSomebodyWonGameKludge = true; break; case leaveinfo_t::LEAVE_DROP: pszFmt = _("Player '{:s}' dropped due to timeout"); break; } if (!IsLoopback) { const uint8_t remainingPlayers = gbActivePlayers > 0 ? gbActivePlayers - 1 : 0; if (player._pName[0] != '\0') LogInfo("Player '{}' left the {} game ({}, {}/{} players)", player._pName, ConnectionNames[provider], reasonDescription, remainingPlayers, MAX_PLRS); else LogInfo("Player left the {} game ({}, {}/{} players)", ConnectionNames[provider], reasonDescription, remainingPlayers, MAX_PLRS); } EventPlrMsg(fmt::format(fmt::runtime(pszFmt), player._pName)); } player.plractive = false; player._pName[0] = '\0'; ResetPlayerGFX(player); gbActivePlayers--; } void ClearPlayerLeftState() { for (uint8_t i = 0; i < Players.size(); i++) { if (sgbPlayerLeftGameTbl[i]) { if (gbBufferMsgs == 1) msg_send_drop_pkt(i, sgdwPlayerLeftReasonTbl[i]); else PlayerLeftMsg(Players[i], true); sgbPlayerLeftGameTbl[i] = false; sgdwPlayerLeftReasonTbl[i] = static_cast(0); } } } void CheckDropPlayer() { for (uint8_t i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_ACTIVE) == 0 && (player_state[i] & PS_CONNECTED) != 0) { SNetDropPlayer(i, leaveinfo_t::LEAVE_DROP); } } } void BeginTimeout() { if (!sgbTimeout) { return; } #ifdef _DEBUG if (DebugDisableNetworkTimeout) { return; } #endif uint32_t nTicks = SDL_GetTicks() - sglTimeoutStart; if (nTicks > 20000) { gbRunGame = false; return; } if (nTicks < 10000) { return; } CheckDropPlayer(); } void HandleAllPackets(uint8_t pnum, const std::byte *data, size_t size) { for (size_t offset = 0; offset < size;) { const size_t messageSize = ParseCmd(pnum, reinterpret_cast(&data[offset]), size - offset); if (messageSize == 0) { break; } offset += messageSize; } } void ProcessTmsgs() { while (true) { std::unique_ptr msg; const uint8_t size = tmsg_get(&msg); if (size == 0) break; HandleAllPackets(MyPlayerId, msg.get(), size); } } void SendPlayerInfo(uint8_t pnum, _cmd_id cmd) { PlayerNetPack packed; const Player &myPlayer = *MyPlayer; PackNetPlayer(packed, myPlayer); multi_send_zero_packet(pnum, cmd, reinterpret_cast(&packed), sizeof(PlayerNetPack)); } void SetupLocalPositions() { currlevel = 0; leveltype = DTYPE_TOWN; setlevel = false; const WorldTilePosition spawns[9] = { { 75, 68 }, { 77, 70 }, { 75, 70 }, { 77, 68 }, { 76, 69 }, { 75, 69 }, { 76, 68 }, { 77, 69 }, { 76, 70 } }; Player &myPlayer = *MyPlayer; myPlayer.position.tile = spawns[MyPlayerId]; myPlayer.position.future = myPlayer.position.tile; myPlayer.setLevel(currlevel); myPlayer._pLvlChanging = true; myPlayer.pLvlLoad = 0; myPlayer._pmode = PM_NEWLVL; myPlayer.destAction = ACTION_NONE; } void HandleEvents(_SNETEVENT *pEvt) { const uint32_t playerId = pEvt->playerid; switch (pEvt->eventid) { case EVENT_TYPE_PLAYER_CREATE_GAME: { GameData gameData; if (pEvt->databytes < sizeof(GameData)) app_fatal(StrCat("Invalid packet size (databytes)); std::memcpy(&gameData, pEvt->data, sizeof(gameData)); gameData.swapLE(); if (gameData.size != sizeof(GameData)) app_fatal(StrCat("Invalid size of game data: ", gameData.size)); sgGameInitInfo = gameData; sgbPlayerTurnBitTbl[playerId] = true; } break; case EVENT_TYPE_PLAYER_LEAVE_GAME: { sgbPlayerLeftGameTbl[playerId] = true; sgbPlayerTurnBitTbl[playerId] = false; uint32_t leftReasonRaw = 0; if (pEvt->data != nullptr && pEvt->databytes >= sizeof(leftReasonRaw)) { std::memcpy(&leftReasonRaw, pEvt->data, sizeof(leftReasonRaw)); leftReasonRaw = Swap32LE(leftReasonRaw); } leaveinfo_t leftReason = static_cast(leftReasonRaw); sgdwPlayerLeftReasonTbl[playerId] = leftReason; if (leftReason == leaveinfo_t::LEAVE_ENDING) gbSomebodyWonGameKludge = true; sgbSendDeltaTbl[playerId] = false; if (gbDeltaSender == playerId) gbDeltaSender = MAX_PLRS; } break; case EVENT_TYPE_PLAYER_MESSAGE: { std::string_view data(static_cast(pEvt->data), pEvt->databytes); if (const size_t nullPos = data.find('\0'); nullPos != std::string_view::npos) { data.remove_suffix(data.size() - nullPos); } EventPlrMsg(data); } break; } } void RegisterNetEventHandlers() { for (auto eventType : EventTypes) { if (!SNetRegisterEventHandler(eventType, HandleEvents)) { app_fatal(StrCat("SNetRegisterEventHandler:\n", SDL_GetError())); } } } void UnregisterNetEventHandlers() { for (auto eventType : EventTypes) { SNetUnregisterEventHandler(eventType); } } bool InitSingle(GameData *gameData) { Players.resize(1); if (!SNetInitializeProvider(SELCONN_LOOPBACK, gameData)) { return false; } int unused = 0; GameData gameInitInfo = sgGameInitInfo; gameInitInfo.swapLE(); if (!SNetCreateGame("local", "local", reinterpret_cast(&gameInitInfo), sizeof(gameInitInfo), &unused)) { app_fatal(StrCat("SNetCreateGame1:\n", SDL_GetError())); } MyPlayerId = 0; MyPlayer = &Players[MyPlayerId]; InspectPlayer = MyPlayer; gbIsMultiplayer = false; pfile_read_player_from_save(gSaveNumber, *MyPlayer); return true; } bool InitMulti(GameData *gameData) { Players.resize(MAX_PLRS); int playerId; while (true) { if (gbSelectProvider && !UiSelectProvider(gameData)) { return false; } RegisterNetEventHandlers(); if (UiSelectGame(gameData, &playerId)) break; gbSelectProvider = true; } if (static_cast(playerId) >= Players.size()) { return false; } MyPlayerId = playerId; MyPlayer = &Players[MyPlayerId]; InspectPlayer = MyPlayer; gbIsMultiplayer = true; pfile_read_player_from_save(gSaveNumber, *MyPlayer); return true; } } // namespace DVL_API_FOR_TEST std::string DescribeLeaveReason(leaveinfo_t leaveReason) { switch (leaveReason) { case leaveinfo_t::LEAVE_EXIT: return "normal exit"; case leaveinfo_t::LEAVE_ENDING: return "Diablo defeated"; case leaveinfo_t::LEAVE_DROP: return "connection timeout"; default: return fmt::format("code 0x{:08X}", static_cast(leaveReason)); } } std::string FormatGameSeed(const uint32_t gameSeed[4]) { return fmt::format("{:08X}{:08X}{:08X}{:08X}", gameSeed[0], gameSeed[1], gameSeed[2], gameSeed[3]); } void InitGameInfo() { const xoshiro128plusplus gameGenerator = ReserveSeedSequence(); gameGenerator.save(sgGameInitInfo.gameSeed); sgGameInitInfo.size = sizeof(sgGameInitInfo); sgGameInitInfo.programid = GAME_ID; sgGameInitInfo.versionMajor = PROJECT_VERSION_MAJOR; sgGameInitInfo.versionMinor = PROJECT_VERSION_MINOR; sgGameInitInfo.versionPatch = PROJECT_VERSION_PATCH; const Options &options = GetOptions(); sgGameInitInfo.nTickRate = *options.Gameplay.tickRate; sgGameInitInfo.bRunInTown = *options.Gameplay.runInTown ? 1 : 0; sgGameInitInfo.bTheoQuest = *options.Gameplay.theoQuest ? 1 : 0; sgGameInitInfo.bCowQuest = *options.Gameplay.cowQuest ? 1 : 0; sgGameInitInfo.bFriendlyFire = *options.Gameplay.friendlyFire ? 1 : 0; sgGameInitInfo.fullQuests = (!gbIsMultiplayer || *options.Gameplay.multiplayerFullQuests) ? 1 : 0; } void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size) { if (data != nullptr && size != 0) { CopyPacket(&lowPriorityBuffer, data, size); SendPacket(playerId, data, size); } } void NetSendHiPri(uint8_t playerId, const std::byte *data, size_t size) { if (data != nullptr && size != 0) { CopyPacket(&highPriorityBuffer, data, size); SendPacket(playerId, data, size); } if (shareNextHighPriorityMessage) { shareNextHighPriorityMessage = false; TPkt pkt; NetReceivePlayerData(&pkt); std::byte *destination = pkt.body; size_t remainingSpace = gdwNormalMsgSize - sizeof(TPktHdr); destination = CopyBufferedPackets(destination, &highPriorityBuffer, &remainingSpace); destination = CopyBufferedPackets(destination, &lowPriorityBuffer, &remainingSpace); remainingSpace = sync_all_monsters(destination, remainingSpace); const size_t len = gdwNormalMsgSize - remainingSpace; pkt.hdr.wLen = Swap16LE(static_cast(len)); if (!SNetSendMessage(SNPLAYER_OTHERS, &pkt.hdr, len)) nthread_terminate_game("SNetSendMessage"); } } void multi_send_msg_packet(uint32_t pmask, const std::byte *data, size_t size) { TPkt pkt; NetReceivePlayerData(&pkt); const size_t len = size + sizeof(pkt.hdr); pkt.hdr.wLen = Swap16LE(static_cast(len)); memcpy(pkt.body, data, size); uint8_t playerID = 0; for (uint32_t v = 1; playerID < Players.size(); playerID++, v <<= 1) { if ((v & pmask) != 0) { if (!SNetSendMessage(playerID, &pkt.hdr, len)) { nthread_terminate_game("SNetSendMessage"); return; } } } } void multi_msg_countdown() { for (uint8_t i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_TURN_ARRIVED) != 0) { if (gdwMsgLenTbl[i] == sizeof(int32_t)) ParseTurn(i, *(int32_t *)glpMsgTbl[i]); } } } void multi_player_left(uint8_t pnum, leaveinfo_t reason) { sgbPlayerLeftGameTbl[pnum] = true; sgdwPlayerLeftReasonTbl[pnum] = reason; ClearPlayerLeftState(); } void multi_net_ping() { sgbTimeout = true; sglTimeoutStart = SDL_GetTicks(); } bool multi_handle_delta() { if (gbGameDestroyed) { gbRunGame = false; return false; } for (uint8_t i = 0; i < Players.size(); i++) { if (sgbSendDeltaTbl[i]) { sgbSendDeltaTbl[i] = false; DeltaExportData(i); } } sgbSentThisCycle = nthread_send_and_recv_turn(sgbSentThisCycle, 1); bool received; if (!nthread_recv_turns(&received)) { BeginTimeout(); return false; } sgbTimeout = false; if (received) { if (!shareNextHighPriorityMessage) { // If there are any high priority messages pending, // share them with other players now shareNextHighPriorityMessage = true; if (highPriorityBuffer.dwNextWriteOffset != 0) NetSendHiPri(MyPlayerId, nullptr, 0); } else { // If there were no high priority messages in at least two consecutive game // ticks, this shares the low priority messages and monster sync data NetSendHiPri(MyPlayerId, nullptr, 0); shareNextHighPriorityMessage = true; } } MonsterSeeds(); return true; } void ProcessGameMessagePackets() { ClearPlayerLeftState(); ProcessTmsgs(); uint8_t playerId = std::numeric_limits::max(); TPktHdr *pkt; size_t dwMsgSize = 0; while (SNetReceiveMessage(&playerId, (void **)&pkt, &dwMsgSize)) { dwRecCount++; ClearPlayerLeftState(); if (dwMsgSize < sizeof(TPktHdr)) continue; if (playerId >= Players.size()) continue; if (pkt->wCheck != HeaderCheckVal) continue; if (Swap16LE(pkt->wLen) != dwMsgSize) continue; Player &player = Players[playerId]; if (!IsNetPlayerValid(player)) { const _cmd_id cmd = *(const _cmd_id *)(pkt + 1); if (gbBufferMsgs == 0 && IsNoneOf(cmd, CMD_SEND_PLRINFO, CMD_ACK_PLRINFO)) { // Distrust all messages until // player info is received continue; } } const Point syncPosition = { pkt->px, pkt->py }; player.position.last = syncPosition; if (&player != MyPlayer) { assert(gbBufferMsgs != 2); player._pHitPoints = Swap32LE(pkt->php); player._pMaxHP = Swap32LE(pkt->pmhp); player._pMana = Swap32LE(pkt->mana); player._pMaxMana = Swap32LE(pkt->maxmana); const bool cond = gbBufferMsgs == 1; player._pBaseStr = pkt->bstr; player._pBaseMag = pkt->bmag; player._pBaseDex = pkt->bdex; const uint8_t rawDir = pkt->pdir; if (rawDir <= static_cast(Direction::SouthEast)) { const Direction newDir = static_cast(rawDir); if (player._pdir != newDir && player._pmode == PM_STAND) { player._pdir = newDir; StartStand(player, newDir); } } if (!cond && player.plractive && !player.hasNoLife()) { if (player.isOnActiveLevel() && !player._pLvlChanging) { if (player.position.tile.WalkingDistance(syncPosition) > 3 && PosOkPlayer(player, syncPosition)) { // got out of sync, clear the tiles around where we last thought the player was located FixPlrWalkTags(player); player.position.old = player.position.tile; // then just in case clear the tiles around the current position (probably unnecessary) FixPlrWalkTags(player); player.position.tile = syncPosition; player.position.future = syncPosition; if (player.isWalking()) player.position.temp = syncPosition; SetPlayerOld(player); player.occupyTile(player.position.tile, false); } if (player.position.future.WalkingDistance(player.position.tile) > 1) { player.position.future = player.position.tile; } const Point target = { pkt->targx, pkt->targy }; if (target != Point {}) // does the client send a desired (future) position of remote player? MakePlrPath(player, target, true); } else { player.position.tile = syncPosition; player.position.future = syncPosition; SetPlayerOld(player); } } } HandleAllPackets(playerId, (const std::byte *)(pkt + 1), dwMsgSize - sizeof(TPktHdr)); } CheckPlayerInfoTimeouts(); } void multi_send_zero_packet(uint8_t pnum, _cmd_id bCmd, const std::byte *data, size_t size) { assert(pnum != MyPlayerId); assert(data != nullptr); assert(size <= 0x0ffff); for (size_t offset = 0; offset < size;) { TPkt pkt {}; pkt.hdr.wCheck = HeaderCheckVal; auto &message = *reinterpret_cast(pkt.body); message.bCmd = bCmd; assert(offset <= 0x0ffff); message.wOffset = Swap16LE(static_cast(offset)); size_t dwBody = gdwLargestMsgSize - sizeof(pkt.hdr) - sizeof(message); dwBody = std::min(dwBody, size - offset); assert(dwBody <= 0x0ffff); message.wBytes = Swap16LE(static_cast(dwBody)); memcpy(&pkt.body[sizeof(message)], &data[offset], dwBody); const size_t dwMsg = sizeof(pkt.hdr) + sizeof(message) + dwBody; assert(dwMsg <= 0x0ffff); pkt.hdr.wLen = Swap16LE(static_cast(dwMsg)); if (!SNetSendMessage(pnum, &pkt, dwMsg)) { nthread_terminate_game("SNetSendMessage2"); return; } offset += dwBody; } } void NetClose() { if (!sgbNetInited) { return; } sgbNetInited = false; nthread_cleanup(); tmsg_cleanup(); UnregisterNetEventHandlers(); SNetLeaveGame(leaveinfo_t::LEAVE_EXIT); if (gbIsMultiplayer) SDL_Delay(2000); if (!demo::IsRunning()) { Players.clear(); MyPlayer = nullptr; } } bool NetInit(bool bSinglePlayer) { while (true) { SetRndSeed(0); InitGameInfo(); memset(sgbPlayerTurnBitTbl, 0, sizeof(sgbPlayerTurnBitTbl)); gbGameDestroyed = false; memset(sgbPlayerLeftGameTbl, 0, sizeof(sgbPlayerLeftGameTbl)); memset(sgdwPlayerLeftReasonTbl, 0, sizeof(sgdwPlayerLeftReasonTbl)); memset(sgbSendDeltaTbl, 0, sizeof(sgbSendDeltaTbl)); Players.clear(); MyPlayer = nullptr; memset(sgwPackPlrOffsetTbl, 0, sizeof(sgwPackPlrOffsetTbl)); SNetSetBasePlayer(0); if (bSinglePlayer) { if (!InitSingle(&sgGameInitInfo)) return false; } else { if (!InitMulti(&sgGameInitInfo)) return false; } sgbNetInited = true; sgbTimeout = false; delta_init(); InitPlrMsg(); BufferInit(&highPriorityBuffer); BufferInit(&lowPriorityBuffer); shareNextHighPriorityMessage = true; sync_init(); nthread_start(sgbPlayerTurnBitTbl[MyPlayerId]); tmsg_start(); sgdwGameLoops = 0; sgbSentThisCycle = 0; gbDeltaSender = MyPlayerId; gbSomebodyWonGameKludge = false; nthread_send_and_recv_turn(0, 0); SetupLocalPositions(); SendPlayerInfo(SNPLAYER_OTHERS, CMD_SEND_PLRINFO); Player &myPlayer = *MyPlayer; ResetPlayerGFX(myPlayer); myPlayer.plractive = true; gbActivePlayers = 1; if (!sgbPlayerTurnBitTbl[MyPlayerId] || msg_wait_resync()) break; NetClose(); gbSelectProvider = false; } xoshiro128plusplus gameGenerator(sgGameInitInfo.gameSeed); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; for (int i = 0; i < NUMLEVELS; i++) { DungeonSeeds[i] = gameGenerator.next(); LevelSeeds[i] = std::nullopt; } // explicitly randomize the town seed to divorce shops from the game seed DungeonSeeds[0] = GenerateSeed(); PublicGame = DvlNet_IsPublicGame(); Player &myPlayer = *MyPlayer; // separator for marking messages from a different game AddMessageToChatLog(_("New Game"), nullptr, UiFlags::ColorRed); AddMessageToChatLog(fmt::format(fmt::runtime(_("Player '{:s}' (level {:d}) just joined the game")), myPlayer._pName, myPlayer.getCharacterLevel())); // Log join message with seed for joining players (creator already logged it in SNetCreateGame) if (gbIsMultiplayer && !IsLoopback && MyPlayerId != 0) { std::string upperGameName = GameName; std::transform(upperGameName.begin(), upperGameName.end(), upperGameName.begin(), ::toupper); const char *privacy = PublicGame ? "public" : "private"; LogInfo("Joined {} {} multiplayer game '{}' (player id: {}, seed: {})", privacy, ConnectionNames[provider], upperGameName, MyPlayerId, FormatGameSeed(sgGameInitInfo.gameSeed)); } return true; } void recv_plrinfo(Player &player, const TCmdPlrInfoHdr &header, bool recv) { static PlayerNetPack PackedPlayerBuffer[MAX_PLRS]; if (&player == MyPlayer) { return; } const uint8_t pnum = player.getId(); auto &packedPlayer = PackedPlayerBuffer[pnum]; if (sgwPackPlrOffsetTbl[pnum] != Swap16LE(header.wOffset)) { sgwPackPlrOffsetTbl[pnum] = 0; if (header.wOffset != 0) { return; } } if (!recv && sgwPackPlrOffsetTbl[pnum] == 0) { SendPlayerInfo(pnum, CMD_ACK_PLRINFO); } memcpy(reinterpret_cast(&packedPlayer) + Swap16LE(header.wOffset), reinterpret_cast(&header) + sizeof(header), Swap16LE(header.wBytes)); sgwPackPlrOffsetTbl[pnum] += Swap16LE(header.wBytes); if (sgwPackPlrOffsetTbl[pnum] != sizeof(packedPlayer)) { return; } sgwPackPlrOffsetTbl[pnum] = 0; PlayerLeftMsg(player, false); if (!UnPackNetPlayer(packedPlayer, player)) { player = {}; SNetDropPlayer(pnum, leaveinfo_t::LEAVE_DROP); return; } if (!recv) { return; } ResetPlayerGFX(player); player.plractive = true; gbActivePlayers++; std::string_view szEvent; if (sgbPlayerTurnBitTbl[pnum]) { szEvent = _("Player '{:s}' (level {:d}) just joined the game"); if (!IsLoopback) LogInfo("Player '{}' joined the {} game (level {}, {}/{} players)", player._pName, ConnectionNames[provider], player.getCharacterLevel(), gbActivePlayers, MAX_PLRS); } else { szEvent = _("Player '{:s}' (level {:d}) is already in the game"); } EventPlrMsg(fmt::format(fmt::runtime(szEvent), player._pName, player.getCharacterLevel())); SyncInitPlr(player); if (!player.isOnActiveLevel()) { return; } if (!player.hasNoLife()) { StartStand(player, player._pdir); return; } player._pgfxnum &= ~0xFU; player._pmode = PM_DEATH; NewPlrAnim(player, player_graphic::Death, player._pdir); player.AnimInfo.currentFrame = player.AnimInfo.numberOfFrames - 2; dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; } } // namespace devilution ================================================ FILE: Source/multi.h ================================================ /** * @file multi.h * * Interface of functions for keeping multiplayer games in sync. */ #pragma once #include #include #include #include "dvlnet/leaveinfo.hpp" #include "msg.h" #include "utils/attributes.h" namespace devilution { using net::leaveinfo_t; // Defined in player.h, forward declared here to allow for functions which operate in the context of a player. struct Player; // must be unsigned to generate unsigned comparisons with pnum #define MAX_PLRS 4 struct GameData { int32_t size; uint8_t reserved[4]; uint32_t programid; uint8_t versionMajor; uint8_t versionMinor; uint8_t versionPatch; _difficulty nDifficulty; uint8_t nTickRate; uint8_t bRunInTown; uint8_t bTheoQuest; uint8_t bCowQuest; uint8_t bFriendlyFire; uint8_t fullQuests; /** Used to initialise the seed table for dungeon levels so players in multiplayer games generate the same layout */ uint32_t gameSeed[4]; void swapLE(); }; /* @brief Contains info of running public game (for game list browsing) */ struct GameInfo { std::string name; GameData gameData; std::vector players; std::optional latency; std::optional peerIsRelayed; }; extern bool gbSomebodyWonGameKludge; extern uint16_t sgwPackPlrOffsetTbl[MAX_PLRS]; extern uint8_t gbActivePlayers; extern bool gbGameDestroyed; extern DVL_API_FOR_TEST GameData sgGameInitInfo; extern bool gbSelectProvider; extern DVL_API_FOR_TEST bool gbIsMultiplayer; extern std::string GameName; extern std::string GamePassword; extern bool PublicGame; extern uint8_t gbDeltaSender; extern uint32_t player_state[MAX_PLRS]; extern bool IsLoopback; DVL_API_FOR_TEST std::string DescribeLeaveReason(leaveinfo_t leaveReason); std::string FormatGameSeed(const uint32_t gameSeed[4]); void InitGameInfo(); void NetSendLoPri(uint8_t playerId, const std::byte *data, size_t size); void NetSendHiPri(uint8_t playerId, const std::byte *data, size_t size); void multi_send_msg_packet(uint32_t pmask, const std::byte *data, size_t size); void multi_msg_countdown(); void multi_player_left(uint8_t pnum, leaveinfo_t reason); void multi_net_ping(); /** * @return Always true for singleplayer */ bool multi_handle_delta(); void ProcessGameMessagePackets(); void multi_send_zero_packet(uint8_t pnum, _cmd_id bCmd, const std::byte *data, size_t size); void NetClose(); bool NetInit(bool bSinglePlayer); void recv_plrinfo(Player &player, const TCmdPlrInfoHdr &header, bool recv); } // namespace devilution ================================================ FILE: Source/nthread.cpp ================================================ /** * @file nthread.cpp * * Implementation of functions for managing game ticks. */ #include "nthread.h" #include #include #ifdef USE_SDL3 #include #else #include #endif #include "diablo.h" #include "engine/animationinfo.h" #include "engine/demomode.h" #include "game_mode.hpp" #include "gmenu.h" #include "storm/storm_net.hpp" #include "utils/sdl_mutex.h" #include "utils/sdl_thread.h" namespace devilution { uint8_t sgbNetUpdateRate; size_t gdwMsgLenTbl[MAX_PLRS]; uint32_t gdwTurnsInTransit; uintptr_t glpMsgTbl[MAX_PLRS]; uint32_t gdwLargestMsgSize; uint32_t gdwNormalMsgSize; int last_tick; uint8_t ProgressToNextGameTick = 0; namespace { SdlMutex MemCrit; bool nthread_should_run; int8_t sgbSyncCountdown; uint32_t turn_upper_bit; bool sgbTicsOutOfSync; int8_t sgbPacketCountdown; bool sgbThreadIsRunning; SdlThread Thread; void NthreadHandler() { if (!nthread_should_run) { return; } while (true) { MemCrit.lock(); if (!nthread_should_run) { MemCrit.unlock(); break; } nthread_send_and_recv_turn(0, 0); int delta = gnTickDelay; if (nthread_recv_turns()) delta = last_tick - SDL_GetTicks(); MemCrit.unlock(); if (delta > 0) SDL_Delay(delta); if (!nthread_should_run) return; } } } // namespace void nthread_terminate_game(const char *pszFcn) { app_fatal(pszFcn); gbGameDestroyed = true; } uint32_t nthread_send_and_recv_turn(uint32_t curTurn, int turnDelta) { uint32_t curTurnsInTransit; if (!SNetGetTurnsInTransit(&curTurnsInTransit)) { nthread_terminate_game("SNetGetTurnsInTransit"); return 0; } while (curTurnsInTransit++ < gdwTurnsInTransit) { const uint32_t turnTmp = turn_upper_bit | (curTurn & 0x7FFFFFFF); turn_upper_bit = 0; uint32_t turn = turnTmp; if (!SNetSendTurn((char *)&turn, sizeof(turn))) { nthread_terminate_game("SNetSendTurn"); return 0; } curTurn += turnDelta; if (curTurn >= 0x7FFFFFFF) curTurn &= 0xFFFF; } return curTurn; } bool nthread_recv_turns(bool *pfSendAsync) { if (pfSendAsync != nullptr) *pfSendAsync = false; sgbPacketCountdown--; if (sgbPacketCountdown > 0) { last_tick += gnTickDelay; return true; } sgbSyncCountdown--; sgbPacketCountdown = sgbNetUpdateRate; if (sgbSyncCountdown != 0) { if (pfSendAsync != nullptr) *pfSendAsync = true; last_tick += gnTickDelay; return true; } if (!SNetReceiveTurns(MAX_PLRS, (char **)glpMsgTbl, gdwMsgLenTbl, &player_state[0])) { sgbTicsOutOfSync = false; sgbSyncCountdown = 1; sgbPacketCountdown = 1; return false; } if (!sgbTicsOutOfSync) { sgbTicsOutOfSync = true; last_tick = SDL_GetTicks(); } sgbSyncCountdown = 4; multi_msg_countdown(); if (pfSendAsync != nullptr) *pfSendAsync = true; last_tick += gnTickDelay; return true; } void nthread_set_turn_upper_bit() { turn_upper_bit = 0x80000000; } void nthread_start(bool setTurnUpperBit) { last_tick = SDL_GetTicks(); sgbPacketCountdown = 1; sgbSyncCountdown = 1; sgbTicsOutOfSync = true; if (setTurnUpperBit) nthread_set_turn_upper_bit(); else turn_upper_bit = 0; _SNETCAPS caps; caps.size = 36; SNetGetProviderCaps(&caps); gdwTurnsInTransit = caps.defaultturnsintransit; if (gdwTurnsInTransit == 0) gdwTurnsInTransit = 1; if (caps.defaultturnssec <= 20 && caps.defaultturnssec != 0) sgbNetUpdateRate = 20 / caps.defaultturnssec; else sgbNetUpdateRate = 1; uint32_t largestMsgSize = 512; if (caps.maxmessagesize < 0x200) largestMsgSize = caps.maxmessagesize; gdwLargestMsgSize = largestMsgSize; gdwNormalMsgSize = caps.bytessec * sgbNetUpdateRate / 20; gdwNormalMsgSize *= 3; gdwNormalMsgSize >>= 2; if (caps.maxplayers > MAX_PLRS) caps.maxplayers = MAX_PLRS; gdwNormalMsgSize /= caps.maxplayers; while (gdwNormalMsgSize < 0x80) { gdwNormalMsgSize *= 2; sgbNetUpdateRate *= 2; } if (gdwNormalMsgSize > largestMsgSize) gdwNormalMsgSize = largestMsgSize; if (gbIsMultiplayer) { sgbThreadIsRunning = false; MemCrit.lock(); nthread_should_run = true; Thread = SdlThread { NthreadHandler }; } } void nthread_cleanup() { nthread_should_run = false; gdwTurnsInTransit = 0; gdwNormalMsgSize = 0; gdwLargestMsgSize = 0; if (Thread.joinable() && Thread.get_id() != this_sdl_thread::get_id()) { if (!sgbThreadIsRunning) MemCrit.unlock(); Thread.join(); } } void nthread_ignore_mutex(bool bStart) { if (!Thread.joinable()) return; if (bStart) MemCrit.unlock(); else MemCrit.lock(); sgbThreadIsRunning = bStart; } bool nthread_has_500ms_passed(bool *drawGame /*= nullptr*/) { const int currentTickCount = SDL_GetTicks(); int ticksElapsed = currentTickCount - last_tick; // Check if we missed multiple game ticks (> 10) if (ticksElapsed > gnTickDelay * 10) { bool resetLastTick = true; if (gbIsMultiplayer) { for (size_t i = 0; i < Players.size(); i++) { if ((player_state[i] & PS_CONNECTED) != 0 && i != MyPlayerId) { // Reset last tick is not allowed when other players are connected, because the elapsed time is needed to sync the game ticks between the clients resetLastTick = false; break; } } } if (resetLastTick) { // Reset last tick to avoid catching up with all missed game ticks (game speed is dramatically increased for a short time) last_tick = currentTickCount; ticksElapsed = 0; } } if (drawGame != nullptr) { // Check if we missed a game tick. // This can happen when we run a low-end device that can't render fast enough (typically 20fps). // If this happens, try to speed-up the game by skipping the rendering. // This avoids desyncs and hourglasses when running multiplayer and slowdowns in singleplayer. *drawGame = ticksElapsed <= gnTickDelay; } return ticksElapsed >= 0; } void nthread_UpdateProgressToNextGameTick() { if (!gbRunGame || PauseMode != 0 || (!gbIsMultiplayer && gmenu_is_active()) || !gbProcessPlayers || demo::IsRunning()) // if game is not running or paused there is no next gametick in the near future return; const int currentTickCount = SDL_GetTicks(); const int ticksMissing = last_tick - currentTickCount; if (ticksMissing <= 0) { ProgressToNextGameTick = AnimationInfo::baseValueFraction; // game tick is due return; } const int ticksAdvanced = gnTickDelay - ticksMissing; int32_t fraction = ticksAdvanced * AnimationInfo::baseValueFraction / gnTickDelay; fraction = std::clamp(fraction, 0, AnimationInfo::baseValueFraction); ProgressToNextGameTick = static_cast(fraction); } } // namespace devilution ================================================ FILE: Source/nthread.h ================================================ /** * @file nthread.h * * Interface of functions for managing game ticks. */ #pragma once #include #include "player.h" #include "utils/attributes.h" namespace devilution { extern uint8_t sgbNetUpdateRate; extern size_t gdwMsgLenTbl[MAX_PLRS]; extern uint32_t gdwTurnsInTransit; extern uintptr_t glpMsgTbl[MAX_PLRS]; extern uint32_t gdwLargestMsgSize; extern uint32_t gdwNormalMsgSize; /** @brief the progress as a fraction (see AnimationInfo::baseValueFraction) in time to the next game tick */ extern DVL_API_FOR_TEST uint8_t ProgressToNextGameTick; extern int last_tick; void nthread_terminate_game(const char *pszFcn); uint32_t nthread_send_and_recv_turn(uint32_t curTurn, int turnDelta); bool nthread_recv_turns(bool *pfSendAsync = nullptr); void nthread_set_turn_upper_bit(); void nthread_start(bool setTurnUpperBit); void nthread_cleanup(); void nthread_ignore_mutex(bool bStart); /** * @brief Checks if it's time for the logic to advance * @return True if the engine should tick */ bool nthread_has_500ms_passed(bool *drawGame = nullptr); /** * @brief Updates the progress in time to the next game tick */ void nthread_UpdateProgressToNextGameTick(); } // namespace devilution ================================================ FILE: Source/objects.cpp ================================================ /** * @file objects.cpp * * Implementation of object functionality, interaction, spawning, loading, etc. */ #include #include #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "cursor.h" #ifdef _DEBUG #include "debug.h" #endif #include "diablo_msg.hpp" #include "engine/backbuffer_state.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "headless_mode.hpp" #include "inv.h" #include "inv_iterators.hpp" #include "levels/crypt.h" #include "levels/drlg_l4.h" #include "levels/setmaps.h" #include "levels/themes.h" #include "levels/tile_properties.hpp" #include "lighting.h" #include "minitext.h" #include "missiles.h" #include "monster.h" #include "options.h" #include "qol/stash.h" #include "stores.h" #include "tables/objdat.h" #include "towners.h" #include "track.h" #include "utils/algorithm/container.hpp" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" namespace devilution { Object Objects[MAXOBJECTS]; int AvailableObjects[MAXOBJECTS]; int ActiveObjects[MAXOBJECTS]; int ActiveObjectCount; bool LoadingMapObjects; int NaKrulTomeSequence; namespace { enum shrine_type : uint8_t { ShrineMysterious, ShrineHidden, ShrineGloomy, ShrineWeird, ShrineMagical, ShrineStone, ShrineReligious, ShrineEnchanted, ShrineThaumaturgic, ShrineFascinating, ShrineCryptic, ShrineMagicaL2, ShrineEldritch, ShrineEerie, ShrineDivine, ShrineHoly, ShrineSacred, ShrineSpiritual, ShrineSpooky, ShrineAbandoned, ShrineCreepy, ShrineQuiet, ShrineSecluded, ShrineOrnate, ShrineGlimmering, ShrineTainted, ShrineOily, ShrineGlowing, ShrineMendicant, ShrineSparkling, ShrineTown, ShrineShimmering, ShrineSolar, ShrineMurphys, NumberOfShrineTypes }; enum { // clang-format off DOOR_CLOSED = 0, DOOR_OPEN = 1, DOOR_BLOCKED = 2, // clang-format on }; int trapid; int trapdir; OptionalOwnedClxSpriteList pObjCels[40]; object_graphic_id ObjFileList[40]; /** Specifies the number of active objects. */ int leverid; int numobjfiles; /** Specifies the X-coordinate delta between barrels. */ int bxadd[8] = { -1, 0, 1, -1, 1, -1, 0, 1 }; /** Specifies the Y-coordinate delta between barrels. */ int byadd[8] = { -1, -1, -1, 0, 0, 1, 1, 1 }; /** Maps from shrine_id to shrine name. */ const char *const ShrineNames[] = { // TRANSLATORS: Shrine Name Block N_("Mysterious"), N_("Hidden"), N_("Gloomy"), N_("Weird"), N_("Magical"), N_("Stone"), N_("Religious"), N_("Enchanted"), N_("Thaumaturgic"), N_("Fascinating"), N_("Cryptic"), N_("Magical"), N_("Eldritch"), N_("Eerie"), N_("Divine"), N_("Holy"), N_("Sacred"), N_("Spiritual"), N_("Spooky"), N_("Abandoned"), N_("Creepy"), N_("Quiet"), N_("Secluded"), N_("Ornate"), N_("Glimmering"), N_("Tainted"), N_("Oily"), N_("Glowing"), N_("Mendicant's"), N_("Sparkling"), N_("Town"), N_("Shimmering"), N_("Solar"), // TRANSLATORS: Shrine Name Block end N_("Murphy's"), }; /** * Specifies the game type for which each shrine may appear. * ShrineTypeAny - sp & mp * ShrineTypeSingle - sp only * ShrineTypeMulti - mp only */ enum shrine_gametype : uint8_t { ShrineTypeAny, ShrineTypeSingle, ShrineTypeMulti, }; shrine_gametype shrineavail[] = { ShrineTypeAny, // Mysterious ShrineTypeAny, // Hidden ShrineTypeSingle, // Gloomy ShrineTypeSingle, // Weird ShrineTypeAny, // Magical ShrineTypeAny, // Stone ShrineTypeAny, // Religious ShrineTypeAny, // Enchanted ShrineTypeSingle, // Thaumaturgic ShrineTypeAny, // Fascinating ShrineTypeAny, // Cryptic ShrineTypeAny, // Magical ShrineTypeAny, // Eldritch ShrineTypeAny, // Eerie ShrineTypeAny, // Divine ShrineTypeAny, // Holy ShrineTypeAny, // Sacred ShrineTypeAny, // Spiritual ShrineTypeMulti, // Spooky ShrineTypeAny, // Abandoned ShrineTypeAny, // Creepy ShrineTypeAny, // Quiet ShrineTypeAny, // Secluded ShrineTypeAny, // Ornate ShrineTypeAny, // Glimmering ShrineTypeMulti, // Tainted ShrineTypeAny, // Oily ShrineTypeAny, // Glowing ShrineTypeAny, // Mendicant's ShrineTypeAny, // Sparkling ShrineTypeAny, // Town ShrineTypeAny, // Shimmering ShrineTypeSingle, // Solar, ShrineTypeAny, // Murphy's }; /** Maps from book_id to book name. */ const char *const StoryBookName[] = { N_(/* TRANSLATORS: Book Title */ "The Great Conflict"), N_(/* TRANSLATORS: Book Title */ "The Wages of Sin are War"), N_(/* TRANSLATORS: Book Title */ "The Tale of the Horadrim"), N_(/* TRANSLATORS: Book Title */ "The Dark Exile"), N_(/* TRANSLATORS: Book Title */ "The Sin War"), N_(/* TRANSLATORS: Book Title */ "The Binding of the Three"), N_(/* TRANSLATORS: Book Title */ "The Realms Beyond"), N_(/* TRANSLATORS: Book Title */ "Tale of the Three"), N_(/* TRANSLATORS: Book Title */ "The Black King"), N_(/* TRANSLATORS: Book Title */ "Journal: The Ensorcellment"), N_(/* TRANSLATORS: Book Title */ "Journal: The Meeting"), N_(/* TRANSLATORS: Book Title */ "Journal: The Tirade"), N_(/* TRANSLATORS: Book Title */ "Journal: His Power Grows"), N_(/* TRANSLATORS: Book Title */ "Journal: NA-KRUL"), N_(/* TRANSLATORS: Book Title */ "Journal: The End"), N_(/* TRANSLATORS: Book Title */ "A Spellbook"), }; /** Specifies the speech IDs of each dungeon type narrator book, for each player class. */ _speech_id StoryText[3][3] = { { TEXT_BOOK11, TEXT_BOOK12, TEXT_BOOK13 }, { TEXT_BOOK21, TEXT_BOOK22, TEXT_BOOK23 }, { TEXT_BOOK31, TEXT_BOOK32, TEXT_BOOK33 } }; bool RndLocOk(Point p) { if (dMonster[p.x][p.y] != 0) return false; if (dPlayer[p.x][p.y] != 0) return false; if (IsObjectAtPosition(p)) return false; if (TileContainsSetPiece(p)) return false; if (TileHasAny(p, TileProperties::Solid)) return false; return IsNoneOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CRYPT) || dPiece[p.x][p.y] <= 125 || dPiece[p.x][p.y] >= 143; } bool IsAreaOk(Rectangle rect) { return c_all_of(PointsInRectangle(rect), &RndLocOk); } bool CanPlaceWallTrap(Point pos) { if (dObject[pos.x][pos.y] != 0) return false; if (TileContainsSetPiece(pos)) return false; return TileHasAny(pos, TileProperties::Trap); } void InitRndLocObj(int min, int max, _object_id objtype) { const int numobjs = GenerateRnd(max - min) + min; for (int i = 0; i < numobjs; i++) { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 1 }, { 3, 3 } })) { AddObject(objtype, { xp, yp }); break; } } } } void InitRndLocBigObj(int min, int max, _object_id objtype) { const int numobjs = GenerateRnd(max - min) + min; for (int i = 0; i < numobjs; i++) { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 2 }, { 3, 4 } })) { AddObject(objtype, { xp, yp }); break; } } } } bool CanPlaceRandomObject(Point position, Displacement standoff) { return IsAreaOk(Rectangle { position - standoff, Size { standoff.deltaX * 2 + 1, standoff.deltaY * 2 + 1 } }); } std::optional GetRandomObjectPosition(Displacement standoff) { for (int i = 0; i <= 20000; i++) { Point position = Point { GenerateRnd(80), GenerateRnd(80) } + Displacement { 16, 16 }; if (CanPlaceRandomObject(position, standoff)) return position; } return {}; } void InitRndLocObj5x5(int min, int max, _object_id objtype) { const int numobjs = min + GenerateRnd(max - min); for (int i = 0; i < numobjs; i++) { std::optional position = GetRandomObjectPosition({ 2, 2 }); if (!position) return; AddObject(objtype, *position); } } void ClrAllObjects() { for (Object &object : Objects) { object = {}; } ActiveObjectCount = 0; for (int i = 0; i < MAXOBJECTS; i++) { AvailableObjects[i] = i; } memset(ActiveObjects, 0, sizeof(ActiveObjects)); trapdir = 0; trapid = 1; leverid = 1; } void AddTortures() { for (int oy = 0; oy < MAXDUNY; oy++) { for (int ox = 0; ox < MAXDUNX; ox++) { if (dPiece[ox][oy] == 366) { AddObject(OBJ_TORTURE1, { ox, oy + 1 }); AddObject(OBJ_TORTURE3, { ox + 2, oy - 1 }); AddObject(OBJ_TORTURE2, { ox, oy + 3 }); AddObject(OBJ_TORTURE4, { ox + 4, oy - 1 }); AddObject(OBJ_TORTURE5, { ox, oy + 5 }); AddObject(OBJ_TNUDEM1, { ox + 1, oy + 3 }); AddObject(OBJ_TNUDEM2, { ox + 4, oy + 5 }); AddObject(OBJ_TNUDEM3, { ox + 2, oy }); AddObject(OBJ_TNUDEM4, { ox + 3, oy + 2 }); AddObject(OBJ_TNUDEW1, { ox + 2, oy + 4 }); AddObject(OBJ_TNUDEW2, { ox + 2, oy + 1 }); AddObject(OBJ_TNUDEW3, { ox + 4, oy + 2 }); } } } } void AddCandles() { const int tx = Quests[Q_PWATER].position.x; const int ty = Quests[Q_PWATER].position.y; AddObject(OBJ_STORYCANDLE, { tx - 2, ty + 1 }); AddObject(OBJ_STORYCANDLE, { tx + 3, ty + 1 }); AddObject(OBJ_STORYCANDLE, { tx - 1, ty + 2 }); AddObject(OBJ_STORYCANDLE, { tx + 2, ty + 2 }); } /** * @brief Attempts to spawn a book somewhere on the current floor which when activated will change a region of the map. * * This object acts like a lever and will cause a change to the map based on what quest is active. The exact effect is * determined by OperateBookLever(). * * @param affectedArea The map region to be updated when this object is activated by the player. * @param msg The quest text to play when the player activates the book. */ void AddBookLever(_object_id type, WorldTileRectangle affectedArea, _speech_id msg) { std::optional position = GetRandomObjectPosition({ 2, 2 }); if (!position) return; if (type == OBJ_BLOODBOOK) position = SetPiece.position.megaToWorld() + Displacement { 9, 24 }; Object *lever = AddObject(type, *position); assert(lever != nullptr); lever->InitializeQuestBook(affectedArea, leverid, msg); leverid++; } void InitRndBarrels() { _object_id barrelId = OBJ_BARREL; _object_id explosiveBarrelId = OBJ_BARRELEX; if (leveltype == DTYPE_NEST) { barrelId = OBJ_POD; explosiveBarrelId = OBJ_PODEX; } else if (leveltype == DTYPE_CRYPT) { barrelId = OBJ_URN; explosiveBarrelId = OBJ_URNEX; } /** number of groups of barrels to generate */ const int numobjs = GenerateRnd(5) + 3; for (int i = 0; i < numobjs; i++) { int xp; int yp; do { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; } while (!RndLocOk({ xp, yp })); _object_id o = FlipCoin(4) ? explosiveBarrelId : barrelId; AddObject(o, { xp, yp }); bool found = true; /** regulates chance to stop placing barrels in current group */ int p = 0; /** number of barrels in current group */ int c = 1; while (FlipCoin(p) && found) { /** number of tries of placing next barrel in current group */ int t = 0; found = false; while (true) { if (t >= 3) break; const int dir = GenerateRnd(8); xp += bxadd[dir]; yp += byadd[dir]; found = RndLocOk({ xp, yp }); t++; if (found) break; } if (found) { o = FlipCoin(5) ? explosiveBarrelId : barrelId; AddObject(o, { xp, yp }); c++; } p = c / 2; } } } void AddL2Torches() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { const Point testPosition = { i, j }; if (TileContainsSetPiece(testPosition)) continue; const int pn = dPiece[i][j]; if (pn == 0 && FlipCoin(3)) { AddObject(OBJ_TORCHL2, testPosition); } if (pn == 4 && FlipCoin(3)) { AddObject(OBJ_TORCHR2, testPosition); } if (pn == 36 && FlipCoin(10) && !IsObjectAtPosition(testPosition + Direction::NorthWest)) { AddObject(OBJ_TORCHL, testPosition + Direction::NorthWest); } if (pn == 40 && FlipCoin(10) && !IsObjectAtPosition(testPosition + Direction::NorthEast)) { AddObject(OBJ_TORCHR, testPosition + Direction::NorthEast); } } } } void AddObjTraps() { int rndv; if (currlevel == 1) rndv = 10; if (currlevel >= 2) rndv = 15; if (currlevel >= 5) rndv = 20; if (currlevel >= 7) rndv = 25; for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { Object *triggerObject = FindObjectAtPosition({ i, j }, false); if (triggerObject == nullptr || GenerateRnd(100) >= rndv) continue; if (!AllObjects[triggerObject->_otype].isTrap()) continue; Object *trapObject = nullptr; if (FlipCoin()) { int xp = i - 1; while (IsTileNotSolid({ xp, j })) xp--; if (!CanPlaceWallTrap({ xp, j }) || i - xp <= 1) continue; trapObject = AddObject(OBJ_TRAPL, { xp, j }); } else { int yp = j - 1; while (IsTileNotSolid({ i, yp })) yp--; if (!CanPlaceWallTrap({ i, yp }) || j - yp <= 1) continue; trapObject = AddObject(OBJ_TRAPR, { i, yp }); } if (trapObject != nullptr) { // nullptr check just in case we fail to find a valid location to place a trap in the chosen direction trapObject->_oVar1 = i; trapObject->_oVar2 = j; triggerObject->_oTrapFlag = true; } } } } void AddChestTraps() { for (int j = 0; j < MAXDUNY; j++) { for (int i = 0; i < MAXDUNX; i++) { // NOLINT(modernize-loop-convert) Object *chestObject = FindObjectAtPosition({ i, j }, false); if (chestObject != nullptr && chestObject->IsUntrappedChest() && GenerateRnd(100) < 10) { switch (chestObject->_otype) { case OBJ_CHEST1: chestObject->_otype = OBJ_TCHEST1; break; case OBJ_CHEST2: chestObject->_otype = OBJ_TCHEST2; break; case OBJ_CHEST3: chestObject->_otype = OBJ_TCHEST3; break; default: break; } chestObject->_oTrapFlag = true; if (leveltype == DTYPE_CATACOMBS) { chestObject->_oVar4 = GenerateRnd(2); } else { chestObject->_oVar4 = GenerateRnd(gbIsHellfire ? 6 : 3); } } } } } void LoadMapObjects(const char *path, Point start, WorldTileRectangle mapRange = {}, int leveridx = 0) { LoadingMapObjects = true; auto dunData = LoadFileInMem(path); WorldTileSize size = GetDunSize(dunData.get()); const int layer2Offset = 2 + size.width * size.height; // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *objectLayer = &dunData[layer2Offset + size.width * size.height * 2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(Swap16LE(objectLayer[j * size.width + i])); if (objectId != 0) { const Point mapPos = start + Displacement { i, j }; Object *mapObject = AddObject(ObjTypeConv[objectId], mapPos); if (leveridx > 0 && mapObject != nullptr) mapObject->InitializeLoadedObject(mapRange, leveridx); } } } LoadingMapObjects = false; } void AddDiabObjs() { LoadMapObjects("levels\\l4data\\diab1.dun", DiabloQuad1.megaToWorld(), { DiabloQuad2, { 11, 12 } }, 1); LoadMapObjects("levels\\l4data\\diab2a.dun", DiabloQuad2.megaToWorld(), { DiabloQuad3, { 11, 11 } }, 2); LoadMapObjects("levels\\l4data\\diab3a.dun", DiabloQuad3.megaToWorld(), { DiabloQuad4, { 9, 9 } }, 3); } void AddCryptObject(Object &object, int a2) { if (a2 > 5) { const Player &myPlayer = *MyPlayer; switch (a2) { case 6: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKA; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKA; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKA; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKA; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKA; break; default: break; } break; case 7: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKB; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKB; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKB; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKB; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKB; break; default: break; } break; case 8: switch (myPlayer._pClass) { case HeroClass::Warrior: case HeroClass::Barbarian: object._oVar2 = TEXT_BOOKC; break; case HeroClass::Rogue: object._oVar2 = TEXT_RBOOKC; break; case HeroClass::Sorcerer: object._oVar2 = TEXT_MBOOKC; break; case HeroClass::Monk: object._oVar2 = TEXT_OBOOKC; break; case HeroClass::Bard: object._oVar2 = TEXT_BBOOKC; break; default: break; } break; } object._oVar3 = 15; object._oVar8 = a2; } else { object._oVar2 = a2 + TEXT_SKLJRN; object._oVar3 = a2 + 9; object._oVar8 = 0; } object._oVar1 = 1; object._oAnimFrame = 5 - 2 * object._oVar1; object._oVar4 = object._oAnimFrame + 1; } void SetupObject(Object &object, Point position, _object_id ot) { const ObjectData &objectData = AllObjects[ot]; object._otype = ot; object_graphic_id ofi = objectData.ofindex; object.position = position; if (!HeadlessMode) { const auto &found = c_find(ObjFileList, ofi); if (found == std::end(ObjFileList)) { LogCritical("Unable to find object_graphic_id {} in list of objects to load, level generation error.", static_cast(ofi)); return; } const size_t j = std::distance(std::begin(ObjFileList), found); if (pObjCels[j]) { object._oAnimData.emplace(*pObjCels[j]); } else { object._oAnimData = std::nullopt; } } object._oAnimFlag = objectData.isAnimated(); if (object._oAnimFlag) { object._oAnimDelay = objectData.animDelay; object._oAnimCnt = GenerateRnd(object._oAnimDelay); object._oAnimLen = objectData.animLen; object._oAnimFrame = GenerateRnd(object._oAnimLen - 1) + 1; } else { object._oAnimDelay = 1000; object._oAnimCnt = 0; object._oAnimLen = objectData.animLen; object._oAnimFrame = objectData.animDelay; } object._oAnimWidth = objectData.animWidth; object._oSolidFlag = objectData.isSolid() ? 1 : 0; object._oMissFlag = objectData.missilesPassThrough() ? 1 : 0; object.applyLighting = objectData.applyLighting(); object._oDelFlag = false; object._oBreak = objectData.isBreakable() ? 1 : 0; object.selectionRegion = objectData.selectionRegion; object._oPreFlag = false; object._oTrapFlag = false; object._oDoorFlag = false; } void AddCryptBook(_object_id ot, int v2, Point position) { if (ActiveObjectCount >= MAXOBJECTS) return; const int oi = AvailableObjects[0]; AvailableObjects[0] = AvailableObjects[MAXOBJECTS - 1 - ActiveObjectCount]; ActiveObjects[ActiveObjectCount] = oi; dObject[position.x][position.y] = oi + 1; Object &object = Objects[oi]; SetupObject(object, position, ot); AddCryptObject(object, v2); ActiveObjectCount++; } void AddCryptStoryBook(int s) { std::optional position = GetRandomObjectPosition({ 3, 2 }); if (!position) return; AddCryptBook(OBJ_L5BOOKS, s, *position); AddObject(OBJ_L5CANDLE, *position + Displacement { -2, 1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { -2, 0 }); AddObject(OBJ_L5CANDLE, *position + Displacement { -1, -1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 1, -1 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 2, 0 }); AddObject(OBJ_L5CANDLE, *position + Displacement { 2, 1 }); } void AddNakrulLever() { while (true) { const int xp = GenerateRnd(80) + 16; const int yp = GenerateRnd(80) + 16; if (IsAreaOk(Rectangle { { xp - 1, yp - 1 }, { 3, 3 } })) { break; } } AddObject(OBJ_L5LEVER, { UberRow + 3, UberCol - 1 }); } void AddNakrulBook(int a1, Point position) { AddCryptBook(OBJ_L5BOOKS, a1, position); } void AddNakrulGate() { AddNakrulLever(); switch (GenerateRnd(6)) { case 0: AddNakrulBook(6, { UberRow + 3, UberCol }); AddNakrulBook(7, { UberRow + 2, UberCol - 3 }); AddNakrulBook(8, { UberRow + 2, UberCol + 2 }); break; case 1: AddNakrulBook(6, { UberRow + 3, UberCol }); AddNakrulBook(8, { UberRow + 2, UberCol - 3 }); AddNakrulBook(7, { UberRow + 2, UberCol + 2 }); break; case 2: AddNakrulBook(7, { UberRow + 3, UberCol }); AddNakrulBook(6, { UberRow + 2, UberCol - 3 }); AddNakrulBook(8, { UberRow + 2, UberCol + 2 }); break; case 3: AddNakrulBook(7, { UberRow + 3, UberCol }); AddNakrulBook(8, { UberRow + 2, UberCol - 3 }); AddNakrulBook(6, { UberRow + 2, UberCol + 2 }); break; case 4: AddNakrulBook(8, { UberRow + 3, UberCol }); AddNakrulBook(7, { UberRow + 2, UberCol - 3 }); AddNakrulBook(6, { UberRow + 2, UberCol + 2 }); break; case 5: AddNakrulBook(8, { UberRow + 3, UberCol }); AddNakrulBook(6, { UberRow + 2, UberCol - 3 }); AddNakrulBook(7, { UberRow + 2, UberCol + 2 }); break; } } void AddStoryBooks() { std::optional position = GetRandomObjectPosition({ 3, 2 }); if (!position) return; AddObject(OBJ_STORYBOOK, *position); AddObject(OBJ_STORYCANDLE, *position + Displacement { -2, 1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { -2, 0 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { -1, -1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 1, -1 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 2, 0 }); AddObject(OBJ_STORYCANDLE, *position + Displacement { 2, 1 }); } void AddHookedBodies(int freq) { for (WorldTileCoord j = 0; j < DMAXY; j++) { const WorldTileCoord jj = 16 + j * 2; for (WorldTileCoord i = 0; i < DMAXX; i++) { const WorldTileCoord ii = 16 + i * 2; if (dungeon[i][j] != 1 && dungeon[i][j] != 2) continue; if (!FlipCoin(freq)) continue; if (IsNearThemeRoom({ i, j })) continue; if (dungeon[i][j] == 1 && dungeon[i + 1][j] == 6) { switch (GenerateRnd(3)) { case 0: AddObject(OBJ_TORTURE1, { ii + 1, jj }); break; case 1: AddObject(OBJ_TORTURE2, { ii + 1, jj }); break; case 2: AddObject(OBJ_TORTURE5, { ii + 1, jj }); break; } continue; } if (dungeon[i][j] == 2 && dungeon[i][j + 1] == 6) { AddObject(PickRandomlyAmong({ OBJ_TORTURE3, OBJ_TORTURE4 }), { ii, jj }); } } } } void AddL4Goodies() { AddHookedBodies(6); InitRndLocObj(2, 6, OBJ_TNUDEM1); InitRndLocObj(2, 6, OBJ_TNUDEM2); InitRndLocObj(2, 6, OBJ_TNUDEM3); InitRndLocObj(2, 6, OBJ_TNUDEM4); InitRndLocObj(2, 6, OBJ_TNUDEW1); InitRndLocObj(2, 6, OBJ_TNUDEW2); InitRndLocObj(2, 6, OBJ_TNUDEW3); InitRndLocObj(2, 6, OBJ_DECAP); InitRndLocObj(1, 3, OBJ_CAULDRON); } void AddLazStand() { int cnt = 0; int xp; int yp; while (true) { xp = GenerateRnd(80) + 16; yp = GenerateRnd(80) + 16; if (!IsAreaOk(Rectangle { { xp - 2, yp - 3 }, { 6, 7 } })) { cnt++; if (cnt > 10000) { InitRndLocObj(1, 1, OBJ_LAZSTAND); return; } } else { break; } } AddObject(OBJ_LAZSTAND, { xp, yp }); AddObject(OBJ_TNUDEM2, { xp, yp + 2 }); AddObject(OBJ_STORYCANDLE, { xp + 1, yp + 2 }); AddObject(OBJ_TNUDEM3, { xp + 2, yp + 2 }); AddObject(OBJ_TNUDEW1, { xp, yp - 2 }); AddObject(OBJ_STORYCANDLE, { xp + 1, yp - 2 }); AddObject(OBJ_TNUDEW2, { xp + 2, yp - 2 }); AddObject(OBJ_STORYCANDLE, { xp - 1, yp - 1 }); AddObject(OBJ_TNUDEW3, { xp - 1, yp }); AddObject(OBJ_STORYCANDLE, { xp - 1, yp + 1 }); } void DeleteObject(int oi, int i) { const Object &object = Objects[oi]; const Point position = object.position; dObject[position.x][position.y] = 0; AvailableObjects[-ActiveObjectCount + MAXOBJECTS] = oi; ActiveObjectCount--; if (ObjectUnderCursor == &object) // Unselect object if this was highlighted by player ObjectUnderCursor = nullptr; if (ActiveObjectCount > 0 && i != ActiveObjectCount) ActiveObjects[i] = ActiveObjects[ActiveObjectCount]; } void AddChest(Object &chest) { if (FlipCoin()) chest._oAnimFrame += 3; chest._oRndSeed = AdvanceRndSeed(); switch (chest._otype) { case OBJ_CHEST1: case OBJ_TCHEST1: if (setlevel) { chest._oVar1 = 1; break; } chest._oVar1 = GenerateRnd(2); break; case OBJ_TCHEST2: case OBJ_CHEST2: if (setlevel) { chest._oVar1 = 2; break; } chest._oVar1 = GenerateRnd(3); break; case OBJ_TCHEST3: case OBJ_CHEST3: if (setlevel) { chest._oVar1 = 3; break; } chest._oVar1 = GenerateRnd(4); break; default: break; } chest._oVar2 = GenerateRnd(8); } void ObjSetMicro(Point position, int pn) { dPiece[position.x][position.y] = pn; } void DoorSet(Point position, bool isLeftDoor) { const int pn = dPiece[position.x][position.y]; switch (pn) { case 42: ObjSetMicro(position, 391); break; case 44: ObjSetMicro(position, 393); break; case 49: ObjSetMicro(position, isLeftDoor ? 410 : 411); break; case 53: ObjSetMicro(position, 396); break; case 54: ObjSetMicro(position, 397); break; case 60: ObjSetMicro(position, 398); break; case 66: ObjSetMicro(position, 399); break; case 67: ObjSetMicro(position, 400); break; case 68: ObjSetMicro(position, 402); break; case 69: ObjSetMicro(position, 403); break; case 71: ObjSetMicro(position, 405); break; case 211: ObjSetMicro(position, 406); break; case 353: ObjSetMicro(position, 408); break; case 354: ObjSetMicro(position, 409); break; case 410: case 411: ObjSetMicro(position, 395); break; } } void CryptDoorSet(Point position, bool isLeftDoor) { const int pn = dPiece[position.x][position.y]; switch (pn) { case 74: ObjSetMicro(position, 203); break; case 78: ObjSetMicro(position, 207); break; case 85: ObjSetMicro(position, isLeftDoor ? 231 : 233); break; case 90: ObjSetMicro(position, 214); break; case 92: ObjSetMicro(position, 217); break; case 98: ObjSetMicro(position, 219); break; case 110: ObjSetMicro(position, 221); break; case 112: ObjSetMicro(position, 223); break; case 114: ObjSetMicro(position, 225); break; case 116: ObjSetMicro(position, 227); break; case 118: ObjSetMicro(position, 229); break; case 231: case 233: ObjSetMicro(position, 211); break; } } void SetDoorStateOpen(Object &door) { door._oVar4 = DOOR_OPEN; door._oPreFlag = true; door._oMissFlag = true; door.selectionRegion = SelectionRegion::Middle; switch (door._otype) { case OBJ_L1LDOOR: // 214: blood splater // 407: blood pool // 392: open door (no frame) ObjSetMicro(door.position, door._oVar1 == 214 ? 407 : 392); dSpecial[door.position.x][door.position.y] = 7; DoorSet(door.position + Direction::NorthEast, true); break; case OBJ_L1RDOOR: ObjSetMicro(door.position, 394); dSpecial[door.position.x][door.position.y] = 8; DoorSet(door.position + Direction::NorthWest, false); break; case OBJ_L2LDOOR: ObjSetMicro(door.position, 12); dSpecial[door.position.x][door.position.y] = 5; break; case OBJ_L2RDOOR: ObjSetMicro(door.position, 16); dSpecial[door.position.x][door.position.y] = 6; break; case OBJ_L3LDOOR: ObjSetMicro(door.position, 537); break; case OBJ_L3RDOOR: ObjSetMicro(door.position, 540); break; case OBJ_L5LDOOR: ObjSetMicro(door.position, 205); CryptDoorSet(door.position + Direction::NorthEast, true); break; case OBJ_L5RDOOR: ObjSetMicro(door.position, 208); CryptDoorSet(door.position + Direction::NorthWest, false); break; default: break; } } void SetDoorStateClosed(Object &door) { door._oVar4 = DOOR_CLOSED; door._oPreFlag = false; door._oMissFlag = false; door.selectionRegion = SelectionRegion::Bottom | SelectionRegion::Middle; switch (door._otype) { case OBJ_L1LDOOR: { // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthEast; if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) ObjSetMicro(openPosition, 411); else ObjSetMicro(openPosition, door._oVar2 - 1); break; } break; case OBJ_L1RDOOR: { // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthWest; if (door._oVar2 == 50 && dPiece[openPosition.x][openPosition.y] == 395) ObjSetMicro(openPosition, 410); else ObjSetMicro(openPosition, door._oVar2 - 1); break; } break; case OBJ_L2LDOOR: // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, 537); break; case OBJ_L2RDOOR: // Clear overlapping arches dSpecial[door.position.x][door.position.y] = 0; ObjSetMicro(door.position, 539); break; case OBJ_L3LDOOR: ObjSetMicro(door.position, 530); break; case OBJ_L3RDOOR: ObjSetMicro(door.position, 533); break; case OBJ_L5LDOOR: { ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthEast; if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) ObjSetMicro(openPosition, 233); else ObjSetMicro(openPosition, door._oVar2 - 1); } break; case OBJ_L5RDOOR: { ObjSetMicro(door.position, door._oVar1 - 1); // Restore the normal tile where the open door used to be auto openPosition = door.position + Direction::NorthWest; if (door._oVar2 == 86 && dPiece[openPosition.x][openPosition.y] == 209) ObjSetMicro(openPosition, 231); else ObjSetMicro(openPosition, door._oVar2 - 1); } break; default: break; } } void AddDoor(Object &door) { door._oDoorFlag = true; switch (door._otype) { case OBJ_L1LDOOR: case OBJ_L5LDOOR: door._oVar1 = dPiece[door.position.x][door.position.y] + 1; door._oVar2 = dPiece[door.position.x][door.position.y - 1] + 1; break; case OBJ_L1RDOOR: case OBJ_L5RDOOR: door._oVar1 = dPiece[door.position.x][door.position.y] + 1; door._oVar2 = dPiece[door.position.x - 1][door.position.y] + 1; break; default: break; } SetDoorStateClosed(door); } void AddSarcophagus(Object &sarcophagus) { dObject[sarcophagus.position.x][sarcophagus.position.y - 1] = -(static_cast(sarcophagus.GetId()) + 1); sarcophagus._oVar1 = GenerateRnd(10); sarcophagus._oRndSeed = AdvanceRndSeed(); if (sarcophagus._oVar1 >= 8) { Monster *monster = PreSpawnSkeleton(); if (monster != nullptr) { sarcophagus._oVar2 = static_cast(monster->getId()); } else { sarcophagus._oVar2 = -1; } } } void AddFlameTrap(Object &flameTrap) { flameTrap._oVar1 = trapid; flameTrap._oVar2 = 0; flameTrap._oVar3 = trapdir; flameTrap._oVar4 = 0; } void AddFlameLever(Object &flameLever) { flameLever._oVar1 = trapid; flameLever._oVar2 = static_cast(MissileID::InfernoControl); } void AddTrap(Object &trap) { int effectiveLevel = currlevel; if (leveltype == DTYPE_NEST) effectiveLevel -= 4; else if (leveltype == DTYPE_CRYPT) effectiveLevel -= 8; const int missileType = GenerateRnd(effectiveLevel / 3 + 1); if (missileType == 0) trap._oVar3 = static_cast(MissileID::Arrow); if (missileType == 1) trap._oVar3 = static_cast(MissileID::Firebolt); if (missileType == 2) trap._oVar3 = static_cast(MissileID::LightningControl); trap._oVar4 = 0; } void AddObjectLight(Object &object) { int radius; switch (object._otype) { case OBJ_STORYCANDLE: case OBJ_L5CANDLE: radius = 3; break; case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: case OBJ_BCROSS: case OBJ_TBCROSS: radius = 5; break; case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: radius = 8; break; default: return; } DoLighting(object.position, radius, {}); if (LoadingMapObjects) { DoUnLight(object.position, radius); UpdateLighting = true; } object._oVar1 = -1; } void AddBarrel(Object &barrel) { barrel._oVar1 = 0; barrel._oRndSeed = AdvanceRndSeed(); barrel._oVar2 = barrel.isExplosive() ? 0 : GenerateRnd(10); barrel._oVar3 = GenerateRnd(3); if (barrel._oVar2 >= 8) { Monster *skeleton = PreSpawnSkeleton(); if (skeleton != nullptr) { barrel._oVar4 = static_cast(skeleton->getId()); } else { barrel._oVar4 = -1; } } } void AddShrine(Object &shrine) { shrine._oRndSeed = AdvanceRndSeed(); shrine._oPreFlag = true; const int shrineCount = gbIsHellfire ? NumberOfShrineTypes : 26; bool slist[NumberOfShrineTypes] = {}; for (int i = 0; i < shrineCount; i++) { bool isShrineAvailable = true; if (gbIsMultiplayer) { isShrineAvailable = (shrineavail[i] != ShrineTypeSingle); } else { isShrineAvailable = (shrineavail[i] != ShrineTypeMulti); } const bool isEnchantedShrine = (i == ShrineEnchanted); const bool isCorrectLevelType = IsAnyOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS); slist[i] = isShrineAvailable && (!isEnchantedShrine || isCorrectLevelType); } int selectedIndex; do { selectedIndex = GenerateRnd(shrineCount); } while (!slist[selectedIndex]); shrine._oVar1 = selectedIndex; if (!FlipCoin()) { shrine._oAnimFrame = 12; shrine._oAnimLen = 22; } } void AddBookcase(Object &bookcase) { bookcase._oRndSeed = AdvanceRndSeed(); bookcase._oPreFlag = true; } void AddLargeFountain(Object &fountain) { const int ox = fountain.position.x; const int oy = fountain.position.y; const uint8_t id = -(static_cast(fountain.GetId()) + 1); dObject[ox][oy - 1] = id; dObject[ox - 1][oy] = id; dObject[ox - 1][oy - 1] = id; fountain._oRndSeed = AdvanceRndSeed(); } void AddArmorStand(Object &armorStand) { if (!armorFlag) { armorStand._oAnimFlag = true; armorStand.selectionRegion = SelectionRegion::None; } armorStand._oRndSeed = AdvanceRndSeed(); } void AddDecapitatedBody(Object &decapitatedBody) { decapitatedBody._oRndSeed = AdvanceRndSeed(); decapitatedBody._oAnimFrame = GenerateRnd(8) + 1; decapitatedBody._oPreFlag = true; } void AddBookOfVileness(Object &bookOfVileness) { if (setlevel && setlvlnum == SL_VILEBETRAYER) { bookOfVileness._oAnimFrame = 4; } } void AddMagicCircle(Object &magicCircle) { magicCircle._oRndSeed = AdvanceRndSeed(); magicCircle._oPreFlag = true; magicCircle._oVar6 = 0; magicCircle._oVar5 = 1; } void AddPedestalOfBlood(Object &pedestalOfBlood) { pedestalOfBlood._oVar1 = SetPiece.position.x; pedestalOfBlood._oVar2 = SetPiece.position.y; pedestalOfBlood._oVar3 = SetPiece.position.x + SetPiece.size.width; pedestalOfBlood._oVar4 = SetPiece.position.y + SetPiece.size.height; pedestalOfBlood._oVar6 = 0; } void AddStoryBook(Object &storyBook) { storyBook._oVar1 = (DungeonSeeds[16] >> 16) % 3; if (currlevel == 4) storyBook._oVar2 = StoryText[storyBook._oVar1][0]; else if (currlevel == 8) storyBook._oVar2 = StoryText[storyBook._oVar1][1]; else if (currlevel == 12) storyBook._oVar2 = StoryText[storyBook._oVar1][2]; storyBook._oVar3 = (currlevel / 4) + 3 * storyBook._oVar1 - 1; storyBook._oAnimFrame = 5 - 2 * storyBook._oVar1; storyBook._oVar4 = storyBook._oAnimFrame + 1; } void AddWeaponRack(Object &weaponRack) { if (!weaponFlag) { weaponRack._oAnimFlag = true; weaponRack.selectionRegion = SelectionRegion::None; } weaponRack._oRndSeed = AdvanceRndSeed(); } void AddTorturedBody(Object &torturedBody) { torturedBody._oRndSeed = AdvanceRndSeed(); torturedBody._oAnimFrame = GenerateRnd(4) + 1; torturedBody._oPreFlag = true; } Point GetRndObjLoc(int randarea) { if (randarea == 0) return { 0, 0 }; int tries = 0; int x; int y; while (true) { tries++; if (tries > 1000 && randarea > 1) randarea--; x = GenerateRnd(MAXDUNX); y = GenerateRnd(MAXDUNY); if (IsAreaOk(Rectangle { { x, y }, { randarea, randarea } })) break; } return { x, y }; } void AddMushPatch() { if (ActiveObjectCount < MAXOBJECTS) { const int i = AvailableObjects[0]; const Point loc = GetRndObjLoc(5); dObject[loc.x + 1][loc.y + 1] = -(i + 1); dObject[loc.x + 2][loc.y + 1] = -(i + 1); dObject[loc.x + 1][loc.y + 2] = -(i + 1); AddObject(OBJ_MUSHPATCH, { loc.x + 2, loc.y + 2 }); } } bool IsLightVisible(Object &light, int lightRadius) { #ifdef _DEBUG if (DisableLighting) return false; #endif for (const Player &player : Players) { if (!player.plractive) continue; if (!player.isOnActiveLevel()) continue; if (player.position.tile.WalkingDistance(light.position) < lightRadius + 10) { return true; } } return false; } void UpdateObjectLight(Object &light, int lightRadius) { if (light._oVar1 == -1) { return; } if (IsLightVisible(light, lightRadius)) { if (light._oVar1 == 0) light._olid = AddLight(light.position, lightRadius); light._oVar1 = 1; } else { if (light._oVar1 == 1) AddUnLight(light._olid); light._oVar1 = 0; } } void UpdateCircle(Object &circle) { Player *playerOnCircle = PlayerAtPosition(circle.position); if (!playerOnCircle) { if (circle._otype == OBJ_MCIRCLE1) circle._oAnimFrame = 1; if (circle._otype == OBJ_MCIRCLE2) circle._oAnimFrame = 3; circle._oVar6 = 0; return; } if (circle._otype == OBJ_MCIRCLE1) circle._oAnimFrame = 2; if (circle._otype == OBJ_MCIRCLE2) circle._oAnimFrame = 4; if (circle.position == Point { 45, 47 }) { circle._oVar6 = 2; } else if (circle.position == Point { 26, 46 }) { circle._oVar6 = 1; } else { circle._oVar6 = 0; } if (circle.position == Point { 35, 36 } && circle._oVar5 == 3) { circle._oVar6 = 4; if (Quests[Q_BETRAYER]._qvar1 <= 4) { LoadingMapObjects = true; ObjChangeMap(circle._oVar1, circle._oVar2, circle._oVar3, circle._oVar4); LoadingMapObjects = false; Quests[Q_BETRAYER]._qvar1 = 4; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } AddMissile(playerOnCircle->position.tile, { 35, 46 }, Direction::South, MissileID::Phasing, TARGET_BOTH, *playerOnCircle, 0, 0); if (playerOnCircle == MyPlayer) { LastPlayerAction = PlayerActionType::None; sgbMouseDown = CLICK_NONE; } ClrPlrPath(*playerOnCircle); StartStand(*playerOnCircle, Direction::South); } } void ObjectStopAnim(Object &object) { if (object._oAnimFrame == object._oAnimLen) { object._oAnimCnt = 0; object._oAnimDelay = 1000; } } /** * @brief Checks if an open door can be closed * * In order to be able to close a door the space where the closed door would be must be free of bodies, monsters, players, and items * * @param doorPosition Map tile where the door is in its closed position * @return true if the door is free to be closed, false if anything is blocking it */ inline bool IsDoorClear(const Object &door) { return dCorpse[door.position.x][door.position.y] == 0 && dMonster[door.position.x][door.position.y] == 0 && dItem[door.position.x][door.position.y] == 0 && dPlayer[door.position.x][door.position.y] == 0; } void UpdateDoor(Object &door) { if (door._oVar4 == DOOR_CLOSED) { return; } door._oVar4 = IsDoorClear(door) ? DOOR_OPEN : DOOR_BLOCKED; } void UpdateSarcophagus(Object &sarcophagus) { if (sarcophagus._oAnimFrame == sarcophagus._oAnimLen) sarcophagus._oAnimFlag = false; } void ActivateTrapLine(int ttype, int tid) { for (int i = 0; i < ActiveObjectCount; i++) { Object &trap = Objects[ActiveObjects[i]]; if (trap._otype == ttype && trap._oVar1 == tid) { trap._oVar4 = 1; trap._oAnimFlag = true; trap._oAnimDelay = 1; trap._olid = AddLight(trap.position, 1); } } } void UpdateFlameTrap(Object &trap) { if (trap._oVar2 != 0) { if (trap._oVar4 != 0) { trap._oAnimFrame--; if (trap._oAnimFrame == 1) { trap._oVar4 = 0; AddUnLight(trap._olid); } else if (trap._oAnimFrame <= 4) { ChangeLightRadius(trap._olid, trap._oAnimFrame); } } } else if (trap._oVar4 == 0) { if (trap._oVar3 == 2) { int x = trap.position.x - 2; const int y = trap.position.y; for (int j = 0; j < 5; j++) { if (dPlayer[x][y] != 0 || dMonster[x][y] != 0) trap._oVar4 = 1; x++; } } else { const int x = trap.position.x; int y = trap.position.y - 2; for (int k = 0; k < 5; k++) { if (dPlayer[x][y] != 0 || dMonster[x][y] != 0) trap._oVar4 = 1; y++; } } if (trap._oVar4 != 0) ActivateTrapLine(trap._otype, trap._oVar1); } else { const int damage[6] = { 6, 8, 10, 12, 10, 12 }; const int mindam = damage[leveltype - 1]; const int maxdam = mindam * 2; constexpr MissileID TrapMissile = MissileID::FireWallControl; Monster *monster = FindMonsterAtPosition(trap.position, true); if (monster != nullptr) MonsterTrapHit(*monster, mindam / 2, maxdam / 2, 0, TrapMissile, GetMissileData(TrapMissile).damageType(), false); Player *player = PlayerAtPosition(trap.position, true); if (player != nullptr) { bool unused; PlayerMHit(*player, nullptr, 0, mindam, maxdam, TrapMissile, GetMissileData(TrapMissile).damageType(), false, DeathReason::MonsterOrTrap, &unused); } if (trap._oAnimFrame == trap._oAnimLen) trap._oAnimFrame = 11; if (trap._oAnimFrame <= 5) ChangeLightRadius(trap._olid, trap._oAnimFrame); } } void UpdateBurningCrossDamage(Object &cross) { int damage[6] = { 6, 8, 10, 12, 10, 12 }; Player &myPlayer = *MyPlayer; if (myPlayer._pmode == PM_DEATH) return; const int8_t fireResist = myPlayer._pFireResist; if (fireResist > 0) damage[leveltype - 1] -= fireResist * damage[leveltype - 1] / 100; if (myPlayer.position.tile != cross.position + Displacement { 0, -1 }) return; ApplyPlrDamage(DamageType::Fire, myPlayer, 0, 0, damage[leveltype - 1]); if (!myPlayer.hasNoLife()) { myPlayer.Say(HeroSpeech::Argh); } } void ObjSetMini(Point position, int v) { const MegaTile mega = pMegaTiles[v - 1]; const Point megaOrigin = position.megaToWorld(); ObjSetMicro(megaOrigin, Swap16LE(mega.micro1)); ObjSetMicro(megaOrigin + Direction::SouthEast, Swap16LE(mega.micro2)); ObjSetMicro(megaOrigin + Direction::SouthWest, Swap16LE(mega.micro3)); ObjSetMicro(megaOrigin + Direction::South, Swap16LE(mega.micro4)); } void ObjL1Special(int x1, int y1, int x2, int y2) { for (int i = y1; i <= y2; ++i) { for (int j = x1; j <= x2; ++j) { dSpecial[j][i] = 0; if (dPiece[j][i] == 11) dSpecial[j][i] = 1; if (dPiece[j][i] == 10) dSpecial[j][i] = 2; if (dPiece[j][i] == 70) dSpecial[j][i] = 1; if (dPiece[j][i] == 252) dSpecial[j][i] = 3; if (dPiece[j][i] == 266) dSpecial[j][i] = 6; if (dPiece[j][i] == 258) dSpecial[j][i] = 5; if (dPiece[j][i] == 248) dSpecial[j][i] = 2; if (dPiece[j][i] == 324) dSpecial[j][i] = 2; if (dPiece[j][i] == 320) dSpecial[j][i] = 1; if (dPiece[j][i] == 254) dSpecial[j][i] = 4; if (dPiece[j][i] == 210) dSpecial[j][i] = 1; if (dPiece[j][i] == 343) dSpecial[j][i] = 2; if (dPiece[j][i] == 340) dSpecial[j][i] = 1; if (dPiece[j][i] == 330) dSpecial[j][i] = 2; if (dPiece[j][i] == 417) dSpecial[j][i] = 1; if (dPiece[j][i] == 420) dSpecial[j][i] = 2; } } } void ObjL2Special(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { dSpecial[i][j] = 0; if (dPiece[i][j] == 540) dSpecial[i][j] = 5; if (dPiece[i][j] == 177) dSpecial[i][j] = 5; if (dPiece[i][j] == 550) dSpecial[i][j] = 5; if (dPiece[i][j] == 541) dSpecial[i][j] = 6; if (dPiece[i][j] == 552) dSpecial[i][j] = 6; } } for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { if (dPiece[i][j] == 131) { dSpecial[i][j + 1] = 2; dSpecial[i][j + 2] = 1; } if (dPiece[i][j] == 134 || dPiece[i][j] == 138) { dSpecial[i + 1][j] = 3; dSpecial[i + 2][j] = 4; } } } } void OpenDoor(Object &door) { door._oAnimFrame += 2; SetDoorStateOpen(door); } void CloseDoor(Object &door) { door._oAnimFrame -= 2; SetDoorStateClosed(door); } void OperateDoor(Object &door, bool sendflag) { const bool isCrypt = IsAnyOf(door._otype, OBJ_L5LDOOR, OBJ_L5RDOOR); const bool openDoor = door._oVar4 == DOOR_CLOSED; if (!openDoor && !IsDoorClear(door)) { PlaySfxLoc(isCrypt ? SfxID::CryptDoorClose : SfxID::DoorClose, door.position); door._oVar4 = DOOR_BLOCKED; return; } if (openDoor) { PlaySfxLoc(isCrypt ? SfxID::CryptDoorOpen : SfxID::DoorOpen, door.position); OpenDoor(door); } else { PlaySfxLoc(isCrypt ? SfxID::CryptDoorClose : SfxID::DoorClose, door.position); CloseDoor(door); } RedoPlayerVision(); if (sendflag) NetSendCmdLoc(MyPlayerId, true, openDoor ? CMD_OPENDOOR : CMD_CLOSEDOOR, door.position); } bool AreAllLeversActivated(int leverId) { for (int j = 0; j < ActiveObjectCount; j++) { const Object &lever = Objects[ActiveObjects[j]]; if (lever._otype == OBJ_SWITCHSKL && lever._oVar8 == leverId && lever.canInteractWith()) { return false; } } return true; } void UpdateLeverState(Object &object) { if (!object.canInteractWith()) { return; } object.selectionRegion = SelectionRegion::None; object._oAnimFrame++; if (currlevel == 16 && !AreAllLeversActivated(object._oVar8)) return; if (currlevel == 24) { SyncNakrulRoom(); IsUberLeverActivated = true; return; } if (setlevel && setlvlnum == SL_VILEBETRAYER) ObjectAtPosition({ 35, 36 })._oVar5++; ObjChangeMap(object._oVar1, object._oVar2, object._oVar3, object._oVar4); } void OperateLever(Object &object, bool sendmsg) { if (!object.canInteractWith()) { return; } PlaySfxLoc(SfxID::OperateLever, object.position); UpdateLeverState(object); if (currlevel == 24) { PlaySfxLoc(SfxID::CryptDoorOpen, { UberRow, UberCol }); Quests[Q_NAKRUL]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_NAKRUL]); } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, object.position); } void OperateBook(Player &player, Object &book, bool sendmsg) { if (!book.canInteractWith()) { return; } if (setlevel && setlvlnum == SL_VILEBETRAYER) { Point target {}; if (book.position == Point { 26, 45 }) { target = { 27, 29 }; } else if (book.position == Point { 45, 46 }) { target = { 43, 29 }; } else { return; } Object &circle = ObjectAtPosition(book.position + Direction::SouthWest); assert(circle._otype == OBJ_MCIRCLE2); // Only verify that the player stands on the circle when it's the local player (sendmsg), because for remote players the position could be desynced if (sendmsg && circle.position != player.position.tile) { return; } circle._oVar6 = 4; ObjectAtPosition({ 35, 36 })._oVar5++; AddMissile(player.position.tile, target, Direction::South, MissileID::Phasing, TARGET_BOTH, player, 0, 0); } book.selectionRegion = SelectionRegion::None; book._oAnimFrame++; if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, book.position); if (!setlevel) { return; } if (setlvlnum == SL_BONECHAMB) { if (sendmsg) { const uint8_t newSpellLevel = player._pSplLvl[static_cast(SpellID::Guardian)] + 1; if (newSpellLevel <= MaxSpellLevel) { player._pSplLvl[static_cast(SpellID::Guardian)] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast(SpellID::Guardian), newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } Quests[Q_SCHAMB]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_SCHAMB]); } PlaySfxLoc(SfxID::QuestDone, book.position); InitDiabloMsg(EMSG_BONECHAMB); AddMissile( player.position.tile, book.position + Displacement { -2, -4 }, player._pdir, MissileID::Guardian, TARGET_MONSTERS, player, 0, 0); } if (setlvlnum == SL_VILEBETRAYER) { ObjChangeMap( book._oVar1, book._oVar2, book._oVar3, book._oVar4); for (int j = 0; j < ActiveObjectCount; j++) SyncObjectAnim(Objects[ActiveObjects[j]]); } } void OperateBookLever(Object &questBook, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (questBook.canInteractWith() && !qtextflag) { if (questBook._otype == OBJ_BLINDBOOK && Quests[Q_BLIND]._qvar1 == 0) { Quests[Q_BLIND]._qactive = QUEST_ACTIVE; Quests[Q_BLIND]._qlog = true; Quests[Q_BLIND]._qvar1 = 1; NetSendCmdQuest(true, Quests[Q_BLIND]); } if (questBook._otype == OBJ_BLOODBOOK && Quests[Q_BLOOD]._qvar1 == 0) { Quests[Q_BLOOD]._qactive = QUEST_ACTIVE; Quests[Q_BLOOD]._qlog = true; Quests[Q_BLOOD]._qvar1 = 1; NetSendCmdQuest(true, Quests[Q_BLOOD]); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 9, 17 }, 0, SelectionRegion::Bottom, true); } if (questBook._otype == OBJ_STEELTOME && Quests[Q_WARLORD]._qvar1 == QS_WARLORD_INIT) { Quests[Q_WARLORD]._qactive = QUEST_ACTIVE; Quests[Q_WARLORD]._qlog = true; Quests[Q_WARLORD]._qvar1 = QS_WARLORD_STEELTOME_READ; NetSendCmdQuest(true, Quests[Q_WARLORD]); } if (questBook._oAnimFrame != questBook._oVar6) { if (questBook._otype != OBJ_BLOODBOOK) ObjChangeMap(questBook._oVar1, questBook._oVar2, questBook._oVar3, questBook._oVar4); if (questBook._otype == OBJ_BLINDBOOK) { if (sendmsg) SpawnUnique(UITEM_OPTAMULET, SetPiece.position.megaToWorld() + Displacement { 5, 5 }, std::nullopt, true, true); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans(WorldTilePosition(questBook._oVar1, questBook._oVar2), WorldTilePosition(questBook._oVar3, questBook._oVar4)); TransVal = tren; } } questBook._oAnimFrame = questBook._oVar6; InitQTextMsg(questBook.bookMessage); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, questBook.position); } } void OperateChamberOfBoneBook(Object &questBook, bool sendmsg) { if (!questBook.canInteractWith() || qtextflag) { return; } if (questBook._oAnimFrame != questBook._oVar6) { ObjChangeMapResync(questBook._oVar1, questBook._oVar2, questBook._oVar3, questBook._oVar4); for (int j = 0; j < ActiveObjectCount; j++) { SyncObjectAnim(Objects[ActiveObjects[j]]); } } questBook._oAnimFrame = questBook._oVar6; if (Quests[Q_SCHAMB]._qactive == QUEST_INIT) { Quests[Q_SCHAMB]._qactive = QUEST_ACTIVE; Quests[Q_SCHAMB]._qlog = true; } _speech_id textdef; switch (MyPlayer->_pClass) { case HeroClass::Warrior: textdef = TEXT_BONER; break; case HeroClass::Rogue: textdef = TEXT_RBONER; break; case HeroClass::Sorcerer: textdef = TEXT_MBONER; break; case HeroClass::Monk: textdef = TEXT_HBONER; break; case HeroClass::Bard: textdef = TEXT_RBONER; break; case HeroClass::Barbarian: textdef = TEXT_BONER; break; default: break; } if (sendmsg) { Quests[Q_SCHAMB]._qmsg = textdef; NetSendCmdQuest(true, Quests[Q_SCHAMB]); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, questBook.position); InitQTextMsg(textdef); } } void OperateChest(const Player &player, Object &chest, bool sendLootMsg) { if (!chest.canInteractWith()) { return; } PlaySfxLoc(SfxID::ChestOpen, chest.position); chest.selectionRegion = SelectionRegion::None; chest._oAnimFrame += 2; SetRndSeed(chest._oRndSeed); if (setlevel) { for (int j = 0; j < chest._oVar1; j++) { CreateRndItem(chest.position, true, sendLootMsg, false); } } else { for (int j = 0; j < chest._oVar1; j++) { if (chest._oVar2 != 0) CreateRndItem(chest.position, false, sendLootMsg, false); else CreateRndUseful(chest.position, sendLootMsg); } } if (chest.IsTrappedChest()) { const Direction mdir = GetDirection(chest.position, player.position.tile); MissileID mtype; switch (chest._oVar4) { case 0: mtype = MissileID::Arrow; break; case 1: mtype = MissileID::FireArrow; break; case 2: mtype = MissileID::Nova; break; case 3: mtype = MissileID::RingOfFire; break; case 4: mtype = MissileID::StealPotions; break; case 5: mtype = MissileID::StealMana; break; default: mtype = MissileID::Arrow; } AddMissile(chest.position, player.position.tile, mdir, mtype, TARGET_PLAYERS, -1, 0, 0); PlaySfxLoc(SfxID::TriggerTrap, chest.position); chest._oTrapFlag = false; } if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, chest.position); } void OperateMushroomPatch(const Player &player, Object &mushroomPatch) { if (ActiveItemCount >= MAXITEMS) { return; } if (Quests[Q_MUSHROOM]._qactive != QUEST_ACTIVE) { if (&player == MyPlayer) { player.Say(HeroSpeech::ICantUseThisYet); } return; } if (!mushroomPatch.canInteractWith()) { return; } mushroomPatch.selectionRegion = SelectionRegion::None; mushroomPatch._oAnimFrame++; PlaySfxLoc(SfxID::ChestOpen, mushroomPatch.position); const Point pos = GetSuperItemLoc(mushroomPatch.position); if (&player == MyPlayer) { SpawnQuestItem(IDI_MUSHROOM, pos, 0, SelectionRegion::None, true); Quests[Q_MUSHROOM]._qvar1 = QS_MUSHSPAWNED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, mushroomPatch.position); } } void OperateInnSignChest(const Player &player, Object &questContainer, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (Quests[Q_LTBANNER]._qvar1 != 2) { if (&player == MyPlayer) { player.Say(HeroSpeech::ICantOpenThisYet); } return; } if (!questContainer.canInteractWith()) { return; } questContainer.selectionRegion = SelectionRegion::None; questContainer._oAnimFrame += 2; PlaySfxLoc(SfxID::ChestOpen, questContainer.position); if (sendmsg) { const Point pos = GetSuperItemLoc(questContainer.position); SpawnQuestItem(IDI_BANNER, pos, 0, SelectionRegion::None, true); NetSendCmdLoc(MyPlayerId, true, CMD_OPERATEOBJ, questContainer.position); } } void OperateSlainHero(const Player &player, Object &corpse, bool sendmsg) { if (!corpse.canInteractWith()) { return; } corpse.selectionRegion = SelectionRegion::None; SetRndSeed(corpse._oRndSeed); if (player._pClass == HeroClass::Warrior) { CreateMagicArmor(corpse.position, ItemType::HeavyArmor, ICURS_BREAST_PLATE, sendmsg, false); } else if (player._pClass == HeroClass::Rogue) { CreateMagicWeapon(corpse.position, ItemType::Bow, ICURS_LONG_BATTLE_BOW, sendmsg, false); } else if (player._pClass == HeroClass::Sorcerer) { CreateSpellBook(corpse.position, SpellID::Lightning, sendmsg, false); } else if (player._pClass == HeroClass::Monk) { CreateMagicWeapon(corpse.position, ItemType::Staff, ICURS_WAR_STAFF, sendmsg, false); } else if (player._pClass == HeroClass::Bard) { CreateMagicWeapon(corpse.position, ItemType::Sword, ICURS_BASTARD_SWORD, sendmsg, false); } else if (player._pClass == HeroClass::Barbarian) { CreateMagicWeapon(corpse.position, ItemType::Axe, ICURS_BATTLE_AXE, sendmsg, false); } MyPlayer->Say(HeroSpeech::RestInPeaceMyFriend); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, corpse.position); } void OperateTrapLever(Object &flameLever) { PlaySfxLoc(SfxID::OperateLever, flameLever.position); if (flameLever._oAnimFrame == 1) { flameLever._oAnimFrame = 2; for (int j = 0; j < ActiveObjectCount; j++) { Object &target = Objects[ActiveObjects[j]]; if (target._otype == flameLever._oVar2 && target._oVar1 == flameLever._oVar1) { target._oVar2 = 1; target._oAnimFlag = false; } } return; } flameLever._oAnimFrame--; for (int j = 0; j < ActiveObjectCount; j++) { Object &target = Objects[ActiveObjects[j]]; if (target._otype == flameLever._oVar2 && target._oVar1 == flameLever._oVar1) { target._oVar2 = 0; if (target._oVar4 != 0) { target._oAnimFlag = true; } } } } void OperateSarcophagus(Object &sarcophagus, bool sendMsg, bool sendLootMsg) { if (!sarcophagus.canInteractWith()) { return; } PlaySfxLoc(SfxID::Sarcophagus, sarcophagus.position); sarcophagus.selectionRegion = SelectionRegion::None; sarcophagus._oAnimFlag = true; sarcophagus._oAnimDelay = 3; SetRndSeed(sarcophagus._oRndSeed); if (sarcophagus._oVar1 <= 2) CreateRndItem(sarcophagus.position, false, sendLootMsg, false); if (sarcophagus._oVar1 >= 8 && sarcophagus._oVar2 >= 0) ActivateSkeleton(Monsters[sarcophagus._oVar2], sarcophagus.position); if (sendMsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, sarcophagus.position); } void OperatePedestal(Player &player, Object &pedestal, bool sendmsg) { if (ActiveItemCount >= MAXITEMS) { return; } if (pedestal._oVar6 == 3 || (sendmsg && !RemoveInventoryItemById(player, IDI_BLDSTONE))) { return; } if (sendmsg) { NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, pedestal.position); if (gbIsMultiplayer) { // Store added stones to pedestal in qvar2, because we get only one CMD_OPERATEOBJ from DeltaLoadLevel even if we add multiple stones Quests[Q_BLOOD]._qvar2++; NetSendCmdQuest(true, Quests[Q_BLOOD]); } } pedestal._oAnimFrame++; pedestal._oVar6++; if (pedestal._oVar6 == 1) { PlaySfxLoc(SfxID::SpellPuddle, pedestal.position); ObjChangeMap(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 3, 10 }, 0, SelectionRegion::Bottom, true); } if (pedestal._oVar6 == 2) { PlaySfxLoc(SfxID::SpellPuddle, pedestal.position); ObjChangeMap(SetPiece.position.x + 6, SetPiece.position.y + 3, SetPiece.position.x + SetPiece.size.width, SetPiece.position.y + 7); if (sendmsg) SpawnQuestItem(IDI_BLDSTONE, SetPiece.position.megaToWorld() + Displacement { 15, 10 }, 0, SelectionRegion::Bottom, true); } if (pedestal._oVar6 == 3) { PlaySfxLoc(SfxID::SpellBloodStar, pedestal.position); ObjChangeMap(pedestal._oVar1, pedestal._oVar2, pedestal._oVar3, pedestal._oVar4); LoadMapObjects("levels\\l2data\\blood2.dun", SetPiece.position.megaToWorld()); if (sendmsg) SpawnUnique(UITEM_ARMOFVAL, SetPiece.position.megaToWorld() + Displacement { 9, 3 }, std::nullopt, true, true); pedestal.selectionRegion = SelectionRegion::None; } } void OperateShrineMysterious(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; ModifyPlrStr(player, -1); ModifyPlrMag(player, -1); ModifyPlrDex(player, -1); ModifyPlrVit(player, -1); switch (static_cast(rng.generateRnd(4))) { case CharacterAttribute::Strength: ModifyPlrStr(player, 6); break; case CharacterAttribute::Magic: ModifyPlrMag(player, 6); break; case CharacterAttribute::Dexterity: ModifyPlrDex(player, 6); break; case CharacterAttribute::Vitality: ModifyPlrVit(player, 6); break; } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_MYSTERIOUS); } void OperateShrineHidden(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; int cnt = 0; for (const auto &item : player.InvBody) { if (!item.isEmpty()) cnt++; } if (cnt > 0) { for (auto &item : player.InvBody) { if (!item.isEmpty() && item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0) { item._iDurability += 10; item._iMaxDur += 10; if (item._iDurability > item._iMaxDur) item._iDurability = item._iMaxDur; } } while (true) { cnt = 0; for (auto &item : player.InvBody) { if (!item.isEmpty() && item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0) { cnt++; } } if (cnt == 0) break; const int r = rng.generateRnd(NUM_INVLOC); if (player.InvBody[r].isEmpty() || player.InvBody[r]._iMaxDur == DUR_INDESTRUCTIBLE || player.InvBody[r]._iMaxDur == 0) continue; player.InvBody[r]._iDurability -= 20; player.InvBody[r]._iMaxDur -= 20; if (player.InvBody[r]._iDurability <= 0) player.InvBody[r]._iDurability = 1; if (player.InvBody[r]._iMaxDur <= 0) player.InvBody[r]._iMaxDur = 1; break; } } InitDiabloMsg(EMSG_SHRINE_HIDDEN); } void OperateShrineGloomy(Player &player) { if (&player != MyPlayer) return; // Increment armor class by 2 and decrements max damage by 1. for (Item &item : PlayerItemsRange(player)) { switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Staff: item._iMaxDam--; if (item._iMaxDam < item._iMinDam) item._iMaxDam = item._iMinDam; break; case ItemType::Shield: case ItemType::Helm: case ItemType::LightArmor: case ItemType::MediumArmor: case ItemType::HeavyArmor: item._iAC += 2; break; default: break; } } CalcPlrInv(player, true); InitDiabloMsg(EMSG_SHRINE_GLOOMY); } void OperateShrineWeird(Player &player) { if (&player != MyPlayer) return; if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype != ItemType::Shield) player.InvBody[INVLOC_HAND_LEFT]._iMaxDam++; if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype != ItemType::Shield) player.InvBody[INVLOC_HAND_RIGHT]._iMaxDam++; for (Item &item : InventoryPlayerItemsRange { player }) { switch (item._itype) { case ItemType::Sword: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Staff: item._iMaxDam++; break; default: break; } } CalcPlrInv(player, true); InitDiabloMsg(EMSG_SHRINE_WEIRD); } void OperateShrineMagical(const Player &player) { AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::ManaShield, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_MAGICAL); } void OperateShrineStone(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { if (item._itype == ItemType::Staff) item._iCharges = item._iMaxCharges; } CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_STONE); } void OperateShrineReligious(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { item._iDurability = item._iMaxDur; } InitDiabloMsg(EMSG_SHRINE_RELIGIOUS); } void OperateShrineEnchanted(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; int cnt = 0; uint64_t spell = 1; const uint64_t spells = player._pMemSpells; for (uint16_t j = 0; j < SpellsData.size(); j++) { if ((spell & spells) != 0) cnt++; spell *= 2; } if (cnt > 1) { int spellToReduce; do { spellToReduce = rng.generateRnd(static_cast(SpellsData.size())) + 1; } while ((player._pMemSpells & GetSpellBitmask(static_cast(spellToReduce))) == 0); spell = 1; for (uint8_t j = static_cast(SpellID::Firebolt); j < SpellsData.size(); j++) { if ((player._pMemSpells & spell) != 0 && player._pSplLvl[j] < MaxSpellLevel && j != spellToReduce) { const uint8_t newSpellLevel = static_cast(player._pSplLvl[j] + 1); player._pSplLvl[j] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, j, newSpellLevel); } spell *= 2; } if (player._pSplLvl[spellToReduce] > 0) { const uint8_t newSpellLevel = static_cast(player._pSplLvl[spellToReduce] - 1); player._pSplLvl[spellToReduce] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, spellToReduce, newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } } InitDiabloMsg(EMSG_SHRINE_ENCHANTED); } void OperateShrineThaumaturgic(DiabloGenerator &rng, const Player &player) { for (int j = 0; j < ActiveObjectCount; j++) { Object &object = Objects[ActiveObjects[j]]; if (object.IsChest() && !object.canInteractWith()) { object._oRndSeed = rng.advanceRndSeed(); object.selectionRegion = SelectionRegion::Bottom; object._oAnimFrame -= 2; } } if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_THAUMATURGIC); } void OperateShrineCostOfWisdom(Player &player, SpellID spellId, diablo_message message) { if (&player != MyPlayer) return; player._pMemSpells |= GetSpellBitmask(spellId); const uint8_t curSpellLevel = player._pSplLvl[static_cast(spellId)]; if (curSpellLevel < MaxSpellLevel) { const uint8_t newSpellLevel = std::min(static_cast(curSpellLevel + 2), MaxSpellLevel); player._pSplLvl[static_cast(spellId)] = newSpellLevel; NetSendCmdParam2(true, CMD_CHANGE_SPELL_LEVEL, static_cast(spellId), newSpellLevel); } if (&player == MyPlayer) { for (Item &item : InventoryPlayerItemsRange { player }) { item.updateRequiredStatsCacheForPlayer(player); } if (IsStashOpen) { Stash.RefreshItemStatFlags(); } } int maxBase = player._pMaxManaBase; if (maxBase < 0) { // Fix bugged state; do not turn this into a "negative penalty" mana boost. player._pMaxManaBase = 0; maxBase = 0; } const int penalty = maxBase / 10; // 10% of max base mana (>= 0) player._pMaxManaBase -= penalty; // will remain >= 0 player._pManaBase -= penalty; // may go negative, allowed player._pMaxMana -= penalty; // may go negative, allowed player._pMana -= penalty; // may go negative, allowed RedrawEverything(); InitDiabloMsg(message); } void OperateShrineCryptic(Player &player) { AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::Nova, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; InitDiabloMsg(EMSG_SHRINE_CRYPTIC); RedrawEverything(); } void OperateShrineEldritch(Player &player) { if (&player != MyPlayer) return; for (Item &item : InventoryAndBeltPlayerItemsRange { player }) { if (item._itype != ItemType::Misc) { continue; } if (IsAnyOf(item._iMiscId, IMISC_HEAL, IMISC_MANA)) { // Reinitializing the item zeroes out the seed, we save and restore here to avoid triggering false // positives on duplicated item checks (e.g. when picking up the item). auto seed = item._iSeed; InitializeItem(item, ItemMiscIdIdx(IMISC_REJUV)); item._iSeed = seed; item._iStatFlag = true; continue; } if (IsAnyOf(item._iMiscId, IMISC_FULLHEAL, IMISC_FULLMANA)) { // As above. auto seed = item._iSeed; InitializeItem(item, ItemMiscIdIdx(IMISC_FULLREJUV)); item._iSeed = seed; item._iStatFlag = true; continue; } } RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_ELDRITCH); } void OperateShrineEerie(Player &player) { if (&player != MyPlayer) return; ModifyPlrMag(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_EERIE); } /** * @brief Fully restores HP and Mana of the active player and spawns a pair of potions * in response to the player activating a Divine shrine * @param player The player who activated the shrine * @param spawnPosition The map tile where the potions will be spawned */ void OperateShrineDivine(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; if (currlevel < 4) { CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLMANA, false, false, true); CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLHEAL, false, false, true); } else { CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLREJUV, false, false, true); CreateTypeItem(spawnPosition, false, ItemType::Misc, IMISC_FULLREJUV, false, false, true); } player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_DIVINE); } void OperateShrineHoly(const Player &player) { AddMissile(player.position.tile, { 0, 0 }, Direction::South, MissileID::Phasing, TARGET_MONSTERS, player, 0, 2 * leveltype); if (&player != MyPlayer) return; InitDiabloMsg(EMSG_SHRINE_HOLY); } void OperateShrineSpiritual(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; for (int8_t &itemIndex : player.InvGrid) { if (itemIndex == 0) { Item &goldItem = player.InvList[player._pNumInv]; MakeGoldStack(goldItem, 5 * leveltype + rng.generateRnd(10 * leveltype)); player._pNumInv++; itemIndex = player._pNumInv; player._pGold += goldItem._ivalue; } } InitDiabloMsg(EMSG_SHRINE_SPIRITUAL); } void OperateShrineSpooky(const Player &player) { if (&player == MyPlayer) { InitDiabloMsg(EMSG_SHRINE_SPOOKY1); return; } Player &myPlayer = *MyPlayer; myPlayer._pHitPoints = myPlayer._pMaxHP; myPlayer._pHPBase = myPlayer._pMaxHPBase; myPlayer._pMana = myPlayer._pMaxMana; myPlayer._pManaBase = myPlayer._pMaxManaBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SPOOKY2); } void OperateShrineAbandoned(Player &player) { if (&player != MyPlayer) return; ModifyPlrDex(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_ABANDONED); } void OperateShrineCreepy(Player &player) { if (&player != MyPlayer) return; ModifyPlrStr(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_CREEPY); } void OperateShrineQuiet(Player &player) { if (&player != MyPlayer) return; ModifyPlrVit(player, 2); CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_QUIET); } void OperateShrineSecluded(const Player &player) { if (&player != MyPlayer) return; for (int x = 0; x < DMAXX; x++) for (int y = 0; y < DMAXY; y++) UpdateAutomapExplorer({ x, y }, MAP_EXP_SHRINE); InitDiabloMsg(EMSG_SHRINE_SECLUDED); } void OperateShrineGlimmering(Player &player) { if (&player != MyPlayer) return; for (Item &item : PlayerItemsRange { player }) { if (item._iMagical != ITEM_QUALITY_NORMAL && !item._iIdentified) { item._iIdentified = true; } } CalcPlrInv(player, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_GLIMMERING); } void OperateShrineTainted(DiabloGenerator &rng, const Player &player) { if (&player == MyPlayer) { InitDiabloMsg(EMSG_SHRINE_TAINTED1); return; } const int r = rng.generateRnd(4); const int v1 = r == 0 ? 1 : -1; const int v2 = r == 1 ? 1 : -1; const int v3 = r == 2 ? 1 : -1; const int v4 = r == 3 ? 1 : -1; Player &myPlayer = *MyPlayer; ModifyPlrStr(myPlayer, v1); ModifyPlrMag(myPlayer, v2); ModifyPlrDex(myPlayer, v3); ModifyPlrVit(myPlayer, v4); CheckStats(myPlayer); CalcPlrInv(myPlayer, true); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_TAINTED2); } /** * @brief Oily shrines increase the players primary stat(s) by a total of two, but spawn a * firewall near the shrine that will spread towards the player * @param player The player that will be affected by the shrine * @param spawnPosition Start location for the firewall */ void OperateShrineOily(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; switch (player._pClass) { case HeroClass::Warrior: ModifyPlrStr(player, 2); break; case HeroClass::Rogue: ModifyPlrDex(player, 2); break; case HeroClass::Sorcerer: ModifyPlrMag(player, 2); break; case HeroClass::Barbarian: ModifyPlrVit(player, 2); break; case HeroClass::Monk: ModifyPlrStr(player, 1); ModifyPlrDex(player, 1); break; case HeroClass::Bard: ModifyPlrDex(player, 1); ModifyPlrMag(player, 1); break; default: break; } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::FireWall, TARGET_PLAYERS, -1, 2 * currlevel + 2, 0); InitDiabloMsg(EMSG_SHRINE_OILY); } void OperateShrineGlowing(Player &player) { if (&player != MyPlayer) return; // Add 0-5 points to Magic (0.1% of the players XP) ModifyPlrMag(player, static_cast(std::min(player._pExperience / 1000, 5))); // Take 5% of the players experience to offset the bonus, unless they're very low level in which case take all their experience. if (player._pExperience > 5000) player._pExperience = static_cast(player._pExperience * 0.95); else player._pExperience = 0; CheckStats(player); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_GLOWING); } void OperateShrineMendicant(Player &player) { if (&player != MyPlayer) return; const int gold = player._pGold / 2; player.addExperience(gold); TakePlrsMoney(gold); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_MENDICANT); } /** * @brief Grants experience to the player based on the current dungeon level while also triggering a magic trap * @param player The player that will be affected by the shrine * @param spawnPosition The trap results in casting flash from this location targeting the player */ void OperateShrineSparkling(Player &player, Point spawnPosition) { if (&player != MyPlayer) return; player.addExperience(1000 * currlevel); AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::FlashBottom, TARGET_PLAYERS, -1, 3 * currlevel + 2, 0); RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SPARKLING); } /** * @brief Spawns a town portal near the active player * @param pnum The player that activated the shrine * @param spawnPosition The position of the shrine, the portal will be placed on the side closest to the player */ void OperateShrineTown(const Player &player, Point spawnPosition) { if (&player != MyPlayer) return; AddMissile( spawnPosition, player.position.tile, player._pdir, MissileID::TownPortal, TARGET_MONSTERS, player, 0, 0); InitDiabloMsg(EMSG_SHRINE_TOWN); } void OperateShrineShimmering(Player &player) { if (&player != MyPlayer) return; player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; RedrawEverything(); InitDiabloMsg(EMSG_SHRINE_SHIMMERING); } void OperateShrineSolar(Player &player) { if (&player != MyPlayer) return; const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); const int hour = localtimeResult != nullptr ? localtimeResult->tm_hour : 20; if (hour >= 20 || hour < 4) { InitDiabloMsg(EMSG_SHRINE_SOLAR4); ModifyPlrVit(player, 2); } else if (hour >= 18) { InitDiabloMsg(EMSG_SHRINE_SOLAR3); ModifyPlrMag(player, 2); } else if (hour >= 12) { InitDiabloMsg(EMSG_SHRINE_SOLAR2); ModifyPlrStr(player, 2); } else /* 4:00 to 11:59 */ { InitDiabloMsg(EMSG_SHRINE_SOLAR1); ModifyPlrDex(player, 2); } CheckStats(player); CalcPlrInv(player, true); RedrawEverything(); } void OperateShrineMurphys(DiabloGenerator &rng, Player &player) { if (&player != MyPlayer) return; bool broke = false; for (auto &item : player.InvBody) { if (!item.isEmpty() && rng.flipCoin(3)) { if (item._iDurability != DUR_INDESTRUCTIBLE) { if (item._iDurability > 0) { item._iDurability /= 2; broke = true; break; } } } } if (!broke) { TakePlrsMoney(player._pGold / 3); } InitDiabloMsg(EMSG_SHRINE_MURPHYS); } void OperateShrine(Player &player, Object &shrine, SfxID sType) { if (!shrine.canInteractWith()) return; CloseGoldDrop(); DiabloGenerator rng(shrine._oRndSeed); shrine.selectionRegion = SelectionRegion::None; PlaySfxLoc(sType, shrine.position); shrine._oAnimFlag = true; shrine._oAnimDelay = 1; switch (shrine._oVar1) { case ShrineMysterious: OperateShrineMysterious(rng, player); break; case ShrineHidden: OperateShrineHidden(rng, player); break; case ShrineGloomy: OperateShrineGloomy(player); break; case ShrineWeird: OperateShrineWeird(player); break; case ShrineMagical: case ShrineMagicaL2: OperateShrineMagical(player); break; case ShrineStone: OperateShrineStone(player); break; case ShrineReligious: OperateShrineReligious(player); break; case ShrineEnchanted: OperateShrineEnchanted(rng, player); break; case ShrineThaumaturgic: OperateShrineThaumaturgic(rng, player); break; case ShrineFascinating: OperateShrineCostOfWisdom(player, SpellID::Firebolt, EMSG_SHRINE_FASCINATING); break; case ShrineCryptic: OperateShrineCryptic(player); break; case ShrineEldritch: OperateShrineEldritch(player); break; case ShrineEerie: OperateShrineEerie(player); break; case ShrineDivine: OperateShrineDivine(player, shrine.position); break; case ShrineHoly: OperateShrineHoly(player); break; case ShrineSacred: OperateShrineCostOfWisdom(player, SpellID::ChargedBolt, EMSG_SHRINE_SACRED); break; case ShrineSpiritual: OperateShrineSpiritual(rng, player); break; case ShrineSpooky: OperateShrineSpooky(player); break; case ShrineAbandoned: OperateShrineAbandoned(player); break; case ShrineCreepy: OperateShrineCreepy(player); break; case ShrineQuiet: OperateShrineQuiet(player); break; case ShrineSecluded: OperateShrineSecluded(player); break; case ShrineOrnate: OperateShrineCostOfWisdom(player, SpellID::HolyBolt, EMSG_SHRINE_ORNATE); break; case ShrineGlimmering: OperateShrineGlimmering(player); break; case ShrineTainted: OperateShrineTainted(rng, player); break; case ShrineOily: OperateShrineOily(player, shrine.position); break; case ShrineGlowing: OperateShrineGlowing(player); break; case ShrineMendicant: OperateShrineMendicant(player); break; case ShrineSparkling: OperateShrineSparkling(player, shrine.position); break; case ShrineTown: OperateShrineTown(player, shrine.position); break; case ShrineShimmering: OperateShrineShimmering(player); break; case ShrineSolar: OperateShrineSolar(player); break; case ShrineMurphys: OperateShrineMurphys(rng, player); break; } if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, shrine.position); } void OperateBookStand(Object &bookStand, bool sendmsg, bool sendLootMsg) { if (!bookStand.canInteractWith()) { return; } PlaySfxLoc(SfxID::ItemScroll, bookStand.position); bookStand.selectionRegion = SelectionRegion::None; bookStand._oAnimFrame += 2; SetRndSeed(bookStand._oRndSeed); if (FlipCoin(5)) CreateTypeItem(bookStand.position, false, ItemType::Misc, IMISC_BOOK, sendLootMsg, false); else CreateTypeItem(bookStand.position, false, ItemType::Misc, IMISC_SCROLL, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, bookStand.position); } void OperateBookcase(Object &bookcase, bool sendmsg, bool sendLootMsg) { if (!bookcase.canInteractWith()) { return; } PlaySfxLoc(SfxID::ItemScroll, bookcase.position); bookcase.selectionRegion = SelectionRegion::None; bookcase._oAnimFrame -= 2; SetRndSeed(bookcase._oRndSeed); CreateTypeItem(bookcase.position, false, ItemType::Misc, IMISC_BOOK, sendLootMsg, false); if (Quests[Q_ZHAR].IsAvailable()) { Monster &zhar = Monsters[MAX_PLRS]; if (zhar.mode == MonsterMode::Stand // prevents playing the "angry" message for the second time if zhar got aggroed by losing vision and talking again && zhar.uniqueType == UniqueMonsterType::Zhar && zhar.activeForTicks == UINT8_MAX && zhar.hitPoints > 0) { zhar.talkMsg = TEXT_ZHAR2; M_StartStand(zhar, zhar.direction); // BUGFIX: first parameter in call to M_StartStand should be MAX_PLRS, not 0. (fixed) zhar.goal = MonsterGoal::Attack; if (sendmsg) zhar.mode = MonsterMode::Talk; } } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, bookcase.position); } void OperateDecapitatedBody(Object &corpse, bool sendmsg, bool sendLootMsg) { if (!corpse.canInteractWith()) { return; } corpse.selectionRegion = SelectionRegion::None; SetRndSeed(corpse._oRndSeed); CreateRndItem(corpse.position, false, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, corpse.position); } void OperateArmorStand(Object &armorStand, bool sendmsg, bool sendLootMsg) { if (!armorStand.canInteractWith()) { return; } armorStand.selectionRegion = SelectionRegion::None; armorStand._oAnimFrame++; SetRndSeed(armorStand._oRndSeed); const bool uniqueRnd = !FlipCoin(); if (currlevel <= 5) { CreateTypeItem(armorStand.position, true, ItemType::LightArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 6 && currlevel <= 9) { CreateTypeItem(armorStand.position, uniqueRnd, ItemType::MediumArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 10 && currlevel <= 12) { CreateTypeItem(armorStand.position, false, ItemType::HeavyArmor, IMISC_NONE, sendLootMsg, false); } else if (currlevel >= 13) { CreateTypeItem(armorStand.position, true, ItemType::HeavyArmor, IMISC_NONE, sendLootMsg, false); } if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, armorStand.position); } int FindValidShrine() { for (;;) { const int rv = GenerateRnd(gbIsHellfire ? NumberOfShrineTypes : 26); if ((rv == ShrineEnchanted && !IsAnyOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS)) || rv == ShrineThaumaturgic) continue; if (gbIsMultiplayer && shrineavail[rv] == ShrineTypeSingle) continue; if (!gbIsMultiplayer && shrineavail[rv] == ShrineTypeMulti) continue; return rv; } } void OperateGoatShrine(Player &player, Object &object, SfxID sType) { SetRndSeed(object._oRndSeed); object._oVar1 = FindValidShrine(); OperateShrine(player, object, sType); object._oAnimDelay = 2; RedrawEverything(); } void OperateCauldron(Player &player, Object &object, SfxID sType) { SetRndSeed(object._oRndSeed); object._oVar1 = FindValidShrine(); OperateShrine(player, object, sType); object._oAnimFrame = 3; object._oAnimFlag = false; RedrawEverything(); } bool OperateFountains(Player &player, Object &fountain) { bool applied = false; switch (fountain._otype) { case OBJ_BLOODFTN: if (&player != MyPlayer) return false; if (player._pHitPoints < player._pMaxHP) { PlaySfxLoc(SfxID::OperateFountain, fountain.position); player._pHitPoints += 64; player._pHPBase += 64; if (player._pHitPoints > player._pMaxHP) { player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; } applied = true; } else PlaySfxLoc(SfxID::OperateFountain, fountain.position); break; case OBJ_PURIFYINGFTN: if (&player != MyPlayer) return false; if (player._pMana < player._pMaxMana) { PlaySfxLoc(SfxID::OperateFountain, fountain.position); player._pMana += 64; player._pManaBase += 64; if (player._pMana > player._pMaxMana) { player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; } applied = true; } else PlaySfxLoc(SfxID::OperateFountain, fountain.position); break; case OBJ_MURKYFTN: if (!fountain.canInteractWith()) break; PlaySfxLoc(SfxID::OperateFountain, fountain.position); fountain.selectionRegion = SelectionRegion::None; AddMissile( player.position.tile, player.position.tile, player._pdir, MissileID::Infravision, TARGET_MONSTERS, player, 0, 2 * leveltype); applied = true; if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, fountain.position); break; case OBJ_TEARFTN: { if (!fountain.canInteractWith()) break; PlaySfxLoc(SfxID::OperateFountain, fountain.position); fountain.selectionRegion = SelectionRegion::None; if (&player != MyPlayer) return false; const unsigned randomValue = (fountain._oRndSeed >> 16) % 12; const unsigned fromStat = randomValue / 3; unsigned toStat = randomValue % 3; if (toStat >= fromStat) toStat++; const std::pair alterations[] = { { fromStat, -1 }, { toStat, 1 } }; for (const auto &[stat, delta] : alterations) { switch (stat) { case 0: ModifyPlrStr(player, delta); break; case 1: ModifyPlrMag(player, delta); break; case 2: ModifyPlrDex(player, delta); break; case 3: ModifyPlrVit(player, delta); break; } } CheckStats(player); applied = true; if (&player == MyPlayer) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, fountain.position); } break; default: break; } RedrawEverything(); return applied; } void OperateWeaponRack(Object &weaponRack, bool sendmsg, bool sendLootMsg) { if (!weaponRack.canInteractWith()) return; SetRndSeed(weaponRack._oRndSeed); const ItemType weaponType { PickRandomlyAmong({ ItemType::Sword, ItemType::Axe, ItemType::Bow, ItemType::Mace }) }; weaponRack.selectionRegion = SelectionRegion::None; weaponRack._oAnimFrame++; CreateTypeItem(weaponRack.position, leveltype != DTYPE_CATHEDRAL, weaponType, IMISC_NONE, sendLootMsg, false); if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, weaponRack.position); } /** * @brief Checks whether the player is activating Na-Krul's spell tomes in the correct order * * Used as part of the final Diablo: Hellfire quest (from the hints provided to the player in the * reconstructed note). This function both updates the state of the variable that tracks progress * and also determines whether the spawn conditions are met (i.e. all tomes have been triggered * in the correct order). * * @param s the id of the spell tome * @return true if the player has activated all three tomes in the correct order, false otherwise */ bool OperateNakrulBook(int s) { switch (s) { case 6: NaKrulTomeSequence = 1; break; case 7: if (NaKrulTomeSequence == 1) { NaKrulTomeSequence = 2; } else { NaKrulTomeSequence = 0; } break; case 8: if (NaKrulTomeSequence == 2) return true; NaKrulTomeSequence = 0; break; } return false; } void OperateStoryBook(Object &storyBook) { if (!storyBook.canInteractWith() || qtextflag) { return; } storyBook._oAnimFrame = storyBook._oVar4; PlaySfxLoc(SfxID::ItemScroll, storyBook.position); auto msg = static_cast<_speech_id>(storyBook._oVar2); if (storyBook._oVar8 != 0 && currlevel == 24) { if (!IsUberLeverActivated && Quests[Q_NAKRUL]._qactive != QUEST_DONE && OperateNakrulBook(storyBook._oVar8)) { NetSendCmd(false, CMD_NAKRUL); return; } } else if (leveltype == DTYPE_CRYPT && Quests[Q_NAKRUL]._qactive != QUEST_DONE) { Quests[Q_NAKRUL]._qactive = QUEST_ACTIVE; Quests[Q_NAKRUL]._qlog = true; Quests[Q_NAKRUL]._qmsg = msg; NetSendCmdQuest(true, Quests[Q_NAKRUL]); } InitQTextMsg(msg); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, storyBook.position); } void OperateLazStand(Object &stand) { if (ActiveItemCount >= MAXITEMS) { return; } if (!stand.canInteractWith() || qtextflag) { return; } stand._oAnimFrame++; stand.selectionRegion = SelectionRegion::None; const Point pos = GetSuperItemLoc(stand.position); SpawnQuestItem(IDI_LAZSTAFF, pos, 0, SelectionRegion::None, true); NetSendCmdLoc(MyPlayerId, false, CMD_OPERATEOBJ, stand.position); } /** * @brief Checks if all active crux objects of the given type have been broken. * * Called by BreakCrux and SyncCrux to see if the linked map area needs to be updated. In practice I think this is * always true when called by BreakCrux as there *should* only be one instance of each crux with a given _oVar8 value? * * @param cruxType Discriminator/type (_oVar8 value) of the crux object which is currently changing state * @return true if all active cruxes of that type on the level are broken, false if at least one remains unbroken */ bool AreAllCruxesOfTypeBroken(int cruxType) { for (int j = 0; j < ActiveObjectCount; j++) { const auto &testObject = Objects[ActiveObjects[j]]; if (!testObject.IsCrux()) continue; // Not a Crux object, keep searching if (cruxType != testObject._oVar8 || testObject._oBreak == -1) continue; // Found either a different crux or a previously broken crux, keep searching // Found an unbroken crux of this type return false; } return true; } void BreakCrux(Object &crux, bool sendmsg) { if (!crux.canInteractWith()) return; crux._oAnimFlag = true; crux._oAnimFrame = 1; crux._oAnimDelay = 1; crux._oSolidFlag = true; crux._oMissFlag = true; crux._oBreak = -1; crux.selectionRegion = SelectionRegion::None; if (sendmsg) NetSendCmdLoc(MyPlayerId, false, CMD_BREAKOBJ, crux.position); if (!AreAllCruxesOfTypeBroken(crux._oVar8)) return; PlaySfxLoc(SfxID::OperateLever, crux.position); ObjChangeMap(crux._oVar1, crux._oVar2, crux._oVar3, crux._oVar4); } void BreakBarrel(const Player &player, Object &barrel, bool forcebreak, bool sendmsg) { if (!barrel.canInteractWith()) return; if (!forcebreak && &player != MyPlayer) { return; } barrel._oAnimFlag = true; barrel._oAnimFrame = 1; barrel._oAnimDelay = 1; barrel._oSolidFlag = false; barrel._oMissFlag = true; barrel._oBreak = -1; barrel.selectionRegion = SelectionRegion::None; barrel._oPreFlag = true; if (barrel.isExplosive()) { if (barrel._otype == _object_id::OBJ_URNEX) PlaySfxLoc(SfxID::UrnExpload, barrel.position); else if (barrel._otype == _object_id::OBJ_PODEX) PlaySfxLoc(SfxID::PodExpload, barrel.position); else PlaySfxLoc(SfxID::BarrelExpload, barrel.position); for (int yp = barrel.position.y - 1; yp <= barrel.position.y + 1; yp++) { for (int xp = barrel.position.x - 1; xp <= barrel.position.x + 1; xp++) { constexpr MissileID TrapMissile = MissileID::Firebolt; Monster *monster = FindMonsterAtPosition({ xp, yp }, true); if (monster != nullptr) { MonsterTrapHit(*monster, 1, 4, 0, TrapMissile, GetMissileData(TrapMissile).damageType(), false); } Player *adjacentPlayer = PlayerAtPosition({ xp, yp }, true); if (adjacentPlayer != nullptr) { bool unused; PlayerMHit(*adjacentPlayer, nullptr, 0, 8, 16, TrapMissile, GetMissileData(TrapMissile).damageType(), false, DeathReason::MonsterOrTrap, &unused); } // don't really need to exclude large objects as explosive barrels are single tile objects, but using considerLargeObjects == false as this matches the old logic. Object *adjacentObject = FindObjectAtPosition({ xp, yp }, false); if (adjacentObject != nullptr && adjacentObject->isExplosive() && !adjacentObject->IsBroken()) { BreakBarrel(player, *adjacentObject, true, sendmsg); } } } } else { if (barrel._otype == _object_id::OBJ_URN) PlaySfxLoc(SfxID::UrnBreak, barrel.position); else if (barrel._otype == _object_id::OBJ_POD) PlaySfxLoc(SfxID::PodPop, barrel.position); else PlaySfxLoc(SfxID::BarrelBreak, barrel.position); SetRndSeed(barrel._oRndSeed); if (barrel._oVar2 <= 1) { if (barrel._oVar3 == 0) CreateRndUseful(barrel.position, sendmsg); else CreateRndItem(barrel.position, false, sendmsg, false); } if (barrel._oVar2 >= 8 && barrel._oVar4 >= 0) ActivateSkeleton(Monsters[barrel._oVar4], barrel.position); } if (&player == MyPlayer) { NetSendCmdLoc(MyPlayerId, false, CMD_BREAKOBJ, barrel.position); } } void SyncCrux(const Object &crux) { if (AreAllCruxesOfTypeBroken(crux._oVar8)) ObjChangeMap(crux._oVar1, crux._oVar2, crux._oVar3, crux._oVar4); } void SyncLever(const Object &lever) { if (lever.canInteractWith()) return; if (currlevel == 16 && !AreAllLeversActivated(lever._oVar8)) return; ObjChangeMap(lever._oVar1, lever._oVar2, lever._oVar3, lever._oVar4); } void SyncQSTLever(const Object &qstLever) { if (qstLever._oAnimFrame == qstLever._oVar6) { if (qstLever._otype != OBJ_BLOODBOOK) ObjChangeMapResync(qstLever._oVar1, qstLever._oVar2, qstLever._oVar3, qstLever._oVar4); if (qstLever._otype == OBJ_BLINDBOOK) { auto tren = TransVal; TransVal = 9; DRLG_MRectTrans(WorldTilePosition(qstLever._oVar1, qstLever._oVar2), WorldTilePosition(qstLever._oVar3, qstLever._oVar4)); TransVal = tren; } } } void SyncPedestal(const Object &pedestal) { if (pedestal._oVar6 == 1) ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); if (pedestal._oVar6 == 2) { ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y + 3, SetPiece.position.x + 2, SetPiece.position.y + 7); ObjChangeMapResync(SetPiece.position.x + 6, SetPiece.position.y + 3, SetPiece.position.x + SetPiece.size.width, SetPiece.position.y + 7); } if (pedestal._oVar6 >= 3) { ObjChangeMapResync(pedestal._oVar1, pedestal._oVar2, pedestal._oVar3, pedestal._oVar4); LoadMapObjects("levels\\l2data\\blood2.dun", SetPiece.position.megaToWorld()); } } void UpdatePedestalState(Object &pedestal) { const int addedStones = Quests[Q_BLOOD]._qvar2; pedestal._oAnimFrame += addedStones; pedestal._oVar6 += addedStones; SyncPedestal(pedestal); if (pedestal._oVar6 >= 3) pedestal.selectionRegion = SelectionRegion::None; } void SyncDoor(Object &door) { if (door._oVar4 == DOOR_CLOSED) { SetDoorStateClosed(door); } else { SetDoorStateOpen(door); } } void ResyncDoors(WorldTilePosition p1, WorldTilePosition p2, bool sendmsg) { const WorldTileSize size { static_cast(p2.x - p1.x), static_cast(p2.y - p1.y) }; const WorldTileRectangle area { p1, size }; for (const WorldTilePosition p : PointsInRectangle { area }) { Object *obj = FindObjectAtPosition(p); if (obj == nullptr) continue; if (IsNoneOf(obj->_otype, OBJ_L1LDOOR, OBJ_L1RDOOR, OBJ_L2LDOOR, OBJ_L2RDOOR, OBJ_L3LDOOR, OBJ_L3RDOOR, OBJ_L5LDOOR, OBJ_L5RDOOR)) continue; SyncDoor(*obj); if (sendmsg) { const bool isOpen = obj->_oVar4 == DOOR_OPEN; NetSendCmdLoc(MyPlayerId, true, isOpen ? CMD_OPENDOOR : CMD_CLOSEDOOR, obj->position); } } } void UpdateState(Object &object, int frame) { if (!object.canInteractWith()) { return; } object.selectionRegion = SelectionRegion::None; object._oAnimFrame = frame; object._oAnimFlag = false; } } // namespace unsigned int Object::GetId() const { return std::abs(dObject[position.x][position.y]) - 1; } bool Object::IsDisabled() const { if (!*GetOptions().Gameplay.disableCripplingShrines) { return false; } if (IsAnyOf(_otype, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON)) { return true; } if (!IsShrine()) { return false; } return IsAnyOf(static_cast(_oVar1), shrine_type::ShrineFascinating, shrine_type::ShrineOrnate, shrine_type::ShrineSacred, shrine_type::ShrineMurphys); } Object *FindObjectAtPosition(Point position, bool considerLargeObjects) { if (!InDungeonBounds(position)) { return nullptr; } auto objectId = dObject[position.x][position.y]; if (objectId > 0 || (considerLargeObjects && objectId != 0)) { return &Objects[std::abs(objectId) - 1]; } // nothing at this position, return a nullptr return nullptr; } bool IsItemBlockingObjectAtPosition(Point position) { Object *object = FindObjectAtPosition(position); if (object != nullptr && object->_oSolidFlag) { // solid object return true; } object = FindObjectAtPosition(position + Direction::South); if (object != nullptr && object->canInteractWith()) { // An unopened container or breakable object exists which potentially overlaps this tile, the player might not be able to pick up an item dropped here. return true; } object = FindObjectAtPosition(position + Direction::SouthEast, false); if (object != nullptr) { Object *otherDoor = FindObjectAtPosition(position + Direction::SouthWest, false); if (otherDoor != nullptr && object->canInteractWith() && otherDoor->canInteractWith()) { // Two interactive objects potentially overlap both sides of this tile, as above the player might not be able to pick up an item which is dropped here. return true; } } return false; } tl::expected LoadLevelObjects(uint16_t filesWidths[65]) { if (HeadlessMode) return {}; for (const ObjectData objectData : AllObjects) { if (leveltype == objectData.olvltype) { filesWidths[objectData.ofindex] = objectData.animWidth; } } for (size_t i = 0, n = ObjMasterLoadList.size(); i < n; ++i) { if (filesWidths[i] == 0) { continue; } ObjFileList[numobjfiles] = static_cast(i); char filestr[32]; *BufCopy(filestr, "objects\\", ObjMasterLoadList[i]) = '\0'; ASSIGN_OR_RETURN(pObjCels[numobjfiles], LoadCelWithStatus(filestr, filesWidths[i])); numobjfiles++; } return {}; } tl::expected InitObjectGFX() { uint16_t filesWidths[65] = {}; if (IsAnyOf(currlevel, 4, 8, 12)) { for (const auto id : { OBJ_STORYBOOK, OBJ_STORYCANDLE }) { const ObjectData &obj = AllObjects[id]; filesWidths[obj.ofindex] = obj.animWidth; } } for (size_t id = 0, n = AllObjects.size(); id < n; ++id) { const ObjectData &objectData = AllObjects[id]; if (objectData.minlvl != 0 && currlevel >= objectData.minlvl && currlevel <= objectData.maxlvl) { if (IsAnyOf(static_cast<_object_id>(id), OBJ_TRAPL, OBJ_TRAPR) && leveltype == DTYPE_HELL) { continue; } filesWidths[objectData.ofindex] = objectData.animWidth; } if (objectData.otheme != THEME_NONE) { for (int j = 0; j < numthemes; j++) { if (themes[j].ttype == objectData.otheme) { filesWidths[objectData.ofindex] = objectData.animWidth; } } } if (objectData.oquest != Q_INVALID && Quests[objectData.oquest].IsAvailable()) { filesWidths[objectData.ofindex] = objectData.animWidth; } } return LoadLevelObjects(filesWidths); } void FreeObjectGFX() { for (int i = 0; i < numobjfiles; i++) { pObjCels[i] = std::nullopt; } numobjfiles = 0; } void AddL1Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 269) AddObject(OBJ_L1LIGHT, { i, j }); if (pn == 43 || pn == 50 || pn == 213) AddObject(OBJ_L1LDOOR, { i, j }); if (pn == 45 || pn == 55) AddObject(OBJ_L1RDOOR, { i, j }); } } } void AddL2Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 12 || pn == 540) AddObject(OBJ_L2LDOOR, { i, j }); if (pn == 16 || pn == 541) AddObject(OBJ_L2RDOOR, { i, j }); } } } void AddL3Objs(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 530) AddObject(OBJ_L3LDOOR, { i, j }); if (pn == 533) AddObject(OBJ_L3RDOOR, { i, j }); } } } void AddCryptObjects(int x1, int y1, int x2, int y2) { for (int j = y1; j < y2; j++) { for (int i = x1; i < x2; i++) { const int pn = dPiece[i][j]; if (pn == 76) AddObject(OBJ_L5LDOOR, { i, j }); if (pn == 79) AddObject(OBJ_L5RDOOR, { i, j }); } } } void AddSlainHero() { const Point rndObjLoc = GetRndObjLoc(5); AddObject(OBJ_SLAINHERO, rndObjLoc + Displacement { 2, 2 }); } void InitObjects() { ClrAllObjects(); NaKrulTomeSequence = 0; if (currlevel == 16) { AddDiabObjs(); } else { DiscardRandomValues(1); if (currlevel == 9 && !UseMultiplayerQuests()) AddSlainHero(); if (Quests[Q_MUSHROOM].IsAvailable()) AddMushPatch(); if (currlevel == 4 || currlevel == 8 || currlevel == 12) AddStoryBooks(); if (currlevel == 21) { AddCryptStoryBook(1); } else if (currlevel == 22) { AddCryptStoryBook(2); AddCryptStoryBook(3); } else if (currlevel == 23) { AddCryptStoryBook(4); AddCryptStoryBook(5); } if (currlevel == 24) { AddNakrulGate(); } if (leveltype == DTYPE_CATHEDRAL) { if (Quests[Q_BUTCHER].IsAvailable()) AddTortures(); if (Quests[Q_PWATER].IsAvailable()) AddCandles(); if (Quests[Q_LTBANNER].IsAvailable()) AddObject(OBJ_SIGNCHEST, SetPiece.position.megaToWorld() + Displacement { 10, 3 }); InitRndLocBigObj(10, 15, OBJ_SARC); AddL1Objs(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } if (leveltype == DTYPE_CATACOMBS) { if (Quests[Q_ROCK].IsAvailable()) InitRndLocObj5x5(1, 1, OBJ_STAND); if (Quests[Q_SCHAMB].IsAvailable()) InitRndLocObj5x5(1, 1, OBJ_BOOK2R); AddL2Objs(0, 0, MAXDUNX, MAXDUNY); AddL2Torches(); if (Quests[Q_BLIND].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLINDING; break; case HeroClass::Rogue: spId = TEXT_RBLINDING; break; case HeroClass::Sorcerer: spId = TEXT_MBLINDING; break; case HeroClass::Monk: spId = TEXT_HBLINDING; break; case HeroClass::Bard: spId = TEXT_RBLINDING; break; case HeroClass::Barbarian: spId = TEXT_BLINDING; break; default: break; } Quests[Q_BLIND]._qmsg = spId; AddBookLever(OBJ_BLINDBOOK, { SetPiece.position, SetPiece.size + 1 }, spId); LoadMapObjects("levels\\l2data\\blind2.dun", SetPiece.position.megaToWorld()); } if (Quests[Q_BLOOD].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLOODY; break; case HeroClass::Rogue: spId = TEXT_RBLOODY; break; case HeroClass::Sorcerer: spId = TEXT_MBLOODY; break; case HeroClass::Monk: spId = TEXT_HBLOODY; break; case HeroClass::Bard: spId = TEXT_RBLOODY; break; case HeroClass::Barbarian: spId = TEXT_BLOODY; break; default: break; } Quests[Q_BLOOD]._qmsg = spId; AddBookLever(OBJ_BLOODBOOK, { SetPiece.position + Displacement { 0, 3 }, { 2, 4 } }, spId); AddObject(OBJ_PEDESTAL, SetPiece.position.megaToWorld() + Displacement { 9, 16 }); } InitRndBarrels(); } if (leveltype == DTYPE_CAVES) { AddL3Objs(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } if (leveltype == DTYPE_HELL) { if (Quests[Q_WARLORD].IsAvailable()) { _speech_id spId; switch (MyPlayer->_pClass) { case HeroClass::Warrior: spId = TEXT_BLOODWAR; break; case HeroClass::Rogue: spId = TEXT_RBLOODWAR; break; case HeroClass::Sorcerer: spId = TEXT_MBLOODWAR; break; case HeroClass::Monk: spId = TEXT_HBLOODWAR; break; case HeroClass::Bard: spId = TEXT_RBLOODWAR; break; case HeroClass::Barbarian: spId = TEXT_BLOODWAR; break; default: break; } Quests[Q_WARLORD]._qmsg = spId; AddBookLever(OBJ_STEELTOME, SetPiece, spId); LoadMapObjects("levels\\l4data\\warlord.dun", SetPiece.position.megaToWorld()); } if (Quests[Q_BETRAYER].IsAvailable() && !UseMultiplayerQuests()) AddLazStand(); InitRndBarrels(); AddL4Goodies(); } if (leveltype == DTYPE_NEST) { InitRndBarrels(); } if (leveltype == DTYPE_CRYPT) { InitRndLocBigObj(10, 15, OBJ_L5SARC); AddCryptObjects(0, 0, MAXDUNX, MAXDUNY); InitRndBarrels(); } InitRndLocObj(5, 10, OBJ_CHEST1); InitRndLocObj(3, 6, OBJ_CHEST2); InitRndLocObj(1, 5, OBJ_CHEST3); if (leveltype != DTYPE_HELL) AddObjTraps(); if (IsAnyOf(leveltype, DTYPE_CATACOMBS, DTYPE_CAVES, DTYPE_HELL, DTYPE_NEST)) AddChestTraps(); } } void SetMapObjects(const uint16_t *dunData, int startx, int starty) { uint16_t filesWidths[65] = {}; ClrAllObjects(); WorldTileSize size = GetDunSize(dunData); const int layer2Offset = 2 + size.width * size.height; // The rest of the layers are at dPiece scale size *= static_cast(2); const uint16_t *objectLayer = &dunData[layer2Offset + size.width * size.height * 2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(Swap16LE(objectLayer[j * size.width + i])); if (objectId != 0) { const ObjectData &objectData = AllObjects[ObjTypeConv[objectId]]; filesWidths[objectData.ofindex] = objectData.animWidth; } } } LoadLevelObjects(filesWidths); for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto objectId = static_cast(Swap16LE(objectLayer[j * size.width + i])); if (objectId != 0) { AddObject(ObjTypeConv[objectId], { startx + 16 + i, starty + 16 + j }); } } } } Object *AddObject(_object_id objType, Point objPos) { if (ActiveObjectCount >= MAXOBJECTS) return nullptr; const int oi = AvailableObjects[0]; AvailableObjects[0] = AvailableObjects[MAXOBJECTS - 1 - ActiveObjectCount]; ActiveObjects[ActiveObjectCount] = oi; dObject[objPos.x][objPos.y] = oi + 1; Object &object = Objects[oi]; SetupObject(object, objPos, objType); switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: AddDoor(object); break; case OBJ_BOOK2R: object.InitializeBook({ SetPiece.position, WorldTileSize(SetPiece.size.width + 1, SetPiece.size.height + 1) }); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: AddChest(object); break; case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: AddChest(object); object._oTrapFlag = true; if (leveltype == DTYPE_CATACOMBS) { object._oVar4 = GenerateRnd(2); } else { object._oVar4 = GenerateRnd(3); } break; case OBJ_SARC: case OBJ_L5SARC: AddSarcophagus(object); break; case OBJ_FLAMEHOLE: AddFlameTrap(object); break; case OBJ_FLAMELVR: AddFlameLever(object); break; case OBJ_WATER: object._oAnimFrame = 1; break; case OBJ_TRAPL: case OBJ_TRAPR: AddTrap(object); break; case OBJ_BARREL: case OBJ_BARRELEX: case OBJ_POD: case OBJ_PODEX: case OBJ_URN: case OBJ_URNEX: AddBarrel(object); break; case OBJ_SHRINEL: case OBJ_SHRINER: AddShrine(object); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: AddBookcase(object); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: case OBJ_BLOODFTN: case OBJ_GOATSHRINE: case OBJ_CAULDRON: case OBJ_TEARFTN: case OBJ_SLAINHERO: object._oRndSeed = AdvanceRndSeed(); break; case OBJ_DECAP: AddDecapitatedBody(object); break; case OBJ_PURIFYINGFTN: case OBJ_MURKYFTN: AddLargeFountain(object); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: AddArmorStand(object); break; case OBJ_BOOK2L: AddBookOfVileness(object); break; case OBJ_MCIRCLE1: case OBJ_MCIRCLE2: AddMagicCircle(object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: AddStoryBook(object); break; case OBJ_BCROSS: case OBJ_TBCROSS: object._oRndSeed = AdvanceRndSeed(); break; case OBJ_PEDESTAL: AddPedestalOfBlood(object); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: AddWeaponRack(object); break; case OBJ_TNUDEM2: AddTorturedBody(object); break; default: break; } AddObjectLight(object); ActiveObjectCount++; return &object; } bool UpdateTrapState(Object &trap) { if (trap._oVar4 != 0) return false; Object &trigger = ObjectAtPosition({ trap._oVar1, trap._oVar2 }); switch (trigger._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (trigger._oVar4 == DOOR_CLOSED && trigger._oTrapFlag) return false; break; case OBJ_LEVER: case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_SWITCHSKL: case OBJ_SARC: case OBJ_L5LEVER: case OBJ_L5SARC: if (trigger.canInteractWith() && trigger._oTrapFlag) return false; break; default: return false; } trap._oVar4 = 1; trigger._oTrapFlag = false; return true; } void OperateTrap(Object &trap) { if (!UpdateTrapState(trap)) return; // default to firing at the trigger object const Point triggerPosition = { trap._oVar1, trap._oVar2 }; Point target = triggerPosition; auto searchArea = PointsInRectangle(Rectangle { target, 1 }); // look for a player near the trigger (using a reverse search to match vanilla behaviour) auto foundPosition = std::find_if(searchArea.crbegin(), searchArea.crend(), [](Point testPosition) { return InDungeonBounds(testPosition) && dPlayer[testPosition.x][testPosition.y] != 0; }); if (foundPosition != searchArea.crend()) { // if a player is standing near the trigger then target them instead target = *foundPosition; } const Direction dir = GetDirection(trap.position, target); AddMissile(trap.position, target, dir, static_cast(trap._oVar3), TARGET_PLAYERS, -1, 0, 0); PlaySfxLoc(SfxID::TriggerTrap, triggerPosition); } void ProcessObjects() { for (int i = 0; i < ActiveObjectCount; ++i) { Object &object = Objects[ActiveObjects[i]]; switch (object._otype) { case OBJ_L1LIGHT: case OBJ_SKFIRE: case OBJ_CANDLE1: case OBJ_CANDLE2: case OBJ_BOOKCANDLE: UpdateObjectLight(object, 5); break; case OBJ_STORYCANDLE: case OBJ_L5CANDLE: UpdateObjectLight(object, 3); break; case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: case OBJ_BARREL: case OBJ_BARRELEX: case OBJ_POD: case OBJ_PODEX: case OBJ_URN: case OBJ_URNEX: case OBJ_SHRINEL: case OBJ_SHRINER: ObjectStopAnim(object); break; case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: UpdateDoor(object); break; case OBJ_TORCHL: case OBJ_TORCHR: case OBJ_TORCHL2: case OBJ_TORCHR2: UpdateObjectLight(object, 8); break; case OBJ_SARC: case OBJ_L5SARC: UpdateSarcophagus(object); break; case OBJ_FLAMEHOLE: UpdateFlameTrap(object); break; case OBJ_TRAPL: case OBJ_TRAPR: OperateTrap(object); break; case OBJ_MCIRCLE1: case OBJ_MCIRCLE2: UpdateCircle(object); break; case OBJ_BCROSS: case OBJ_TBCROSS: UpdateObjectLight(object, 5); UpdateBurningCrossDamage(object); break; default: break; } if (!object._oAnimFlag) continue; object._oAnimCnt++; if (object._oAnimCnt < object._oAnimDelay) continue; object._oAnimCnt = 0; object._oAnimFrame++; if (object._oAnimFrame > object._oAnimLen) object._oAnimFrame = 1; } for (int i = 0; i < ActiveObjectCount;) { const int oi = ActiveObjects[i]; if (Objects[oi]._oDelFlag) { DeleteObject(oi, i); } else { i++; } } } void RedoPlayerVision() { for (const Player &player : Players) { if (player.plractive && player.isOnActiveLevel()) { ChangeVisionXY(player.getId(), player.position.tile); } } } void MonstCheckDoors(const Monster &monster) { for (const Direction dir : { Direction::NorthEast, Direction::SouthWest, Direction::North, Direction::East, Direction::South, Direction::West, Direction::NorthWest, Direction::SouthEast }) { Object *object = FindObjectAtPosition(monster.position.tile + dir); if (object == nullptr) continue; Object &door = *object; // Doors use _oVar4 to track open/closed state, non-zero values indicate an open door if (!door.isDoor() || door._oVar4 != DOOR_CLOSED) continue; OperateDoor(door, true); } } void ObjChangeMap(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { ObjSetMini({ i, j }, pdungeon[i][j]); dungeon[i][j] = pdungeon[i][j]; } } const WorldTilePosition mega1 { static_cast(x1), static_cast(y1) }; const WorldTilePosition mega2 { static_cast(x2), static_cast(y2) }; const WorldTilePosition world1 = mega1.megaToWorld(); const WorldTilePosition world2 = mega2.megaToWorld() + Displacement { 1, 1 }; if (leveltype == DTYPE_CATHEDRAL) { ObjL1Special(world1.x, world1.y, world2.x, world2.y); AddL1Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CATACOMBS) { ObjL2Special(world1.x, world1.y, world2.x, world2.y); AddL2Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CAVES) { AddL3Objs(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CRYPT) { AddCryptObjects(world1.x, world1.y, world2.x, world2.y); } ResyncDoors(world1, world2, true); } void ObjChangeMapResync(int x1, int y1, int x2, int y2) { for (int j = y1; j <= y2; j++) { for (int i = x1; i <= x2; i++) { ObjSetMini({ i, j }, pdungeon[i][j]); dungeon[i][j] = pdungeon[i][j]; } } const WorldTilePosition mega1 { static_cast(x1), static_cast(y1) }; const WorldTilePosition mega2 { static_cast(x2), static_cast(y2) }; const WorldTilePosition world1 = mega1.megaToWorld(); const WorldTilePosition world2 = mega2.megaToWorld() + Displacement { 1, 1 }; if (leveltype == DTYPE_CATHEDRAL) { ObjL1Special(world1.x, world1.y, world2.x, world2.y); } if (leveltype == DTYPE_CATACOMBS) { ObjL2Special(world1.x, world1.y, world2.x, world2.y); } ResyncDoors(world1, world2, false); } _item_indexes ItemMiscIdIdx(item_misc_id imiscid) { std::underlying_type_t<_item_indexes> i = IDI_GOLD; while (AllItemsList[i].dropRate == 0 || AllItemsList[i].iMiscId != imiscid) { i++; } return static_cast<_item_indexes>(i); } void OperateObject(Player &player, Object &object) { const bool sendmsg = &player == MyPlayer; switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (sendmsg) OperateDoor(object, sendmsg); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: OperateLever(object, sendmsg); break; case OBJ_BOOK2L: if (sendmsg) OperateBook(player, object, sendmsg); break; case OBJ_BOOK2R: OperateChamberOfBoneBook(object, sendmsg); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: OperateChest(player, object, sendmsg); break; case OBJ_SARC: case OBJ_L5SARC: OperateSarcophagus(object, sendmsg, sendmsg); break; case OBJ_FLAMELVR: OperateTrapLever(object); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: if (sendmsg) OperateBookLever(object, sendmsg); break; case OBJ_SHRINEL: case OBJ_SHRINER: OperateShrine(player, object, SfxID::OperateShrine); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: OperateBookStand(object, sendmsg, sendmsg); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: OperateBookcase(object, sendmsg, sendmsg); break; case OBJ_DECAP: OperateDecapitatedBody(object, sendmsg, sendmsg); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: OperateArmorStand(object, sendmsg, sendmsg); break; case OBJ_GOATSHRINE: OperateGoatShrine(player, object, SfxID::OperateGoatShrine); break; case OBJ_CAULDRON: OperateCauldron(player, object, SfxID::OperateCaldron); break; case OBJ_BLOODFTN: case OBJ_PURIFYINGFTN: case OBJ_MURKYFTN: case OBJ_TEARFTN: OperateFountains(player, object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: if (sendmsg) OperateStoryBook(object); break; case OBJ_PEDESTAL: if (sendmsg) OperatePedestal(player, object, sendmsg); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: OperateWeaponRack(object, sendmsg, sendmsg); break; case OBJ_MUSHPATCH: OperateMushroomPatch(player, object); break; case OBJ_LAZSTAND: if (sendmsg) OperateLazStand(object); break; case OBJ_SLAINHERO: OperateSlainHero(player, object, sendmsg); break; case OBJ_SIGNCHEST: OperateInnSignChest(player, object, sendmsg); break; default: break; } } void DeltaSyncOpObject(Object &object) { switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: OpenDoor(object); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: case OBJ_BOOK2L: UpdateLeverState(object); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: case OBJ_SKELBOOK: case OBJ_BOOKSTAND: UpdateState(object, object._oAnimFrame + 2); break; case OBJ_SARC: case OBJ_L5SARC: case OBJ_GOATSHRINE: case OBJ_SHRINEL: case OBJ_SHRINER: UpdateState(object, object._oAnimLen); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: case OBJ_BOOK2R: object._oAnimFrame = object._oVar6; SyncQSTLever(object); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: UpdateState(object, object._oAnimFrame - 2); break; case OBJ_DECAP: case OBJ_MURKYFTN: case OBJ_TEARFTN: case OBJ_SLAINHERO: UpdateState(object, object._oAnimFrame); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: case OBJ_WARWEAP: case OBJ_WEAPONRACK: case OBJ_LAZSTAND: UpdateState(object, object._oAnimFrame + 1); break; case OBJ_CAULDRON: UpdateState(object, 3); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: object._oAnimFrame = object._oVar4; break; case OBJ_MUSHPATCH: if (Quests[Q_MUSHROOM]._qvar1 >= QS_MUSHSPAWNED) { UpdateState(object, object._oAnimFrame + 1); } break; case OBJ_SIGNCHEST: if (Quests[Q_LTBANNER]._qvar1 >= 2) { UpdateState(object, object._oAnimFrame + 2); } break; case OBJ_PEDESTAL: UpdatePedestalState(object); break; default: break; } } void DeltaSyncCloseObj(Object &object) { // Object was closed. // That means it was opened once, so all traps have been activated. object._oTrapFlag = false; } void SyncOpObject(Player &player, int cmd, Object &object) { const bool sendmsg = &player == MyPlayer; switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (sendmsg) break; if (cmd == CMD_CLOSEDOOR && object._oVar4 == DOOR_CLOSED) break; if (cmd == CMD_OPENDOOR && object._oVar4 == DOOR_OPEN) break; OperateDoor(object, false); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_SWITCHSKL: OperateLever(object, sendmsg); break; case OBJ_BOOK2L: if (!sendmsg) OperateBook(player, object, sendmsg); break; case OBJ_CHEST1: case OBJ_CHEST2: case OBJ_CHEST3: case OBJ_TCHEST1: case OBJ_TCHEST2: case OBJ_TCHEST3: OperateChest(player, object, false); break; case OBJ_SARC: case OBJ_L5SARC: OperateSarcophagus(object, sendmsg, false); break; case OBJ_BLINDBOOK: case OBJ_BLOODBOOK: case OBJ_STEELTOME: if (sendmsg) break; object._oAnimFrame = object._oVar6; SyncQSTLever(object); break; case OBJ_SHRINEL: case OBJ_SHRINER: OperateShrine(player, object, SfxID::OperateShrine); break; case OBJ_SKELBOOK: case OBJ_BOOKSTAND: OperateBookStand(object, sendmsg, false); break; case OBJ_BOOKCASEL: case OBJ_BOOKCASER: OperateBookcase(object, sendmsg, false); break; case OBJ_DECAP: OperateDecapitatedBody(object, sendmsg, false); break; case OBJ_ARMORSTAND: case OBJ_WARARMOR: OperateArmorStand(object, sendmsg, false); break; case OBJ_GOATSHRINE: OperateGoatShrine(player, object, SfxID::OperateGoatShrine); break; case OBJ_LAZSTAND: if (!sendmsg) UpdateState(object, object._oAnimFrame + 1); break; case OBJ_CAULDRON: OperateCauldron(player, object, SfxID::OperateCaldron); break; case OBJ_MURKYFTN: case OBJ_TEARFTN: OperateFountains(player, object); break; case OBJ_STORYBOOK: case OBJ_L5BOOKS: if (sendmsg) OperateStoryBook(object); break; case OBJ_PEDESTAL: if (!sendmsg) OperatePedestal(player, object, sendmsg); break; case OBJ_WARWEAP: case OBJ_WEAPONRACK: OperateWeaponRack(object, sendmsg, false); break; case OBJ_MUSHPATCH: OperateMushroomPatch(player, object); break; case OBJ_SLAINHERO: OperateSlainHero(player, object, sendmsg); break; case OBJ_SIGNCHEST: OperateInnSignChest(player, object, sendmsg); break; default: break; } } void BreakObjectMissile(const Player *player, Object &object) { if (object.IsCrux()) BreakCrux(object, true); } void BreakObject(const Player &player, Object &object) { if (object.IsBarrel()) { BreakBarrel(player, object, false, true); } else if (object.IsCrux()) { BreakCrux(object, true); } } void DeltaSyncBreakObj(Object &object) { if (!object.IsBreakable() || !object.canInteractWith()) return; object._oMissFlag = true; object._oBreak = -1; object.selectionRegion = SelectionRegion::None; object._oPreFlag = true; object._oAnimFlag = false; object._oAnimFrame = object._oAnimLen; if (object.IsBarrel()) { object._oSolidFlag = false; } else if (object.IsCrux() && AreAllCruxesOfTypeBroken(object._oVar8)) { ObjChangeMap(object._oVar1, object._oVar2, object._oVar3, object._oVar4); } } void SyncBreakObj(const Player &player, Object &object) { if (object.IsBarrel()) { BreakBarrel(player, object, true, false); } else if (object.IsCrux()) { BreakCrux(object, false); } } void SyncObjectAnim(Object &object) { object_graphic_id index = AllObjects[object._otype].ofindex; if (!HeadlessMode) { const auto &found = c_find(ObjFileList, index); if (found == std::end(ObjFileList)) { LogCritical("Unable to find object_graphic_id {} in list of objects to load, level generation error.", static_cast(index)); return; } const size_t i = std::distance(std::begin(ObjFileList), found); if (pObjCels[i]) { object._oAnimData.emplace(*pObjCels[i]); } else { object._oAnimData = std::nullopt; } } switch (object._otype) { case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: SyncDoor(object); break; case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: SyncCrux(object); break; case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_BOOK2L: case OBJ_SWITCHSKL: SyncLever(object); break; case OBJ_BOOK2R: case OBJ_BLINDBOOK: case OBJ_STEELTOME: SyncQSTLever(object); break; case OBJ_PEDESTAL: SyncPedestal(object); break; default: break; } } StringOrView Object::name() const { switch (_otype) { case OBJ_CRUX1: case OBJ_CRUX2: case OBJ_CRUX3: return _("Crucified Skeleton"); case OBJ_LEVER: case OBJ_L5LEVER: case OBJ_FLAMELVR: return _("Lever"); case OBJ_L1LDOOR: case OBJ_L1RDOOR: case OBJ_L2LDOOR: case OBJ_L2RDOOR: case OBJ_L3LDOOR: case OBJ_L3RDOOR: case OBJ_L5LDOOR: case OBJ_L5RDOOR: if (_oVar4 == DOOR_OPEN) return _("Open Door"); if (_oVar4 == DOOR_CLOSED) return _("Closed Door"); if (_oVar4 == DOOR_BLOCKED) return _("Blocked Door"); break; case OBJ_BOOK2L: if (setlevel) { if (setlvlnum == SL_BONECHAMB) { return _("Ancient Tome"); } else if (setlvlnum == SL_VILEBETRAYER) { return _("Book of Vileness"); } } break; case OBJ_SWITCHSKL: return _("Skull Lever"); case OBJ_BOOK2R: return _("Mythical Book"); case OBJ_CHEST1: case OBJ_TCHEST1: return _("Small Chest"); case OBJ_CHEST2: case OBJ_TCHEST2: return _("Chest"); case OBJ_CHEST3: case OBJ_TCHEST3: case OBJ_SIGNCHEST: return _("Large Chest"); case OBJ_SARC: case OBJ_L5SARC: return _("Sarcophagus"); case OBJ_BOOKSHELF: return _("Bookshelf"); case OBJ_BOOKCASEL: case OBJ_BOOKCASER: return _("Bookcase"); case OBJ_BARREL: case OBJ_BARRELEX: return _("Barrel"); case OBJ_POD: case OBJ_PODEX: return _("Pod"); case OBJ_URN: case OBJ_URNEX: return _("Urn"); case OBJ_SHRINEL: case OBJ_SHRINER: return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will be a name from the Shrine block above */ "{:s} Shrine")), _(ShrineNames[_oVar1])); case OBJ_SKELBOOK: return _("Skeleton Tome"); case OBJ_BOOKSTAND: return _("Library Book"); case OBJ_BLOODFTN: return _("Blood Fountain"); case OBJ_DECAP: return _("Decapitated Body"); case OBJ_BLINDBOOK: return _("Book of the Blind"); case OBJ_BLOODBOOK: return _("Book of Blood"); case OBJ_PURIFYINGFTN: return _("Purifying Spring"); case OBJ_ARMORSTAND: case OBJ_WARARMOR: return _("Armor"); case OBJ_WARWEAP: return _("Weapon Rack"); case OBJ_GOATSHRINE: return _("Goat Shrine"); case OBJ_CAULDRON: return _("Cauldron"); case OBJ_MURKYFTN: return _("Murky Pool"); case OBJ_TEARFTN: return _("Fountain of Tears"); case OBJ_STEELTOME: return _("Steel Tome"); case OBJ_PEDESTAL: return _("Pedestal of Blood"); case OBJ_STORYBOOK: case OBJ_L5BOOKS: return _(StoryBookName[_oVar3]); case OBJ_WEAPONRACK: return _("Weapon Rack"); case OBJ_MUSHPATCH: return _("Mushroom Patch"); case OBJ_LAZSTAND: return _("Vile Stand"); case OBJ_SLAINHERO: return _("Slain Hero"); default: break; } return std::string_view(); } void GetObjectStr(const Object &object) { InfoString = object.name(); const ClassAttributes &classAttributes = GetClassAttributes(MyPlayer->_pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::TrapSense)) { if (object._oTrapFlag) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} will either be a chest or a door */ "Trapped {:s}")), InfoString.str()); InfoColor = UiFlags::ColorRed; } } if (object.IsDisabled()) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever */ "{:s} (disabled)")), InfoString.str()); InfoColor = UiFlags::ColorRed; } } void SyncNakrulRoom() { dPiece[UberRow][UberCol] = 297; dPiece[UberRow][UberCol - 1] = 300; dPiece[UberRow][UberCol - 2] = 299; dPiece[UberRow][UberCol + 1] = 298; } } // namespace devilution ================================================ FILE: Source/objects.h ================================================ /** * @file objects.h * * Interface of object functionality, interaction, spawning, loading, etc. */ #pragma once #include #include #include #include #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/rectangle.hpp" #include "engine/world_tile.hpp" #include "levels/dun_tile.hpp" #include "monster.h" #include "tables/itemdat.h" #include "tables/objdat.h" #include "tables/textdat.h" #include "utils/attributes.h" #include "utils/is_of.hpp" #include "utils/string_or_view.hpp" namespace devilution { #define MAXOBJECTS 127 struct Object { _object_id _otype = OBJ_NULL; bool applyLighting = false; bool _oTrapFlag = false; bool _oDoorFlag = false; Point position; uint32_t _oAnimFlag = 0; OptionalClxSpriteList _oAnimData; int _oAnimDelay = 0; // Tick length of each frame in the current animation int _oAnimCnt = 0; // Increases by one each game tick, counting how close we are to _pAnimDelay uint32_t _oAnimLen = 0; // Number of frames in current animation uint32_t _oAnimFrame = 0; // Current frame of animation. // TODO: Remove this field, it is unused and always equal to: // (*_oAnimData)[0].width() uint16_t _oAnimWidth = 0; bool _oDelFlag = false; int8_t _oBreak = 0; bool _oSolidFlag = false; /** True if the object allows missiles to pass through, false if it collides with missiles */ bool _oMissFlag = false; SelectionRegion selectionRegion = SelectionRegion::None; bool _oPreFlag = false; int _olid = 0; /** * Saves the absolute value of the engine state (typically from a call to AdvanceRndSeed()) to later use when spawning items from a container object * This is an unsigned value to avoid implementation defined behaviour when reading from this variable. */ uint32_t _oRndSeed = 0; int _oVar1 = 0; int _oVar2 = 0; int _oVar3 = 0; int _oVar4 = 0; int _oVar5 = 0; uint32_t _oVar6 = 0; int _oVar8 = 0; /** * @brief ID of a quest message to play when this object is activated. * * Used by spell book objects which trigger quest progress for Halls of the Blind, Valor, or Warlord of Blood */ _speech_id bookMessage = TEXT_NONE; /** * @brief Returns the network identifier for this object * * This is currently the index into the Objects array, but may change in the future. */ [[nodiscard]] unsigned int GetId() const; /** * @brief Marks the map region to be refreshed when the player interacts with the object. * * Some objects will cause a map region to change when a player interacts with them (e.g. Skeleton King * antechamber levers). The coordinates used for this region are based on a 40*40 grid overlaying the central * 80*80 region of the dungeon. * * @param topLeftPosition corner of the map region closest to the origin. * @param bottomRightPosition corner of the map region furthest from the origin. */ constexpr void SetMapRange(WorldTilePosition topLeftPosition, WorldTilePosition bottomRightPosition) { _oVar1 = topLeftPosition.x; _oVar2 = topLeftPosition.y; _oVar3 = bottomRightPosition.x; _oVar4 = bottomRightPosition.y; } /** * @brief Convenience function for SetMapRange(Point, Point). * @param mapRange A rectangle defining the top left corner and size of the affected region. */ constexpr void SetMapRange(WorldTileRectangle mapRange) { SetMapRange(mapRange.position, mapRange.position + DisplacementOf(mapRange.size)); } /** * @brief Sets up a generic quest book which will trigger a change in the map when activated. * * Books of this type use a generic message (see OperateSChambBook()) compared to the more specific quest books * initialized by IntializeQuestBook(). * * @param mapRange The region to be updated when this object is activated. */ constexpr void InitializeBook(WorldTileRectangle mapRange) { SetMapRange(mapRange); _oVar6 = _oAnimFrame + 1; // Save the frame number for the open book frame } /** * @brief Initializes this object as a quest book which will cause further changes and play a message when activated. * @param mapRange The region to be updated when this object is activated. * @param leverID An ID (distinct from the object index) to identify the new objects spawned after updating the map. * @param message The quest text to play when this object is activated. */ constexpr void InitializeQuestBook(WorldTileRectangle mapRange, int leverID, _speech_id message) { InitializeBook(mapRange); _oVar8 = leverID; bookMessage = message; } /** * @brief Marks an object which was spawned from a sublevel in response to a lever activation. * @param mapRange The region which was updated to spawn this object. * @param leverID The id (*not* an object ID/index) of the lever responsible for the map change. */ constexpr void InitializeLoadedObject(WorldTileRectangle mapRange, int leverID) { SetMapRange(mapRange); _oVar8 = leverID; } /** * @brief Check if the object can be broken (is an intact barrel or crux) * @return True if the object is intact and breakable, false if already broken or not a breakable object. */ [[nodiscard]] constexpr bool IsBreakable() const { return _oBreak == 1; } /** * @brief Check if the object has been broken * @return True if the object is breakable and has been broken, false if unbroken or not a breakable object. */ [[nodiscard]] constexpr bool IsBroken() const { return _oBreak == -1; } /** * Returns true if the object is a harmful shrine and the player has disabled permanent shrine effects. */ [[nodiscard]] bool IsDisabled() const; [[nodiscard]] constexpr bool canInteractWith() const { return selectionRegion != SelectionRegion::None; } /** * @brief Check if this object is barrel (or explosive barrel) * @return True if the object is one of the barrel types (see _object_id) */ [[nodiscard]] constexpr bool IsBarrel() const { return IsAnyOf(_otype, _object_id::OBJ_BARREL, _object_id::OBJ_BARRELEX, _object_id::OBJ_POD, _object_id::OBJ_PODEX, _object_id::OBJ_URN, _object_id::OBJ_URNEX); } /** * @brief Check if this object contains explosives or caustic material */ [[nodiscard]] constexpr bool isExplosive() const { return IsAnyOf(_otype, _object_id::OBJ_BARRELEX, _object_id::OBJ_PODEX, _object_id::OBJ_URNEX); } /** * @brief Check if this object is a chest (or trapped chest). * * Trapped chests get their base type change in addition to having the trap flag set, but if they get "refilled" by * a Thaumaturgic shrine the base type is not reverted. This means you need to consider both the base type and the * trap flag to differentiate between chests that are currently trapped and chests which have never been trapped. * * @return True if the object is any of the chest types (see _object_id) */ [[nodiscard]] constexpr bool IsChest() const { return IsAnyOf(_otype, _object_id::OBJ_CHEST1, _object_id::OBJ_CHEST2, _object_id::OBJ_CHEST3, _object_id::OBJ_TCHEST1, _object_id::OBJ_TCHEST2, _object_id::OBJ_TCHEST3); } /** * @brief Check if this object is a trapped chest (specifically a chest which is currently trapped). * @return True if the object is one of the trapped chest types (see _object_id) and has an active trap. */ [[nodiscard]] constexpr bool IsTrappedChest() const { return IsAnyOf(_otype, _object_id::OBJ_TCHEST1, _object_id::OBJ_TCHEST2, _object_id::OBJ_TCHEST3) && _oTrapFlag; } /** * @brief Check if this object is an untrapped chest (specifically a chest which has not been trapped). * @return True if the object is one of the untrapped chest types (see _object_id) and has no active trap. */ [[nodiscard]] constexpr bool IsUntrappedChest() const { return IsAnyOf(_otype, _object_id::OBJ_CHEST1, _object_id::OBJ_CHEST2, _object_id::OBJ_CHEST3) && !_oTrapFlag; } /** * @brief Check if this object is a crucifix * @return True if the object is one of the crux types (see _object_id) */ [[nodiscard]] constexpr bool IsCrux() const { return IsAnyOf(_otype, _object_id::OBJ_CRUX1, _object_id::OBJ_CRUX2, _object_id::OBJ_CRUX3); } /** * @brief Check if this object is a door * @return True if the object is one of the door types (see _object_id) */ [[nodiscard]] constexpr bool isDoor() const { return IsAnyOf(_otype, _object_id::OBJ_L1LDOOR, _object_id::OBJ_L1RDOOR, _object_id::OBJ_L2LDOOR, _object_id::OBJ_L2RDOOR, _object_id::OBJ_L3LDOOR, _object_id::OBJ_L3RDOOR, _object_id::OBJ_L5LDOOR, _object_id::OBJ_L5RDOOR); } /** * @brief Check if this object is a shrine * @return True if the object is one of the shrine types (see _object_id) */ [[nodiscard]] constexpr bool IsShrine() const { return IsAnyOf(_otype, _object_id::OBJ_SHRINEL, _object_id::OBJ_SHRINER); } /** * @brief Check if this object is a trap source * @return True if the object is one of the trap types (see _object_id) */ [[nodiscard]] constexpr bool IsTrap() const { return IsAnyOf(_otype, _object_id::OBJ_TRAPL, _object_id::OBJ_TRAPR); } /** * @brief Returns the name of the object as shown in the info box */ [[nodiscard]] StringOrView name() const; [[nodiscard]] ClxSprite currentSprite() const { return (*_oAnimData)[_oAnimFrame - 1]; } [[nodiscard]] Displacement getRenderingOffset(const ClxSprite sprite, Point tilePosition) const { Displacement offset = Displacement { -CalculateSpriteTileCenterX(sprite.width()), 0 }; if (position != tilePosition) { // drawing a large or offset object, calculate the correct position for the center of the sprite Displacement worldOffset = position - tilePosition; offset -= worldOffset.worldToScreen(); } return offset; } }; extern DVL_API_FOR_TEST Object Objects[MAXOBJECTS]; extern int AvailableObjects[MAXOBJECTS]; extern int ActiveObjects[MAXOBJECTS]; extern int ActiveObjectCount; /** @brief Indicates that objects are being loaded during gameplay and pre calculated data should be updated. */ extern bool LoadingMapObjects; /** Tracks progress through the tome sequence that spawns Na-Krul (see OperateNakrulBook()) */ extern int NaKrulTomeSequence; /** * @brief Find an object given a point in map coordinates * * @param position The map coordinate to test * @param considerLargeObjects Default behaviour will return a pointer to a large object that covers this tile, set * this param to false if you only want the object whose base position matches this tile * @return A pointer to the object or nullptr if no object exists at this location */ Object *FindObjectAtPosition(Point position, bool considerLargeObjects = true); /** * @brief Check whether an item occupies this tile position * @param position The map coordinate to test * @return true if the tile is occupied */ inline bool IsObjectAtPosition(Point position) { return FindObjectAtPosition(position) != nullptr; } /** * @brief Get a reference to the object located at this tile * * This function is unchecked. Trying to access an invalid position will result in out of bounds memory access * @param position The map coordinate of the object * @return a reference to the object */ inline Object &ObjectAtPosition(Point position) { return Objects[std::abs(dObject[position.x][position.y]) - 1]; } /** * @brief Check whether an item blocking object (solid object or open door) is located at this tile position * @param position The map coordinate to test * @return true if the tile is blocked */ bool IsItemBlockingObjectAtPosition(Point position); tl::expected InitObjectGFX(); void FreeObjectGFX(); void AddL1Objs(int x1, int y1, int x2, int y2); void AddL2Objs(int x1, int y1, int x2, int y2); void AddL3Objs(int x1, int y1, int x2, int y2); void AddCryptObjects(int x1, int y1, int x2, int y2); void InitObjects(); void SetMapObjects(const uint16_t *dunData, int startx, int starty); /** * @brief Spawns an object of the given type at the map coordinates provided * @param objType Type specifier * @param objPos tile coordinates */ Object *AddObject(_object_id objType, Point objPos); bool UpdateTrapState(Object &trap); void OperateTrap(Object &trap); void ProcessObjects(); void RedoPlayerVision(); void MonstCheckDoors(const Monster &monster); void ObjChangeMap(int x1, int y1, int x2, int y2); void ObjChangeMapResync(int x1, int y1, int x2, int y2); _item_indexes ItemMiscIdIdx(item_misc_id imiscid); void OperateObject(Player &player, Object &object); void SyncOpObject(Player &player, int cmd, Object &object); void BreakObjectMissile(const Player *player, Object &object); void BreakObject(const Player &player, Object &object); void DeltaSyncOpObject(Object &object); void DeltaSyncCloseObj(Object &object); void DeltaSyncBreakObj(Object &object); void SyncBreakObj(const Player &player, Object &object); void SyncObjectAnim(Object &object); /** * @brief Updates the text drawn in the info box to describe the given object * @param object The currently highlighted object */ void GetObjectStr(const Object &object); void SyncNakrulRoom(); } // namespace devilution ================================================ FILE: Source/options.cpp ================================================ /** * @file options.cpp * * Load and save options from the diablo.ini file. */ #include "options.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #else #include #endif #include #include #include #include "appfat.h" #include "controls/control_mode.hpp" #include "controls/controller_buttons.h" #include "engine/assets.hpp" #include "engine/sound_defs.hpp" #include "platform/locale.hpp" #include "quick_messages.hpp" #include "utils/algorithm/container.hpp" #include "utils/file_util.h" #include "utils/ini.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/logged_fstream.hpp" #include "utils/paths.h" #include "utils/sdl_ptrs.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #include "utils/utf8.hpp" namespace devilution { #ifndef DEFAULT_AUDIO_SAMPLE_RATE #define DEFAULT_AUDIO_SAMPLE_RATE 22050 #endif #ifndef DEFAULT_AUDIO_CHANNELS #define DEFAULT_AUDIO_CHANNELS 2 #endif #ifndef DEFAULT_AUDIO_BUFFER_SIZE #define DEFAULT_AUDIO_BUFFER_SIZE 2048 #endif #ifndef DEFAULT_AUDIO_RESAMPLING_QUALITY #define DEFAULT_AUDIO_RESAMPLING_QUALITY 3 #endif #ifndef DEFAULT_PER_PIXEL_LIGHTING #define DEFAULT_PER_PIXEL_LIGHTING true #endif namespace { void DiscoverMods() { // Add mods available by default: std::unordered_set modNames = { "clock", "adria_refills_mana", "Floating Numbers - Damage", "Floating Numbers - XP" }; if (HaveHellfire()) { modNames.insert("Hellfire"); } // Check if the mods directory exists. const std::string modsPath = StrCat(paths::PrefPath(), "mods"); if (DirectoryExists(modsPath.c_str())) { // Find unpacked mods for (const std::string &modFolder : ListDirectories(modsPath.c_str())) { // Only consider this folder if the init.lua file exists. const std::string modScriptPath = modsPath + DIRECTORY_SEPARATOR_STR + modFolder + DIRECTORY_SEPARATOR_STR + "lua" + DIRECTORY_SEPARATOR_STR + "mods" + DIRECTORY_SEPARATOR_STR + modFolder + DIRECTORY_SEPARATOR_STR + "init.lua"; if (!FileExists(modScriptPath.c_str())) continue; modNames.insert(modFolder); } // Find packed mods for (const std::string &modMpq : ListFiles(modsPath.c_str())) { if (!modMpq.ends_with(".mpq")) continue; modNames.insert(modMpq.substr(0, modMpq.size() - 4)); } } // Get the list of mods currently stored in the INI. std::vector existingMods = GetOptions().Mods.GetModList(); // Add new mods. for (const std::string &modName : modNames) { if (std::find(existingMods.begin(), existingMods.end(), modName) == existingMods.end()) GetOptions().Mods.AddModEntry(modName); } // Remove mods that are no longer installed. for (const std::string_view &modName : existingMods) { if (modNames.find(std::string(modName)) == modNames.end()) GetOptions().Mods.RemoveModEntry(std::string(modName)); } } std::optional ini; #if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags::Invisible; #else constexpr OptionEntryFlags OnlyIfSupportsWindowed = OptionEntryFlags::None; #endif constexpr size_t NumResamplers = #ifdef DEVILUTIONX_RESAMPLER_SPEEX 1 + #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER 1 + #endif 0; std::string GetIniPath() { auto path = paths::ConfigPath() + std::string("diablo.ini"); return path; } void LoadIni() { std::vector buffer; auto path = GetIniPath(); FILE *file = OpenFile(path.c_str(), "rb"); if (file != nullptr) { uintmax_t size; if (GetFileSize(path.c_str(), &size)) { buffer.resize(static_cast(size)); if (std::fread(buffer.data(), static_cast(size), 1, file) != 1) { const char *errorMessage = std::strerror(errno); if (errorMessage == nullptr) errorMessage = ""; LogError(LogCategory::System, "std::fread: failed with \"{}\"", errorMessage); buffer.clear(); } } std::fclose(file); } tl::expected result = Ini::parse(std::string_view(buffer.data(), buffer.size())); if (!result.has_value()) app_fatal(result.error()); ini.emplace(std::move(result).value()); } void SaveIni() { if (!ini.has_value()) return; if (!ini->changed()) return; if (!paths::ConfigPath().empty()) { RecursivelyCreateDir(paths::ConfigPath().c_str()); } const std::string iniPath = GetIniPath(); LoggedFStream out; if (!out.Open(iniPath.c_str(), "wb")) { LogError("Failed to open ini file for writing at {}: {}", iniPath, std::strerror(errno)); return; } const std::string newContents = ini->serialize(); if (out.Write(newContents.data(), newContents.size())) { ini->markAsUnchanged(); } out.Close(); } #if SDL_VERSION_ATLEAST(2, 0, 0) bool HardwareCursorDefault() { #if defined(__ANDROID__) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) // See https://github.com/diasurgical/devilutionX/issues/2502 return false; #else return HardwareCursorSupported(); #endif } #endif } // namespace Options &GetOptions() { static Options options; return options; } #if SDL_VERSION_ATLEAST(2, 0, 0) bool HardwareCursorSupported() { #if (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE == 1) || __DJGPP__ return false; #elif USE_SDL3 return true; #else SDL_version v; SDL_GetVersion(&v); return SDL_VERSIONNUM(v.major, v.minor, v.patch) >= SDL_VERSIONNUM(2, 0, 12); #endif } #endif void LoadOptions() { LoadIni(); DiscoverMods(); Options &options = GetOptions(); for (OptionCategoryBase *pCategory : options.GetCategories()) { for (OptionEntryBase *pEntry : pCategory->GetEntries()) { pEntry->LoadFromIni(pCategory->GetKey()); } } ini->getUtf8Buf("Hellfire", "SItem", options.Hellfire.szItem, sizeof(options.Hellfire.szItem)); ini->getUtf8Buf("Network", "Bind Address", "0.0.0.0", options.Network.szBindAddress, sizeof(options.Network.szBindAddress)); ini->getUtf8Buf("Network", "Previous Game ID", options.Network.szPreviousZTGame, sizeof(options.Network.szPreviousZTGame)); ini->getUtf8Buf("Network", "Previous Host", options.Network.szPreviousHost, sizeof(options.Network.szPreviousHost)); for (size_t i = 0; i < QuickMessages.size(); i++) { const std::span values = ini->get("NetMsg", QuickMessages[i].key); std::vector &result = options.Chat.szHotKeyMsgs[i]; result.clear(); result.reserve(values.size()); for (const Ini::Value &value : values) { result.emplace_back(value.value); } } ini->getUtf8Buf("Controller", "Mapping", options.Controller.szMapping, sizeof(options.Controller.szMapping)); options.Controller.fDeadzone = ini->getFloat("Controller", "deadzone", 0.07F); #ifdef __vita__ options.Controller.bRearTouch = ini->getBool("Controller", "Enable Rear Touchpad", true); #endif } void SaveOptions() { Options &options = GetOptions(); for (OptionCategoryBase *pCategory : options.GetCategories()) { for (const OptionEntryBase *pEntry : pCategory->GetEntries()) { pEntry->SaveToIni(pCategory->GetKey()); } } ini->set("Hellfire", "SItem", options.Hellfire.szItem); ini->set("Network", "Bind Address", options.Network.szBindAddress); ini->set("Network", "Previous Game ID", options.Network.szPreviousZTGame); ini->set("Network", "Previous Host", options.Network.szPreviousHost); for (size_t i = 0; i < QuickMessages.size(); i++) { ini->set("NetMsg", QuickMessages[i].key, options.Chat.szHotKeyMsgs[i]); } ini->set("Controller", "Mapping", options.Controller.szMapping); ini->set("Controller", "deadzone", options.Controller.fDeadzone); #ifdef __vita__ ini->set("Controller", "Enable Rear Touchpad", options.Controller.bRearTouch); #endif SaveIni(); } std::string_view OptionEntryBase::GetName() const { return _(name); } std::string_view OptionEntryBase::GetDescription() const { return _(description); } OptionEntryFlags OptionEntryBase::GetFlags() const { return flags; } void OptionEntryBase::SetValueChangedCallback(tl::function_ref callback) { callback_ = callback; } void OptionEntryBase::NotifyValueChanged() { if (callback_.has_value()) (*callback_)(); } void OptionEntryBoolean::LoadFromIni(std::string_view category) { value = ini->getBool(category, key, defaultValue); } void OptionEntryBoolean::SaveToIni(std::string_view category) const { ini->set(category, key, value); } void OptionEntryBoolean::SetValue(bool newValue) { this->value = newValue; this->NotifyValueChanged(); } OptionEntryType OptionEntryBoolean::GetType() const { return OptionEntryType::Boolean; } std::string_view OptionEntryBoolean::GetValueDescription() const { return value ? _("ON") : _("OFF"); } OptionEntryType OptionEntryListBase::GetType() const { return OptionEntryType::List; } std::string_view OptionEntryListBase::GetValueDescription() const { return GetListDescription(GetActiveListIndex()); } void OptionEntryEnumBase::LoadFromIni(std::string_view category) { value = ini->getInt(category, key, defaultValue); } void OptionEntryEnumBase::SaveToIni(std::string_view category) const { ini->set(category, key, value); } void OptionEntryEnumBase::SetValueInternal(int newValue) { this->value = newValue; this->NotifyValueChanged(); } void OptionEntryEnumBase::AddEntry(int entryValue, std::string_view name) { entryValues.push_back(entryValue); entryNames.push_back(name); } size_t OptionEntryEnumBase::GetListSize() const { return entryValues.size(); } std::string_view OptionEntryEnumBase::GetListDescription(size_t index) const { return _(entryNames[index].data()); } size_t OptionEntryEnumBase::GetActiveListIndex() const { auto iterator = c_find(entryValues, value); if (iterator == entryValues.end()) return 0; return std::distance(entryValues.begin(), iterator); } void OptionEntryEnumBase::SetActiveListIndex(size_t index) { this->value = entryValues[index]; this->NotifyValueChanged(); } void OptionEntryIntBase::LoadFromIni(std::string_view category) { value = ini->getInt(category, key, defaultValue); if (c_find(entryValues, value) == entryValues.end()) { entryValues.insert(c_lower_bound(entryValues, value), value); entryNames.clear(); } } void OptionEntryIntBase::SaveToIni(std::string_view category) const { ini->set(category, key, value); } void OptionEntryIntBase::SetValueInternal(int newValue) { this->value = newValue; this->NotifyValueChanged(); } void OptionEntryIntBase::AddEntry(int entryValue) { entryValues.push_back(entryValue); } size_t OptionEntryIntBase::GetListSize() const { return entryValues.size(); } std::string_view OptionEntryIntBase::GetListDescription(size_t index) const { if (entryNames.empty()) { for (auto entryValue : entryValues) { entryNames.push_back(StrCat(entryValue)); } } return entryNames[index].data(); } size_t OptionEntryIntBase::GetActiveListIndex() const { auto iterator = c_find(entryValues, value); if (iterator == entryValues.end()) return 0; return std::distance(entryValues.begin(), iterator); } void OptionEntryIntBase::SetActiveListIndex(size_t index) { this->value = entryValues[index]; this->NotifyValueChanged(); } std::string_view OptionCategoryBase::GetKey() const { return key; } std::string_view OptionCategoryBase::GetName() const { return _(name); } std::string_view OptionCategoryBase::GetDescription() const { return _(description); } GameModeOptions::GameModeOptions() : OptionCategoryBase("GameMode", N_("Game Mode"), N_("Game Mode Settings")) , gameMode("Game", OptionEntryFlags::Invisible, N_("Game Mode"), N_("Play Diablo or Hellfire."), StartUpGameMode::Ask, { { StartUpGameMode::Diablo, N_("Diablo") }, // Ask is missing, because we want to hide it from UI-Settings. { StartUpGameMode::Hellfire, N_("Hellfire") }, }) , shareware("Shareware", OptionEntryFlags::NeedDiabloMpq | OptionEntryFlags::RecreateUI, N_("Restrict to Shareware"), N_("Makes the game compatible with the demo. Enables multiplayer with friends who don't own a full copy of Diablo."), false) { } std::vector GameModeOptions::GetEntries() { return { &gameMode, &shareware, }; } StartUpOptions::StartUpOptions() : OptionCategoryBase("StartUp", N_("Start Up"), N_("Start Up Settings")) , diabloIntro("Diablo Intro", OptionEntryFlags::OnlyDiablo, N_("Intro"), N_("Shown Intro cinematic."), StartUpIntro::Once, { { StartUpIntro::Off, N_("OFF") }, // Once is missing, because we want to hide it from UI-Settings. { StartUpIntro::On, N_("ON") }, }) , hellfireIntro("Hellfire Intro", OptionEntryFlags::OnlyHellfire, N_("Intro"), N_("Shown Intro cinematic."), StartUpIntro::Once, { { StartUpIntro::Off, N_("OFF") }, // Once is missing, because we want to hide it from UI-Settings. { StartUpIntro::On, N_("ON") }, }) , splash("Splash", OptionEntryFlags::None, N_("Splash"), N_("Shown splash screen."), StartUpSplash::LogoAndTitleDialog, { { StartUpSplash::LogoAndTitleDialog, N_("Logo and Title Screen") }, { StartUpSplash::TitleDialog, N_("Title Screen") }, { StartUpSplash::None, N_("None") }, }) { } std::vector StartUpOptions::GetEntries() { return { &diabloIntro, &hellfireIntro, &splash, }; } DiabloOptions::DiabloOptions() : OptionCategoryBase("Diablo", N_("Diablo"), N_("Diablo specific Settings")) , lastSinglePlayerHero("LastSinglePlayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyDiablo, "Sample Rate", "Remembers what singleplayer hero/save was last used.", 0) , lastMultiplayerHero("LastMultiplayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyDiablo, "Sample Rate", "Remembers what multiplayer hero/save was last used.", 0) { } std::vector DiabloOptions::GetEntries() { return { &lastSinglePlayerHero, &lastMultiplayerHero, }; } HellfireOptions::HellfireOptions() : OptionCategoryBase("Hellfire", N_("Hellfire"), N_("Hellfire specific Settings")) , lastSinglePlayerHero("LastSinglePlayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyHellfire, "Sample Rate", "Remembers what singleplayer hero/save was last used.", 0) , lastMultiplayerHero("LastMultiplayerHero", OptionEntryFlags::Invisible | OptionEntryFlags::OnlyHellfire, "Sample Rate", "Remembers what multiplayer hero/save was last used.", 0) { } std::vector HellfireOptions::GetEntries() { return { &lastSinglePlayerHero, &lastMultiplayerHero, }; } AudioOptions::AudioOptions() : OptionCategoryBase("Audio", N_("Audio"), N_("Audio Settings")) , soundVolume("Sound Volume", OptionEntryFlags::Invisible, "Sound Volume", "Movie and SFX volume.", VOLUME_MAX) , audioCuesVolume("Audio Cues Volume", OptionEntryFlags::Invisible, "Audio Cues Volume", "Navigation audio cues volume.", VOLUME_MAX) , musicVolume("Music Volume", OptionEntryFlags::Invisible, "Music Volume", "Music Volume.", VOLUME_MAX) , walkingSound("Walking Sound", OptionEntryFlags::None, N_("Walking Sound"), N_("Player emits sound when walking."), true) , autoEquipSound("Auto Equip Sound", OptionEntryFlags::None, N_("Auto Equip Sound"), N_("Automatically equipping items on pickup emits the equipment sound."), false) , itemPickupSound("Item Pickup Sound", OptionEntryFlags::None, N_("Item Pickup Sound"), N_("Picking up items emits the items pickup sound."), false) , sampleRate("Sample Rate", OptionEntryFlags::CantChangeInGame, N_("Sample Rate"), N_("Output sample rate (Hz)."), DEFAULT_AUDIO_SAMPLE_RATE, { 22050, 44100, 48000 }) , channels("Channels", OptionEntryFlags::CantChangeInGame, N_("Channels"), N_("Number of output channels."), DEFAULT_AUDIO_CHANNELS, { 1, 2 }) , bufferSize("Buffer Size", OptionEntryFlags::CantChangeInGame, N_("Buffer Size"), N_("Buffer size (number of frames per channel)."), DEFAULT_AUDIO_BUFFER_SIZE, { 1024, 2048, 5120 }) , resamplingQuality("Resampling Quality", OptionEntryFlags::CantChangeInGame, N_("Resampling Quality"), N_("Quality of the resampler, from 0 (lowest) to 5 (highest)."), DEFAULT_AUDIO_RESAMPLING_QUALITY, { 0, 1, 2, 3, 4, 5 }) { } std::vector AudioOptions::GetEntries() { // clang-format off return { &soundVolume, &audioCuesVolume, &musicVolume, &walkingSound, &autoEquipSound, &itemPickupSound, &sampleRate, &channels, &bufferSize, &resampler, &resamplingQuality, #if SDL_VERSION_ATLEAST(2, 0, 0) &device, #endif }; // clang-format on } OptionEntryResolution::OptionEntryResolution() : OptionEntryListBase("", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Resolution"), N_("Affect the game's internal resolution and determine your view area. Note: This can differ from screen resolution, when Upscaling, Integer Scaling or Fit to Screen is used.")) { } void OptionEntryResolution::LoadFromIni(std::string_view category) { size_ = { ini->getInt(category, "Width", DEFAULT_WIDTH), ini->getInt(category, "Height", DEFAULT_HEIGHT) }; } void OptionEntryResolution::SaveToIni(std::string_view category) const { ini->set(category, "Width", size_.width); ini->set(category, "Height", size_.height); } size_t OptionEntryResolution::GetListSize() const { return resolutions_.size(); } std::string_view OptionEntryResolution::GetListDescription(size_t index) const { return resolutions_[index].second; } size_t OptionEntryResolution::GetActiveListIndex() const { auto found = c_find_if(resolutions_, [this](const auto &x) { return x.first == size_; }); if (found == resolutions_.end()) return 0; return std::distance(resolutions_.begin(), found); } void OptionEntryResolution::SetActiveListIndex(size_t index) { size_ = resolutions_[index].first; NotifyValueChanged(); } OptionEntryResampler::OptionEntryResampler() : OptionEntryListBase("Resampler", OptionEntryFlags::CantChangeInGame // When there are exactly 2 options there is no submenu, so we need to recreate the UI // to reflect the change in the "Resampling quality" setting visibility. | (NumResamplers == 2 ? OptionEntryFlags::RecreateUI : OptionEntryFlags::None), N_("Resampler"), N_("Audio resampler")) { } void OptionEntryResampler::LoadFromIni(std::string_view category) { const std::string_view resamplerStr = ini->getString(category, key); if (!resamplerStr.empty()) { std::optional resampler = ResamplerFromString(resamplerStr); if (resampler) { resampler_ = *resampler; UpdateDependentOptions(); return; } } resampler_ = Resampler::DEVILUTIONX_DEFAULT_RESAMPLER; UpdateDependentOptions(); } void OptionEntryResampler::SaveToIni(std::string_view category) const { ini->set(category, key, ResamplerToString(resampler_)); } size_t OptionEntryResampler::GetListSize() const { return NumResamplers; } std::string_view OptionEntryResampler::GetListDescription(size_t index) const { return ResamplerToString(static_cast(index)); } size_t OptionEntryResampler::GetActiveListIndex() const { return static_cast(resampler_); } void OptionEntryResampler::SetActiveListIndex(size_t index) { resampler_ = static_cast(index); UpdateDependentOptions(); NotifyValueChanged(); } void OptionEntryResampler::UpdateDependentOptions() const { #ifdef DEVILUTIONX_RESAMPLER_SPEEX if (resampler_ == Resampler::Speex) { GetOptions().Audio.resamplingQuality.flags &= ~OptionEntryFlags::Invisible; } else { GetOptions().Audio.resamplingQuality.flags |= OptionEntryFlags::Invisible; } #endif } OptionEntryAudioDevice::OptionEntryAudioDevice() : OptionEntryListBase("Device", OptionEntryFlags::CantChangeInGame, N_("Device"), N_("Audio device")) { } void OptionEntryAudioDevice::LoadFromIni(std::string_view category) { deviceName_ = ini->getString(category, key); } void OptionEntryAudioDevice::SaveToIni(std::string_view category) const { #if SDL_VERSION_ATLEAST(2, 0, 0) ini->set(category, key, deviceName_); #endif } size_t OptionEntryAudioDevice::GetListSize() const { #if defined(USE_SDL3) int numDevices = 0; SDLUniquePtr devices { SDL_GetAudioPlaybackDevices(&numDevices) }; return static_cast(numDevices) + 1; #elif SDL_VERSION_ATLEAST(2, 0, 0) return SDL_GetNumAudioDevices(false) + 1; #else return 1; #endif } std::string_view OptionEntryAudioDevice::GetListDescription(size_t index) const { std::string_view deviceName = GetDeviceName(index); if (deviceName.empty()) deviceName = "System Default"; return deviceName; } size_t OptionEntryAudioDevice::GetActiveListIndex() const { #ifdef USE_SDL3 int numDevices; SDLUniquePtr devices { SDL_GetAudioPlaybackDevices(&numDevices) }; if (devices == nullptr) return 0; for (int i = 0; i < numDevices; ++i) { const char *deviceName = SDL_GetAudioDeviceName(devices.get()[i]); if (deviceName_ == deviceName) return i; } return 0; #else for (size_t i = 0; i < GetListSize(); i++) { const std::string_view deviceName = GetDeviceName(i); if (deviceName_ == deviceName) return i; } return 0; #endif } void OptionEntryAudioDevice::SetActiveListIndex(size_t index) { deviceName_ = std::string { GetDeviceName(index) }; NotifyValueChanged(); } std::string_view OptionEntryAudioDevice::GetDeviceName(size_t index) const { if (index == 0) return {}; // System Default #if defined(USE_SDL3) int numDevices = 0; SDLUniquePtr devices { SDL_GetAudioPlaybackDevices(&numDevices) }; if (devices == nullptr || static_cast(index) > numDevices) return "Unknown"; const char *deviceName = SDL_GetAudioDeviceName(devices.get()[index - 1]); if (deviceName == nullptr) return "Unknown"; return deviceName; #elif SDL_VERSION_ATLEAST(2, 0, 0) return SDL_GetAudioDeviceName(static_cast(index) - 1, false); #endif return {}; } #ifdef USE_SDL3 SDL_AudioDeviceID OptionEntryAudioDevice::id() const { if (deviceName_.empty()) return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; int numDevices = 0; SDLUniquePtr devices { SDL_GetAudioPlaybackDevices(&numDevices) }; if (devices == nullptr) { LogWarn("Failed to get audio devices: {}", SDL_GetError()); SDL_ClearError(); return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; } for (int i = 0; i < numDevices; ++i) { const SDL_AudioDeviceID id = devices.get()[i]; if (deviceName_ == SDL_GetAudioDeviceName(id)) return id; } return SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; } #endif GraphicsOptions::GraphicsOptions() : OptionCategoryBase("Graphics", N_("Graphics"), N_("Graphics Settings")) , fullscreen("Fullscreen", OnlyIfSupportsWindowed | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fullscreen"), N_("Display the game in windowed or fullscreen mode."), true) #if !defined(USE_SDL1) || defined(__3DS__) , fitToScreen("Fit to Screen", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Fit to Screen"), N_("Automatically adjust the game window to your current desktop screen aspect ratio and resolution."), #ifdef __DJGPP__ false #else true #endif ) #endif #ifndef USE_SDL1 , upscale("Upscale", OptionEntryFlags::Invisible | OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Upscale"), N_("Enables image scaling from the game resolution to your monitor resolution. Prevents changing the monitor resolution and allows window resizing."), #if defined(NXDK) || defined(__DJGPP__) false #else true #endif ) , scaleQuality("Scaling Quality", OptionEntryFlags::None, N_("Scaling Quality"), N_("Enables optional filters to the output image when upscaling."), ScalingQuality::AnisotropicFiltering, { { ScalingQuality::NearestPixel, N_("Nearest Pixel") }, { ScalingQuality::BilinearFiltering, N_("Bilinear") }, { ScalingQuality::AnisotropicFiltering, N_("Anisotropic") }, }) , integerScaling("Integer Scaling", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Integer Scaling"), N_("Scales the image using whole number pixel ratio."), false) #endif , frameRateControl("Frame Rate Control", OptionEntryFlags::RecreateUI #if defined(NXDK) || defined(__ANDROID__) | OptionEntryFlags::Invisible #endif , N_("Frame Rate Control"), N_("Manages frame rate to balance performance, reduce tearing, or save power."), #if defined(NXDK) || defined(USE_SDL1) FrameRateControl::CPUSleep #else FrameRateControl::VerticalSync #endif , { { FrameRateControl::None, N_("None") }, #ifndef USE_SDL1 { FrameRateControl::VerticalSync, N_("Vertical Sync") }, #endif { FrameRateControl::CPUSleep, N_("Limit FPS") }, }) , brightness("Brightness Correction", OptionEntryFlags::Invisible, "Brightness Correction", "Brightness correction level.", 0) , zoom("Zoom", OptionEntryFlags::None, N_("Zoom"), N_("Zoom on when enabled."), false) , perPixelLighting("Per-pixel Lighting", OptionEntryFlags::None, N_("Per-pixel Lighting"), N_("Subtile lighting for smoother light gradients."), DEFAULT_PER_PIXEL_LIGHTING) , colorCycling("Color Cycling", OptionEntryFlags::None, N_("Color Cycling"), N_("Color cycling effect used for water, lava, and acid animation."), true) , alternateNestArt("Alternate nest art", OptionEntryFlags::OnlyHellfire | OptionEntryFlags::CantChangeInGame, N_("Alternate nest art"), N_("The game will use an alternative palette for Hellfire’s nest tileset."), false) #if SDL_VERSION_ATLEAST(2, 0, 0) , hardwareCursor("Hardware Cursor", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI | (HardwareCursorSupported() ? OptionEntryFlags::None : OptionEntryFlags::Invisible), N_("Hardware Cursor"), N_("Use a hardware cursor"), HardwareCursorDefault()) , hardwareCursorForItems("Hardware Cursor For Items", OptionEntryFlags::CantChangeInGame | (HardwareCursorSupported() ? OptionEntryFlags::None : OptionEntryFlags::Invisible), N_("Hardware Cursor For Items"), N_("Use a hardware cursor for items."), false) , hardwareCursorMaxSize("Hardware Cursor Maximum Size", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI | (HardwareCursorSupported() ? OptionEntryFlags::None : OptionEntryFlags::Invisible), N_("Hardware Cursor Maximum Size"), N_("Maximum width / height for the hardware cursor. Larger cursors fall back to software."), 128, { 0, 64, 128, 256, 512 }) #endif , showFPS("Show FPS", OptionEntryFlags::None, N_("Show FPS"), N_("Displays the FPS in the upper left corner of the screen."), false) { } std::vector GraphicsOptions::GetEntries() { // clang-format off return { &resolution, #ifndef __vita__ &fullscreen, #endif #if !defined(USE_SDL1) || defined(__3DS__) &fitToScreen, #endif #ifndef USE_SDL1 &upscale, &scaleQuality, &integerScaling, #endif &frameRateControl, &brightness, &zoom, &showFPS, &perPixelLighting, &colorCycling, &alternateNestArt, #if SDL_VERSION_ATLEAST(2, 0, 0) &hardwareCursor, &hardwareCursorForItems, &hardwareCursorMaxSize, #endif }; // clang-format on } GameplayOptions::GameplayOptions() : OptionCategoryBase("Game", N_("Gameplay"), N_("Gameplay Settings")) , tickRate("Speed", OptionEntryFlags::Invisible, "Speed", "Gameplay ticks per second.", 20) , runInTown("Run in Town", OptionEntryFlags::CantChangeInMultiPlayer, N_("Run in Town"), N_("Enable jogging/fast walking in town for Diablo and Hellfire. This option was introduced in the expansion."), false) , grabInput("Grab Input", OptionEntryFlags::None, N_("Grab Input"), N_("When enabled mouse is locked to the game window."), false) , pauseOnFocusLoss("Pause Game When Window Loses Focus", OptionEntryFlags::None, N_("Pause Game When Window Loses Focus"), N_("When enabled, the game will pause when focus is lost."), true) , theoQuest("Theo Quest", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, N_("Theo Quest"), N_("Enable Little Girl quest."), false) , cowQuest("Cow Quest", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, N_("Cow Quest"), N_("Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut."), false) , friendlyFire("Friendly Fire", OptionEntryFlags::CantChangeInMultiPlayer, N_("Friendly Fire"), N_("Allow arrow/spell damage between players in multiplayer even when the friendly mode is on."), true) , multiplayerFullQuests("MultiplayerFullQuests", OptionEntryFlags::CantChangeInMultiPlayer, N_("Full quests in Multiplayer"), N_("Enables the full/uncut singleplayer version of quests."), false) , testBard("Test Bard", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, N_("Test Bard"), N_("Force the Bard character type to appear in the hero selection menu."), false) , testBarbarian("Test Barbarian", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::OnlyHellfire, N_("Test Barbarian"), N_("Force the Barbarian character type to appear in the hero selection menu."), false) , experienceBar("Experience Bar", OptionEntryFlags::None, N_("Experience Bar"), N_("Experience Bar is added to the UI at the bottom of the screen."), false) , showItemGraphicsInStores("Show Item Graphics in Stores", OptionEntryFlags::None, N_("Show Item Graphics in Stores"), N_("Show item graphics to the left of item descriptions in store menus."), false) , showHealthValues("Show health values", OptionEntryFlags::None, N_("Show health values"), N_("Displays current / max health value on health globe."), false) , showManaValues("Show mana values", OptionEntryFlags::None, N_("Show mana values"), N_("Displays current / max mana value on mana globe."), false) , showMultiplayerPartyInfo("Show Multiplayer Party Information", OptionEntryFlags::CantChangeInMultiPlayer, N_("Show Party Information"), N_("Displays the health and mana of all connected multiplayer party members."), false) , enemyHealthBar("Enemy Health Bar", OptionEntryFlags::None, N_("Enemy Health Bar"), N_("Enemy Health Bar is displayed at the top of the screen."), false) , floatingInfoBox("Floating Item Info Box", OptionEntryFlags::None, N_("Floating Item Info Box"), N_("Displays item info in a floating box when hovering over an item."), false) , autoGoldPickup("Auto Gold Pickup", OptionEntryFlags::None, N_("Auto Gold Pickup"), N_("Gold is automatically collected when in close proximity to the player."), false) , autoElixirPickup("Auto Elixir Pickup", OptionEntryFlags::None, N_("Auto Elixir Pickup"), N_("Elixirs are automatically collected when in close proximity to the player."), false) , autoOilPickup("Auto Oil Pickup", OptionEntryFlags::OnlyHellfire, N_("Auto Oil Pickup"), N_("Oils are automatically collected when in close proximity to the player."), false) , autoPickupInTown("Auto Pickup in Town", OptionEntryFlags::None, N_("Auto Pickup in Town"), N_("Automatically pickup items in town."), false) , autoEquipWeapons("Auto Equip Weapons", OptionEntryFlags::None, N_("Auto Equip Weapons"), N_("Weapons will be automatically equipped on pickup or purchase if enabled."), true) , autoEquipArmor("Auto Equip Armor", OptionEntryFlags::None, N_("Auto Equip Armor"), N_("Armor will be automatically equipped on pickup or purchase if enabled."), false) , autoEquipHelms("Auto Equip Helms", OptionEntryFlags::None, N_("Auto Equip Helms"), N_("Helms will be automatically equipped on pickup or purchase if enabled."), false) , autoEquipShields("Auto Equip Shields", OptionEntryFlags::None, N_("Auto Equip Shields"), N_("Shields will be automatically equipped on pickup or purchase if enabled."), false) , autoEquipJewelry("Auto Equip Jewelry", OptionEntryFlags::None, N_("Auto Equip Jewelry"), N_("Jewelry will be automatically equipped on pickup or purchase if enabled."), false) , randomizeQuests("Randomize Quests", OptionEntryFlags::CantChangeInGame, N_("Randomize Quests"), N_("Randomly selecting available quests for new games."), true) , showMonsterType("Show Monster Type", OptionEntryFlags::None, N_("Show Monster Type"), N_("Hovering over a monster will display the type of monster in the description box in the UI."), false) , showItemLabels("Show Item Labels", OptionEntryFlags::None, N_("Show Item Labels"), N_("Show labels for items on the ground when enabled."), false) , autoRefillBelt("Auto Refill Belt", OptionEntryFlags::None, N_("Auto Refill Belt"), N_("Refill belt from inventory when belt item is consumed."), false) , disableCripplingShrines("Disable Crippling Shrines", OptionEntryFlags::None, N_("Disable Crippling Shrines"), N_("When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, Sacred Shrines and Murphy's Shrines are not able to be clicked on and labeled as disabled."), false) , quickCast("Quick Cast", OptionEntryFlags::None, N_("Quick Cast"), N_("Spell hotkeys instantly cast the spell, rather than switching the readied spell."), false) , numHealPotionPickup("Heal Potion Pickup", OptionEntryFlags::None, N_("Heal Potion Pickup"), N_("Number of Healing potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , numFullHealPotionPickup("Full Heal Potion Pickup", OptionEntryFlags::None, N_("Full Heal Potion Pickup"), N_("Number of Full Healing potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , numManaPotionPickup("Mana Potion Pickup", OptionEntryFlags::None, N_("Mana Potion Pickup"), N_("Number of Mana potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , numFullManaPotionPickup("Full Mana Potion Pickup", OptionEntryFlags::None, N_("Full Mana Potion Pickup"), N_("Number of Full Mana potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , numRejuPotionPickup("Rejuvenation Potion Pickup", OptionEntryFlags::None, N_("Rejuvenation Potion Pickup"), N_("Number of Rejuvenation potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , numFullRejuPotionPickup("Full Rejuvenation Potion Pickup", OptionEntryFlags::None, N_("Full Rejuvenation Potion Pickup"), N_("Number of Full Rejuvenation potions to pick up automatically."), 0, { 0, 1, 2, 4, 8, 16 }) , skipLoadingScreenThresholdMs("Skip loading screen threshold, ms", OptionEntryFlags::Invisible, "", "", 0) { } std::vector GameplayOptions::GetEntries() { return { &tickRate, &friendlyFire, &multiplayerFullQuests, &randomizeQuests, &theoQuest, &cowQuest, &runInTown, &quickCast, &testBard, &testBarbarian, &experienceBar, &showItemGraphicsInStores, &showHealthValues, &showManaValues, &showMultiplayerPartyInfo, &enemyHealthBar, &floatingInfoBox, &showMonsterType, &showItemLabels, &autoRefillBelt, &autoEquipWeapons, &autoEquipArmor, &autoEquipHelms, &autoEquipShields, &autoEquipJewelry, &autoGoldPickup, &autoElixirPickup, &autoOilPickup, &numHealPotionPickup, &numFullHealPotionPickup, &numManaPotionPickup, &numFullManaPotionPickup, &numRejuPotionPickup, &numFullRejuPotionPickup, &autoPickupInTown, &disableCripplingShrines, &grabInput, &pauseOnFocusLoss, &skipLoadingScreenThresholdMs, }; } ControllerOptions::ControllerOptions() : OptionCategoryBase("Controller", N_("Controller"), N_("Controller Settings")) { } std::vector ControllerOptions::GetEntries() { return {}; } NetworkOptions::NetworkOptions() : OptionCategoryBase("Network", N_("Network"), N_("Network Settings")) , port("Port", OptionEntryFlags::Invisible, "Port", "What network port to use.", 6112) { } std::vector NetworkOptions::GetEntries() { return { &port, }; } ChatOptions::ChatOptions() : OptionCategoryBase("NetMsg", N_("Chat"), N_("Chat Settings")) { } std::vector ChatOptions::GetEntries() { return {}; } OptionEntryLanguageCode::OptionEntryLanguageCode() : OptionEntryListBase("Code", OptionEntryFlags::CantChangeInGame | OptionEntryFlags::RecreateUI, N_("Language"), N_("Define what language to use in game.")) { } void OptionEntryLanguageCode::LoadFromIni(std::string_view category) { ini->getUtf8Buf(category, key, szCode, sizeof(szCode)); if (szCode[0] != '\0' && HasTranslation(szCode)) { // User preferred language is available return; } // Might be a first run or the user has attempted to load a translation that doesn't exist via manual ini edit. Try // find a best fit from the platform locale information. std::vector locales = GetLocales(); // So that the correct language is shown in the settings menu for users with US english set as a preferred language // we need to replace the "en_US" locale code with the neutral string "en" as expected by the available options std::replace(locales.begin(), locales.end(), std::string { "en_US" }, std::string { "en" }); // Insert non-regional locale codes after the last regional variation so we fallback to neutral translations if no // regional translation exists that meets user preferences. for (auto localeIter = locales.rbegin(); localeIter != locales.rend(); localeIter++) { auto regionSeparator = localeIter->find('_'); if (regionSeparator != std::string::npos) { const std::string neutralLocale = localeIter->substr(0, regionSeparator); if (std::find(locales.rbegin(), localeIter, neutralLocale) == localeIter) { localeIter = std::make_reverse_iterator(locales.insert(localeIter.base(), neutralLocale)); } } } LogVerbose("Found user preferred locales: {}", fmt::join(locales, ", ")); for (const auto &locale : locales) { LogVerbose("Trying to load translation: {}", locale); if (HasTranslation(locale)) { LogVerbose("Best match locale: {}", locale); CopyUtf8(szCode, locale, sizeof(szCode)); return; } } LogVerbose("No suitable translation found"); strcpy(szCode, "en"); } void OptionEntryLanguageCode::SaveToIni(std::string_view category) const { ini->set(category, key, szCode); } void OptionEntryLanguageCode::CheckLanguagesAreInitialized() const { if (!languages.empty()) return; const bool haveExtraFonts = HaveExtraFonts(); // Add well-known supported languages languages.emplace_back("da", "Dansk"); languages.emplace_back("de", "Deutsch"); languages.emplace_back("et", "Eesti"); languages.emplace_back("en", "English"); languages.emplace_back("es", "Español"); languages.emplace_back("fr", "Français"); languages.emplace_back("hr", "Hrvatski"); languages.emplace_back("it", "Italiano"); languages.emplace_back("hu", "Magyar"); languages.emplace_back("pl", "Polski"); languages.emplace_back("pt_BR", "Português do Brasil"); languages.emplace_back("ro", "Română"); languages.emplace_back("fi", "Suomi"); languages.emplace_back("sv", "Svenska"); languages.emplace_back("tr", "Türkçe"); languages.emplace_back("cs", "Čeština"); languages.emplace_back("el", "Ελληνικά"); languages.emplace_back("be", "беларуская"); languages.emplace_back("bg", "Български"); languages.emplace_back("ru", "Русский"); languages.emplace_back("uk", "Українська"); if (haveExtraFonts) { languages.emplace_back("ja", "日本語"); languages.emplace_back("ko", "한국어"); languages.emplace_back("zh_CN", "汉语"); languages.emplace_back("zh_TW", "漢語"); } // Ensures that the ini specified language is present in languages list even if unknown (for example if someone starts to translate a new language) if (c_find_if(languages, [this](const auto &x) { return x.first == this->szCode; }) == languages.end()) { languages.emplace_back(szCode, szCode); } } size_t OptionEntryLanguageCode::GetListSize() const { CheckLanguagesAreInitialized(); return languages.size(); } std::string_view OptionEntryLanguageCode::GetListDescription(size_t index) const { CheckLanguagesAreInitialized(); return languages[index].second; } size_t OptionEntryLanguageCode::GetActiveListIndex() const { CheckLanguagesAreInitialized(); auto found = c_find_if(languages, [this](const auto &x) { return x.first == this->szCode; }); if (found == languages.end()) return 0; return std::distance(languages.begin(), found); } void OptionEntryLanguageCode::SetActiveListIndex(size_t index) { CopyUtf8(szCode, languages[index].first, sizeof(szCode)); NotifyValueChanged(); } LanguageOptions::LanguageOptions() : OptionCategoryBase("Language", N_("Language"), N_("Language Settings")) { } std::vector LanguageOptions::GetEntries() { return { &code, }; } KeymapperOptions::KeymapperOptions() : OptionCategoryBase("Keymapping", N_("Keymapping"), N_("Keymapping Settings")) { // Insert all supported keys: a-z, 0-9 and F1-F24. keyIDToKeyName.reserve(('Z' - 'A' + 1) + ('9' - '0' + 1) + 12); for (char c = 'A'; c <= 'Z'; ++c) { keyIDToKeyName.emplace(c, std::string(1, c)); } for (char c = '0'; c <= '9'; ++c) { keyIDToKeyName.emplace(c, std::string(1, c)); } for (int i = 0; i < 12; ++i) { keyIDToKeyName.emplace(SDLK_F1 + i, StrCat("F", i + 1)); } for (int i = 0; i < 12; ++i) { keyIDToKeyName.emplace(SDLK_F13 + i, StrCat("F", i + 13)); } keyIDToKeyName.emplace(SDLK_KP_0, "KEYPADNUM 0"); for (int i = 0; i < 9; i++) { keyIDToKeyName.emplace(SDLK_KP_1 + i, StrCat("KEYPADNUM ", i + 1)); } keyIDToKeyName.emplace(SDLK_LALT, "LALT"); keyIDToKeyName.emplace(SDLK_RALT, "RALT"); keyIDToKeyName.emplace(SDLK_SPACE, "SPACE"); keyIDToKeyName.emplace(SDLK_RCTRL, "RCONTROL"); keyIDToKeyName.emplace(SDLK_LCTRL, "LCONTROL"); keyIDToKeyName.emplace(SDLK_PRINTSCREEN, "PRINT"); keyIDToKeyName.emplace(SDLK_PAUSE, "PAUSE"); keyIDToKeyName.emplace(SDLK_TAB, "TAB"); keyIDToKeyName.emplace(SDL_BUTTON_MIDDLE | KeymapperMouseButtonMask, "MMOUSE"); keyIDToKeyName.emplace(SDL_BUTTON_X1 | KeymapperMouseButtonMask, "X1MOUSE"); keyIDToKeyName.emplace(SDL_BUTTON_X2 | KeymapperMouseButtonMask, "X2MOUSE"); keyIDToKeyName.emplace(MouseScrollUpButton, "SCROLLUPMOUSE"); keyIDToKeyName.emplace(MouseScrollDownButton, "SCROLLDOWNMOUSE"); keyIDToKeyName.emplace(MouseScrollLeftButton, "SCROLLLEFTMOUSE"); keyIDToKeyName.emplace(MouseScrollRightButton, "SCROLLRIGHTMOUSE"); keyIDToKeyName.emplace(SDLK_GRAVE, "`"); keyIDToKeyName.emplace(SDLK_LEFTBRACKET, "["); keyIDToKeyName.emplace(SDLK_RIGHTBRACKET, "]"); keyIDToKeyName.emplace(SDLK_BACKSLASH, "\\"); keyIDToKeyName.emplace(SDLK_SEMICOLON, ";"); keyIDToKeyName.emplace(SDLK_APOSTROPHE, "'"); keyIDToKeyName.emplace(SDLK_COMMA, ","); keyIDToKeyName.emplace(SDLK_PERIOD, "."); keyIDToKeyName.emplace(SDLK_SLASH, "/"); keyIDToKeyName.emplace(SDLK_BACKSPACE, "BACKSPACE"); keyIDToKeyName.emplace(SDLK_CAPSLOCK, "CAPSLOCK"); keyIDToKeyName.emplace(SDLK_SCROLLLOCK, "SCROLLLOCK"); keyIDToKeyName.emplace(SDLK_INSERT, "INSERT"); keyIDToKeyName.emplace(SDLK_DELETE, "DELETE"); keyIDToKeyName.emplace(SDLK_HOME, "HOME"); keyIDToKeyName.emplace(SDLK_END, "END"); keyIDToKeyName.emplace(SDLK_KP_DIVIDE, "KEYPAD /"); keyIDToKeyName.emplace(SDLK_KP_MULTIPLY, "KEYPAD *"); keyIDToKeyName.emplace(SDLK_KP_ENTER, "KEYPAD ENTER"); keyIDToKeyName.emplace(SDLK_KP_PERIOD, "KEYPAD DECIMAL"); keyNameToKeyID.reserve(keyIDToKeyName.size()); for (const auto &[key, value] : keyIDToKeyName) { keyNameToKeyID.emplace(value, key); } } std::vector KeymapperOptions::GetEntries() { std::vector entries; for (Action &action : actions) { entries.push_back(&action); } return entries; } KeymapperOptions::Action::Action(std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) : OptionEntryBase(key, OptionEntryFlags::None, name, description) , actionPressed(std::move(actionPressed)) , actionReleased(std::move(actionReleased)) , defaultKey(defaultKey) , enable(std::move(enable)) , dynamicIndex(index) { if (index != 0) { dynamicKey = fmt::format(fmt::runtime(std::string_view(key.data(), key.size())), index); this->key = dynamicKey; } } std::string_view KeymapperOptions::Action::GetName() const { if (dynamicIndex == 0) return _(name); dynamicName = fmt::format(fmt::runtime(_(name)), dynamicIndex); return dynamicName; } void KeymapperOptions::Action::LoadFromIni(std::string_view category) { const std::span iniValues = ini->get(category, key); if (iniValues.empty()) { SetValue(defaultKey); return; // Use the default key if no key has been set. } const std::string_view iniValue = iniValues.back().value; if (iniValue.empty()) { SetValue(SDLK_UNKNOWN); return; } auto keyIt = GetOptions().Keymapper.keyNameToKeyID.find(iniValue); if (keyIt == GetOptions().Keymapper.keyNameToKeyID.end()) { // Use the default key if the key is unknown. Log("Keymapper: unknown key '{}'", iniValue); SetValue(defaultKey); return; } // Store the key in action.key and in the map so we can save() the // actions while keeping the same order as they have been added. SetValue(keyIt->second); } void KeymapperOptions::Action::SaveToIni(std::string_view category) const { if (boundKey == SDLK_UNKNOWN) { // Just add an empty config entry if the action is unbound. ini->set(category, key, std::string {}); return; } auto keyNameIt = GetOptions().Keymapper.keyIDToKeyName.find(boundKey); if (keyNameIt == GetOptions().Keymapper.keyIDToKeyName.end()) { LogVerbose("Keymapper: no name found for key {} bound to {}", boundKey, key); return; } ini->set(category, key, keyNameIt->second); } std::string_view KeymapperOptions::Action::GetValueDescription() const { if (boundKey == SDLK_UNKNOWN) return ""; auto keyNameIt = GetOptions().Keymapper.keyIDToKeyName.find(boundKey); if (keyNameIt == GetOptions().Keymapper.keyIDToKeyName.end()) { return ""; } return keyNameIt->second; } bool KeymapperOptions::Action::SetValue(int value) { if (value != SDLK_UNKNOWN && GetOptions().Keymapper.keyIDToKeyName.find(value) == GetOptions().Keymapper.keyIDToKeyName.end()) { // Ignore invalid key values return false; } // Remove old key if (boundKey != SDLK_UNKNOWN) { GetOptions().Keymapper.keyIDToAction.erase(boundKey); boundKey = SDLK_UNKNOWN; } // Add new key if (value != SDLK_UNKNOWN) { auto it = GetOptions().Keymapper.keyIDToAction.find(value); if (it != GetOptions().Keymapper.keyIDToAction.end()) { // Warn about overwriting keys. Log("Keymapper: key '{}' is already bound to action '{}', overwriting", value, it->second.get().name); it->second.get().boundKey = SDLK_UNKNOWN; } GetOptions().Keymapper.keyIDToAction.insert_or_assign(value, *this); boundKey = value; } return true; } void KeymapperOptions::AddAction(std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) { actions.emplace_front(key, name, description, defaultKey, std::move(actionPressed), std::move(actionReleased), std::move(enable), index); } void KeymapperOptions::CommitActions() { actions.reverse(); } const KeymapperOptions::Action *KeymapperOptions::findAction(uint32_t key) const { auto it = keyIDToAction.find(key); if (it == keyIDToAction.end()) return nullptr; return &it->second.get(); } std::string_view KeymapperOptions::KeyNameForAction(std::string_view actionName) const { for (const Action &action : actions) { if (action.key == actionName && action.boundKey != SDLK_UNKNOWN) { return action.GetValueDescription(); } } return ""; } uint32_t KeymapperOptions::KeyForAction(std::string_view actionName) const { for (const Action &action : actions) { if (action.key == actionName && action.boundKey != SDLK_UNKNOWN) { return action.boundKey; } } return SDLK_UNKNOWN; } PadmapperOptions::PadmapperOptions() : OptionCategoryBase("Padmapping", N_("Padmapping"), N_("Padmapping Settings")) , buttonToButtonName { { /*ControllerButton_NONE*/ {}, /*ControllerButton_IGNORE*/ {}, /*ControllerButton_AXIS_TRIGGERLEFT*/ "LT", /*ControllerButton_AXIS_TRIGGERRIGHT*/ "RT", /*ControllerButton_BUTTON_A*/ "A", /*ControllerButton_BUTTON_B*/ "B", /*ControllerButton_BUTTON_X*/ "X", /*ControllerButton_BUTTON_Y*/ "Y", /*ControllerButton_BUTTON_LEFTSTICK*/ "LS", /*ControllerButton_BUTTON_RIGHTSTICK*/ "RS", /*ControllerButton_BUTTON_LEFTSHOULDER*/ "LB", /*ControllerButton_BUTTON_RIGHTSHOULDER*/ "RB", /*ControllerButton_BUTTON_START*/ "Start", /*ControllerButton_BUTTON_BACK*/ "Select", /*ControllerButton_BUTTON_DPAD_UP*/ "Up", /*ControllerButton_BUTTON_DPAD_DOWN*/ "Down", /*ControllerButton_BUTTON_DPAD_LEFT*/ "Left", /*ControllerButton_BUTTON_DPAD_RIGHT*/ "Right", } } { buttonNameToButton.reserve(buttonToButtonName.size()); for (size_t i = 0; i < buttonToButtonName.size(); ++i) { buttonNameToButton.emplace(buttonToButtonName[i], static_cast(i)); } } std::vector PadmapperOptions::GetEntries() { std::vector entries; for (Action &action : actions) { entries.push_back(&action); } return entries; } PadmapperOptions::Action::Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) : OptionEntryBase(key, OptionEntryFlags::None, name, description) , actionPressed(std::move(actionPressed)) , actionReleased(std::move(actionReleased)) , defaultInput(defaultInput) , enable(std::move(enable)) , dynamicIndex(index) { if (index != 0) { dynamicKey = fmt::format(fmt::runtime(std::string_view(key.data(), key.size())), index); this->key = dynamicKey; } } std::string_view PadmapperOptions::Action::GetName() const { if (dynamicIndex == 0) return _(name); dynamicName = fmt::format(fmt::runtime(_(name)), dynamicIndex); return dynamicName; } void PadmapperOptions::Action::LoadFromIni(std::string_view category) { const std::span iniValues = ini->get(category, key); if (iniValues.empty()) { SetValue(defaultInput); return; // Use the default button combo if no mapping has been set. } const std::string_view iniValue = iniValues.back().value; std::string modName; std::string buttonName; auto parts = SplitByChar(iniValue, '+'); auto it = parts.begin(); if (it == parts.end()) { SetValue(ControllerButtonCombo {}); return; } buttonName = std::string(*it); if (++it != parts.end()) { modName = std::move(buttonName); buttonName = std::string(*it); } ControllerButtonCombo input {}; if (!modName.empty()) { auto modifierIt = GetOptions().Padmapper.buttonNameToButton.find(modName); if (modifierIt == GetOptions().Padmapper.buttonNameToButton.end()) { // Use the default button combo if the modifier name is unknown. LogWarn("Padmapper: unknown button '{}'", modName); SetValue(defaultInput); return; } input.modifier = modifierIt->second; } auto buttonIt = GetOptions().Padmapper.buttonNameToButton.find(buttonName); if (buttonIt == GetOptions().Padmapper.buttonNameToButton.end()) { // Use the default button combo if the button name is unknown. LogWarn("Padmapper: unknown button '{}'", buttonName); SetValue(defaultInput); return; } input.button = buttonIt->second; // Store the input in action.boundInput and in the map so we can save() // the actions while keeping the same order as they have been added. SetValue(input); } void PadmapperOptions::Action::SaveToIni(std::string_view category) const { if (boundInput.button == ControllerButton_NONE) { // Just add an empty config entry if the action is unbound. ini->set(category, key, ""); return; } std::string inputName = GetOptions().Padmapper.buttonToButtonName[static_cast(boundInput.button)]; if (inputName.empty()) { LogVerbose("Padmapper: no name found for button {} bound to {}", static_cast(boundInput.button), key); return; } if (boundInput.modifier != ControllerButton_NONE) { const std::string &modifierName = GetOptions().Padmapper.buttonToButtonName[static_cast(boundInput.modifier)]; if (modifierName.empty()) { LogVerbose("Padmapper: no name found for modifier button {} bound to {}", static_cast(boundInput.button), key); return; } inputName = StrCat(modifierName, "+", inputName); } ini->set(category, key, inputName.data()); } void PadmapperOptions::Action::UpdateValueDescription() const { boundInputDescriptionType = GamepadType; if (boundInput.button == ControllerButton_NONE) { boundInputDescription = ""; boundInputShortDescription = ""; return; } const std::string_view buttonName = ToString(GamepadType, boundInput.button); if (boundInput.modifier == ControllerButton_NONE) { boundInputDescription = std::string(buttonName); boundInputShortDescription = std::string(Shorten(buttonName)); return; } const std::string_view modifierName = ToString(GamepadType, boundInput.modifier); boundInputDescription = StrCat(modifierName, "+", buttonName); boundInputShortDescription = StrCat(Shorten(modifierName), "+", Shorten(buttonName)); } std::string_view PadmapperOptions::Action::Shorten(std::string_view buttonName) const { size_t index = 0; size_t chars = 0; while (index < buttonName.size()) { if (!IsTrailUtf8CodeUnit(buttonName[index])) chars++; if (chars == 3) break; index++; } return std::string_view(buttonName.data(), index); } std::string_view PadmapperOptions::Action::GetValueDescription() const { return GetValueDescription(false); } std::string_view PadmapperOptions::Action::GetValueDescription(bool useShortName) const { if (GamepadType != boundInputDescriptionType) UpdateValueDescription(); return useShortName ? boundInputShortDescription : boundInputDescription; } bool PadmapperOptions::Action::SetValue(ControllerButtonCombo value) { if (boundInput.button != ControllerButton_NONE) boundInput = {}; if (value.button != ControllerButton_NONE) boundInput = value; UpdateValueDescription(); return true; } void PadmapperOptions::AddAction(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index) { if (committed) return; actions.emplace_front(key, name, description, defaultInput, std::move(actionPressed), std::move(actionReleased), std::move(enable), index); } void PadmapperOptions::CommitActions() { if (committed) return; actions.reverse(); committed = true; } std::string_view PadmapperOptions::InputNameForAction(std::string_view actionName, bool useShortName) const { for (const Action &action : actions) { if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { return action.GetValueDescription(useShortName); } } return ""; } ControllerButtonCombo PadmapperOptions::ButtonComboForAction(std::string_view actionName) const { for (const auto &action : actions) { if (action.key == actionName && action.boundInput.button != ControllerButton_NONE) { return action.boundInput; } } return ControllerButton_NONE; } const PadmapperOptions::Action *PadmapperOptions::findAction(ControllerButton button, tl::function_ref isModifierPressed) const { // To give preference to button combinations, // first pass ignores mappings where no modifier is bound for (const Action &action : actions) { const ControllerButtonCombo combo = action.boundInput; if (combo.modifier == ControllerButton_NONE) continue; if (button != combo.button) continue; if (!isModifierPressed(combo.modifier)) continue; if (action.enable && !action.enable()) continue; return &action; } for (const Action &action : actions) { const ControllerButtonCombo combo = action.boundInput; if (combo.modifier != ControllerButton_NONE) continue; if (button != combo.button) continue; if (action.enable && !action.enable()) continue; return &action; } return nullptr; } ModOptions::ModOptions() : OptionCategoryBase("Mods", N_("Mods"), N_("Mod Settings")) { } std::vector ModOptions::GetActiveModList() { std::vector modList; for (auto &modEntry : GetModEntries()) { if (*modEntry.enabled) modList.emplace_back(modEntry.name); } return modList; } std::vector ModOptions::GetModList() { std::vector modList; for (auto &modEntry : GetModEntries()) { modList.emplace_back(modEntry.name); } return modList; } std::vector ModOptions::GetEntries() { std::vector optionEntries; for (auto &modEntry : GetModEntries()) { optionEntries.emplace_back(&modEntry.enabled); } return optionEntries; } void ModOptions::AddModEntry(const std::string &modName) { auto &entries = GetModEntries(); entries.emplace_front(modName); } void ModOptions::RemoveModEntry(const std::string &modName) { if (!modEntries) { return; } auto &entries = *modEntries; entries.remove_if([&](const ModEntry &entry) { return entry.name == modName; }); } void ModOptions::SetHellfireEnabled(bool enableHellfire) { for (auto &modEntry : GetModEntries()) { if (modEntry.name == "Hellfire") { modEntry.enabled.SetValue(enableHellfire); break; } } } std::forward_list &ModOptions::GetModEntries() { if (modEntries) return *modEntries; const std::vector modNames = ini->getKeys(key); std::forward_list &newModEntries = modEntries.emplace(); for (auto &modName : modNames) { newModEntries.emplace_front(modName); } newModEntries.reverse(); return newModEntries; } ModOptions::ModEntry::ModEntry(std::string_view name) : name(name) , enabled(this->name, OptionEntryFlags::RecreateUI, this->name.c_str(), "", false) { } namespace { #ifdef DEVILUTIONX_RESAMPLER_SPEEX constexpr char ResamplerSpeex[] = "Speex"; #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER constexpr char ResamplerSDL[] = "SDL"; #endif } // namespace std::string_view ResamplerToString(Resampler resampler) { switch (resampler) { #ifdef DEVILUTIONX_RESAMPLER_SPEEX case Resampler::Speex: return ResamplerSpeex; #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER case Resampler::SDL: return ResamplerSDL; #endif default: return ""; } } std::optional ResamplerFromString(std::string_view resampler) { #ifdef DEVILUTIONX_RESAMPLER_SPEEX if (resampler == ResamplerSpeex) return Resampler::Speex; #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER if (resampler == ResamplerSDL) return Resampler::SDL; #endif return std::nullopt; } } // namespace devilution ================================================ FILE: Source/options.h ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #ifndef NOSOUND #include #endif #else #include #endif #include #include #include "appfat.h" #include "controls/controller_buttons.h" #include "engine/size.hpp" #include "engine/sound_defs.hpp" #include "pack.h" #include "quick_messages.hpp" #include "utils/enum_traits.h" #include "utils/string_view_hash.hpp" namespace devilution { #ifndef DEFAULT_WIDTH #define DEFAULT_WIDTH 640 #endif #ifndef DEFAULT_HEIGHT #define DEFAULT_HEIGHT 480 #endif enum class StartUpGameMode : uint8_t { /** @brief If hellfire is present, asks the user what game they want to start. */ Ask = 0, Hellfire = 1, Diablo = 2, }; enum class StartUpIntro : uint8_t { Off = 0, Once = 1, On = 2, }; /** @brief Defines what splash screen should be shown at startup. */ enum class StartUpSplash : uint8_t { /** @brief Show no splash screen. */ None = 0, /** @brief Show only TitleDialog. */ TitleDialog = 1, /** @brief Show Logo and TitleDialog. */ LogoAndTitleDialog = 2, }; enum class ScalingQuality : uint8_t { NearestPixel, BilinearFiltering, AnisotropicFiltering, }; enum class FrameRateControl : uint8_t { None = 0, #ifndef USE_SDL1 VerticalSync = 1, #endif CPUSleep = 2, }; enum class Resampler : uint8_t { #ifdef DEVILUTIONX_RESAMPLER_SPEEX Speex = 0, #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER SDL, #endif }; std::string_view ResamplerToString(Resampler resampler); std::optional ResamplerFromString(std::string_view resampler); enum class OptionEntryType : uint8_t { Boolean, List, Key, PadButton, }; enum class OptionEntryFlags : uint8_t { /** @brief No special logic. */ None = 0, /** @brief Shouldn't be shown in settings dialog. */ Invisible = 1 << 0, /** @brief Need to restart the current running game (single- or multiplayer) to take effect. */ CantChangeInGame = 1 << 1, /** @brief Need to restart the current running multiplayer game to take effect. */ CantChangeInMultiPlayer = 1 << 2, /** @brief Option is only relevant for Hellfire. */ OnlyHellfire = 1 << 3, /** @brief Option is only relevant for Diablo. */ OnlyDiablo = 1 << 4, /** @brief After option is changed the UI needs to be recreated. */ RecreateUI = 1 << 5, /** @brief diablo.mpq must be present. */ NeedDiabloMpq = 1 << 6, }; use_enum_as_flags(OptionEntryFlags); class OptionEntryBase { public: OptionEntryBase(std::string_view key, OptionEntryFlags flags, const char *name, const char *description) : flags(flags) , key(key) , name(name) , description(description) { } [[nodiscard]] virtual std::string_view GetName() const; [[nodiscard]] std::string_view GetDescription() const; [[nodiscard]] virtual OptionEntryType GetType() const = 0; [[nodiscard]] OptionEntryFlags GetFlags() const; void SetValueChangedCallback(tl::function_ref callback); [[nodiscard]] virtual std::string_view GetValueDescription() const = 0; virtual void LoadFromIni(std::string_view category) = 0; virtual void SaveToIni(std::string_view category) const = 0; OptionEntryFlags flags; public: std::string_view key; protected: const char *name; const char *description; void NotifyValueChanged(); private: std::optional> callback_; }; class OptionEntryBoolean : public OptionEntryBase { public: OptionEntryBoolean(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, bool defaultValue) : OptionEntryBase(key, flags, name, description) , defaultValue(defaultValue) , value(defaultValue) { } [[nodiscard]] bool operator*() const { return value; } void SetValue(bool value); [[nodiscard]] OptionEntryType GetType() const override; [[nodiscard]] std::string_view GetValueDescription() const override; void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; private: bool defaultValue; bool value; }; class OptionEntryListBase : public OptionEntryBase { public: [[nodiscard]] virtual size_t GetListSize() const = 0; [[nodiscard]] virtual std::string_view GetListDescription(size_t index) const = 0; [[nodiscard]] virtual size_t GetActiveListIndex() const = 0; virtual void SetActiveListIndex(size_t index) = 0; [[nodiscard]] OptionEntryType GetType() const override; [[nodiscard]] std::string_view GetValueDescription() const override; protected: OptionEntryListBase(std::string_view key, OptionEntryFlags flags, const char *name, const char *description) : OptionEntryBase(key, flags, name, description) { } }; class OptionEntryEnumBase : public OptionEntryListBase { public: void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; protected: OptionEntryEnumBase(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, int defaultValue) : OptionEntryListBase(key, flags, name, description) , defaultValue(defaultValue) , value(defaultValue) { } [[nodiscard]] int GetValueInternal() const { return value; } void SetValueInternal(int value); void AddEntry(int value, std::string_view name); private: int defaultValue; int value; std::vector entryNames; std::vector entryValues; }; template class OptionEntryEnum : public OptionEntryEnumBase { public: OptionEntryEnum(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, T defaultValue, std::initializer_list> entries) : OptionEntryEnumBase(key, flags, name, description, static_cast(defaultValue)) { for (auto &&[entryValue, entryName] : entries) { AddEntry(static_cast(entryValue), entryName); } } [[nodiscard]] T operator*() const { return static_cast(GetValueInternal()); } void SetValue(T value) { SetValueInternal(static_cast(value)); } }; class OptionEntryIntBase : public OptionEntryListBase { public: void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; protected: OptionEntryIntBase(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, int defaultValue) : OptionEntryListBase(key, flags, name, description) , defaultValue(defaultValue) , value(defaultValue) { } [[nodiscard]] int GetValueInternal() const { return value; } void SetValueInternal(int value); void AddEntry(int value); private: int defaultValue; int value; mutable std::vector entryNames; std::vector entryValues; }; template class OptionEntryInt : public OptionEntryIntBase { public: OptionEntryInt(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, T defaultValue, std::initializer_list entries) : OptionEntryIntBase(key, flags, name, description, static_cast(defaultValue)) { for (auto entry : entries) { AddEntry(static_cast(entry)); } } OptionEntryInt(std::string_view key, OptionEntryFlags flags, const char *name, const char *description, T defaultValue) : OptionEntryInt(key, flags, name, description, defaultValue, { defaultValue }) { } [[nodiscard]] T operator*() const { return static_cast(GetValueInternal()); } void SetValue(T value) { SetValueInternal(static_cast(value)); } }; class OptionEntryLanguageCode : public OptionEntryListBase { public: OptionEntryLanguageCode(); void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; std::string_view operator*() const { return szCode; } OptionEntryLanguageCode &operator=(std::string_view code) { assert(code.size() < 6); memcpy(szCode, code.data(), code.size()); szCode[code.size()] = '\0'; return *this; } private: /** @brief Language code (ISO-15897) for text. */ char szCode[6]; mutable std::vector> languages; void CheckLanguagesAreInitialized() const; }; class OptionEntryResolution : public OptionEntryListBase { public: OptionEntryResolution(); void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; void setAvailableResolutions(std::vector> &&resolutions) { resolutions_ = std::move(resolutions); } Size operator*() const { return size_; } private: /** @brief View size. */ Size size_; std::vector> resolutions_; }; class OptionEntryResampler : public OptionEntryListBase { public: OptionEntryResampler(); void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; Resampler operator*() const { return resampler_; } private: void UpdateDependentOptions() const; Resampler resampler_; }; class OptionEntryAudioDevice : public OptionEntryListBase { public: OptionEntryAudioDevice(); void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] size_t GetListSize() const override; [[nodiscard]] std::string_view GetListDescription(size_t index) const override; [[nodiscard]] size_t GetActiveListIndex() const override; void SetActiveListIndex(size_t index) override; std::string operator*() const { for (size_t i = 0; i < GetListSize(); i++) { std::string_view deviceName = GetDeviceName(i); if (deviceName == deviceName_) return deviceName_; } return ""; } #ifdef USE_SDL3 [[nodiscard]] SDL_AudioDeviceID id() const; #endif private: std::string_view GetDeviceName(size_t index) const; std::string deviceName_; #ifdef USE_SDL3 SDL_AudioDeviceID deviceId_ = SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK; #endif }; struct OptionCategoryBase { OptionCategoryBase(std::string_view key, const char *name, const char *description) : key(key) , name(name) , description(description) { } [[nodiscard]] std::string_view GetKey() const; [[nodiscard]] std::string_view GetName() const; [[nodiscard]] std::string_view GetDescription() const; virtual std::vector GetEntries() = 0; protected: std::string_view key; const char *name; const char *description; }; struct GameModeOptions : OptionCategoryBase { GameModeOptions(); std::vector GetEntries() override; OptionEntryEnum gameMode; OptionEntryBoolean shareware; }; struct StartUpOptions : OptionCategoryBase { StartUpOptions(); std::vector GetEntries() override; /** @brief Play game intro video on diablo startup. */ OptionEntryEnum diabloIntro; /** @brief Play game intro video on hellfire startup. */ OptionEntryEnum hellfireIntro; OptionEntryEnum splash; }; struct DiabloOptions : OptionCategoryBase { DiabloOptions(); std::vector GetEntries() override; /** @brief Remembers what singleplayer hero/save was last used. */ OptionEntryInt lastSinglePlayerHero; /** @brief Remembers what multiplayer hero/save was last used. */ OptionEntryInt lastMultiplayerHero; }; struct HellfireOptions : OptionCategoryBase { HellfireOptions(); std::vector GetEntries() override; /** @brief Cornerstone of the world item. */ char szItem[sizeof(ItemPack) * 2 + 1]; /** @brief Remembers what singleplayer hero/save was last used. */ OptionEntryInt lastSinglePlayerHero; /** @brief Remembers what multiplayer hero/save was last used. */ OptionEntryInt lastMultiplayerHero; }; struct AudioOptions : OptionCategoryBase { AudioOptions(); std::vector GetEntries() override; /** @brief Movie and SFX volume. */ OptionEntryInt soundVolume; /** @brief Accessibility / navigation cues volume. */ OptionEntryInt audioCuesVolume; /** @brief Music volume. */ OptionEntryInt musicVolume; /** @brief Player emits sound when walking. */ OptionEntryBoolean walkingSound; /** @brief Automatically equipping items on pickup emits the equipment sound. */ OptionEntryBoolean autoEquipSound; /** @brief Picking up items emits the items pickup sound. */ OptionEntryBoolean itemPickupSound; /** @brief Output sample rate (Hz). */ OptionEntryInt sampleRate; /** @brief The number of output channels (1 or 2) */ OptionEntryInt channels; /** @brief Buffer size (number of frames per channel) */ OptionEntryInt bufferSize; /** @brief Resampler implementation. */ OptionEntryResampler resampler; /** @brief Quality of the resampler, from 0 (lowest) to 10 (highest). Available for the speex resampler only. */ OptionEntryInt resamplingQuality; /** @brief Audio device. */ OptionEntryAudioDevice device; }; struct GraphicsOptions : OptionCategoryBase { GraphicsOptions(); std::vector GetEntries() override; OptionEntryResolution resolution; /** @brief Run in fullscreen or windowed mode. */ OptionEntryBoolean fullscreen; #if !defined(USE_SDL1) || defined(__3DS__) /** @brief Expand the aspect ratio to match the screen. */ OptionEntryBoolean fitToScreen; #endif #ifndef USE_SDL1 /** @brief Scale the image after rendering. */ OptionEntryBoolean upscale; /** @brief See SDL_HINT_RENDER_SCALE_QUALITY. */ OptionEntryEnum scaleQuality; /** @brief Only scale by values divisible by the width and height. */ OptionEntryBoolean integerScaling; #endif /** @brief Limit frame rate either for vsync or CPU load. */ OptionEntryEnum frameRateControl; /** @brief Brightness level. */ OptionEntryInt brightness; /** @brief Zoom on start. */ OptionEntryBoolean zoom; /** @brief Subtile lighting for smoother light gradients. */ OptionEntryBoolean perPixelLighting; /** @brief Enable color cycling animations. */ OptionEntryBoolean colorCycling; /** @brief Use alternate nest palette. */ OptionEntryBoolean alternateNestArt; #if SDL_VERSION_ATLEAST(2, 0, 0) /** @brief Use a hardware cursor (SDL2 only). */ OptionEntryBoolean hardwareCursor; /** @brief Use a hardware cursor for items. */ OptionEntryBoolean hardwareCursorForItems; /** @brief Maximum width / height for the hardware cursor. Larger cursors fall back to software. */ OptionEntryInt hardwareCursorMaxSize; #endif /** @brief Show FPS, even without the -f command line flag. */ OptionEntryBoolean showFPS; }; struct GameplayOptions : OptionCategoryBase { GameplayOptions(); std::vector GetEntries() override; /** @brief Gameplay ticks per second. */ OptionEntryInt tickRate; /** @brief Enable double walk speed when in town. */ OptionEntryBoolean runInTown; /** @brief Do not let the mouse leave the application window. */ OptionEntryBoolean grabInput; /** @brief Pause the game when focus is lost. */ OptionEntryBoolean pauseOnFocusLoss; /** @brief Enable the Theo quest. */ OptionEntryBoolean theoQuest; /** @brief Enable the cow quest. */ OptionEntryBoolean cowQuest; /** @brief Will players still damage other players in non-PvP mode. */ OptionEntryBoolean friendlyFire; /** @brief Enables the full/uncut singleplayer version of quests. */ OptionEntryBoolean multiplayerFullQuests; /** @brief Enable the bard hero class. */ OptionEntryBoolean testBard; /** @brief Enable the babarian hero class. */ OptionEntryBoolean testBarbarian; /** @brief Show the current level progress. */ OptionEntryBoolean experienceBar; /** @brief Show item graphics to the left of item descriptions in store menus. */ OptionEntryBoolean showItemGraphicsInStores; /** @brief Display current/max health values on health globe. */ OptionEntryBoolean showHealthValues; /** @brief Display current/max mana values on mana globe. */ OptionEntryBoolean showManaValues; /** @brief Enable the multiplayer party information display */ OptionEntryBoolean showMultiplayerPartyInfo; /** @brief Show enemy health at the top of the screen. */ OptionEntryBoolean enemyHealthBar; /** @brief Displays item info in a floating box when hovering over an ite. */ OptionEntryBoolean floatingInfoBox; /** @brief Automatically pick up gold when walking over it. */ OptionEntryBoolean autoGoldPickup; /** @brief Auto-pickup elixirs */ OptionEntryBoolean autoElixirPickup; /** @brief Auto-pickup oils */ OptionEntryBoolean autoOilPickup; /** @brief Enable or Disable auto-pickup in town */ OptionEntryBoolean autoPickupInTown; /** @brief Automatically attempt to equip weapon-type items when picking them up. */ OptionEntryBoolean autoEquipWeapons; /** @brief Automatically attempt to equip armor-type items when picking them up. */ OptionEntryBoolean autoEquipArmor; /** @brief Automatically attempt to equip helm-type items when picking them up. */ OptionEntryBoolean autoEquipHelms; /** @brief Automatically attempt to equip shield-type items when picking them up. */ OptionEntryBoolean autoEquipShields; /** @brief Automatically attempt to equip jewelry-type items when picking them up. */ OptionEntryBoolean autoEquipJewelry; /** @brief Only enable 2/3 quests in each game session */ OptionEntryBoolean randomizeQuests; /** @brief Indicates whether or not monster type (Animal, Demon, Undead) is shown along with other monster information. */ OptionEntryBoolean showMonsterType; /** @brief Displays item labels for items on the ground. */ OptionEntryBoolean showItemLabels; /** @brief Refill belt from inventory, or rather, use potions/scrolls from inventory first when belt item is consumed. */ OptionEntryBoolean autoRefillBelt; /** @brief Locally disable clicking on shrines which permanently cripple character. */ OptionEntryBoolean disableCripplingShrines; /** @brief Spell hotkeys instantly cast the spell. */ OptionEntryBoolean quickCast; /** @brief Number of Healing potions to pick up automatically */ OptionEntryInt numHealPotionPickup; /** @brief Number of Full Healing potions to pick up automatically */ OptionEntryInt numFullHealPotionPickup; /** @brief Number of Mana potions to pick up automatically */ OptionEntryInt numManaPotionPickup; /** @brief Number of Full Mana potions to pick up automatically */ OptionEntryInt numFullManaPotionPickup; /** @brief Number of Rejuvenating potions to pick up automatically */ OptionEntryInt numRejuPotionPickup; /** @brief Number of Full Rejuvenating potions to pick up automatically */ OptionEntryInt numFullRejuPotionPickup; /** * @brief If loading takes less than this value, skips displaying the loading screen. * * Advanced option, not displayed in the UI. */ OptionEntryInt skipLoadingScreenThresholdMs; }; struct ControllerOptions : OptionCategoryBase { ControllerOptions(); std::vector GetEntries() override; /** @brief SDL Controller mapping, see SDL_GameControllerDB. */ char szMapping[1024]; /** @brief Configure gamepad joysticks deadzone */ float fDeadzone; #ifdef __vita__ /** @brief Enable input via rear touchpad */ bool bRearTouch; #endif }; struct NetworkOptions : OptionCategoryBase { NetworkOptions(); std::vector GetEntries() override; /** @brief Optionally bind to a specific network interface. */ char szBindAddress[129]; /** @brief Most recently entered ZeroTier Game ID. */ char szPreviousZTGame[129]; /** @brief Most recently entered Hostname in join dialog. */ char szPreviousHost[129]; /** @brief What network port to use. */ OptionEntryInt port; }; struct ChatOptions : OptionCategoryBase { ChatOptions(); std::vector GetEntries() override; /** @brief Quick chat messages. */ std::vector szHotKeyMsgs[QuickMessages.size()]; }; struct LanguageOptions : OptionCategoryBase { LanguageOptions(); std::vector GetEntries() override; OptionEntryLanguageCode code; }; constexpr uint32_t KeymapperMouseButtonMask = 1 << 31; constexpr uint32_t MouseScrollUpButton = 65536 | KeymapperMouseButtonMask; constexpr uint32_t MouseScrollDownButton = 65537 | KeymapperMouseButtonMask; constexpr uint32_t MouseScrollLeftButton = 65538 | KeymapperMouseButtonMask; constexpr uint32_t MouseScrollRightButton = 65539 | KeymapperMouseButtonMask; /** The Keymapper maps keys to actions. */ struct KeymapperOptions : OptionCategoryBase { /** * Action represents an action that can be triggered using a keyboard * shortcut. */ class Action final : public OptionEntryBase { public: // OptionEntryBase::key may be referencing Action::dynamicKey. // The implicit copy constructor would copy that reference instead of referencing the copy. Action(const Action &) = delete; Action(std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index); [[nodiscard]] std::string_view GetName() const override; [[nodiscard]] OptionEntryType GetType() const override { return OptionEntryType::Key; } void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] std::string_view GetValueDescription() const override; bool SetValue(int value); [[nodiscard]] bool isEnabled() const { return !enable || enable(); } std::function actionPressed; std::function actionReleased; private: uint32_t defaultKey; std::function enable; uint32_t boundKey = SDLK_UNKNOWN; unsigned dynamicIndex; std::string dynamicKey; mutable std::string dynamicName; friend struct KeymapperOptions; }; KeymapperOptions(); std::vector GetEntries() override; void AddAction( std::string_view key, const char *name, const char *description, uint32_t defaultKey, std::function actionPressed, std::function actionReleased = nullptr, std::function enable = nullptr, unsigned index = 0); void CommitActions(); [[nodiscard]] const Action *findAction(uint32_t key) const; std::string_view KeyNameForAction(std::string_view actionName) const; uint32_t KeyForAction(std::string_view actionName) const; private: std::forward_list actions; ankerl::unordered_dense::segmented_map> keyIDToAction; ankerl::unordered_dense::segmented_map keyIDToKeyName; ankerl::unordered_dense::segmented_map keyNameToKeyID; }; /** The Padmapper maps gamepad buttons to actions. */ struct PadmapperOptions : OptionCategoryBase { /** * Action represents an action that can be triggered using a gamepad * button combo. */ class Action final : public OptionEntryBase { public: Action(std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased, std::function enable, unsigned index); // OptionEntryBase::key may be referencing Action::dynamicKey. // The implicit copy constructor would copy that reference instead of referencing the copy. Action(const Action &) = delete; [[nodiscard]] std::string_view GetName() const override; [[nodiscard]] OptionEntryType GetType() const override { return OptionEntryType::PadButton; } void LoadFromIni(std::string_view category) override; void SaveToIni(std::string_view category) const override; [[nodiscard]] std::string_view GetValueDescription() const override; [[nodiscard]] std::string_view GetValueDescription(bool useShortName) const; bool SetValue(ControllerButtonCombo value); [[nodiscard]] bool isEnabled() const { return !enable || enable(); } std::function actionPressed; std::function actionReleased; ControllerButtonCombo boundInput; private: ControllerButtonCombo defaultInput; std::function enable; mutable GamepadLayout boundInputDescriptionType = GamepadLayout::Generic; mutable std::string boundInputDescription; mutable std::string boundInputShortDescription; unsigned dynamicIndex; std::string dynamicKey; mutable std::string dynamicName; void UpdateValueDescription() const; std::string_view Shorten(std::string_view buttonName) const; friend struct PadmapperOptions; }; PadmapperOptions(); std::vector GetEntries() override; void AddAction( std::string_view key, const char *name, const char *description, ControllerButtonCombo defaultInput, std::function actionPressed, std::function actionReleased = nullptr, std::function enable = nullptr, unsigned index = 0); void CommitActions(); std::string_view InputNameForAction(std::string_view actionName, bool useShortName = false) const; ControllerButtonCombo ButtonComboForAction(std::string_view actionName) const; [[nodiscard]] const Action *findAction(ControllerButton button, tl::function_ref isModifierPressed) const; std::forward_list actions; private: std::array::value> buttonToButtonName; ankerl::unordered_dense::segmented_map buttonNameToButton; bool committed = false; }; struct ModOptions : OptionCategoryBase { ModOptions(); std::vector GetActiveModList(); std::vector GetModList(); std::vector GetEntries() override; void AddModEntry(const std::string &modName); void RemoveModEntry(const std::string &modName); void SetHellfireEnabled(bool enableHellfire); private: struct ModEntry { // OptionEntryBase::key references ModEntry::name. // The implicit copy constructor would copy that reference instead of referencing the copy. ModEntry(const ModEntry &) = delete; ModEntry(std::string_view name); std::string name; OptionEntryBoolean enabled; }; std::forward_list &GetModEntries(); std::optional> modEntries; }; struct Options { GameModeOptions GameMode; StartUpOptions StartUp; DiabloOptions Diablo; HellfireOptions Hellfire; AudioOptions Audio; GameplayOptions Gameplay; GraphicsOptions Graphics; ControllerOptions Controller; NetworkOptions Network; ChatOptions Chat; LanguageOptions Language; KeymapperOptions Keymapper; PadmapperOptions Padmapper; ModOptions Mods; [[nodiscard]] std::vector GetCategories() { return { &Language, &Mods, &GameMode, &StartUp, &Graphics, &Audio, &Diablo, &Hellfire, &Gameplay, &Controller, &Network, &Chat, &Keymapper, &Padmapper, }; } }; /** * @brief Get the Options singleton object */ [[nodiscard]] Options &GetOptions(); bool HardwareCursorSupported(); /** * @brief Save game configurations to ini file */ void SaveOptions(); /** * @brief Load game configurations from ini file */ void LoadOptions(); } // namespace devilution ================================================ FILE: Source/pack.cpp ================================================ /** * @file pack.cpp * * Implementation of functions for minifying player data structure. */ #include "pack.h" #include #include "engine/random.hpp" #include "game_mode.hpp" #include "items/validation.h" #include "loadsave.h" #include "plrmsg.h" #include "stores.h" #include "tables/playerdat.hpp" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/log.hpp" #include "utils/utf8.hpp" #define ValidateField(logValue, condition) \ do { \ if (!(condition)) { \ LogFailedJoinAttempt(#condition, #logValue, logValue); \ EventFailedJoinAttempt(player._pName); \ return false; \ } \ } while (0) #define ValidateFields(logValue1, logValue2, condition) \ do { \ if (!(condition)) { \ LogFailedJoinAttempt(#condition, #logValue1, logValue1, #logValue2, logValue2); \ EventFailedJoinAttempt(player._pName); \ return false; \ } \ } while (0) namespace devilution { namespace { void EventFailedJoinAttempt(const char *playerName) { const std::string message = fmt::format("Player '{}' sent invalid player data during attempt to join the game.", playerName); EventPlrMsg(message); } template void LogFailedJoinAttempt(const char *condition, const char *name, T value) { LogDebug("Remote player validation failed: ValidateField({}: {}, {})", name, value, condition); } template void LogFailedJoinAttempt(const char *condition, const char *name1, T1 value1, const char *name2, T2 value2) { LogDebug("Remote player validation failed: ValidateFields({}: {}, {}: {}, {})", name1, value1, name2, value2, condition); } void VerifyGoldSeeds(Player &player) { for (int i = 0; i < player._pNumInv; i++) { if (player.InvList[i].IDidx != IDI_GOLD) continue; for (int j = 0; j < player._pNumInv; j++) { if (i == j) continue; if (player.InvList[j].IDidx != IDI_GOLD) continue; if (player.InvList[i]._iSeed != player.InvList[j]._iSeed) continue; player.InvList[i]._iSeed = AdvanceRndSeed(); j = -1; } } } } // namespace bool RecreateHellfireSpellBook(const Player &player, const TItem &packedItem, Item *item) { Item spellBook {}; RecreateItem(player, packedItem, spellBook); // Hellfire uses the spell book level when generating items via CreateSpellBook() int spellBookLevel = GetSpellBookLevel(spellBook._iSpell); // CreateSpellBook() adds 1 to the spell level for ilvl spellBookLevel++; if (spellBookLevel >= 1 && (spellBook._iCreateInfo & CF_LEVEL) == spellBookLevel * 2) { // The ilvl matches the result for a spell book drop, so we confirm the item is legitimate if (item != nullptr) *item = spellBook; return true; } ValidateFields(spellBook._iCreateInfo, spellBook.dwBuff, IsDungeonItemValid(spellBook._iCreateInfo, spellBook.dwBuff)); if (item != nullptr) *item = spellBook; return true; } void PackItem(ItemPack &packedItem, const Item &item, bool isHellfire) { packedItem = {}; // Arena potions don't exist in vanilla so don't save them to stay backward compatible if (item.isEmpty() || item._iMiscId == IMISC_ARENAPOT) { packedItem.idx = 0xFFFF; } else { auto idx = item.IDidx; if (!isHellfire) { idx = RemapItemIdxToDiablo(idx); } if (gbIsSpawn) { idx = RemapItemIdxToSpawn(idx); } packedItem.idx = Swap16LE(idx); if (item.IDidx == IDI_EAR) { packedItem.iCreateInfo = Swap16LE(item._iIName[1] | (item._iIName[0] << 8)); packedItem.iSeed = Swap32LE(LoadBE32(&item._iIName[2])); packedItem.bId = item._iIName[6]; packedItem.bDur = item._iIName[7]; packedItem.bMDur = item._iIName[8]; packedItem.bCh = item._iIName[9]; packedItem.bMCh = item._iIName[10]; packedItem.wValue = Swap16LE(item._ivalue | (item._iIName[11] << 8) | ((item._iCurs - ICURS_EAR_SORCERER) << 6)); packedItem.dwBuff = Swap32LE(LoadBE32(&item._iIName[12])); } else { packedItem.iSeed = Swap32LE(item._iSeed); packedItem.iCreateInfo = Swap16LE(item._iCreateInfo); packedItem.bId = (item._iMagical << 1) | (item._iIdentified ? 1 : 0); if (item._iMaxDur > 255) packedItem.bMDur = 254; else packedItem.bMDur = item._iMaxDur; packedItem.bDur = std::min(item._iDurability, packedItem.bMDur); packedItem.bCh = item._iCharges; packedItem.bMCh = item._iMaxCharges; if (item.IDidx == IDI_GOLD) packedItem.wValue = Swap16LE(item._ivalue); packedItem.dwBuff = item.dwBuff; } } } void PackPlayer(PlayerPack &packed, const Player &player) { memset(&packed, 0, sizeof(packed)); packed.destAction = player.destAction; packed.destParam1 = player.destParam1; packed.destParam2 = player.destParam2; packed.plrlevel = player.plrlevel; packed.px = player.position.tile.x; packed.py = player.position.tile.y; if (gbVanilla) { packed.targx = player.position.tile.x; packed.targy = player.position.tile.y; } CopyUtf8(packed.pName, player._pName, sizeof(packed.pName)); packed.pClass = static_cast(player._pClass); packed.pBaseStr = player._pBaseStr; packed.pBaseMag = player._pBaseMag; packed.pBaseDex = player._pBaseDex; packed.pBaseVit = player._pBaseVit; packed.pLevel = player.getCharacterLevel(); packed.pStatPts = player._pStatPts; packed.pExperience = Swap32LE(player._pExperience); packed.pGold = Swap32LE(player._pGold); packed.pHPBase = Swap32LE(player._pHPBase); packed.pMaxHPBase = Swap32LE(player._pMaxHPBase); packed.pManaBase = Swap32LE(player._pManaBase); packed.pMaxManaBase = Swap32LE(player._pMaxManaBase); packed.pMemSpells = Swap64LE(player._pMemSpells); for (int i = 0; i < 37; i++) // Should be MAX_SPELLS but set to 37 to make save games compatible packed.pSplLvl[i] = player._pSplLvl[i]; for (int i = 37; i < 47; i++) packed.pSplLvl2[i - 37] = player._pSplLvl[i]; for (int i = 0; i < NUM_INVLOC; i++) PackItem(packed.InvBody[i], player.InvBody[i], gbIsHellfire); packed._pNumInv = player._pNumInv; for (int i = 0; i < packed._pNumInv; i++) PackItem(packed.InvList[i], player.InvList[i], gbIsHellfire); for (int i = 0; i < InventoryGridCells; i++) packed.InvGrid[i] = player.InvGrid[i]; for (int i = 0; i < MaxBeltItems; i++) PackItem(packed.SpdList[i], player.SpdList[i], gbIsHellfire); packed.wReflections = Swap16LE(player.wReflections); packed.pDamAcFlags = Swap32LE(static_cast(player.pDamAcFlags)); packed.pDiabloKillLevel = Swap32LE(player.pDiabloKillLevel); packed.bIsHellfire = gbIsHellfire ? 1 : 0; } void PackNetItem(const Item &item, ItemNetPack &packedItem) { if (item.isEmpty()) { packedItem.def.wIndx = static_cast<_item_indexes>(0xFFFF); return; } packedItem.def.wIndx = static_cast<_item_indexes>(Swap16LE(item.IDidx)); packedItem.def.wCI = Swap16LE(item._iCreateInfo); packedItem.def.dwSeed = Swap32LE(item._iSeed); if (item.IDidx != IDI_EAR) PrepareItemForNetwork(item, packedItem.item); else PrepareEarForNetwork(item, packedItem.ear); } void PackNetPlayer(PlayerNetPack &packed, const Player &player) { packed.plrlevel = player.plrlevel; packed.px = player.position.tile.x; packed.py = player.position.tile.y; packed.pdir = static_cast(player._pdir); CopyUtf8(packed.pName, player._pName, sizeof(packed.pName)); packed.pClass = static_cast(player._pClass); packed.pBaseStr = player._pBaseStr; packed.pBaseMag = player._pBaseMag; packed.pBaseDex = player._pBaseDex; packed.pBaseVit = player._pBaseVit; packed.pLevel = player.getCharacterLevel(); packed.pStatPts = player._pStatPts; packed.pExperience = Swap32LE(player._pExperience); packed.pHPBase = Swap32LE(player._pHPBase); packed.pMaxHPBase = Swap32LE(player._pMaxHPBase); packed.pManaBase = Swap32LE(player._pManaBase); packed.pMaxManaBase = Swap32LE(player._pMaxManaBase); packed.pMemSpells = Swap64LE(player._pMemSpells); for (int i = 0; i < MAX_SPELLS; i++) packed.pSplLvl[i] = player._pSplLvl[i]; for (int i = 0; i < NUM_INVLOC; i++) PackNetItem(player.InvBody[i], packed.InvBody[i]); packed._pNumInv = player._pNumInv; for (int i = 0; i < packed._pNumInv; i++) PackNetItem(player.InvList[i], packed.InvList[i]); for (int i = 0; i < InventoryGridCells; i++) packed.InvGrid[i] = player.InvGrid[i]; for (int i = 0; i < MaxBeltItems; i++) PackNetItem(player.SpdList[i], packed.SpdList[i]); packed.wReflections = Swap16LE(player.wReflections); packed.pDiabloKillLevel = player.pDiabloKillLevel; packed.pManaShield = player.pManaShield; packed.friendlyMode = player.friendlyMode ? 1 : 0; packed.isOnSetLevel = player.plrIsOnSetLevel; packed.pStrength = Swap32LE(player._pStrength); packed.pMagic = Swap32LE(player._pMagic); packed.pDexterity = Swap32LE(player._pDexterity); packed.pVitality = Swap32LE(player._pVitality); packed.pHitPoints = Swap32LE(player._pHitPoints); packed.pMaxHP = Swap32LE(player._pMaxHP); packed.pMana = Swap32LE(player._pMana); packed.pMaxMana = Swap32LE(player._pMaxMana); packed.pDamageMod = Swap32LE(player._pDamageMod); // we pack base to block as a basic check that remote players are using the same playerdat values as we are packed.pBaseToBlk = Swap32LE(player.getBaseToBlock()); packed.pIMinDam = Swap32LE(player._pIMinDam); packed.pIMaxDam = Swap32LE(player._pIMaxDam); packed.pIAC = Swap32LE(player._pIAC); packed.pIBonusDam = Swap32LE(player._pIBonusDam); packed.pIBonusToHit = Swap32LE(player._pIBonusToHit); packed.pIBonusAC = Swap32LE(player._pIBonusAC); packed.pIBonusDamMod = Swap32LE(player._pIBonusDamMod); packed.pIGetHit = Swap32LE(player._pIGetHit); packed.pIEnAc = Swap32LE(player._pIEnAc); packed.pIFMinDam = Swap32LE(player._pIFMinDam); packed.pIFMaxDam = Swap32LE(player._pIFMaxDam); packed.pILMinDam = Swap32LE(player._pILMinDam); packed.pILMaxDam = Swap32LE(player._pILMaxDam); } void UnPackItem(const ItemPack &packedItem, const Player &player, Item &item, bool isHellfire) { if (packedItem.idx == 0xFFFF) { item.clear(); return; } auto idx = static_cast<_item_indexes>(Swap16LE(packedItem.idx)); if (gbIsSpawn) { idx = RemapItemIdxFromSpawn(idx); } if (!isHellfire) { idx = RemapItemIdxFromDiablo(idx); } if (!IsItemAvailable(idx)) { item.clear(); return; } if (idx == IDI_EAR) { const uint16_t ic = Swap16LE(packedItem.iCreateInfo); const uint32_t iseed = Swap32LE(packedItem.iSeed); const uint16_t ivalue = Swap16LE(packedItem.wValue); const int32_t ibuff = Swap32LE(packedItem.dwBuff); char heroName[17]; heroName[0] = static_cast((ic >> 8) & 0x7F); heroName[1] = static_cast(ic & 0x7F); heroName[2] = static_cast((iseed >> 24) & 0x7F); heroName[3] = static_cast((iseed >> 16) & 0x7F); heroName[4] = static_cast((iseed >> 8) & 0x7F); heroName[5] = static_cast(iseed & 0x7F); heroName[6] = static_cast(packedItem.bId & 0x7F); heroName[7] = static_cast(packedItem.bDur & 0x7F); heroName[8] = static_cast(packedItem.bMDur & 0x7F); heroName[9] = static_cast(packedItem.bCh & 0x7F); heroName[10] = static_cast(packedItem.bMCh & 0x7F); heroName[11] = static_cast((ivalue >> 8) & 0x7F); heroName[12] = static_cast((ibuff >> 24) & 0x7F); heroName[13] = static_cast((ibuff >> 16) & 0x7F); heroName[14] = static_cast((ibuff >> 8) & 0x7F); heroName[15] = static_cast(ibuff & 0x7F); heroName[16] = '\0'; RecreateEar(item, ic, iseed, ivalue & 0xFF, heroName); } else { item = {}; // Item generation logic will assign CF_HELLFIRE based on isHellfire // so if we carry it over from packedItem, it may be incorrect const uint32_t dwBuff = Swap32LE(packedItem.dwBuff) | (isHellfire ? CF_HELLFIRE : 0); RecreateItem(player, item, idx, Swap16LE(packedItem.iCreateInfo), Swap32LE(packedItem.iSeed), Swap16LE(packedItem.wValue), dwBuff); item._iIdentified = (packedItem.bId & 1) != 0; item._iMaxDur = packedItem.bMDur; item._iDurability = ClampDurability(item, packedItem.bDur); item._iMaxCharges = std::clamp(packedItem.bMCh, 0, item._iMaxCharges); item._iCharges = std::clamp(packedItem.bCh, 0, item._iMaxCharges); } } void UnPackPlayer(const PlayerPack &packed, Player &player) { const Point position { packed.px, packed.py }; player = {}; player.setCharacterLevel(packed.pLevel); player._pMaxHPBase = Swap32LE(packed.pMaxHPBase); player._pHPBase = Swap32LE(packed.pHPBase); player._pHPBase = std::clamp(player._pHPBase, 0, player._pMaxHPBase); player._pMaxHP = player._pMaxHPBase; player._pHitPoints = player._pHPBase; player.position.tile = position; player.position.future = position; player.setLevel(std::clamp(packed.plrlevel, 0, NUMLEVELS)); player._pClass = static_cast(std::clamp(packed.pClass, 0, static_cast(GetNumPlayerClasses() - 1))); ClrPlrPath(player); player.destAction = ACTION_NONE; CopyUtf8(player._pName, packed.pName, sizeof(player._pName)); InitPlayer(player, true); player._pBaseStr = std::min(packed.pBaseStr, player.GetMaximumAttributeValue(CharacterAttribute::Strength)); player._pStrength = player._pBaseStr; player._pBaseMag = std::min(packed.pBaseMag, player.GetMaximumAttributeValue(CharacterAttribute::Magic)); player._pMagic = player._pBaseMag; player._pBaseDex = std::min(packed.pBaseDex, player.GetMaximumAttributeValue(CharacterAttribute::Dexterity)); player._pDexterity = player._pBaseDex; player._pBaseVit = std::min(packed.pBaseVit, player.GetMaximumAttributeValue(CharacterAttribute::Vitality)); player._pVitality = player._pBaseVit; player._pStatPts = packed.pStatPts; player._pExperience = Swap32LE(packed.pExperience); player._pGold = Swap32LE(packed.pGold); if ((int)(player._pHPBase & 0xFFFFFFC0) < 64) player._pHPBase = 64; player._pMaxManaBase = Swap32LE(packed.pMaxManaBase); player._pManaBase = Swap32LE(packed.pManaBase); player._pManaBase = std::min(player._pManaBase, player._pMaxManaBase); player._pMemSpells = Swap64LE(packed.pMemSpells); // Only read spell levels for learnable spells (Diablo) for (int i = 0; i < 37; i++) { // Should be MAX_SPELLS but set to 36 to make save games compatible auto spl = static_cast(i); if (GetSpellBookLevel(spl) != -1) player._pSplLvl[i] = packed.pSplLvl[i]; else player._pSplLvl[i] = 0; } // Only read spell levels for learnable spells (Hellfire) for (int i = 37; i < 47; i++) { auto spl = static_cast(i); if (GetSpellBookLevel(spl) != -1) player._pSplLvl[i] = packed.pSplLvl2[i - 37]; else player._pSplLvl[i] = 0; } // These spells are unavailable in Diablo as learnable spells if (!gbIsHellfire) { player._pSplLvl[static_cast(SpellID::Apocalypse)] = 0; player._pSplLvl[static_cast(SpellID::Nova)] = 0; } const bool isHellfire = packed.bIsHellfire != 0; for (int i = 0; i < NUM_INVLOC; i++) UnPackItem(packed.InvBody[i], player, player.InvBody[i], isHellfire); player._pNumInv = packed._pNumInv; for (int i = 0; i < player._pNumInv; i++) UnPackItem(packed.InvList[i], player, player.InvList[i], isHellfire); for (int i = 0; i < InventoryGridCells; i++) player.InvGrid[i] = packed.InvGrid[i]; VerifyGoldSeeds(player); for (int i = 0; i < MaxBeltItems; i++) UnPackItem(packed.SpdList[i], player, player.SpdList[i], isHellfire); CalcPlrInv(player, false); player.wReflections = Swap16LE(packed.wReflections); player.pDiabloKillLevel = Swap32LE(packed.pDiabloKillLevel); } bool UnPackNetItem(const Player &player, const ItemNetPack &packedItem, Item &item) { item = {}; const _item_indexes idx = static_cast<_item_indexes>(Swap16LE(packedItem.def.wIndx)); if (idx < 0 || idx >= static_cast<_item_indexes>(AllItemsList.size())) return true; if (idx == IDI_EAR) { RecreateEar(item, Swap16LE(packedItem.ear.wCI), Swap32LE(packedItem.ear.dwSeed), packedItem.ear.bCursval, packedItem.ear.heroname); return true; } const uint16_t creationFlags = Swap16LE(packedItem.item.wCI); const uint32_t dwBuff = Swap16LE(packedItem.item.dwBuff); if (idx != IDI_GOLD) ValidateField(creationFlags, IsCreationFlagComboValid(creationFlags)); if ((creationFlags & CF_TOWN) != 0) ValidateField(creationFlags, IsTownItemValid(creationFlags, player)); else if ((creationFlags & CF_USEFUL) == CF_UPER15) ValidateFields(creationFlags, dwBuff, IsUniqueMonsterItemValid(creationFlags, dwBuff)); else if ((dwBuff & CF_HELLFIRE) != 0 && AllItemsList[idx].iMiscId == IMISC_BOOK) return RecreateHellfireSpellBook(player, packedItem.item, &item); else ValidateFields(creationFlags, dwBuff, IsDungeonItemValid(creationFlags, dwBuff)); RecreateItem(player, packedItem.item, item); return true; } bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) { CopyUtf8(player._pName, packed.pName, sizeof(player._pName)); ValidateField(packed.pClass, packed.pClass < GetNumPlayerClasses()); player._pClass = static_cast(packed.pClass); const Point position { packed.px, packed.py }; ValidateFields(position.x, position.y, InDungeonBounds(position)); ValidateField(packed.plrlevel, packed.plrlevel < NUMLEVELS); ValidateField(packed.pLevel, packed.pLevel >= 1 && packed.pLevel <= player.getMaxCharacterLevel()); const int32_t baseHpMax = Swap32LE(packed.pMaxHPBase); const int32_t baseHp = Swap32LE(packed.pHPBase); const int32_t hpMax = Swap32LE(packed.pMaxHP); ValidateFields(baseHp, baseHpMax, baseHp >= (baseHpMax - hpMax) && baseHp <= baseHpMax); const int32_t baseManaMax = Swap32LE(packed.pMaxManaBase); const int32_t baseMana = Swap32LE(packed.pManaBase); ValidateFields(baseMana, baseManaMax, baseMana <= baseManaMax); ValidateFields(packed.pClass, packed.pBaseStr, packed.pBaseStr <= player.GetMaximumAttributeValue(CharacterAttribute::Strength)); ValidateFields(packed.pClass, packed.pBaseMag, packed.pBaseMag <= player.GetMaximumAttributeValue(CharacterAttribute::Magic)); ValidateFields(packed.pClass, packed.pBaseDex, packed.pBaseDex <= player.GetMaximumAttributeValue(CharacterAttribute::Dexterity)); ValidateFields(packed.pClass, packed.pBaseVit, packed.pBaseVit <= player.GetMaximumAttributeValue(CharacterAttribute::Vitality)); ValidateField(packed._pNumInv, packed._pNumInv <= InventoryGridCells); ValidateField(packed.pdir, packed.pdir <= static_cast(Direction::SouthEast)); player.setCharacterLevel(packed.pLevel); player.position.tile = position; player.position.future = position; player._pdir = static_cast(packed.pdir); player.plrlevel = packed.plrlevel; player.plrIsOnSetLevel = packed.isOnSetLevel != 0; player._pMaxHPBase = baseHpMax; player._pHPBase = baseHp; player._pMaxHP = baseHpMax; player._pHitPoints = baseHp; ClrPlrPath(player); player.destAction = ACTION_NONE; InitPlayer(player, true); player._pBaseStr = packed.pBaseStr; player._pStrength = player._pBaseStr; player._pBaseMag = packed.pBaseMag; player._pMagic = player._pBaseMag; player._pBaseDex = packed.pBaseDex; player._pDexterity = player._pBaseDex; player._pBaseVit = packed.pBaseVit; player._pVitality = player._pBaseVit; player._pStatPts = packed.pStatPts; player._pExperience = Swap32LE(packed.pExperience); player._pMaxManaBase = baseManaMax; player._pManaBase = baseMana; player._pMemSpells = Swap64LE(packed.pMemSpells); player.wReflections = Swap16LE(packed.wReflections); player.pDiabloKillLevel = packed.pDiabloKillLevel; player.pManaShield = packed.pManaShield != 0; player.friendlyMode = packed.friendlyMode != 0; for (int i = 0; i < MAX_SPELLS; i++) player._pSplLvl[i] = packed.pSplLvl[i]; for (int i = 0; i < NUM_INVLOC; i++) { if (!UnPackNetItem(player, packed.InvBody[i], player.InvBody[i])) return false; if (player.InvBody[i].isEmpty()) continue; auto loc = static_cast(player.GetItemLocation(player.InvBody[i])); switch (i) { case INVLOC_HEAD: ValidateField(loc, loc == ILOC_HELM); break; case INVLOC_RING_LEFT: case INVLOC_RING_RIGHT: ValidateField(loc, loc == ILOC_RING); break; case INVLOC_AMULET: ValidateField(loc, loc == ILOC_AMULET); break; case INVLOC_HAND_LEFT: case INVLOC_HAND_RIGHT: ValidateField(loc, IsAnyOf(loc, ILOC_ONEHAND, ILOC_TWOHAND)); break; case INVLOC_CHEST: ValidateField(loc, loc == ILOC_ARMOR); break; } } player._pNumInv = packed._pNumInv; for (int i = 0; i < player._pNumInv; i++) { if (!UnPackNetItem(player, packed.InvList[i], player.InvList[i])) return false; } for (int i = 0; i < InventoryGridCells; i++) player.InvGrid[i] = packed.InvGrid[i]; for (int i = 0; i < MaxBeltItems; i++) { Item &item = player.SpdList[i]; if (!UnPackNetItem(player, packed.SpdList[i], item)) return false; if (item.isEmpty()) continue; const Size beltItemSize = GetInventorySize(item); const int8_t beltItemType = static_cast(item._itype); const bool beltItemUsable = item.isUsable(); ValidateFields(beltItemSize.width, beltItemSize.height, (beltItemSize == Size { 1, 1 })); ValidateField(beltItemType, item._itype != ItemType::Gold); ValidateField(beltItemUsable, beltItemUsable); } CalcPlrInv(player, false); player._pGold = CalculateGold(player); ValidateFields(player._pStrength, SwapSigned32LE(packed.pStrength), player._pStrength == SwapSigned32LE(packed.pStrength)); ValidateFields(player._pMagic, SwapSigned32LE(packed.pMagic), player._pMagic == SwapSigned32LE(packed.pMagic)); ValidateFields(player._pDexterity, SwapSigned32LE(packed.pDexterity), player._pDexterity == SwapSigned32LE(packed.pDexterity)); ValidateFields(player._pVitality, SwapSigned32LE(packed.pVitality), player._pVitality == SwapSigned32LE(packed.pVitality)); ValidateFields(player._pHitPoints, SwapSigned32LE(packed.pHitPoints), player._pHitPoints == SwapSigned32LE(packed.pHitPoints)); ValidateFields(player._pMaxHP, SwapSigned32LE(packed.pMaxHP), player._pMaxHP == SwapSigned32LE(packed.pMaxHP)); ValidateFields(player._pMana, SwapSigned32LE(packed.pMana), player._pMana == SwapSigned32LE(packed.pMana)); ValidateFields(player._pMaxMana, SwapSigned32LE(packed.pMaxMana), player._pMaxMana == SwapSigned32LE(packed.pMaxMana)); ValidateFields(player._pDamageMod, SwapSigned32LE(packed.pDamageMod), player._pDamageMod == SwapSigned32LE(packed.pDamageMod)); ValidateFields(player.getBaseToBlock(), SwapSigned32LE(packed.pBaseToBlk), player.getBaseToBlock() == SwapSigned32LE(packed.pBaseToBlk)); ValidateFields(player._pIMinDam, SwapSigned32LE(packed.pIMinDam), player._pIMinDam == SwapSigned32LE(packed.pIMinDam)); ValidateFields(player._pIMaxDam, SwapSigned32LE(packed.pIMaxDam), player._pIMaxDam == SwapSigned32LE(packed.pIMaxDam)); ValidateFields(player._pIAC, SwapSigned32LE(packed.pIAC), player._pIAC == SwapSigned32LE(packed.pIAC)); ValidateFields(player._pIBonusDam, SwapSigned32LE(packed.pIBonusDam), player._pIBonusDam == SwapSigned32LE(packed.pIBonusDam)); ValidateFields(player._pIBonusToHit, SwapSigned32LE(packed.pIBonusToHit), player._pIBonusToHit == SwapSigned32LE(packed.pIBonusToHit)); ValidateFields(player._pIBonusAC, SwapSigned32LE(packed.pIBonusAC), player._pIBonusAC == SwapSigned32LE(packed.pIBonusAC)); ValidateFields(player._pIBonusDamMod, SwapSigned32LE(packed.pIBonusDamMod), player._pIBonusDamMod == SwapSigned32LE(packed.pIBonusDamMod)); ValidateFields(player._pIGetHit, SwapSigned32LE(packed.pIGetHit), player._pIGetHit == SwapSigned32LE(packed.pIGetHit)); ValidateFields(player._pIEnAc, SwapSigned32LE(packed.pIEnAc), player._pIEnAc == SwapSigned32LE(packed.pIEnAc)); ValidateFields(player._pIFMinDam, SwapSigned32LE(packed.pIFMinDam), player._pIFMinDam == SwapSigned32LE(packed.pIFMinDam)); ValidateFields(player._pIFMaxDam, SwapSigned32LE(packed.pIFMaxDam), player._pIFMaxDam == SwapSigned32LE(packed.pIFMaxDam)); ValidateFields(player._pILMinDam, SwapSigned32LE(packed.pILMinDam), player._pILMinDam == SwapSigned32LE(packed.pILMinDam)); ValidateFields(player._pILMaxDam, SwapSigned32LE(packed.pILMaxDam), player._pILMaxDam == SwapSigned32LE(packed.pILMaxDam)); ValidateFields(player._pMaxHPBase, player.calculateBaseLife(), player._pMaxHPBase <= player.calculateBaseLife()); ValidateFields(player._pMaxManaBase, player.calculateBaseMana(), player._pMaxManaBase <= player.calculateBaseMana()); return true; } } // namespace devilution ================================================ FILE: Source/pack.h ================================================ /** * @file pack.h * * Interface of functions for minifying player data structure. */ #pragma once #include #include "inv.h" #include "items.h" #include "msg.h" #include "player.h" namespace devilution { #define MAX_SPELLS 52 #pragma pack(push, 1) struct ItemPack { uint32_t iSeed; uint16_t iCreateInfo; uint16_t idx; uint8_t bId; uint8_t bDur; uint8_t bMDur; uint8_t bCh; uint8_t bMCh; uint16_t wValue; uint32_t dwBuff; }; struct PlayerPack { uint32_t dwLowDateTime; uint32_t dwHighDateTime; int8_t destAction; int8_t destParam1; int8_t destParam2; uint8_t plrlevel; uint8_t px; uint8_t py; uint8_t targx; uint8_t targy; char pName[PlayerNameLength]; uint8_t pClass; uint8_t pBaseStr; uint8_t pBaseMag; uint8_t pBaseDex; uint8_t pBaseVit; uint8_t pLevel; uint8_t pStatPts; uint32_t pExperience; int32_t pGold; int32_t pHPBase; int32_t pMaxHPBase; int32_t pManaBase; int32_t pMaxManaBase; uint8_t pSplLvl[37]; // Should be MAX_SPELLS but set to 37 to make save games compatible uint64_t pMemSpells; ItemPack InvBody[NUM_INVLOC]; ItemPack InvList[InventoryGridCells]; int8_t InvGrid[InventoryGridCells]; uint8_t _pNumInv; ItemPack SpdList[MaxBeltItems]; int8_t pTownWarps; int8_t pDungMsgs; int8_t pLvlLoad; uint8_t pBattleNet; uint8_t pManaShield; uint8_t pDungMsgs2; /** The format the character is in, 0: Diablo, 1: Hellfire */ int8_t bIsHellfire; uint8_t reserved; // For future use uint16_t wReflections; uint8_t reserved2[2]; // For future use uint8_t pSplLvl2[10]; // Hellfire spells int16_t wReserved8; // For future use uint32_t pDiabloKillLevel; uint32_t pDifficulty; uint32_t pDamAcFlags; // `ItemSpecialEffectHf` is 1 byte but this is 4 bytes. uint8_t reserved3[20]; // For future use }; union ItemNetPack { TItemDef def; TItem item; TEar ear; }; struct PlayerNetPack { uint8_t plrlevel; uint8_t px; uint8_t py; uint8_t pdir; char pName[PlayerNameLength]; uint8_t pClass; uint8_t pBaseStr; uint8_t pBaseMag; uint8_t pBaseDex; uint8_t pBaseVit; int8_t pLevel; uint8_t pStatPts; uint32_t pExperience; int32_t pHPBase; int32_t pMaxHPBase; int32_t pManaBase; int32_t pMaxManaBase; uint8_t pSplLvl[MAX_SPELLS]; uint64_t pMemSpells; ItemNetPack InvBody[NUM_INVLOC]; ItemNetPack InvList[InventoryGridCells]; int8_t InvGrid[InventoryGridCells]; uint8_t _pNumInv; ItemNetPack SpdList[MaxBeltItems]; uint8_t pManaShield; uint16_t wReflections; uint8_t pDiabloKillLevel; uint8_t friendlyMode; uint8_t isOnSetLevel; // For validation int32_t pStrength; int32_t pMagic; int32_t pDexterity; int32_t pVitality; int32_t pHitPoints; int32_t pMaxHP; int32_t pMana; int32_t pMaxMana; int32_t pDamageMod; int32_t pBaseToBlk; int32_t pIMinDam; int32_t pIMaxDam; int32_t pIAC; int32_t pIBonusDam; int32_t pIBonusToHit; int32_t pIBonusAC; int32_t pIBonusDamMod; int32_t pIGetHit; int32_t pIEnAc; int32_t pIFMinDam; int32_t pIFMaxDam; int32_t pILMinDam; int32_t pILMaxDam; }; #pragma pack(pop) bool RecreateHellfireSpellBook(const Player &player, const TItem &packedItem, Item *item = nullptr); void PackPlayer(PlayerPack &pPack, const Player &player); void UnPackPlayer(const PlayerPack &pPack, Player &player); void PackNetPlayer(PlayerNetPack &packed, const Player &player); bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player); /** * @brief Save the attributes needed to recreate this item into an ItemPack struct * * @param packedItem The destination packed struct * @param item The source item * @param isHellfire Whether the item is from Hellfire or not */ void PackItem(ItemPack &packedItem, const Item &item, bool isHellfire); /** * Expand a ItemPack in to a Item * * @param packedItem The source packed item * @param item The destination item * @param isHellfire Whether the item is from Hellfire or not */ void UnPackItem(const ItemPack &packedItem, const Player &player, Item &item, bool isHellfire); /** * @brief Save the attributes needed to recreate this item into an ItemNetPack struct * @param item The source item * @param packedItem The destination packed struct */ void PackNetItem(const Item &item, ItemNetPack &packedItem); /** * @brief Expand a ItemPack in to a Item * @param player The player holding the item * @param packedItem The source packed item * @param item The destination item * @return True if the item is valid */ bool UnPackNetItem(const Player &player, const ItemNetPack &packedItem, Item &item); } // namespace devilution ================================================ FILE: Source/panels/charpanel.cpp ================================================ #include "panels/charpanel.hpp" #include #include #include #include #include #include #include "control/control.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "panels/ui_panels.hpp" #include "player.h" #include "tables/playerdat.hpp" #include "utils/algorithm/container.hpp" #include "utils/display.h" #include "utils/enum_traits.h" #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #include "utils/surface_to_clx.hpp" namespace devilution { OptionalOwnedClxSpriteList pChrButtons; namespace { struct StyledText { UiFlags style; std::string text; int spacing = 1; }; struct PanelEntry { std::string label; Point position; int length; int labelLength; // max label's length - used for line wrapping std::optional> statDisplayFunc; // function responsible for displaying stat }; UiFlags GetBaseStatColor(CharacterAttribute attr) { UiFlags style = UiFlags::ColorWhite; if (InspectPlayer->GetBaseAttributeValue(attr) == InspectPlayer->GetMaximumAttributeValue(attr)) style = UiFlags::ColorWhitegold; return style; } UiFlags GetCurrentStatColor(CharacterAttribute attr) { UiFlags style = UiFlags::ColorWhite; const int current = InspectPlayer->GetCurrentAttributeValue(attr); const int base = InspectPlayer->GetBaseAttributeValue(attr); if (current > base) style = UiFlags::ColorBlue; if (current < base) style = UiFlags::ColorRed; return style; } UiFlags GetValueColor(int value, bool flip = false) { UiFlags style = UiFlags::ColorWhite; if (value > 0) style = (flip ? UiFlags::ColorRed : UiFlags::ColorBlue); if (value < 0) style = (flip ? UiFlags::ColorBlue : UiFlags::ColorRed); return style; } UiFlags GetMaxManaColor() { if (HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana)) return UiFlags::ColorRed; return InspectPlayer->_pMaxMana > InspectPlayer->_pMaxManaBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; } UiFlags GetMaxHealthColor() { return InspectPlayer->_pMaxHP > InspectPlayer->_pMaxHPBase ? UiFlags::ColorBlue : UiFlags::ColorWhite; } std::pair GetDamage() { int damageMod = InspectPlayer->_pIBonusDamMod; if (InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow && InspectPlayer->_pClass != HeroClass::Rogue) { damageMod += InspectPlayer->_pDamageMod / 2; } else { damageMod += InspectPlayer->_pDamageMod; } const int mindam = InspectPlayer->_pIMinDam + InspectPlayer->_pIBonusDam * InspectPlayer->_pIMinDam / 100 + damageMod; const int maxdam = InspectPlayer->_pIMaxDam + InspectPlayer->_pIBonusDam * InspectPlayer->_pIMaxDam / 100 + damageMod; return { mindam, maxdam }; } StyledText GetResistInfo(int8_t resist) { UiFlags style = UiFlags::ColorBlue; if (resist == 0) style = UiFlags::ColorWhite; else if (resist < 0) style = UiFlags::ColorRed; else if (resist >= MaxResistance) style = UiFlags::ColorWhitegold; return { style, StrCat(resist, "%") }; } constexpr int LeftColumnLabelX = 88; constexpr int TopRightLabelX = 211; constexpr int RightColumnLabelX = 253; constexpr int LeftColumnLabelWidth = 76; constexpr int RightColumnLabelWidth = 68; // Indices in `panelEntries`. constexpr unsigned AttributeHeaderEntryIndices[2] = { 5, 6 }; constexpr unsigned GoldHeaderEntryIndex = 16; PanelEntry panelEntries[] = { { "", { 9, 14 }, 150, 0, []() { return StyledText { UiFlags::ColorWhite, InspectPlayer->_pName }; } }, { "", { 161, 14 }, 149, 0, []() { return StyledText { UiFlags::ColorWhite, std::string(InspectPlayer->getClassName()) }; } }, { N_("Level"), { 57, 52 }, 57, 45, []() { return StyledText { UiFlags::ColorWhite, StrCat(InspectPlayer->getCharacterLevel()) }; } }, { N_("Experience"), { TopRightLabelX, 52 }, 99, 91, []() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pExperience) }; } }, { N_("Next level"), { TopRightLabelX, 80 }, 99, 198, []() { if (InspectPlayer->isMaxCharacterLevel()) { return StyledText { UiFlags::ColorWhitegold, std::string(_("None")) }; } const uint32_t nextExperienceThreshold = InspectPlayer->getNextExperienceThreshold(); return StyledText { UiFlags::ColorWhite, FormatInteger(nextExperienceThreshold) }; } }, { N_("Base"), { LeftColumnLabelX, /* set dynamically */ 0 }, 0, 44, {} }, { N_("Now"), { 135, /* set dynamically */ 0 }, 0, 44, {} }, { N_("Strength"), { LeftColumnLabelX, 135 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Strength), StrCat(InspectPlayer->_pBaseStr) }; } }, { "", { 135, 135 }, 45, 0, []() { return StyledText { GetCurrentStatColor(CharacterAttribute::Strength), StrCat(InspectPlayer->_pStrength) }; } }, { N_("Magic"), { LeftColumnLabelX, 163 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Magic), StrCat(InspectPlayer->_pBaseMag) }; } }, { "", { 135, 163 }, 45, 0, []() { return StyledText { GetCurrentStatColor(CharacterAttribute::Magic), StrCat(InspectPlayer->_pMagic) }; } }, { N_("Dexterity"), { LeftColumnLabelX, 191 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Dexterity), StrCat(InspectPlayer->_pBaseDex) }; } }, { "", { 135, 191 }, 45, 0, []() { return StyledText { GetCurrentStatColor(CharacterAttribute::Dexterity), StrCat(InspectPlayer->_pDexterity) }; } }, { N_("Vitality"), { LeftColumnLabelX, 219 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetBaseStatColor(CharacterAttribute::Vitality), StrCat(InspectPlayer->_pBaseVit) }; } }, { "", { 135, 219 }, 45, 0, []() { return StyledText { GetCurrentStatColor(CharacterAttribute::Vitality), StrCat(InspectPlayer->_pVitality) }; } }, { N_("Points to distribute"), { LeftColumnLabelX, 248 }, 45, LeftColumnLabelWidth, []() { InspectPlayer->_pStatPts = std::min(CalcStatDiff(*InspectPlayer), InspectPlayer->_pStatPts); return StyledText { UiFlags::ColorRed, (InspectPlayer->_pStatPts > 0 ? StrCat(InspectPlayer->_pStatPts) : "") }; } }, { N_("Gold"), { TopRightLabelX, /* set dynamically */ 0 }, 0, 98, {} }, { "", { TopRightLabelX, 127 }, 99, 0, []() { return StyledText { UiFlags::ColorWhite, FormatInteger(InspectPlayer->_pGold) }; } }, { N_("Armor class"), { RightColumnLabelX, 163 }, 57, RightColumnLabelWidth, []() { return StyledText { GetValueColor(InspectPlayer->_pIBonusAC), StrCat(InspectPlayer->GetArmor() + InspectPlayer->getCharacterLevel() * 2) }; } }, { N_("Chance To Hit"), { RightColumnLabelX, 191 }, 57, RightColumnLabelWidth, []() { return StyledText { GetValueColor(InspectPlayer->_pIBonusToHit), StrCat(InspectPlayer->InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Bow ? InspectPlayer->GetRangedToHit() : InspectPlayer->GetMeleeToHit(), "%") }; } }, { N_("Damage"), { RightColumnLabelX, 219 }, 57, RightColumnLabelWidth, []() { const auto [dmgMin, dmgMax] = GetDamage(); return StyledText { GetValueColor(InspectPlayer->_pIBonusDam), StrCat(dmgMin, "-", dmgMax) }; } }, { N_("Life"), { LeftColumnLabelX, 284 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetMaxHealthColor(), StrCat(InspectPlayer->_pMaxHP >> 6) }; } }, { "", { 135, 284 }, 45, 0, []() { return StyledText { (InspectPlayer->_pHitPoints != InspectPlayer->_pMaxHP ? UiFlags::ColorRed : GetMaxHealthColor()), StrCat(InspectPlayer->_pHitPoints >> 6) }; } }, { N_("Mana"), { LeftColumnLabelX, 312 }, 45, LeftColumnLabelWidth, []() { return StyledText { GetMaxManaColor(), StrCat(HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) ? 0 : InspectPlayer->_pMaxMana >> 6) }; } }, { "", { 135, 312 }, 45, 0, []() { return StyledText { (InspectPlayer->_pMana != InspectPlayer->_pMaxMana ? UiFlags::ColorRed : GetMaxManaColor()), StrCat((HasAnyOf(InspectPlayer->_pIFlags, ItemSpecialEffect::NoMana) || InspectPlayer->hasNoMana()) ? 0 : InspectPlayer->_pMana >> 6) }; } }, { N_("Resist magic"), { RightColumnLabelX, 256 }, 57, RightColumnLabelWidth, []() { return GetResistInfo(InspectPlayer->_pMagResist); } }, { N_("Resist fire"), { RightColumnLabelX, 284 }, 57, RightColumnLabelWidth, []() { return GetResistInfo(InspectPlayer->_pFireResist); } }, { N_("Resist lightning"), { RightColumnLabelX, 313 }, 57, RightColumnLabelWidth, []() { return GetResistInfo(InspectPlayer->_pLghtResist); } }, }; OptionalOwnedClxSpriteList Panel; constexpr int PanelFieldHeight = 24; constexpr int PanelFieldPaddingTop = 3; constexpr int PanelFieldPaddingBottom = 3; constexpr int PanelFieldPaddingSide = 5; constexpr int PanelFieldInnerHeight = PanelFieldHeight - PanelFieldPaddingTop - PanelFieldPaddingBottom; void DrawPanelField(const Surface &out, Point pos, int len, ClxSprite left, ClxSprite middle, ClxSprite right) { RenderClxSprite(out, left, pos); pos.x += left.width(); len -= left.width() + right.width(); RenderClxSprite(out.subregion(pos.x, pos.y, len, middle.height()), middle, Point { 0, 0 }); pos.x += len; RenderClxSprite(out, right, pos); } void DrawShadowString(const Surface &out, const PanelEntry &entry) { if (entry.label.empty()) return; constexpr int Spacing = 0; const std::string_view textStr = LanguageTranslate(entry.label); std::string_view text; std::string wrapped; if (entry.labelLength > 0) { wrapped = WordWrapString(textStr, entry.labelLength, GameFont12, Spacing); text = wrapped; } else { text = textStr; } UiFlags style = UiFlags::VerticalCenter; Point labelPosition = entry.position; if (entry.length == 0) { style |= UiFlags::AlignCenter; } else { style |= UiFlags::AlignRight; labelPosition += Displacement { -entry.labelLength - (IsSmallFontTall() ? 2 : 3), 0 }; } // If the text is less tall than the field, we center it vertically relative to the field. // Otherwise, we draw from the top of the field. const int textHeight = static_cast((c_count(wrapped, '\n') + 1) * GetLineHeight(wrapped, GameFont12)); const int labelHeight = std::max(PanelFieldHeight, textHeight); DrawString(out, text, { labelPosition + Displacement { -2, 2 }, { entry.labelLength, labelHeight } }, { .flags = style | UiFlags::ColorBlack, .spacing = Spacing }); DrawString(out, text, { labelPosition, { entry.labelLength, labelHeight } }, { .flags = style | UiFlags::ColorWhite, .spacing = Spacing }); } void DrawStatButtons(const Surface &out) { if (InspectPlayer->_pStatPts > 0 && !IsInspectingPlayer()) { if (InspectPlayer->_pBaseStr < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Strength)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 157 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Strength)] ? 2 : 1]); if (InspectPlayer->_pBaseMag < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Magic)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 185 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Magic)] ? 4 : 3]); if (InspectPlayer->_pBaseDex < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Dexterity)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 214 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Dexterity)] ? 6 : 5]); if (InspectPlayer->_pBaseVit < InspectPlayer->GetMaximumAttributeValue(CharacterAttribute::Vitality)) ClxDraw(out, GetPanelPosition(UiPanels::Character, { 137, 242 }), (*pChrButtons)[CharPanelButton[static_cast(CharacterAttribute::Vitality)] ? 8 : 7]); } } } // namespace tl::expected LoadCharPanel() { ASSIGN_OR_RETURN(OptionalOwnedClxSpriteList background, LoadClxWithStatus("data\\charbg.clx")); const OwnedSurface out((*background)[0].width(), (*background)[0].height()); RenderClxSprite(out, (*background)[0], { 0, 0 }); background = std::nullopt; { ASSIGN_OR_RETURN(OwnedClxSpriteList boxLeft, LoadClxWithStatus("data\\boxleftend.clx")); ASSIGN_OR_RETURN(OwnedClxSpriteList boxMiddle, LoadClxWithStatus("data\\boxmiddle.clx")); ASSIGN_OR_RETURN(OwnedClxSpriteList boxRight, LoadClxWithStatus("data\\boxrightend.clx")); const bool isSmallFontTall = IsSmallFontTall(); const int attributeHeadersY = isSmallFontTall ? 112 : 114; for (const unsigned i : AttributeHeaderEntryIndices) { panelEntries[i].position.y = attributeHeadersY; } panelEntries[GoldHeaderEntryIndex].position.y = isSmallFontTall ? 105 : 106; for (auto &entry : panelEntries) { if (entry.statDisplayFunc) { DrawPanelField(out, entry.position, entry.length, boxLeft[0], boxMiddle[0], boxRight[0]); } DrawShadowString(out, entry); } } Panel = SurfaceToClx(out); return {}; } void FreeCharPanel() { Panel = std::nullopt; } void DrawChr(const Surface &out) { const Point pos = GetPanelPosition(UiPanels::Character, { 0, 0 }); RenderClxSprite(out, (*Panel)[0], pos); for (auto &entry : panelEntries) { if (entry.statDisplayFunc) { const StyledText tmp = (*entry.statDisplayFunc)(); DrawString( out, tmp.text, { entry.position + Displacement { pos.x + PanelFieldPaddingSide, pos.y + PanelFieldPaddingTop }, { entry.length - (PanelFieldPaddingSide * 2), PanelFieldInnerHeight } }, { .flags = UiFlags::KerningFitSpacing | UiFlags::AlignCenter | UiFlags::VerticalCenter | tmp.style }); } } DrawStatButtons(out); } } // namespace devilution ================================================ FILE: Source/panels/charpanel.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" namespace devilution { extern OptionalOwnedClxSpriteList pChrButtons; void DrawChr(const Surface &); tl::expected LoadCharPanel(); void FreeCharPanel(); } // namespace devilution ================================================ FILE: Source/panels/console.cpp ================================================ #ifdef _DEBUG #include "panels/console.hpp" #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "DiabloUI/text_input.hpp" #include "control/control.hpp" #include "engine/assets.hpp" #include "engine/displacement.hpp" #include "engine/dx.h" #include "engine/palette.h" #include "engine/rectangle.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "engine/size.hpp" #include "engine/surface.hpp" #include "lua/autocomplete.hpp" #include "lua/repl.hpp" #include "utils/algorithm/container.hpp" #include "utils/display.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" namespace devilution { namespace { constexpr std::string_view Prompt = "> "; constexpr std::string_view HelpText = // Displayed as the first console message "Lua console\n" "Shift+Enter to insert a newline, PageUp/Down to scroll," " Up/Down to fill the input from history," " Shift+Up/Down to fill the input from output history," " Ctrl+L to clear history, Esc to close."; std::optional> ConsolePrelude; bool IsConsoleVisible; char ConsoleInputBuffer[4096]; TextInputCursorState ConsoleInputCursor; TextInputState ConsoleInputState { TextInputState::Options { .value = ConsoleInputBuffer, .cursor = &ConsoleInputCursor, .maxLength = sizeof(ConsoleInputBuffer) - 1, } }; enum class InputTextState { UpToDate, Edited, RestoredFromHistory }; InputTextState CurrentInputTextState = InputTextState::UpToDate; std::string WrappedInputText { Prompt }; std::vector AutocompleteSuggestions; int AutocompleteSuggestionsMaxWidth = -1; int AutocompleteSuggestionFocusIndex = -1; constexpr size_t MaxSuggestions = 12; struct ConsoleLine { enum Type : uint8_t { Help, Input, Output, Warning, Error }; Type type; std::string text; std::string wrapped = {}; int numLines = 0; [[nodiscard]] std::string_view textWithoutPrompt() const { std::string_view result = text; if (type == ConsoleLine::Input) { result.remove_prefix(Prompt.size()); } return result; } }; std::vector ConsoleLines; size_t NumPreparedConsoleLines; int ConsoleLinesTotalHeight; // Index of the currently filled input/output, counting from end. int HistoryIndex = -1; // Draft input, saved when navigating history. std::string DraftInput; Rectangle OuterRect; Rectangle InputRect; int InputRectHeight; constexpr int LineHeight = 20; constexpr int TextPaddingYTop = 0; constexpr int TextPaddingYBottom = 4; constexpr int TextPaddingX = 4; constexpr uint8_t BorderColor = PAL8_YELLOW; bool FirstRender; constexpr UiFlags TextUiFlags = UiFlags::FontSizeDialog; constexpr UiFlags InputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite; constexpr UiFlags OutputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite; constexpr UiFlags WarningTextUiFlags = TextUiFlags | UiFlags::ColorDialogYellow; constexpr UiFlags ErrorTextUiFlags = TextUiFlags | UiFlags::ColorDialogRed; constexpr UiFlags AutocompleteSuggestionsTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite; constexpr UiFlags AutocompleteSuggestionsFocusedTextUiFlags = TextUiFlags | UiFlags::ColorDialogYellow; constexpr int TextSpacing = 0; constexpr GameFontTables TextFontSize = GetFontSizeFromUiFlags(InputTextUiFlags); constexpr GameFontTables AutocompleteSuggestionsTextFontSize = GetFontSizeFromUiFlags(AutocompleteSuggestionsTextUiFlags); // Scroll offset from the bottom (in pages), to be applied on next render. int PendingScrollPages; // Scroll offset from the bottom in pixels. int ScrollOffset; constexpr int ScrollStep = LineHeight * 3; void CloseConsole() { IsConsoleVisible = false; SDLC_StopTextInput(ghMainWnd); } int GetConsoleLinesInnerWidth() { return OuterRect.size.width - 2 * TextPaddingX; } void PrepareForRender(ConsoleLine &consoleLine) { consoleLine.wrapped = WordWrapString(consoleLine.text, GetConsoleLinesInnerWidth(), TextFontSize, TextSpacing); consoleLine.numLines += static_cast(c_count(consoleLine.wrapped, '\n')) + 1; ConsoleLinesTotalHeight += consoleLine.numLines * LineHeight; } void AddConsoleLine(ConsoleLine &&consoleLine) { ConsoleLines.emplace_back(std::move(consoleLine)); } void SendInput() { RunInConsole(ConsoleInputState.value()); ConsoleInputState.clear(); DraftInput.clear(); HistoryIndex = -1; } void DrawAutocompleteSuggestions(const Surface &out, const std::vector &suggestions, Point position) { const int maxInnerWidth = out.w() - TextPaddingX * 2; if (AutocompleteSuggestionsMaxWidth == -1) { int maxWidth = 0; for (const LuaAutocompleteSuggestion &suggestion : suggestions) { maxWidth = std::max(maxWidth, GetLineWidth(suggestion.displayText, AutocompleteSuggestionsTextFontSize, TextSpacing)); } AutocompleteSuggestionsMaxWidth = std::min(maxWidth, maxInnerWidth); } const int outerWidth = AutocompleteSuggestionsMaxWidth + TextPaddingX * 2; if (position.x + outerWidth > out.w()) { position.x = out.w() - outerWidth; } const int height = static_cast(suggestions.size()) * LineHeight + TextPaddingYBottom + TextPaddingYTop; position.y -= height; position.y = std::max(LineHeight, position.y); FillRect(out, position.x, position.y, outerWidth, height, PAL16_BLUE + 14); size_t i = 0; Point textPosition { position.x + TextPaddingX, position.y + TextPaddingYTop }; for (const LuaAutocompleteSuggestion &suggestion : suggestions) { if (static_cast(i) == AutocompleteSuggestionFocusIndex) { const int extraTop = i == 0 ? TextPaddingYTop : 0; const int extraHeight = extraTop + TextPaddingYBottom; FillRect(out, position.x, textPosition.y - extraTop, outerWidth, LineHeight + extraHeight, PAL16_BLUE + 8); } const int textHeight = LineHeight + TextPaddingYBottom; DrawString( out.subregion(textPosition.x, textPosition.y, maxInnerWidth, textHeight), suggestion.displayText, Rectangle { Point { 0, 0 }, Size { maxInnerWidth, textHeight } }, TextRenderOptions { .flags = AutocompleteSuggestionsTextUiFlags, .spacing = TextSpacing, }); textPosition.y += LineHeight; ++i; } } bool IsBreakStart(std::string_view str, size_t &breakLen) { const char32_t cp = DecodeFirstUtf8CodePoint(str, &breakLen); return cp == U'\n' || IsBreakableWhitespace(cp); } void DrawInputText(const Surface &out, Rectangle rect, std::string_view originalInputText, std::string_view wrappedInputText) { int lineY = 0; int numRendered = -static_cast(Prompt.size()); bool prevIsOriginalWhitespace = false; const Surface inputTextSurface = out.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height); std::optional renderedCursorPositionOut; for (const std::string_view line : SplitByChar(wrappedInputText, '\n')) { const int lineCursorPosition = static_cast(ConsoleInputCursor.position) - numRendered; const bool isCursorOnPrevLine = lineCursorPosition == 0 && !prevIsOriginalWhitespace && numRendered > 0; DrawString( inputTextSurface, line, { 0, lineY }, TextRenderOptions { .flags = InputTextUiFlags, .spacing = TextSpacing, .cursorPosition = isCursorOnPrevLine ? -1 : lineCursorPosition, .highlightRange = { static_cast(ConsoleInputCursor.selection.begin) - numRendered, static_cast(ConsoleInputCursor.selection.end) - numRendered }, .renderedCursorPositionOut = &renderedCursorPositionOut }); lineY += LineHeight; numRendered += static_cast(line.size()); size_t whitespaceLength; prevIsOriginalWhitespace = static_cast(numRendered) < originalInputText.size() && IsBreakStart(originalInputText.substr(static_cast(numRendered)), whitespaceLength); if (prevIsOriginalWhitespace) { // If we replaced an original whitespace with a newline, count the original whitespace as rendered. numRendered += static_cast(whitespaceLength); } if (numRendered < 0 && IsBreakStart(Prompt.substr(Prompt.size() - static_cast(-numRendered)), whitespaceLength)) { // If we replaced the whitespace in a prompt with a newline, count it as rendered. numRendered += static_cast(whitespaceLength); } } if (!AutocompleteSuggestions.empty() && renderedCursorPositionOut.has_value()) { Point position = *renderedCursorPositionOut; position.x += rect.position.x; position.y += rect.position.y; DrawAutocompleteSuggestions(out, AutocompleteSuggestions, position); } } void DrawConsoleLines(const Surface &out) { const int innerHeight = out.h() - 4; // Extra space for letters like g. if (PendingScrollPages) { ScrollOffset += innerHeight * PendingScrollPages; PendingScrollPages = 0; } if (NumPreparedConsoleLines != ConsoleLines.size()) { for (size_t i = NumPreparedConsoleLines; i < ConsoleLines.size(); ++i) { PrepareForRender(ConsoleLines[i]); } NumPreparedConsoleLines = ConsoleLines.size(); ScrollOffset = 0; } ScrollOffset = std::clamp(ScrollOffset, 0, std::max(0, ConsoleLinesTotalHeight - innerHeight)); int lineYEnd = innerHeight + ScrollOffset; // NOLINTNEXTLINE(modernize-loop-convert) for (auto it = ConsoleLines.rbegin(), itEnd = ConsoleLines.rend(); it != itEnd; ++it) { ConsoleLine &consoleLine = *it; const int linesYBegin = lineYEnd - LineHeight * consoleLine.numLines; if (linesYBegin > innerHeight) { lineYEnd = linesYBegin; continue; } size_t end = consoleLine.wrapped.size(); while (true) { const size_t begin = consoleLine.wrapped.rfind('\n', end - 1) + 1; const std::string_view line = std::string_view(consoleLine.wrapped.data() + begin, end - begin); lineYEnd -= LineHeight; switch (consoleLine.type) { case ConsoleLine::Input: DrawString(out, line, { 0, lineYEnd }, TextRenderOptions { .flags = InputTextUiFlags, .spacing = TextSpacing }); break; case ConsoleLine::Output: case ConsoleLine::Help: DrawString(out, line, { 0, lineYEnd }, TextRenderOptions { .flags = OutputTextUiFlags, .spacing = TextSpacing }); break; case ConsoleLine::Warning: DrawString(out, line, { 0, lineYEnd }, TextRenderOptions { .flags = WarningTextUiFlags, .spacing = TextSpacing }); break; case ConsoleLine::Error: DrawString(out, line, { 0, lineYEnd }, TextRenderOptions { .flags = ErrorTextUiFlags, .spacing = TextSpacing }); break; } if (lineYEnd < 0 || begin == 0) break; end = begin - 1; } } } const ConsoleLine &GetConsoleLineFromEnd(int index) { return *(ConsoleLines.rbegin() + index); } void SetHistoryIndex(int index) { CurrentInputTextState = InputTextState::RestoredFromHistory; HistoryIndex = static_cast(std::ssize(ConsoleLines)) - (index + 1); if (HistoryIndex == -1) { ConsoleInputState.assign(DraftInput); return; } const ConsoleLine &line = ConsoleLines[index]; ConsoleInputState.assign(line.textWithoutPrompt()); } void PrevHistoryItem(tl::function_ref filter) { if (HistoryIndex == -1) { DraftInput = ConsoleInputState.value(); } const int n = static_cast(std::ssize(ConsoleLines)); for (int i = HistoryIndex + 1; i < n; ++i) { const int index = n - (i + 1); if (filter(ConsoleLines[index])) { SetHistoryIndex(index); return; } } } void NextHistoryItem(tl::function_ref filter) { const int n = static_cast(std::ssize(ConsoleLines)); for (int i = n - HistoryIndex; i < n; ++i) { if (filter(ConsoleLines[i])) { SetHistoryIndex(i); return; } } if (HistoryIndex != -1) { SetHistoryIndex(n); } } bool IsHistoryInputLine(const ConsoleLine &line) { if (line.type != ConsoleLine::Input) return false; std::string_view text = line.text; text.remove_prefix(Prompt.size()); if (text.empty()) return false; return HistoryIndex == -1 || GetConsoleLineFromEnd(HistoryIndex).textWithoutPrompt() != text; } void PrevInput() { PrevHistoryItem(IsHistoryInputLine); } void NextInput() { NextHistoryItem(IsHistoryInputLine); } bool IsHistoryOutputLine(const ConsoleLine &line) { return !line.text.empty() && (line.type == ConsoleLine::Output || line.type == ConsoleLine::Warning || line.type == ConsoleLine::Error) && (HistoryIndex == -1 || GetConsoleLineFromEnd(HistoryIndex).textWithoutPrompt() != line.text); } void PrevOutput() { PrevHistoryItem(IsHistoryOutputLine); } void NextOutput() { NextHistoryItem(IsHistoryOutputLine); } void AddInitialConsoleLines() { if (ConsolePrelude->has_value()) { std::string_view prelude { **ConsolePrelude }; if (!prelude.empty() && prelude.back() == '\n') prelude.remove_suffix(1); AddConsoleLine(ConsoleLine { .type = ConsoleLine::Help, .text = StrCat(HelpText, "\n", prelude) }); } else { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Help, .text = std::string(HelpText) }); AddConsoleLine(ConsoleLine { .type = ConsoleLine::Error, .text = ConsolePrelude->error() }); } } void ClearConsole() { ConsoleLines.clear(); HistoryIndex = -1; ScrollOffset = 0; NumPreparedConsoleLines = 0; ConsoleLinesTotalHeight = 0; AddInitialConsoleLines(); } } // namespace bool IsConsoleOpen() { return IsConsoleVisible; } void OpenConsole() { IsConsoleVisible = true; FirstRender = true; } void AcceptSuggestion() { const LuaAutocompleteSuggestion &suggestion = AutocompleteSuggestions[AutocompleteSuggestionFocusIndex]; ConsoleInputState.type(suggestion.completionText); if (suggestion.cursorAdjust == -1) { ConsoleInputState.moveCursorLeft(/*word=*/false); } } bool ConsoleHandleEvent(const SDL_Event &event) { if (!IsConsoleVisible) { // Make console open on the top-left keyboard key even if it is not a backtick. if (event.type == SDL_EVENT_KEY_DOWN && SDLC_EventScancode(event) == SDL_SCANCODE_GRAVE) { OpenConsole(); return true; } return false; } if (HandleTextInputEvent(event, ConsoleInputState)) { CurrentInputTextState = InputTextState::Edited; return true; } const auto modState = SDL_GetModState(); const bool isShift = (modState & SDL_KMOD_SHIFT) != 0; switch (event.type) { case SDL_EVENT_KEY_DOWN: switch (SDLC_EventKey(event)) { case SDLK_ESCAPE: if (!AutocompleteSuggestions.empty()) { AutocompleteSuggestions.clear(); AutocompleteSuggestionFocusIndex = -1; } else { CloseConsole(); } return true; case SDLK_UP: if (AutocompleteSuggestionFocusIndex != -1) { AutocompleteSuggestionFocusIndex = std::max( 0, AutocompleteSuggestionFocusIndex - 1); } else { isShift ? PrevOutput() : PrevInput(); } return true; case SDLK_DOWN: if (AutocompleteSuggestionFocusIndex != -1) { AutocompleteSuggestionFocusIndex = std::min( static_cast(AutocompleteSuggestions.size()) - 1, AutocompleteSuggestionFocusIndex + 1); } else { isShift ? NextOutput() : NextInput(); } return true; case SDLK_TAB: if (AutocompleteSuggestionFocusIndex != -1) { AcceptSuggestion(); CurrentInputTextState = InputTextState::Edited; } return true; case SDLK_RETURN: case SDLK_KP_ENTER: if (isShift) { ConsoleInputState.type("\n"); } else { if (AutocompleteSuggestionFocusIndex != -1) { AcceptSuggestion(); } else { SendInput(); } } CurrentInputTextState = InputTextState::Edited; return true; case SDLK_PAGEUP: ++PendingScrollPages; return true; case SDLK_PAGEDOWN: --PendingScrollPages; return true; case SDLK_L: ClearConsole(); return true; default: return false; } break; #ifndef USE_SDL1 case SDL_EVENT_MOUSE_WHEEL: if (SDLC_EventWheelIntY(event) > 0) { ScrollOffset += ScrollStep; } else if (SDLC_EventWheelIntY(event) < 0) { ScrollOffset -= ScrollStep; } return true; #else case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: if (event.button.button == SDL_BUTTON_WHEELUP) { ScrollOffset += ScrollStep; return true; } if (event.button.button == SDL_BUTTON_WHEELDOWN) { ScrollOffset -= ScrollStep; return true; } return false; #endif default: return false; } return false; } void DrawConsole(const Surface &out) { if (!IsConsoleVisible) return; OuterRect.position = { 0, 0 }; OuterRect.size = { out.w(), out.h() - GetMainPanel().size.height - 2 }; const std::string_view originalInputText = ConsoleInputState.value(); if (CurrentInputTextState != InputTextState::UpToDate) { WrappedInputText = WordWrapString(StrCat(Prompt, originalInputText), OuterRect.size.width - 2 * TextPaddingX, TextFontSize, TextSpacing); if (CurrentInputTextState == InputTextState::RestoredFromHistory) { AutocompleteSuggestions.clear(); } else { GetLuaAutocompleteSuggestions(originalInputText, ConsoleInputCursor.position, GetLuaReplEnvironment(), /*maxSuggestions=*/MaxSuggestions, AutocompleteSuggestions); } AutocompleteSuggestionsMaxWidth = -1; AutocompleteSuggestionFocusIndex = AutocompleteSuggestions.empty() ? -1 : 0; CurrentInputTextState = InputTextState::UpToDate; } const int numLines = static_cast(c_count(WrappedInputText, '\n')) + 1; InputRectHeight = std::min(OuterRect.size.height, numLines * LineHeight + TextPaddingYTop + TextPaddingYBottom); const int inputTextHeight = InputRectHeight - (TextPaddingYTop + TextPaddingYBottom); InputRect.position = { 0, OuterRect.size.height - InputRectHeight }; InputRect.size = { OuterRect.size.width, InputRectHeight }; const Rectangle inputTextRect { { InputRect.position.x + TextPaddingX, InputRect.position.y + TextPaddingYTop }, { InputRect.size.width - 2 * TextPaddingX, inputTextHeight } }; if (FirstRender) { SDL_Rect sdlInputRect = MakeSdlRect(InputRect); SDL_SetTextInputArea(ghMainWnd, &sdlInputRect, static_cast(ConsoleInputState.cursorPosition())); SDLC_StartTextInput(ghMainWnd); FirstRender = false; if (ConsoleLines.empty()) { InitConsole(); } } const Rectangle bgRect = OuterRect; DrawHalfTransparentRectTo(out, bgRect.position.x, bgRect.position.y, bgRect.size.width, bgRect.size.height); DrawConsoleLines( out.subregion( TextPaddingX, TextPaddingYTop, GetConsoleLinesInnerWidth(), OuterRect.size.height - inputTextRect.size.height - 8)); DrawHorizontalLine(out, InputRect.position - Displacement { 0, 1 }, InputRect.size.width, BorderColor); DrawInputText( out, Rectangle( inputTextRect.position, Size { // Extra space for the cursor on the right: inputTextRect.size.width + TextPaddingX, // Extra space for letters like g. inputTextRect.size.height + TextPaddingYBottom }), originalInputText, WrappedInputText); SDL_Rect sdlRect = MakeSdlRect(OuterRect); BltFast(&sdlRect, &sdlRect); } void InitConsole() { if (!ConsoleLines.empty()) return; ConsolePrelude = LoadAsset("lua\\repl_prelude.lua"); AddInitialConsoleLines(); if (ConsolePrelude->has_value()) RunLuaReplLine(std::string_view(**ConsolePrelude)); } void RunInConsole(std::string_view code) { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Input, .text = StrCat(Prompt, code) }); tl::expected result = RunLuaReplLine(code); if (result.has_value()) { if (!result->empty()) { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Output, .text = *std::move(result) }); } } else { if (!result.error().empty()) { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Error, .text = std::move(result).error() }); } else { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Error, .text = "Unknown error" }); } } } void PrintToConsole(std::string_view text) { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Output, .text = std::string(text) }); } void PrintWarningToConsole(std::string_view text) { AddConsoleLine(ConsoleLine { .type = ConsoleLine::Warning, .text = std::string(text) }); } } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/panels/console.hpp ================================================ #ifdef _DEBUG #pragma once #include #ifdef USE_SDL3 #include #else #include #endif #include "engine/surface.hpp" namespace devilution { void InitConsole(); bool IsConsoleOpen(); void OpenConsole(); bool ConsoleHandleEvent(const SDL_Event &event); void DrawConsole(const Surface &out); void RunInConsole(std::string_view code); void PrintToConsole(std::string_view text); void PrintWarningToConsole(std::string_view text); } // namespace devilution #endif // _DEBUG ================================================ FILE: Source/panels/info_box.cpp ================================================ #include "panels/info_box.hpp" #include "engine/load_cel.hpp" namespace devilution { OptionalOwnedClxSpriteList pSTextBoxCels; OptionalOwnedClxSpriteList pSTextSlidCels; void InitInfoBoxGfx() { pSTextSlidCels = LoadCel("data\\textslid", 12); pSTextBoxCels = LoadCel("data\\textbox2", 271); } void FreeInfoBoxGfx() { pSTextBoxCels = std::nullopt; pSTextSlidCels = std::nullopt; } } // namespace devilution ================================================ FILE: Source/panels/info_box.hpp ================================================ #pragma once #include "engine/clx_sprite.hpp" namespace devilution { /** * @brief Fixed size info box frame * * Used in stores, the quest log, the help window, and the unique item info window. */ extern OptionalOwnedClxSpriteList pSTextBoxCels; /** * @brief Dynamic size info box frame and scrollbar graphics. * * Used in stores and `DrawDiabloMsg`. */ extern OptionalOwnedClxSpriteList pSTextSlidCels; void InitInfoBoxGfx(); void FreeInfoBoxGfx(); } // namespace devilution ================================================ FILE: Source/panels/mainpanel.cpp ================================================ #include "panels/mainpanel.hpp" #include #include #include #include #include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "utils/display.h" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/status_macros.hpp" #include "utils/surface_to_clx.hpp" namespace devilution { OptionalOwnedClxSpriteList PanelButtonDown; OptionalOwnedClxSpriteList TalkButton; namespace { OptionalOwnedClxSpriteList PanelButton; OptionalOwnedClxSpriteList PanelButtonGrime; OptionalOwnedClxSpriteList PanelButtonDownGrime; void DrawButtonText(const Surface &out, std::string_view text, Rectangle placement, UiFlags style, int spacing = 1) { DrawString(out, text, { placement.position + Displacement { 0, 1 }, placement.size }, { .flags = UiFlags::AlignCenter | UiFlags::KerningFitSpacing | UiFlags::ColorBlack, .spacing = spacing }); DrawString(out, text, placement, { .flags = UiFlags::AlignCenter | UiFlags::KerningFitSpacing | style, .spacing = spacing }); } void DrawButtonOnPanel(Point position, std::string_view text, int frame) { RenderClxSprite(*BottomBuffer, (*PanelButton)[frame], position); int spacing = 2; int width = std::min(GetLineWidth(text, GameFont12, spacing), (*PanelButton)[0].width()); if (width > 38) { spacing = 1; width = std::min(GetLineWidth(text, GameFont12, spacing), (*PanelButton)[0].width()); } RenderClxSprite(BottomBuffer->subregion(position.x + ((*PanelButton)[0].width() - width) / 2, position.y + 7, width, BottomBuffer->h() - 7), (*PanelButtonGrime)[frame], { 0, 0 }); DrawButtonText(*BottomBuffer, text, { position, { (*PanelButton)[0].width(), 0 } }, UiFlags::ColorButtonface, spacing); } void RenderMainButton(const Surface &out, int buttonId, std::string_view text, int frame) { const Point panelPosition { MainPanelButtonRect[buttonId].position + Displacement { 4, 17 } }; DrawButtonOnPanel(panelPosition, text, frame); if (IsChatAvailable()) DrawButtonOnPanel(panelPosition + Displacement { 0, GetMainPanel().size.height + 16 }, text, frame); const Point position { 0, 19 * buttonId }; int spacing = 2; int width = std::min(GetLineWidth(text, GameFont12, spacing), (*PanelButton)[0].width()); if (width > 38) { spacing = 1; width = std::min(GetLineWidth(text, GameFont12, spacing), (*PanelButton)[0].width()); } RenderClxSprite(out.subregion(position.x + ((*PanelButton)[0].width() - width) / 2, position.y + 9, width, out.h() - position.y - 9), (*PanelButtonDownGrime)[frame], { 0, 0 }); DrawButtonText(out, text, { position + Displacement { 0, 2 }, { out.w(), 0 } }, UiFlags::ColorButtonpushed, spacing); } } // namespace tl::expected LoadMainPanel() { std::optional out; constexpr uint16_t NumButtonSprites = 6; { ASSIGN_OR_RETURN(OptionalOwnedClxSpriteList background, LoadClxWithStatus("data\\panel8bucp.clx")); out.emplace((*background)[0].width(), (*background)[0].height() * NumButtonSprites); int y = 0; for (const ClxSprite sprite : ClxSpriteList(*background)) { RenderClxSprite(*out, sprite, { 0, y }); y += sprite.height(); } } PanelButton = LoadOptionalClx("data\\panel8buc.clx"); PanelButtonGrime = LoadOptionalClx("data\\dirtybuc.clx"); PanelButtonDownGrime = LoadOptionalClx("data\\dirtybucp.clx"); RenderMainButton(*out, 0, _("char"), 0); RenderMainButton(*out, 1, _("quests"), 1); RenderMainButton(*out, 2, _("map"), 1); RenderMainButton(*out, 3, _("menu"), 0); RenderMainButton(*out, 4, _("inv"), 1); RenderMainButton(*out, 5, _("spells"), 0); PanelButtonDown = SurfaceToClx(*out, NumButtonSprites); out = std::nullopt; if (IsChatAvailable()) { OptionalOwnedClxSpriteList talkButton = LoadClx("data\\talkbutton.clx"); const int talkButtonWidth = (*talkButton)[0].width(); constexpr size_t NumOtherPlayers = 3; // Render the unpressed voice buttons to BottomBuffer. const std::string_view text = _("voice"); const int textWidth = GetLineWidth(text, GameFont12, 1); for (size_t i = 0; i < NumOtherPlayers; ++i) { const Point position { 176, static_cast(GetMainPanel().size.height + 101 + 18 * i) }; RenderClxSprite(*BottomBuffer, (*talkButton)[0], position); const int width = std::min(textWidth, (*PanelButton)[0].width()); RenderClxSprite(BottomBuffer->subregion(position.x + (talkButtonWidth - width) / 2, position.y + 6, width, 9), (*PanelButtonGrime)[1], { 0, 0 }); DrawButtonText(*BottomBuffer, text, { position, { talkButtonWidth, 0 } }, UiFlags::ColorButtonface); } const int talkButtonHeight = (*talkButton)[0].height(); constexpr uint16_t NumTalkButtonSprites = 3; const OwnedSurface talkSurface(talkButtonWidth, talkButtonHeight * NumTalkButtonSprites); // Prerender translated versions of the other button states for voice buttons RenderClxSprite(talkSurface, (*talkButton)[0], { 0, 0 }); // background for unpressed mute button RenderClxSprite(talkSurface, (*talkButton)[1], { 0, talkButtonHeight }); // background for pressed mute button RenderClxSprite(talkSurface, (*talkButton)[1], { 0, talkButtonHeight * 2 }); // background for pressed voice button talkButton = std::nullopt; const int muteWidth = GetLineWidth(_("mute"), GameFont12, 2); RenderClxSprite(talkSurface.subregion((talkButtonWidth - muteWidth) / 2, 6, muteWidth, 9), (*PanelButtonGrime)[1], { 0, 0 }); DrawButtonText(talkSurface, _("mute"), { { 0, 0 }, { talkButtonWidth, 0 } }, UiFlags::ColorButtonface); RenderClxSprite(talkSurface.subregion((talkButtonWidth - muteWidth) / 2, 23, muteWidth, 9), (*PanelButtonGrime)[1], { 0, 0 }); DrawButtonText(talkSurface, _("mute"), { { 0, 17 }, { talkButtonWidth, 0 } }, UiFlags::ColorButtonpushed); const int voiceWidth = GetLineWidth(_("voice"), GameFont12, 2); RenderClxSprite(talkSurface.subregion((talkButtonWidth - voiceWidth) / 2, 39, voiceWidth, 9), (*PanelButtonGrime)[1], { 0, 0 }); DrawButtonText(talkSurface, _("voice"), { { 0, 33 }, { talkButtonWidth, 0 } }, UiFlags::ColorButtonpushed); TalkButton = SurfaceToClx(talkSurface, NumTalkButtonSprites); } PanelButtonDownGrime = std::nullopt; PanelButtonGrime = std::nullopt; PanelButton = std::nullopt; return {}; } void FreeMainPanel() { TalkButton = std::nullopt; PanelButtonDown = std::nullopt; } } // namespace devilution ================================================ FILE: Source/panels/mainpanel.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" namespace devilution { extern OptionalOwnedClxSpriteList PanelButtonDown; extern OptionalOwnedClxSpriteList TalkButton; tl::expected LoadMainPanel(); void FreeMainPanel(); } // namespace devilution ================================================ FILE: Source/panels/partypanel.cpp ================================================ #include "panels/partypanel.hpp" #include #include #include "automap.h" #include "control/control.hpp" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_clx.hpp" #include "engine/palette.h" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/size.hpp" #include "inv.h" #include "options.h" #include "pfile.h" #include "qol/monhealthbar.h" #include "qol/stash.h" #include "stores.h" #include "tables/playerdat.hpp" #include "utils/status_macros.hpp" #include "utils/surface_to_clx.hpp" namespace devilution { namespace { struct PartySpriteOffset { Point inTownOffset; Point inDungeonOffset; Point isDeadOffset; }; const PartySpriteOffset ClassSpriteOffsets[] = { { { -4, -18 }, { 6, -21 }, { -6, -50 } }, { { -2, -18 }, { 1, -20 }, { -8, -35 } }, { { -2, -16 }, { 3, -20 }, { 0, -50 } }, { { -2, -19 }, { 1, -19 }, { 28, -60 } } }; OptionalOwnedClxSpriteList PartyMemberFrame; OptionalOwnedClxSpriteList PlayerTags; Point PartyPanelPos = { 8, 8 }; Rectangle PortraitFrameRects[MAX_PLRS]; int RightClickedPortraitIndex = -1; constexpr int HealthBarHeight = 7; constexpr int ManaBarHeight = 7; constexpr int FrameGap = 15; constexpr int FrameBorderSize = 3; constexpr int FrameSpriteSize = 12; constexpr Size FrameSections = { 4, 4 }; // x/y can't be less than 2 constexpr Size PortraitFrameSize = { FrameSections.width * FrameSpriteSize, FrameSections.height *FrameSpriteSize }; constexpr uint8_t FrameBackgroundColor = PAL16_BLUE + 14; void DrawBar(const Surface &out, Rectangle rect, uint8_t color) { for (int x = 0; x < rect.size.width; x++) { DrawVerticalLine(out, { rect.position.x + x, rect.position.y }, rect.size.height, color); } } void DrawMemberFrame(const Surface &out, OwnedClxSpriteList &frame, Point pos) { // Draw the frame background FillRect(out, pos.x, pos.y, PortraitFrameSize.width, PortraitFrameSize.height, FrameBackgroundColor); // Now draw the frame border const Size adjustedFrame = { FrameSections.width - 1, FrameSections.height - 1 }; for (int x = 0; x <= adjustedFrame.width; x++) { for (int y = 0; y <= adjustedFrame.height; y++) { // Get what section of the frame we're drawing int spriteIndex = -1; if (x == 0 && y == 0) spriteIndex = 0; // Top-left corner else if (x == 0 && y == adjustedFrame.height) spriteIndex = 1; // Bottom-left corner else if (x == adjustedFrame.width && y == adjustedFrame.height) spriteIndex = 2; // Bottom-right corner else if (x == adjustedFrame.width && y == 0) spriteIndex = 3; // Top-right corner else if (y == 0) spriteIndex = 4; // Top border else if (x == 0) spriteIndex = 5; // Left border else if (y == adjustedFrame.height) spriteIndex = 6; // Bottom border else if (x == adjustedFrame.width) spriteIndex = 7; // Right border if (spriteIndex != -1) { // Draw the frame section RenderClxSprite(out, frame[spriteIndex], { pos.x + (x * FrameSpriteSize), pos.y + (y * FrameSpriteSize) }); } } } } void HandleRightClickPortait() { Player &player = Players[RightClickedPortraitIndex]; if (player.plractive && &player != MyPlayer) { InspectPlayer = &player; OpenCharPanel(); if (!SpellbookFlag) invflag = true; RedrawEverything(); RightClickedPortraitIndex = -1; } } PartySpriteOffset GetClassSpriteOffset(HeroClass hClass) { switch (hClass) { case HeroClass::Bard: hClass = HeroClass::Rogue; break; case HeroClass::Barbarian: hClass = HeroClass::Warrior; break; default: break; } return ClassSpriteOffsets[static_cast(hClass)]; } } // namespace bool PartySidePanelOpen = true; bool InspectingFromPartyPanel; int PortraitIdUnderCursor = -1; tl::expected LoadPartyPanel() { ASSIGN_OR_RETURN(OwnedClxSpriteList frame, LoadCelWithStatus("data\\textslid", FrameSpriteSize)); ASSIGN_OR_RETURN(PlayerTags, LoadClxWithStatus("data\\monstertags.clx")); const OwnedSurface out(PortraitFrameSize.width, PortraitFrameSize.height + HealthBarHeight + ManaBarHeight); // Draw the health bar background DrawBar(out, { { 0, 0 }, { PortraitFrameSize.width, HealthBarHeight } }, PAL16_GRAY + 10); // Draw the frame the character portrait sprite will go DrawMemberFrame(out, frame, { 0, HealthBarHeight }); // Draw the mana bar background DrawBar(out, { { 0, HealthBarHeight + PortraitFrameSize.height }, { PortraitFrameSize.width, ManaBarHeight } }, PAL16_GRAY + 10); PartyMemberFrame = SurfaceToClx(out); return {}; } void FreePartyPanel() { PartyMemberFrame = std::nullopt; PlayerTags = std::nullopt; } void DrawPartyMemberInfoPanel(const Surface &out) { // Don't draw based on these criteria if (CharFlag || !gbIsMultiplayer || !MyPlayer->friendlyMode || IsPlayerInStore() || IsStashOpen) { if (PortraitIdUnderCursor != -1) PortraitIdUnderCursor = -1; return; } Point pos = PartyPanelPos; if (AutomapActive) pos.y += (FrameGap * 4); if (*GetOptions().Graphics.showFPS) pos.y += FrameGap; int currentLongestNameWidth = PortraitFrameSize.width; bool portraitUnderCursor = false; for (Player &player : Players) { if (!player.plractive || !player.friendlyMode) continue; #ifndef _DEBUG if (&player == MyPlayer) continue; #endif // Get the rect of the portrait to use later const Rectangle currentPortraitRect = { pos, PortraitFrameSize }; const Surface gameScreen = out.subregionY(0, gnViewportHeight); // Draw the characters frame RenderClxSprite(gameScreen, (*PartyMemberFrame)[0], pos); // Get the players remaining life // If the player is using mana shield change the color const int lifeTicks = ((player._pHitPoints * PortraitFrameSize.width) + (player._pMaxHP / 2)) / player._pMaxHP; const uint8_t hpBarColor = (player.pManaShield) ? PAL8_YELLOW + 5 : PAL8_RED + 4; // Now draw the characters remaining life DrawBar(gameScreen, { pos, { lifeTicks, HealthBarHeight } }, hpBarColor); // Add to the position before continuing to the next item pos.y += HealthBarHeight; // Get the players current portrait sprite const ClxSprite playerPortraitSprite = GetPlayerPortraitSprite(player); // Get the offset of the sprite based on the players class so it get's rendered in the correct position const PartySpriteOffset offsets = GetClassSpriteOffset(player._pClass); Point offset = (player.isOnLevel(0)) ? offsets.inTownOffset : offsets.inDungeonOffset; if (player._pHitPoints <= 0 && IsPlayerUnarmed(player)) offset = offsets.isDeadOffset; // Calculate the players portait position const Point portraitPos = { ((-(playerPortraitSprite.width() / 2)) + (PortraitFrameSize.width / 2)) + offset.x, offset.y }; // Get a subregion of the surface so the portrait doesn't get drawn over the frame const Surface frameSubregion = gameScreen.subregion( pos.x + FrameBorderSize, pos.y + FrameBorderSize, PortraitFrameSize.width - (FrameBorderSize * 2), PortraitFrameSize.height - (FrameBorderSize * 2)); PortraitFrameRects[player.getId()] = { { frameSubregion.region.x, frameSubregion.region.y }, { frameSubregion.region.w, frameSubregion.region.h } }; // Draw the portrait sprite RenderClxSprite( frameSubregion, playerPortraitSprite, portraitPos); if ((player.getId() + 1U) < (*PlayerTags).numSprites()) { // Draw the player tag const int tagWidth = (*PlayerTags)[player.getId() + 1].width(); RenderClxSprite( frameSubregion, (*PlayerTags)[player.getId() + 1], { PortraitFrameSize.width - (tagWidth + (tagWidth / 2)), 0 }); } // Check to see if the player is dead and if so we draw a half transparent red rect over the portrait if (player._pHitPoints <= 0) { DrawHalfTransparentRectTo( frameSubregion, 0, 0, PortraitFrameSize.width, PortraitFrameSize.height, PAL8_RED + 4); } // Add to the position before continuing to the next item pos.y += PortraitFrameSize.height; // Get the players remaining mana const int manaTicks = ((player._pMana * PortraitFrameSize.width) + (player._pMaxMana / 2)) / player._pMaxMana; const uint8_t manaBarColor = PAL8_BLUE + 3; // Now draw the characters remaining mana DrawBar(gameScreen, { pos, { manaTicks, ManaBarHeight } }, manaBarColor); // Add to the position before continuing to the next item pos.y += ManaBarHeight; // Draw the players name under the frame DrawString( gameScreen, player._pName, pos, { .flags = UiFlags::ColorGold | UiFlags::Outlined | UiFlags::FontSize12 }); // Add to the position before continuing onto the next player pos.y += FrameGap + 5; // Check to see if the player is hovering over this portrait and if so draw a string under the cursor saying they can right click to inspect if (currentPortraitRect.contains(MousePosition)) { PortraitIdUnderCursor = player.getId(); portraitUnderCursor = true; } // Get the current players name width const int width = GetLineWidth(player._pName); // Now check to see if it's the current longest name if (width >= currentLongestNameWidth) currentLongestNameWidth = width; // Check to see if the Y position is more then the main panel position if (pos.y >= GetMainPanel().position.y - PortraitFrameSize.height - 10) { // If so we need to draw the next set of portraits back at the top and to the right of the original position pos.y = PartyPanelPos.y; if (AutomapActive) pos.y += (FrameGap * 4); if (*GetOptions().Graphics.showFPS) pos.y += FrameGap; // Add the current longest name width to the X position pos.x += currentLongestNameWidth + (FrameGap / 2); } } if (RightClickedPortraitIndex != -1) HandleRightClickPortait(); if (!portraitUnderCursor) PortraitIdUnderCursor = -1; } bool DidRightClickPartyPortrait() { for (size_t i = 0; i < sizeof(PortraitFrameRects) / sizeof(PortraitFrameRects[0]); i++) { if (PortraitFrameRects[i].contains(MousePosition)) { RightClickedPortraitIndex = static_cast(i); InspectingFromPartyPanel = true; return true; } } return false; } } // namespace devilution ================================================ FILE: Source/panels/partypanel.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" namespace devilution { extern bool PartySidePanelOpen; extern bool InspectingFromPartyPanel; extern int PortraitIdUnderCursor; tl::expected LoadPartyPanel(); void FreePartyPanel(); void DrawPartyMemberInfoPanel(const Surface &out); bool DidRightClickPartyPortrait(); } // namespace devilution ================================================ FILE: Source/panels/spell_book.cpp ================================================ #include "panels/spell_book.hpp" #include #include #include #include #include #include "control/control.hpp" #include "engine/backbuffer_state.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_clx.hpp" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "game_mode.hpp" #include "missiles.h" #include "panels/spell_icons.hpp" #include "panels/ui_panels.hpp" #include "player.h" #include "tables/spelldat.h" #include "utils/language.h" #include "utils/status_macros.hpp" namespace devilution { namespace { OptionalOwnedClxSpriteList spellBookButtons; OptionalOwnedClxSpriteList spellBookBackground; const size_t SpellBookPages = 6; const size_t SpellBookPageEntries = 7; constexpr uint16_t SpellBookButtonWidthDiablo = 76; constexpr uint16_t SpellBookButtonWidthHellfire = 61; uint16_t SpellBookButtonWidth() { return gbIsHellfire ? SpellBookButtonWidthHellfire : SpellBookButtonWidthDiablo; } /** Maps from spellbook page number and position to SpellID. */ const SpellID SpellPages[SpellBookPages][SpellBookPageEntries] = { { SpellID::Null, SpellID::Firebolt, SpellID::ChargedBolt, SpellID::HolyBolt, SpellID::Healing, SpellID::HealOther, SpellID::Inferno }, { SpellID::Resurrect, SpellID::FireWall, SpellID::Telekinesis, SpellID::Lightning, SpellID::TownPortal, SpellID::Flash, SpellID::StoneCurse }, { SpellID::Phasing, SpellID::ManaShield, SpellID::Elemental, SpellID::Fireball, SpellID::FlameWave, SpellID::ChainLightning, SpellID::Guardian }, { SpellID::Nova, SpellID::Golem, SpellID::Teleport, SpellID::Apocalypse, SpellID::BoneSpirit, SpellID::BloodStar, SpellID::Etherealize }, { SpellID::LightningWall, SpellID::Immolation, SpellID::Warp, SpellID::Reflect, SpellID::Berserk, SpellID::RingOfFire, SpellID::Search }, { SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid, SpellID::Invalid } }; SpellID GetSpellFromSpellPage(size_t page, size_t entry) { assert(page <= SpellBookPages && entry <= SpellBookPageEntries); if (page == 0 && entry == 0) return GetPlayerStartingLoadoutForClass(InspectPlayer->_pClass).skill; return SpellPages[page][entry]; } constexpr Size SpellBookDescription { 250, 43 }; constexpr int SpellBookDescriptionPaddingHorizontal = 2; void PrintSBookStr(const Surface &out, Point position, std::string_view text, UiFlags flags = UiFlags::None) { DrawString(out, text, Rectangle(GetPanelPosition(UiPanels::Spell, position + Displacement { SPLICONLENGTH, 0 }), SpellBookDescription) .inset({ SpellBookDescriptionPaddingHorizontal, 0 }), { .flags = UiFlags::ColorWhite | flags }); } SpellType GetSBookTrans(SpellID ii, bool townok) { const Player &player = *InspectPlayer; if (ii == GetPlayerStartingLoadoutForClass(player._pClass).skill) return SpellType::Skill; SpellType st = SpellType::Spell; if ((player._pISpells & GetSpellBitmask(ii)) != 0) { st = SpellType::Charges; } if ((player._pAblSpells & GetSpellBitmask(ii)) != 0) { st = SpellType::Skill; } if (st == SpellType::Spell) { if (CheckSpell(*InspectPlayer, ii, st, true) != SpellCheckResult::Success) { st = SpellType::Invalid; } if (player.GetSpellLevel(ii) == 0) { st = SpellType::Invalid; } } if (townok && leveltype == DTYPE_TOWN && st != SpellType::Invalid && !GetSpellData(ii).isAllowedInTown()) { st = SpellType::Invalid; } return st; } StringOrView GetSpellPowerText(SpellID spell, int spellLevel) { if (spellLevel == 0) { return _("Unusable"); } if (spell == SpellID::BoneSpirit) { return _(/* TRANSLATORS: UI constraints, keep short please.*/ "Dmg: 1/3 target hp"); } const auto [min, max] = GetDamageAmt(spell, spellLevel); if (min == -1) { return StringOrView {}; } if (spell == SpellID::Healing || spell == SpellID::HealOther) { return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Heals: {:d} - {:d}")), min, max); } return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Damage: {:d} - {:d}")), min, max); } } // namespace tl::expected InitSpellBook() { ASSIGN_OR_RETURN(spellBookBackground, LoadCelWithStatus("data\\spellbk", static_cast(SidePanelSize.width))); ASSIGN_OR_RETURN(spellBookButtons, LoadCelWithStatus("data\\spellbkb", SpellBookButtonWidth())); return LoadSmallSpellIcons(); } void FreeSpellBook() { FreeSmallSpellIcons(); spellBookButtons = std::nullopt; spellBookBackground = std::nullopt; } void DrawSpellBook(const Surface &out) { constexpr int SpellBookButtonX = 7; constexpr int SpellBookButtonY = 348; ClxDraw(out, GetPanelPosition(UiPanels::Spell, { 0, 351 }), (*spellBookBackground)[0]); const int buttonX = gbIsHellfire && SpellbookTab < 5 ? SpellBookButtonWidthHellfire * SpellbookTab : SpellBookButtonWidthDiablo * SpellbookTab // BUGFIX: rendering of page 3 and page 4 buttons are both off-by-one pixel (fixed). + (SpellbookTab == 2 || SpellbookTab == 3 ? 1 : 0); ClxDraw(out, GetPanelPosition(UiPanels::Spell, { SpellBookButtonX + buttonX, SpellBookButtonY }), (*spellBookButtons)[SpellbookTab]); const Player &player = *InspectPlayer; const uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; const int lineHeight = 18; int yp = 12; const int textPaddingTop = 7; for (size_t pageEntry = 0; pageEntry < SpellBookPageEntries; pageEntry++) { const SpellID sn = GetSpellFromSpellPage(SpellbookTab, pageEntry); if (IsValidSpell(sn) && (spl & GetSpellBitmask(sn)) != 0) { const SpellType st = GetSBookTrans(sn, true); SetSpellTrans(st); const Point spellCellPosition = GetPanelPosition(UiPanels::Spell, { 11, yp + SpellBookDescription.height }); DrawSmallSpellIcon(out, spellCellPosition, sn); if (sn == player._pRSpell && st == player._pRSplType && !IsInspectingPlayer()) { SetSpellTrans(SpellType::Skill); DrawSmallSpellIconBorder(out, spellCellPosition); } const Point line0 { 0, yp + textPaddingTop }; const Point line1 { 0, yp + textPaddingTop + lineHeight }; PrintSBookStr(out, line0, pgettext("spell", GetSpellData(sn).sNameText)); switch (GetSBookTrans(sn, false)) { case SpellType::Skill: PrintSBookStr(out, line1, _("Skill")); break; case SpellType::Charges: { const int charges = player.InvBody[INVLOC_HAND_LEFT]._iCharges; PrintSBookStr(out, line1, fmt::format(fmt::runtime(ngettext("Staff ({:d} charge)", "Staff ({:d} charges)", charges)), charges)); } break; default: { const int mana = GetManaAmount(player, sn) >> 6; const int lvl = player.GetSpellLevel(sn); PrintSBookStr(out, line0, fmt::format(fmt::runtime(pgettext(/* TRANSLATORS: UI constraints, keep short please.*/ "spellbook", "Level {:d}")), lvl), UiFlags::AlignRight); if (const StringOrView text = GetSpellPowerText(sn, lvl); !text.empty()) { PrintSBookStr(out, line1, text, UiFlags::AlignRight); } PrintSBookStr(out, line1, fmt::format(fmt::runtime(pgettext(/* TRANSLATORS: UI constraints, keep short please.*/ "spellbook", "Mana: {:d}")), mana)); } break; } } yp += SpellBookDescription.height; } } void CheckSBook() { // Icons are drawn in a column near the left side of the panel and aligned with the spell book description entries // Spell icons/buttons are 37x38 pixels, laid out from 11,18 with a 5 pixel margin between each icon. This is close // enough to the height of the space given to spell descriptions that we can reuse that value and subtract the // padding from the end of the area. const Rectangle iconArea = { GetPanelPosition(UiPanels::Spell, { 11, 18 }), Size { 37, SpellBookDescription.height * 7 - 5 } }; if (iconArea.contains(MousePosition) && !IsInspectingPlayer()) { const SpellID sn = GetSpellFromSpellPage(SpellbookTab, (MousePosition.y - iconArea.position.y) / SpellBookDescription.height); Player &player = *InspectPlayer; const uint64_t spl = player._pMemSpells | player._pISpells | player._pAblSpells; if (IsValidSpell(sn) && (spl & GetSpellBitmask(sn)) != 0) { SpellType st = SpellType::Spell; if ((player._pISpells & GetSpellBitmask(sn)) != 0) { st = SpellType::Charges; } if ((player._pAblSpells & GetSpellBitmask(sn)) != 0) { st = SpellType::Skill; } player._pRSpell = sn; player._pRSplType = st; RedrawEverything(); } return; } // The width of the panel excluding the border is 305 pixels. This does not cleanly divide by 4 meaning Diablo tabs // end up with an extra pixel somewhere around the buttons. Vanilla Diablo had the buttons left-aligned, devilutionX // instead justifies the buttons and puts the gap between buttons 2/3. See DrawSpellBook const int buttonWidth = SpellBookButtonWidth(); // Tabs are drawn in a row near the bottom of the panel const Rectangle tabArea = { GetPanelPosition(UiPanels::Spell, { 7, 320 }), Size { 305, 29 } }; if (tabArea.contains(MousePosition)) { int hitColumn = MousePosition.x - tabArea.position.x; // Clicking on the gutter currently activates tab 3. Could make it do nothing by checking for == here and return early. if (!gbIsHellfire && hitColumn > buttonWidth * 2) { // Subtract 1 pixel to account for the gutter between buttons 2/3 hitColumn--; } SpellbookTab = hitColumn / buttonWidth; } } } // namespace devilution ================================================ FILE: Source/panels/spell_book.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" namespace devilution { tl::expected InitSpellBook(); void FreeSpellBook(); void CheckSBook(); void DrawSpellBook(const Surface &out); } // namespace devilution ================================================ FILE: Source/panels/spell_icons.cpp ================================================ #include "panels/spell_icons.hpp" #include #include #include "engine/load_cel.hpp" #include "engine/load_clx.hpp" #include "engine/palette.h" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" namespace devilution { namespace { #ifdef UNPACKED_MPQS OptionalOwnedClxSpriteList LargeSpellIconsBackground; OptionalOwnedClxSpriteList SmallSpellIconsBackground; #endif OptionalOwnedClxSpriteList SmallSpellIcons; OptionalOwnedClxSpriteList LargeSpellIcons; uint8_t SplTransTbl[256]; /** Maps from SpellID to spelicon.cel frame number. */ const SpellIcon SpellITbl[] = { // clang-format off /* SpellID::Null */ SpellIcon::Empty, /* SpellID::Firebolt */ SpellIcon::Firebolt, /* SpellID::Healing */ SpellIcon::Healing, /* SpellID::Lightning */ SpellIcon::Lightning, /* SpellID::Flash */ SpellIcon::Flash, /* SpellID::Identify */ SpellIcon::Identify, /* SpellID::FireWall */ SpellIcon::FireWall, /* SpellID::TownPortal */ SpellIcon::TownPortal, /* SpellID::StoneCurse */ SpellIcon::StoneCurse, /* SpellID::Infravision */ SpellIcon::Infravision, /* SpellID::Phasing */ SpellIcon::Phasing, /* SpellID::ManaShield */ SpellIcon::ManaShield, /* SpellID::Fireball */ SpellIcon::Fireball, /* SpellID::Guardian */ SpellIcon::DoomSerpents, /* SpellID::ChainLightning */ SpellIcon::ChainLightning, /* SpellID::FlameWave */ SpellIcon::FlameWave, /* SpellID::DoomSerpents */ SpellIcon::DoomSerpents, /* SpellID::BloodRitual */ SpellIcon::BloodRitual, /* SpellID::Nova */ SpellIcon::Nova, /* SpellID::Invisibility */ SpellIcon::Invisibility, /* SpellID::Inferno */ SpellIcon::Inferno, /* SpellID::Golem */ SpellIcon::Golem, /* SpellID::Rage */ SpellIcon::BloodBoil, /* SpellID::Teleport */ SpellIcon::Teleport, /* SpellID::Apocalypse */ SpellIcon::Apocalypse, /* SpellID::Etherealize */ SpellIcon::Etherealize, /* SpellID::ItemRepair */ SpellIcon::ItemRepair, /* SpellID::StaffRecharge */ SpellIcon::StaffRecharge, /* SpellID::TrapDisarm */ SpellIcon::TrapDisarm, /* SpellID::Elemental */ SpellIcon::Elemental, /* SpellID::ChargedBolt */ SpellIcon::ChargedBolt, /* SpellID::HolyBolt */ SpellIcon::HolyBolt, /* SpellID::Resurrect */ SpellIcon::Resurrect, /* SpellID::Telekinesis */ SpellIcon::Telekinesis, /* SpellID::HealOther */ SpellIcon::HealOther, /* SpellID::BloodStar */ SpellIcon::BloodStar, /* SpellID::BoneSpirit */ SpellIcon::BoneSpirit, /* SpellID::Mana */ SpellIcon::Mana, /* SpellID::Magi */ SpellIcon::Mana, /* SpellID::Jester */ SpellIcon::Jester, /* SpellID::LightningWall */ SpellIcon::LightningWall, /* SpellID::Immolation */ SpellIcon::Immolation, /* SpellID::Warp */ SpellIcon::Warp, /* SpellID::Reflect */ SpellIcon::Reflect, /* SpellID::Berserk */ SpellIcon::Berserk, /* SpellID::RingOfFire */ SpellIcon::RingOfFire, /* SpellID::Search */ SpellIcon::Search, /* SpellID::RuneOfFire */ SpellIcon::PentaStar, /* SpellID::RuneOfLight */ SpellIcon::PentaStar, /* SpellID::RuneOfNova */ SpellIcon::PentaStar, /* SpellID::RuneOfImmolation */ SpellIcon::PentaStar, /* SpellID::RuneOfStone */ SpellIcon::PentaStar, // clang-format on }; } // namespace tl::expected LoadLargeSpellIcons() { #ifdef UNPACKED_MPQS LargeSpellIcons = LoadOptionalClx("data\\spelicon_fg.clx"); LargeSpellIconsBackground = LoadOptionalClx("data\\spelicon_bg.clx"); if (!LargeSpellIcons.has_value() || !LargeSpellIconsBackground.has_value()) { ASSIGN_OR_RETURN(LargeSpellIcons, LoadClxWithStatus("ctrlpan\\spelicon_fg.clx")); ASSIGN_OR_RETURN(LargeSpellIconsBackground, LoadClxWithStatus("ctrlpan\\spelicon_bg.clx")); } #else LargeSpellIcons = LoadOptionalCel("data\\spelicon", SPLICONLENGTH); if (!LargeSpellIcons.has_value()) { ASSIGN_OR_RETURN(LargeSpellIcons, LoadCelWithStatus("ctrlpan\\spelicon", SPLICONLENGTH)); } #endif SetSpellTrans(SpellType::Skill); return {}; } void FreeLargeSpellIcons() { #ifdef UNPACKED_MPQS LargeSpellIconsBackground = std::nullopt; #endif LargeSpellIcons = std::nullopt; } tl::expected LoadSmallSpellIcons() { #ifdef UNPACKED_MPQS ASSIGN_OR_RETURN(SmallSpellIcons, LoadClxWithStatus("data\\spelli2_fg.clx")); ASSIGN_OR_RETURN(SmallSpellIconsBackground, LoadClxWithStatus("data\\spelli2_bg.clx")); #else ASSIGN_OR_RETURN(SmallSpellIcons, LoadCelWithStatus("data\\spelli2", 37)); #endif return {}; } void FreeSmallSpellIcons() { #ifdef UNPACKED_MPQS SmallSpellIconsBackground = std::nullopt; #endif SmallSpellIcons = std::nullopt; } uint8_t GetSpellIconFrame(SpellID spell) { return static_cast(SpellITbl[static_cast(spell)]); } void DrawLargeSpellIcon(const Surface &out, Point position, SpellID spell) { #ifdef UNPACKED_MPQS ClxDrawTRN(out, position, (*LargeSpellIconsBackground)[0], SplTransTbl); #endif ClxDrawTRN(out, position, (*LargeSpellIcons)[GetSpellIconFrame(spell)], SplTransTbl); } void DrawSmallSpellIcon(const Surface &out, Point position, SpellID spell) { #ifdef UNPACKED_MPQS ClxDrawTRN(out, position, (*SmallSpellIconsBackground)[0], SplTransTbl); #endif ClxDrawTRN(out, position, (*SmallSpellIcons)[GetSpellIconFrame(spell)], SplTransTbl); } void DrawLargeSpellIconBorder(const Surface &out, Point position, uint8_t color) { const int width = (*LargeSpellIcons)[0].width(); const int height = (*LargeSpellIcons)[0].height(); UnsafeDrawBorder2px(out, Rectangle { Point { position.x, position.y - height + 1 }, Size { width, height } }, color); } void DrawSmallSpellIconBorder(const Surface &out, Point position) { const int width = (*SmallSpellIcons)[0].width(); const int height = (*SmallSpellIcons)[0].height(); UnsafeDrawBorder2px(out, Rectangle { Point { position.x, position.y - height + 1 }, Size { width, height } }, SplTransTbl[PAL8_YELLOW + 2]); } void SetSpellTrans(SpellType t) { if (t == SpellType::Skill) { for (int i = 0; i < 128; i++) SplTransTbl[i] = i; } for (int i = 128; i < 256; i++) SplTransTbl[i] = i; SplTransTbl[255] = 0; switch (t) { case SpellType::Spell: SplTransTbl[PAL8_YELLOW] = PAL16_BLUE + 1; SplTransTbl[PAL8_YELLOW + 1] = PAL16_BLUE + 3; SplTransTbl[PAL8_YELLOW + 2] = PAL16_BLUE + 5; for (int i = PAL16_BLUE; i < PAL16_BLUE + 16; i++) { SplTransTbl[PAL16_BEIGE - PAL16_BLUE + i] = i; SplTransTbl[PAL16_YELLOW - PAL16_BLUE + i] = i; SplTransTbl[PAL16_ORANGE - PAL16_BLUE + i] = i; } break; case SpellType::Scroll: SplTransTbl[PAL8_YELLOW] = PAL16_BEIGE + 1; SplTransTbl[PAL8_YELLOW + 1] = PAL16_BEIGE + 3; SplTransTbl[PAL8_YELLOW + 2] = PAL16_BEIGE + 5; for (int i = PAL16_BEIGE; i < PAL16_BEIGE + 16; i++) { SplTransTbl[PAL16_YELLOW - PAL16_BEIGE + i] = i; SplTransTbl[PAL16_ORANGE - PAL16_BEIGE + i] = i; } break; case SpellType::Charges: SplTransTbl[PAL8_YELLOW] = PAL16_ORANGE + 1; SplTransTbl[PAL8_YELLOW + 1] = PAL16_ORANGE + 3; SplTransTbl[PAL8_YELLOW + 2] = PAL16_ORANGE + 5; for (int i = PAL16_ORANGE; i < PAL16_ORANGE + 16; i++) { SplTransTbl[PAL16_BEIGE - PAL16_ORANGE + i] = i; SplTransTbl[PAL16_YELLOW - PAL16_ORANGE + i] = i; } break; case SpellType::Invalid: SplTransTbl[PAL8_YELLOW] = PAL16_GRAY + 1; SplTransTbl[PAL8_YELLOW + 1] = PAL16_GRAY + 3; SplTransTbl[PAL8_YELLOW + 2] = PAL16_GRAY + 5; for (int i = PAL16_GRAY; i < PAL16_GRAY + 15; i++) { SplTransTbl[PAL16_BEIGE - PAL16_GRAY + i] = i; SplTransTbl[PAL16_YELLOW - PAL16_GRAY + i] = i; SplTransTbl[PAL16_ORANGE - PAL16_GRAY + i] = i; } SplTransTbl[PAL16_BEIGE + 15] = 0; SplTransTbl[PAL16_YELLOW + 15] = 0; SplTransTbl[PAL16_ORANGE + 15] = 0; break; case SpellType::Skill: break; } } } // namespace devilution ================================================ FILE: Source/panels/spell_icons.hpp ================================================ #pragma once #include #include #include #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/surface.hpp" #include "tables/spelldat.h" #define SPLICONLENGTH 56 namespace devilution { enum class SpellIcon : uint8_t { Firebolt, Healing, Lightning, Flash, Identify, FireWall, TownPortal, StoneCurse, Infravision, HealOther, Nova, Fireball, ManaShield, FlameWave, Inferno, ChainLightning, Sentinel, // unused DoomSerpents, BloodRitual, // unused Invisibility, // unused Golem, Etherealize, BloodBoil, Teleport, Apocalypse, ItemRepair, Empty, Phasing, StaffRecharge, BoneSpirit, RedSkull, // unused Pentagram, // unused FireCloud, // unused LongHorn, // unused PentaStar, // unused BloodStar, TrapDisarm, Elemental, ChargedBolt, Telekinesis, Resurrect, HolyBolt, Warp, Search, Reflect, LightningWall, Immolation, Berserk, RingOfFire, Jester, Mana, }; /** * Draw a large (56x56) spell icon onto the given buffer. * * @param out Output buffer. * @param position Buffer coordinates (bottom-left). * @param spell Spell ID. */ void DrawLargeSpellIcon(const Surface &out, Point position, SpellID spell); /** * Draw a small (37x38) spell icon onto the given buffer. * * @param out Output buffer. * @param position Buffer coordinates (bottom-left). * @param spell Spell ID. */ void DrawSmallSpellIcon(const Surface &out, Point position, SpellID spell); /** * Draw an inset 2px border for a large (56x56) spell icon. * * @param out Output buffer. * @param position Buffer coordinates (bottom-left). * @param spell Spell ID. */ void DrawLargeSpellIconBorder(const Surface &out, Point position, uint8_t color); /** * Draw an inset 2px border for a small (37x38) spell icon. * * @param out Output buffer. * @param position Buffer coordinates (bottom-left). * @param spell Spell ID. */ void DrawSmallSpellIconBorder(const Surface &out, Point position); /** * @brief Set the color mapping for the `Draw(Small|Large)SpellIcon(Border)` calls. */ void SetSpellTrans(SpellType t); tl::expected LoadLargeSpellIcons(); void FreeLargeSpellIcons(); tl::expected LoadSmallSpellIcons(); void FreeSmallSpellIcons(); } // namespace devilution ================================================ FILE: Source/panels/spell_list.cpp ================================================ #include "panels/spell_list.hpp" #include #include #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "engine/backbuffer_state.hpp" #include "engine/palette.h" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "inv_iterators.hpp" #include "options.h" #include "panels/spell_icons.hpp" #include "player.h" #include "spells.h" #include "utils/algorithm/container.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" #define SPLROWICONLS 10 namespace devilution { namespace { void PrintSBookSpellType(const Surface &out, Point position, std::string_view text, uint8_t rectColorIndex) { DrawLargeSpellIconBorder(out, position, rectColorIndex); // Align the spell type text with bottom of spell icon position += Displacement { SPLICONLENGTH / 2 - GetLineWidth(text) / 2, (IsSmallFontTall() ? -19 : -15) }; // Then draw the text over the top DrawString(out, text, position, { .flags = UiFlags::ColorWhite | UiFlags::Outlined }); } void PrintSBookHotkey(const Surface &out, Point position, const std::string_view text) { // Align the hot key text with the top-right corner of the spell icon position += Displacement { SPLICONLENGTH - (GetLineWidth(text.data()) + 5), 5 - SPLICONLENGTH }; // Then draw the text over the top DrawString(out, text, position, { .flags = UiFlags::ColorWhite | UiFlags::Outlined }); } bool GetSpellListSelection(SpellID &pSpell, SpellType &pSplType) { pSpell = SpellID::Invalid; pSplType = SpellType::Invalid; const Player &myPlayer = *MyPlayer; for (auto &spellListItem : GetSpellListItems()) { if (spellListItem.isSelected) { pSpell = spellListItem.id; pSplType = spellListItem.type; if (spellListItem.id == GetPlayerStartingLoadoutForClass(myPlayer._pClass).skill) pSplType = SpellType::Skill; return true; } } return false; } std::optional GetHotkeyName(SpellID spellId, SpellType spellType, bool useShortName = false) { const Player &myPlayer = *MyPlayer; for (size_t t = 0; t < NumHotkeys; t++) { if (myPlayer._pSplHotKey[t] != spellId || myPlayer._pSplTHotKey[t] != spellType) continue; auto quickSpellActionKey = StrCat("QuickSpell", t + 1); if (ControlMode == ControlTypes::Gamepad) return GetOptions().Padmapper.InputNameForAction(quickSpellActionKey, useShortName); return GetOptions().Keymapper.KeyNameForAction(quickSpellActionKey); } return {}; } } // namespace void DrawSpell(const Surface &out) { const Player &myPlayer = *MyPlayer; SpellID spl = myPlayer._pRSpell; SpellType st = myPlayer._pRSplType; if (!IsValidSpell(spl)) { st = SpellType::Invalid; spl = SpellID::Null; } if (st == SpellType::Spell) { const int tlvl = myPlayer.GetSpellLevel(spl); if (CheckSpell(*MyPlayer, spl, st, true) != SpellCheckResult::Success) st = SpellType::Invalid; if (tlvl <= 0) st = SpellType::Invalid; } if (leveltype == DTYPE_TOWN && st != SpellType::Invalid && !GetSpellData(spl).isAllowedInTown()) st = SpellType::Invalid; SetSpellTrans(st); const Point position = GetMainPanel().position + Displacement { 565, 119 }; DrawLargeSpellIcon(out, position, spl); std::optional hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType, true); if (hotkeyName) PrintSBookHotkey(out, position, *hotkeyName); } void DrawSpellList(const Surface &out) { InfoString = StringOrView {}; const Player &myPlayer = *MyPlayer; for (auto &spellListItem : GetSpellListItems()) { const SpellID spellId = spellListItem.id; SpellType transType = spellListItem.type; int spellLevel = 0; const SpellData &spellDataItem = GetSpellData(spellListItem.id); if (leveltype == DTYPE_TOWN && !spellDataItem.isAllowedInTown()) { transType = SpellType::Invalid; } if (spellListItem.type == SpellType::Spell) { spellLevel = myPlayer.GetSpellLevel(spellListItem.id); if (spellLevel == 0) transType = SpellType::Invalid; } SetSpellTrans(transType); DrawLargeSpellIcon(out, spellListItem.location, spellId); std::optional shortHotkeyName = GetHotkeyName(spellId, spellListItem.type, true); if (shortHotkeyName) PrintSBookHotkey(out, spellListItem.location, *shortHotkeyName); if (!spellListItem.isSelected) continue; uint8_t spellColor = PAL16_GRAY + 5; switch (spellListItem.type) { case SpellType::Skill: spellColor = PAL16_YELLOW - 46; PrintSBookSpellType(out, spellListItem.location, _("Skill"), spellColor); InfoString = fmt::format(fmt::runtime(_("{:s} Skill")), pgettext("spell", spellDataItem.sNameText)); break; case SpellType::Spell: if (!myPlayer.isOnLevel(0)) { spellColor = PAL16_BLUE + 5; } PrintSBookSpellType(out, spellListItem.location, _("Spell"), spellColor); InfoString = fmt::format(fmt::runtime(_("{:s} Spell")), pgettext("spell", spellDataItem.sNameText)); if (spellId == SpellID::HolyBolt) { AddInfoBoxString(_("Damages undead only")); } if (spellLevel == 0) AddInfoBoxString(_("Spell Level 0 - Unusable")); else AddInfoBoxString(fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); break; case SpellType::Scroll: { if (!myPlayer.isOnLevel(0)) { spellColor = PAL16_RED - 59; } PrintSBookSpellType(out, spellListItem.location, _("Scroll"), spellColor); InfoString = fmt::format(fmt::runtime(_("Scroll of {:s}")), pgettext("spell", spellDataItem.sNameText)); const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { myPlayer }, [spellId](const Item &item) { return item.isScrollOf(spellId); }); AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); } break; case SpellType::Charges: { if (!myPlayer.isOnLevel(0)) { spellColor = PAL16_ORANGE + 5; } PrintSBookSpellType(out, spellListItem.location, _("Staff"), spellColor); InfoString = fmt::format(fmt::runtime(_("Staff of {:s}")), pgettext("spell", spellDataItem.sNameText)); int charges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges; AddInfoBoxString(fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", charges)), charges)); } break; case SpellType::Invalid: break; } std::optional fullHotkeyName = GetHotkeyName(spellId, spellListItem.type); if (fullHotkeyName) { AddInfoBoxString(fmt::format(fmt::runtime(_("Spell Hotkey {:s}")), *fullHotkeyName)); } } } std::vector GetSpellListItems() { std::vector spellListItems; uint64_t mask; const Point mainPanelPosition = GetMainPanel().position; int x = mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS; int y = mainPanelPosition.y - 17; for (auto i : enum_values()) { const Player &myPlayer = *MyPlayer; switch (static_cast(i)) { case SpellType::Skill: mask = myPlayer._pAblSpells; break; case SpellType::Spell: mask = myPlayer._pMemSpells; break; case SpellType::Scroll: mask = myPlayer._pScrlSpells; break; case SpellType::Charges: mask = myPlayer._pISpells; break; default: continue; } int8_t j = static_cast(SpellID::Firebolt); for (uint64_t spl = 1; static_cast(j) < SpellsData.size(); spl <<= 1, j++) { if ((mask & spl) == 0) continue; const int lx = x; const int ly = y - SPLICONLENGTH; const bool isSelected = (MousePosition.x >= lx && MousePosition.x < lx + SPLICONLENGTH && MousePosition.y >= ly && MousePosition.y < ly + SPLICONLENGTH); spellListItems.emplace_back(SpellListItem { { x, y }, static_cast(i), static_cast(j), isSelected }); x -= SPLICONLENGTH; if (x == mainPanelPosition.x + 12 - SPLICONLENGTH) { x = mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS; y -= SPLICONLENGTH; } } if (mask != 0 && x != mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS) x -= SPLICONLENGTH; if (x == mainPanelPosition.x + 12 - SPLICONLENGTH) { x = mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS; y -= SPLICONLENGTH; } } return spellListItems; } void SetSpell() { SpellID pSpell; SpellType pSplType; SpellSelectFlag = false; if (!GetSpellListSelection(pSpell, pSplType)) { return; } Player &myPlayer = *MyPlayer; myPlayer._pRSpell = pSpell; myPlayer._pRSplType = pSplType; RedrawEverything(); } void SetSpeedSpell(size_t slot) { SpellID pSpell; SpellType pSplType; if (!GetSpellListSelection(pSpell, pSplType)) { return; } Player &myPlayer = *MyPlayer; if (myPlayer._pSplHotKey[slot] == pSpell && myPlayer._pSplTHotKey[slot] == pSplType) { // Unset spell hotkey myPlayer._pSplHotKey[slot] = SpellID::Invalid; return; } for (size_t i = 0; i < NumHotkeys; ++i) { if (myPlayer._pSplHotKey[i] == pSpell && myPlayer._pSplTHotKey[i] == pSplType) myPlayer._pSplHotKey[i] = SpellID::Invalid; } myPlayer._pSplHotKey[slot] = pSpell; myPlayer._pSplTHotKey[slot] = pSplType; } bool IsValidSpeedSpell(size_t slot) { uint64_t spells; const Player &myPlayer = *MyPlayer; const SpellID spellId = myPlayer._pSplHotKey[slot]; if (!IsValidSpell(spellId)) { return false; } switch (myPlayer._pSplTHotKey[slot]) { case SpellType::Skill: spells = myPlayer._pAblSpells; break; case SpellType::Spell: spells = myPlayer._pMemSpells; break; case SpellType::Scroll: spells = myPlayer._pScrlSpells; break; case SpellType::Charges: spells = myPlayer._pISpells; break; case SpellType::Invalid: return false; } return (spells & GetSpellBitmask(spellId)) != 0; } void ToggleSpell(size_t slot) { if (IsValidSpeedSpell(slot)) { Player &myPlayer = *MyPlayer; myPlayer._pRSpell = myPlayer._pSplHotKey[slot]; myPlayer._pRSplType = myPlayer._pSplTHotKey[slot]; RedrawEverything(); } } void DoSpeedBook() { SpellSelectFlag = true; const Point mainPanelPosition = GetMainPanel().position; int xo = mainPanelPosition.x + 12 + SPLICONLENGTH * 10; int yo = mainPanelPosition.y - 17; int x = xo + SPLICONLENGTH / 2; int y = yo - SPLICONLENGTH / 2; const Player &myPlayer = *MyPlayer; if (IsValidSpell(myPlayer._pRSpell)) { for (auto i : enum_values()) { uint64_t spells; switch (static_cast(i)) { case SpellType::Skill: spells = myPlayer._pAblSpells; break; case SpellType::Spell: spells = myPlayer._pMemSpells; break; case SpellType::Scroll: spells = myPlayer._pScrlSpells; break; case SpellType::Charges: spells = myPlayer._pISpells; break; default: continue; } uint64_t spell = 1; for (size_t j = 1; j < SpellsData.size(); j++) { if ((spell & spells) != 0) { if (j == static_cast(myPlayer._pRSpell) && static_cast(i) == myPlayer._pRSplType) { x = xo + SPLICONLENGTH / 2; y = yo - SPLICONLENGTH / 2; } xo -= SPLICONLENGTH; if (xo == mainPanelPosition.x + 12 - SPLICONLENGTH) { xo = mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS; yo -= SPLICONLENGTH; } } spell <<= 1ULL; } if (spells != 0 && xo != mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS) xo -= SPLICONLENGTH; if (xo == mainPanelPosition.x + 12 - SPLICONLENGTH) { xo = mainPanelPosition.x + 12 + SPLICONLENGTH * SPLROWICONLS; yo -= SPLICONLENGTH; } } } SetCursorPos({ x, y }); } } // namespace devilution ================================================ FILE: Source/panels/spell_list.hpp ================================================ #pragma once #include #include #include "engine/point.hpp" #include "engine/surface.hpp" #include "tables/spelldat.h" namespace devilution { struct SpellListItem { Point location; SpellType type; SpellID id; bool isSelected; }; /** * @brief draws the current right mouse button spell. * @param out screen buffer representing the main UI panel */ void DrawSpell(const Surface &out); void DrawSpellList(const Surface &out); std::vector GetSpellListItems(); void SetSpell(); void SetSpeedSpell(size_t slot); bool IsValidSpeedSpell(size_t slot); void ToggleSpell(size_t slot); /** * Draws the "Speed Book": the rows of known spells for quick-setting a spell that * show up when you click the spell slot at the control panel. */ void DoSpeedBook(); } // namespace devilution ================================================ FILE: Source/panels/ui_panels.hpp ================================================ #pragma once #include namespace devilution { enum class UiPanels : uint8_t { Main, Quest, Character, Spell, Inventory, Stash, }; } // namespace devilution ================================================ FILE: Source/pfile.cpp ================================================ /** * @file pfile.cpp * * Implementation of the save game encoding functionality. */ #include "pfile.h" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include "codec.h" #include "engine/load_file.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" #include "loadsave.h" #include "menu.h" #include "mpq/mpq_common.hpp" #include "pack.h" #include "qol/stash.h" #include "tables/playerdat.hpp" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/file_util.h" #include "utils/language.h" #include "utils/parse_int.hpp" #include "utils/paths.h" #include "utils/sdl_compat.h" #include "utils/stdcompat/filesystem.hpp" #include "utils/str_cat.hpp" #include "utils/str_split.hpp" #include "utils/utf8.hpp" #ifdef UNPACKED_SAVES #include "utils/file_util.h" #else #include "mpq/mpq_reader.hpp" #endif namespace devilution { #define PASSWORD_SPAWN_SINGLE "adslhfb1" #define PASSWORD_SPAWN_MULTI "lshbkfg1" #define PASSWORD_SINGLE "xrgyrkj1" #define PASSWORD_MULTI "szqnlsk1" bool gbValidSaveFile; namespace { /** List of character names for the character selection screen. */ char hero_names[MAX_CHARACTERS][PlayerNameLength]; std::string GetSavePath(uint32_t saveNum, std::string_view savePrefix = {}) { return StrCat(paths::PrefPath(), savePrefix, gbIsSpawn ? (gbIsMultiplayer ? "share_" : "spawn_") : (gbIsMultiplayer ? "multi_" : "single_"), saveNum, #ifdef UNPACKED_SAVES gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR #else gbIsHellfire ? ".hsv" : ".sv" #endif ); } std::string GetStashSavePath() { return StrCat(paths::PrefPath(), gbIsSpawn ? "stash_spawn" : "stash", #ifdef UNPACKED_SAVES gbIsHellfire ? "_hsv" DIRECTORY_SEPARATOR_STR : "_sv" DIRECTORY_SEPARATOR_STR #else gbIsHellfire ? ".hsv" : ".sv" #endif ); } bool GetSaveNames(uint8_t index, std::string_view prefix, char *out) { char suf; if (index < giNumberOfLevels) suf = 'l'; else if (index < giNumberOfLevels * 2) { index -= giNumberOfLevels; suf = 's'; } else { return false; } *BufCopy(out, prefix, std::string_view(&suf, 1), LeftPad(index, 2, '0')) = '\0'; return true; } bool GetPermSaveNames(uint8_t dwIndex, char *szPerm) { return GetSaveNames(dwIndex, "perm", szPerm); } bool GetTempSaveNames(uint8_t dwIndex, char *szTemp) { return GetSaveNames(dwIndex, "temp", szTemp); } void RenameTempToPerm(SaveWriter &saveWriter) { char szTemp[MaxMpqPathSize]; char szPerm[MaxMpqPathSize]; uint32_t dwIndex = 0; while (GetTempSaveNames(dwIndex, szTemp)) { [[maybe_unused]] const bool result = GetPermSaveNames(dwIndex, szPerm); // DO NOT PUT DIRECTLY INTO ASSERT! assert(result); dwIndex++; if (saveWriter.HasFile(szTemp)) { if (saveWriter.HasFile(szPerm)) saveWriter.RemoveHashEntry(szPerm); saveWriter.RenameFile(szTemp, szPerm); } } assert(!GetPermSaveNames(dwIndex, szPerm)); } bool ReadHero(SaveReader &archive, PlayerPack *pPack) { size_t read; auto buf = ReadArchive(archive, "hero", &read); if (buf == nullptr) return false; bool ret = false; if (read == sizeof(*pPack)) { memcpy(pPack, buf.get(), sizeof(*pPack)); ret = true; } return ret; } void EncodeHero(SaveWriter &saveWriter, const PlayerPack *pack) { const size_t packedLen = codec_get_encoded_len(sizeof(*pack)); const std::unique_ptr packed { new std::byte[packedLen] }; memcpy(packed.get(), pack, sizeof(*pack)); codec_encode(packed.get(), sizeof(*pack), packedLen, pfile_get_password()); saveWriter.WriteFile("hero", packed.get(), packedLen); } SaveWriter GetSaveWriter(uint32_t saveNum) { return SaveWriter(GetSavePath(saveNum)); } SaveWriter GetStashWriter() { return SaveWriter(GetStashSavePath()); } #ifndef DISABLE_DEMOMODE void CopySaveFile(uint32_t saveNum, std::string targetPath) { const std::string savePath = GetSavePath(saveNum); #if defined(UNPACKED_SAVES) #ifdef DVL_NO_FILESYSTEM #error "UNPACKED_SAVES requires either DISABLE_DEMOMODE or C++17 " #endif if (!targetPath.empty()) { CreateDir(targetPath.c_str()); } for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(savePath)) { CopyFileOverwrite(entry.path().string().c_str(), (targetPath + entry.path().filename().string()).c_str()); } #else CopyFileOverwrite(savePath.c_str(), targetPath.c_str()); #endif } #endif void Game2UiPlayer(const Player &player, _uiheroinfo *heroinfo, bool bHasSaveFile) { CopyUtf8(heroinfo->name, player._pName, sizeof(heroinfo->name)); heroinfo->level = player.getCharacterLevel(); heroinfo->heroclass = player._pClass; heroinfo->strength = player._pStrength; heroinfo->magic = player._pMagic; heroinfo->dexterity = player._pDexterity; heroinfo->vitality = player._pVitality; heroinfo->hassaved = bHasSaveFile; heroinfo->herorank = player.pDiabloKillLevel; heroinfo->spawned = gbIsSpawn; } bool GetFileName(uint8_t lvl, char *dst) { if (gbIsMultiplayer) { if (lvl != 0) return false; memcpy(dst, "hero", 5); return true; } if (GetPermSaveNames(lvl, dst)) { return true; } if (lvl == giNumberOfLevels * 2) { memcpy(dst, "game", 5); return true; } if (lvl == giNumberOfLevels * 2 + 1) { memcpy(dst, "hero", 5); return true; } return false; } bool ArchiveContainsGame(SaveReader &hsArchive) { if (gbIsMultiplayer) return false; auto gameData = ReadArchive(hsArchive, "game"); if (gameData == nullptr) return false; const uint32_t hdr = LoadLE32(gameData.get()); return IsHeaderValid(hdr); } std::optional CreateSaveReader(std::string &&path) { #ifdef UNPACKED_SAVES if (!FileExists(path)) return std::nullopt; return SaveReader(std::move(path)); #else std::int32_t error; return MpqArchive::Open(path.c_str(), error); #endif } #ifndef DISABLE_DEMOMODE struct CompareInfo { std::unique_ptr &data; size_t currentPosition; size_t size; bool isTownLevel; bool dataExists; }; struct CompareCounter { int reference; int actual; int max() const { return std::max(reference, actual); } void checkIfDataExists(int count, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual) const { if (reference == count) compareInfoReference.dataExists = false; if (actual == count) compareInfoActual.dataExists = false; } }; inline bool string_ends_with(std::string_view value, std::string_view suffix) { if (suffix.size() > value.size()) return false; return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin()); } void CreateDetailDiffs(std::string_view prefix, std::string_view memoryMapFile, CompareInfo &compareInfoReference, CompareInfo &compareInfoActual, ankerl::unordered_dense::segmented_map &foundDiffs) { // Note: Detail diffs are currently only supported in unit tests const std::string memoryMapFileAssetName = StrCat(paths::BasePath(), "/test/fixtures/memory_map/", memoryMapFile, ".txt"); SDL_IOStream *handle = SDL_IOFromFile(memoryMapFileAssetName.c_str(), "r"); if (handle == nullptr) { app_fatal(StrCat("MemoryMapFile ", memoryMapFile, " is missing")); return; } const size_t readBytes = static_cast(SDL_GetIOSize(handle)); const std::unique_ptr memoryMapFileData { new std::byte[readBytes] }; SDL_ReadIO(handle, memoryMapFileData.get(), readBytes); SDL_CloseIO(handle); const std::string_view buffer(reinterpret_cast(memoryMapFileData.get()), readBytes); ankerl::unordered_dense::segmented_map counter; auto getCounter = [&](const std::string &counterAsString) { auto it = counter.find(counterAsString); if (it != counter.end()) return it->second; const ParseIntResult countFromMapFile = ParseInt(counterAsString); if (!countFromMapFile.has_value()) app_fatal(StrCat("Failed to parse ", counterAsString, " as int")); return CompareCounter { countFromMapFile.value(), countFromMapFile.value() }; }; auto addDiff = [&](const std::string &diffKey) { auto it = foundDiffs.find(diffKey); if (it == foundDiffs.end()) { foundDiffs.insert_or_assign(diffKey, 1); } else { foundDiffs.insert_or_assign(diffKey, it->second + 1); } }; auto compareBytes = [&](size_t countBytes) { if (compareInfoReference.dataExists && compareInfoReference.currentPosition + countBytes > compareInfoReference.size) app_fatal(StrCat("Comparison failed. Not enough bytes in reference to compare. Location: ", prefix)); if (compareInfoActual.dataExists && compareInfoActual.currentPosition + countBytes > compareInfoActual.size) app_fatal(StrCat("Comparison failed. Not enough bytes in actual to compare. Location: ", prefix)); bool result = true; if (compareInfoReference.dataExists && compareInfoActual.dataExists) result = memcmp(compareInfoReference.data.get() + compareInfoReference.currentPosition, compareInfoActual.data.get() + compareInfoActual.currentPosition, countBytes) == 0; if (compareInfoReference.dataExists) compareInfoReference.currentPosition += countBytes; if (compareInfoActual.dataExists) compareInfoActual.currentPosition += countBytes; return result; }; auto read32BitInt = [&](CompareInfo &compareInfo, bool useLE) { int32_t value = 0; if (!compareInfo.dataExists) return value; if (compareInfo.currentPosition + sizeof(value) > compareInfo.size) app_fatal("read32BitInt failed. Too less bytes to read."); memcpy(&value, compareInfo.data.get() + compareInfo.currentPosition, sizeof(value)); if (useLE) value = Swap32LE(value); else value = Swap32BE(value); return value; }; for (std::string_view line : SplitByChar(buffer, '\n')) { if (!line.empty() && line.back() == '\r') line.remove_suffix(1); if (line.empty()) continue; const auto tokens = SplitByChar(line, ' '); auto it = tokens.begin(); const auto end = tokens.end(); if (it == end) continue; std::string_view command = *it; const bool dataExistsReference = compareInfoReference.dataExists; const bool dataExistsActual = compareInfoActual.dataExists; if (string_ends_with(command, "_HF")) { if (!gbIsHellfire) continue; command.remove_suffix(3); } if (string_ends_with(command, "_DA")) { if (gbIsHellfire) continue; command.remove_suffix(3); } if (string_ends_with(command, "_DL")) { if (compareInfoReference.isTownLevel && compareInfoActual.isTownLevel) continue; if (compareInfoReference.isTownLevel) compareInfoReference.dataExists = false; if (compareInfoActual.isTownLevel) compareInfoActual.dataExists = false; command.remove_suffix(3); } if (command == "R" || command == "LT" || command == "LC" || command == "LC_LE") { const auto bitsAsString = std::string(*++it); const auto comment = std::string(*++it); const ParseIntResult parsedBytes = ParseInt(bitsAsString); if (!parsedBytes.has_value()) app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); const size_t bytes = static_cast(parsedBytes.value() / 8); if (command == "LT") { const int32_t valueReference = read32BitInt(compareInfoReference, false); const int32_t valueActual = read32BitInt(compareInfoActual, false); assert(sizeof(valueReference) == bytes); compareInfoReference.isTownLevel = valueReference == 0; compareInfoActual.isTownLevel = valueActual == 0; } if (command == "LC" || command == "LC_LE") { const int32_t valueReference = read32BitInt(compareInfoReference, command == "LC_LE"); const int32_t valueActual = read32BitInt(compareInfoActual, command == "LC_LE"); assert(sizeof(valueReference) == bytes); counter.insert_or_assign(std::string(comment), CompareCounter { valueReference, valueActual }); } if (!compareBytes(bytes)) { const std::string diffKey = StrCat(prefix, ".", comment); addDiff(diffKey); } } else if (command == "M") { const auto countAsString = std::string(*++it); const auto bitsAsString = std::string(*++it); const std::string_view comment = *++it; const CompareCounter count = getCounter(countAsString); const ParseIntResult parsedBytes = ParseInt(bitsAsString); if (!parsedBytes.has_value()) app_fatal(StrCat("Failed to parse ", bitsAsString, " as size_t")); const size_t bytes = static_cast(parsedBytes.value() / 8); for (int i = 0; i < count.max(); i++) { count.checkIfDataExists(i, compareInfoReference, compareInfoActual); if (!compareBytes(bytes)) { const std::string diffKey = StrCat(prefix, ".", comment); addDiff(diffKey); } } } else if (command == "C") { const auto countAsString = std::string(*++it); auto subMemoryMapFile = std::string(*++it); const auto comment = std::string(*++it); const CompareCounter count = getCounter(countAsString); subMemoryMapFile.erase(std::remove(subMemoryMapFile.begin(), subMemoryMapFile.end(), '\r'), subMemoryMapFile.end()); for (int i = 0; i < count.max(); i++) { count.checkIfDataExists(i, compareInfoReference, compareInfoActual); const std::string subPrefix = StrCat(prefix, ".", comment); CreateDetailDiffs(subPrefix, subMemoryMapFile, compareInfoReference, compareInfoActual, foundDiffs); } } compareInfoReference.dataExists = dataExistsReference; compareInfoActual.dataExists = dataExistsActual; } } struct CompareTargets { std::string fileName; std::string memoryMapFileName; bool isTownLevel; }; HeroCompareResult CompareSaves(const std::string &actualSavePath, const std::string &referenceSavePath, bool logDetails) { std::vector possibleFileToCheck; possibleFileToCheck.push_back({ "hero", "hero", false }); possibleFileToCheck.push_back({ "game", "game", false }); possibleFileToCheck.push_back({ "additionalMissiles", "additionalMissiles", false }); char szPerm[MaxMpqPathSize]; for (int i = 0; GetPermSaveNames(i, szPerm); i++) { possibleFileToCheck.push_back({ std::string(szPerm), "level", i == 0 }); } SaveReader actualArchive = *CreateSaveReader(std::string(actualSavePath)); SaveReader referenceArchive = *CreateSaveReader(std::string(referenceSavePath)); bool compareResult = true; std::string message; for (const auto &compareTarget : possibleFileToCheck) { size_t fileSizeActual = 0; auto fileDataActual = ReadArchive(actualArchive, compareTarget.fileName.c_str(), &fileSizeActual); size_t fileSizeReference = 0; auto fileDataReference = ReadArchive(referenceArchive, compareTarget.fileName.c_str(), &fileSizeReference); if (fileDataActual.get() == nullptr && fileDataReference.get() == nullptr) { continue; } if (fileSizeActual == fileSizeReference && memcmp(fileDataReference.get(), fileDataActual.get(), fileSizeActual) == 0) continue; compareResult = false; if (!message.empty()) message.append("\n"); if (fileSizeActual != fileSizeReference) StrAppend(message, "file \"", compareTarget.fileName, "\" is different size. Expected: ", fileSizeReference, " Actual: ", fileSizeActual); else StrAppend(message, "file \"", compareTarget.fileName, "\" has different content."); if (!logDetails) continue; ankerl::unordered_dense::segmented_map foundDiffs; CompareInfo compareInfoReference = { fileDataReference, 0, fileSizeReference, compareTarget.isTownLevel, fileSizeReference != 0 }; CompareInfo compareInfoActual = { fileDataActual, 0, fileSizeActual, compareTarget.isTownLevel, fileSizeActual != 0 }; CreateDetailDiffs(compareTarget.fileName, compareTarget.memoryMapFileName, compareInfoReference, compareInfoActual, foundDiffs); if (compareInfoReference.currentPosition != fileSizeReference) app_fatal(StrCat("Comparison failed. Uncompared bytes in reference. File: ", compareTarget.fileName)); if (compareInfoActual.currentPosition != fileSizeActual) app_fatal(StrCat("Comparison failed. Uncompared bytes in actual. File: ", compareTarget.fileName)); for (const auto &[location, count] : foundDiffs) { StrAppend(message, "\nDiff found in ", location, " count: ", count); } } return { compareResult ? HeroCompareResult::Same : HeroCompareResult::Difference, message }; } #endif // !DISABLE_DEMOMODE void pfile_write_hero(SaveWriter &saveWriter, bool writeGameData) { if (writeGameData) { SaveGameData(saveWriter); RenameTempToPerm(saveWriter); } PlayerPack pkplr; Player &myPlayer = *MyPlayer; PackPlayer(pkplr, myPlayer); EncodeHero(saveWriter, &pkplr); if (!gbVanilla) { SaveHotkeys(saveWriter, myPlayer); SaveHeroItems(saveWriter, myPlayer); } } void RemoveAllInvalidItems(Player &player) { for (int i = 0; i < NUM_INVLOC; i++) RemoveInvalidItem(player.InvBody[i]); for (int i = 0; i < player._pNumInv; i++) RemoveInvalidItem(player.InvList[i]); for (int i = 0; i < MaxBeltItems; i++) RemoveInvalidItem(player.SpdList[i]); RemoveEmptyInventory(player); } } // namespace #ifdef UNPACKED_SAVES std::unique_ptr SaveReader::ReadFile(const char *filename, std::size_t &fileSize, int32_t &error) { std::unique_ptr result; error = 0; const std::string path = dir_ + filename; uintmax_t size; if (!GetFileSize(path.c_str(), &size)) { error = 1; return nullptr; } fileSize = size; FILE *file = OpenFile(path.c_str(), "rb"); if (file == nullptr) { error = 1; return nullptr; } result.reset(new std::byte[size]); if (std::fread(result.get(), size, 1, file) != 1) { std::fclose(file); error = 1; return nullptr; } std::fclose(file); return result; } bool SaveWriter::WriteFile(const char *filename, const std::byte *data, size_t size) { const std::string path = dir_ + filename; FILE *file = OpenFile(path.c_str(), "wb"); if (file == nullptr) { return false; } if (std::fwrite(data, size, 1, file) != 1) { std::fclose(file); return false; } std::fclose(file); return true; } void SaveWriter::RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)) { char pszFileName[MaxMpqPathSize]; for (uint8_t i = 0; fnGetName(i, pszFileName); i++) { RemoveHashEntry(pszFileName); } } #endif std::optional OpenSaveArchive(uint32_t saveNum) { return CreateSaveReader(GetSavePath(saveNum)); } std::optional OpenStashArchive() { return CreateSaveReader(GetStashSavePath()); } std::unique_ptr ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen) { int32_t error; std::size_t length; std::unique_ptr result = archive.ReadFile(pszName, length, error); if (error != 0) return nullptr; const std::size_t decodedLength = codec_decode(result.get(), length, pfile_get_password()); if (decodedLength == 0) return nullptr; if (pdwLen != nullptr) *pdwLen = decodedLength; return result; } const char *pfile_get_password() { if (gbIsSpawn) return gbIsMultiplayer ? PASSWORD_SPAWN_MULTI : PASSWORD_SPAWN_SINGLE; return gbIsMultiplayer ? PASSWORD_MULTI : PASSWORD_SINGLE; } void pfile_write_hero(bool writeGameData) { SaveWriter saveWriter = GetSaveWriter(gSaveNumber); pfile_write_hero(saveWriter, writeGameData); } #ifndef DISABLE_DEMOMODE void pfile_write_hero_demo(int demo) { const std::string savePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); CopySaveFile(gSaveNumber, savePath); auto saveWriter = SaveWriter(savePath.c_str()); pfile_write_hero(saveWriter, true); } HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails) { const std::string referenceSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_reference_")); if (!FileExists(referenceSavePath.c_str())) return { HeroCompareResult::ReferenceNotFound, {} }; const std::string actualSavePath = GetSavePath(gSaveNumber, StrCat("demo_", demo, "_actual_")); { CopySaveFile(gSaveNumber, actualSavePath); SaveWriter saveWriter(actualSavePath.c_str()); pfile_write_hero(saveWriter, true); } return CompareSaves(actualSavePath, referenceSavePath, logDetails); } #endif void sfile_write_stash() { if (!Stash.dirty) return; SaveWriter stashWriter = GetStashWriter(); SaveStash(stashWriter); Stash.dirty = false; } bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)) { memset(hero_names, 0, sizeof(hero_names)); for (uint32_t i = 0; i < MAX_CHARACTERS; i++) { std::optional archive = OpenSaveArchive(i); if (archive) { PlayerPack pkplr; if (ReadHero(*archive, &pkplr)) { _uiheroinfo uihero; uihero.saveNumber = i; strcpy(hero_names[i], pkplr.pName); const bool hasSaveGame = ArchiveContainsGame(*archive); if (hasSaveGame) pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0; Player &player = Players[0]; UnPackPlayer(pkplr, player); LoadHeroItems(player); RemoveAllInvalidItems(player); CalcPlrInv(player, false); Game2UiPlayer(player, &uihero, hasSaveGame); uiAddHeroInfo(&uihero); } } } return true; } void pfile_ui_set_class_stats(HeroClass playerClass, _uidefaultstats *classStats) { const ClassAttributes &classAttributes = GetClassAttributes(playerClass); classStats->strength = classAttributes.baseStr; classStats->magic = classAttributes.baseMag; classStats->dexterity = classAttributes.baseDex; classStats->vitality = classAttributes.baseVit; } uint32_t pfile_ui_get_first_unused_save_num() { uint32_t saveNum; for (saveNum = 0; saveNum < MAX_CHARACTERS; saveNum++) { if (hero_names[saveNum][0] == '\0') break; } return saveNum; } bool pfile_ui_save_create(_uiheroinfo *heroinfo) { PlayerPack pkplr; const uint32_t saveNum = heroinfo->saveNumber; if (saveNum >= MAX_CHARACTERS) return false; heroinfo->saveNumber = saveNum; giNumberOfLevels = gbIsHellfire ? 25 : 17; SaveWriter saveWriter = GetSaveWriter(saveNum); saveWriter.RemoveHashEntries(GetFileName); CopyUtf8(hero_names[saveNum], heroinfo->name, sizeof(hero_names[saveNum])); Player &player = Players[0]; CreatePlayer(player, heroinfo->heroclass); CopyUtf8(player._pName, heroinfo->name, PlayerNameLength); PackPlayer(pkplr, player); EncodeHero(saveWriter, &pkplr); Game2UiPlayer(player, heroinfo, false); if (!gbVanilla) { SaveHotkeys(saveWriter, player); SaveHeroItems(saveWriter, player); } return true; } bool pfile_delete_save(_uiheroinfo *heroInfo) { const uint32_t saveNum = heroInfo->saveNumber; if (saveNum < MAX_CHARACTERS) { hero_names[saveNum][0] = '\0'; RemoveFile(GetSavePath(saveNum).c_str()); } return true; } void pfile_read_player_from_save(uint32_t saveNum, Player &player) { PlayerPack pkplr; { std::optional archive = OpenSaveArchive(saveNum); if (!archive) app_fatal(_("Unable to open archive")); if (!ReadHero(*archive, &pkplr)) app_fatal(_("Unable to load character")); gbValidSaveFile = ArchiveContainsGame(*archive); if (gbValidSaveFile) pkplr.bIsHellfire = gbIsHellfireSaveGame ? 1 : 0; } UnPackPlayer(pkplr, player); LoadHeroItems(player); RemoveAllInvalidItems(player); CalcPlrInv(player, false); } void pfile_save_level() { SaveWriter saveWriter = GetSaveWriter(gSaveNumber); SaveLevel(saveWriter); } tl::expected pfile_convert_levels() { SaveWriter saveWriter = GetSaveWriter(gSaveNumber); return ConvertLevels(saveWriter); } void pfile_remove_temp_files() { if (gbIsMultiplayer) return; SaveWriter saveWriter = GetSaveWriter(gSaveNumber); saveWriter.RemoveHashEntries(GetTempSaveNames); } void pfile_update(bool forceSave) { static Uint32 prevTick; if (!gbIsMultiplayer) return; const Uint32 tick = SDL_GetTicks(); if (!forceSave && tick - prevTick <= 60000) return; prevTick = tick; pfile_write_hero(); sfile_write_stash(); } } // namespace devilution ================================================ FILE: Source/pfile.h ================================================ /** * @file pfile.h * * Interface of the save game encoding functionality. */ #pragma once #include #include #include #include "DiabloUI/diabloui.h" #include "player.h" #ifdef UNPACKED_SAVES #include "utils/file_util.h" #else #include "mpq/mpq_reader.hpp" #include "mpq/mpq_writer.hpp" #endif namespace devilution { #define MAX_CHARACTERS 99 extern bool gbValidSaveFile; #ifdef UNPACKED_SAVES struct SaveReader { explicit SaveReader(std::string &&dir) : dir_(std::move(dir)) { } const std::string &dir() const { return dir_; } std::unique_ptr ReadFile(const char *filename, std::size_t &fileSize, int32_t &error); bool HasFile(const char *path) { return ::devilution::FileExists((dir_ + path).c_str()); } private: std::string dir_; }; struct SaveWriter { explicit SaveWriter(std::string &&dir) : dir_(std::move(dir)) { } bool WriteFile(const char *filename, const std::byte *data, size_t size); bool HasFile(const char *path) { return ::devilution::FileExists((dir_ + path).c_str()); } void RenameFile(const char *from, const char *to) { ::devilution::RenameFile((dir_ + from).c_str(), (dir_ + to).c_str()); } void RemoveHashEntry(const char *path) { RemoveFile((dir_ + path).c_str()); } void RemoveHashEntries(bool (*fnGetName)(uint8_t, char *)); private: std::string dir_; }; #else using SaveReader = MpqArchive; using SaveWriter = MpqWriter; #endif /** * @brief Comparison result of pfile_compare_hero_demo */ struct HeroCompareResult { enum Status : uint8_t { ReferenceNotFound, Same, Difference, }; Status status; std::string message; }; std::optional OpenSaveArchive(uint32_t saveNum); std::optional OpenStashArchive(); const char *pfile_get_password(); std::unique_ptr ReadArchive(SaveReader &archive, const char *pszName, size_t *pdwLen = nullptr); void pfile_write_hero(bool writeGameData = false); #ifndef DISABLE_DEMOMODE /** * @brief Save a reference game-state (save game) for the demo recording * @param demo that is recorded */ void pfile_write_hero_demo(int demo); /** * @brief Compares the actual game-state (savegame) with a reference game-state (save game from demo recording) * @param demo for the comparison * @param logDetails in case of a difference log details * @return The comparison result. */ HeroCompareResult pfile_compare_hero_demo(int demo, bool logDetails); #endif void sfile_write_stash(); bool pfile_ui_set_hero_infos(bool (*uiAddHeroInfo)(_uiheroinfo *)); void pfile_ui_set_class_stats(HeroClass playerClass, _uidefaultstats *classStats); uint32_t pfile_ui_get_first_unused_save_num(); bool pfile_ui_save_create(_uiheroinfo *heroinfo); bool pfile_delete_save(_uiheroinfo *heroInfo); void pfile_read_player_from_save(uint32_t saveNum, Player &player); void pfile_save_level(); tl::expected pfile_convert_levels(); void pfile_remove_temp_files(); std::unique_ptr pfile_read(const char *pszName, size_t *pdwLen); void pfile_update(bool forceSave); } // namespace devilution ================================================ FILE: Source/platform/android/CMakeLists.txt ================================================ include(functions/devilutionx_library) add_devilutionx_object_library(libdevilutionx_android android.cpp) target_link_dependencies(libdevilutionx_android PUBLIC DevilutionX::SDL libdevilutionx_init libdevilutionx_mpq ) ================================================ FILE: Source/platform/android/android.cpp ================================================ #include "init.hpp" #include "mpq/mpq_reader.hpp" #include namespace devilution { namespace { bool AreExtraFontsOutOfDateForMpqPath(const char *mpqPath) { int32_t error = 0; std::optional archive = MpqArchive::Open(mpqPath, error); return error == 0 && archive && AreExtraFontsOutOfDate(*archive); } } // namespace } // namespace devilution extern "C" { JNIEXPORT jboolean JNICALL Java_org_diasurgical_devilutionx_DevilutionXSDLActivity_areFontsOutOfDate(JNIEnv *env, jclass cls, jstring fonts_mpq) { const char *mpqPath = env->GetStringUTFChars(fonts_mpq, nullptr); bool outOfDate = devilution::AreExtraFontsOutOfDateForMpqPath(mpqPath); env->ReleaseStringUTFChars(fonts_mpq, mpqPath); return outOfDate; } } ================================================ FILE: Source/platform/ctr/CMakeLists.txt ================================================ include(functions/devilutionx_library) add_devilutionx_object_library(libdevilutionx_ctr system.cpp keyboard.cpp display.cpp messagebox.cpp sockets.cpp locale.cpp asio/net/if.c asio/sys/socket.c asio/sys/uio.c ) if(NOT NONET) if(NOT DISABLE_TCP) target_link_libraries(libdevilutionx_ctr PUBLIC asio) endif() if(PACKET_ENCRYPTION) target_sources(libdevilutionx_ctr PRIVATE random.cpp) target_link_libraries(libdevilutionx_ctr PUBLIC sodium) endif() endif() target_link_libraries(libdevilutionx_ctr PUBLIC DevilutionX::SDL fmt::fmt 3ds::citro3d 3ds::ctrulib ) ================================================ FILE: Source/platform/ctr/asio/include/errno.h ================================================ #pragma once #include_next #define ESHUTDOWN (__ELASTERROR + 1) ================================================ FILE: Source/platform/ctr/asio/include/net/if.h ================================================ #ifndef _NET_IF_H #define _NET_IF_H 1 #define IF_NAMESIZE 16 struct if_nameindex { unsigned int if_index; char *if_name; }; #ifdef __cplusplus extern "C" { #endif unsigned int if_nametoindex(const char *__ifname); char *if_indextoname(unsigned int __ifindex, char *__ifname); struct if_nameindex *if_nameindex(); void if_freenameindex(struct if_nameindex *__ptr); #ifdef __cplusplus } #endif #endif ================================================ FILE: Source/platform/ctr/asio/include/netdb.h ================================================ #pragma once #include_next #define EAI_SERVICE -401 #define EAI_AGAIN -402 #define EAI_BADFLAGS -403 #define EAI_FAIL -404 ================================================ FILE: Source/platform/ctr/asio/include/netinet/in.h ================================================ #pragma once #include #include_next struct in6_addr { uint8_t s6_addr[16]; }; struct ipv6_mreq { struct in6_addr ipv6mr_multiaddr; unsigned ipv6mr_interface; }; struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; }; #define IPPROTO_IPV6 -1 #define IP_MULTICAST_IF -1 #define IPV6_JOIN_GROUP -1 #define IPV6_LEAVE_GROUP -1 #define IPV6_UNICAST_HOPS -1 #define IPV6_MULTICAST_HOPS -1 #define IPV6_MULTICAST_IF -1 #define IPV6_MULTICAST_LOOP -1 ================================================ FILE: Source/platform/ctr/asio/include/sys/ioctl.h ================================================ #pragma once #include_next #define FIONREAD -999 ================================================ FILE: Source/platform/ctr/asio/include/sys/poll.h ================================================ #include ================================================ FILE: Source/platform/ctr/asio/include/sys/socket.h ================================================ #pragma once #include_next #define SO_DEBUG 0 #define SO_DONTROUTE 0 #define SO_KEEPALIVE 0 #define SOMAXCONN 10 #define MSG_EOR 0 struct msghdr { void *msg_name; socklen_t msg_namelen; struct iovec *msg_iov; int msg_iovlen; void *msg_control; socklen_t msg_controllen; int msg_flags; }; #ifdef __cplusplus extern "C" { #endif ssize_t recvmsg(int socket, struct msghdr *message, int flags); ssize_t sendmsg(int socket, const struct msghdr *message, int flags); int socketpair(int domain, int type, int protocol, int socket_vector[2]); #ifdef __cplusplus } #endif ================================================ FILE: Source/platform/ctr/asio/include/sys/uio.h ================================================ #ifndef _SYS_UIO_H #define _SYS_UIO_H 1 #include struct iovec { void *iov_base; size_t iov_len; }; ssize_t readv(int __fd, const struct iovec *__iovec, int __count); ssize_t writev(int __fd, const struct iovec *__iovec, int __count); #endif ================================================ FILE: Source/platform/ctr/asio/include/sys/un.h ================================================ #ifndef _SYS_UN_H #define _SYS_UN_H 1 typedef unsigned short int sa_family_t; struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; }; #endif ================================================ FILE: Source/platform/ctr/asio/net/if.c ================================================ #include #include #include unsigned int if_nametoindex(const char *__ifname) { return ENOTSUP; } char *if_indextoname(unsigned int __ifindex, char *__ifname) { return NULL; } struct if_nameindex *if_nameindex() { return NULL; } void if_freenameindex(struct if_nameindex *__ptr) { } ================================================ FILE: Source/platform/ctr/asio/sys/socket.c ================================================ #include #include #include #include ssize_t stream_recvmsg(int socket, struct msghdr *message, int flags) { struct iovec *next = message->msg_iov; int iovcount = message->msg_iovlen; ssize_t total = 0; for (int i = 0; i < iovcount; ++i, ++next) { struct iovec *iov = next; void *base = iov->iov_base; size_t length = iov->iov_len; while (length > 0) { ssize_t bytesReceived = recv(socket, base, length, flags); if (bytesReceived == -1) { if (total > 0 && errno == EAGAIN) return total; if (total > 0 && errno == EWOULDBLOCK) return total; return -1; } base += bytesReceived; length -= bytesReceived; total += bytesReceived; if ((flags & MSG_WAITALL) == 0) return total; } } return total; } ssize_t dgram_recvmsg(int socket, struct msghdr *message, int flags) { return ENOTSUP; } ssize_t recvmsg(int socket, struct msghdr *message, int flags) { int type; socklen_t length = sizeof(int); if (getsockopt(socket, SOL_SOCKET, SO_TYPE, &type, &length) == -1) return -1; if (type == SOCK_STREAM) return stream_recvmsg(socket, message, flags); if (type == SOCK_DGRAM) return dgram_recvmsg(socket, message, flags); errno = ENOTSOCK; return -1; } ssize_t stream_sendmsg(int socket, const struct msghdr *message, int flags) { struct iovec *next = message->msg_iov; int iovcount = message->msg_iovlen; ssize_t total = 0; for (int i = 0; i < iovcount; ++i, ++next) { struct iovec *iov = next; void *base = iov->iov_base; size_t length = iov->iov_len; if (length == 0) continue; ssize_t bytesSent = send(socket, base, length, flags); if (bytesSent == -1) { if (total > 0 && errno == EAGAIN) return total; if (total > 0 && errno == EWOULDBLOCK) return total; return -1; } total += bytesSent; } return total; } ssize_t dgram_sendmsg(int socket, const struct msghdr *message, int flags) { return ENOTSUP; } ssize_t sendmsg(int socket, const struct msghdr *message, int flags) { int type; socklen_t length = sizeof(int); if (getsockopt(socket, SOL_SOCKET, SO_TYPE, &type, &length) == -1) return -1; if (type == SOCK_STREAM) return stream_sendmsg(socket, message, flags); if (type == SOCK_DGRAM) return dgram_sendmsg(socket, message, flags); errno = ENOTSOCK; return -1; } int socketpair(int domain, int type, int protocol, int socket_vector[2]) { return ENOTSUP; } ================================================ FILE: Source/platform/ctr/asio/sys/uio.c ================================================ #include #include #include ssize_t readv(int __fd, const struct iovec *__iovec, int __count) { return ENOTSUP; } ssize_t writev(int __fd, const struct iovec *__iovec, int __count) { return ENOTSUP; } ================================================ FILE: Source/platform/ctr/cfgu_service.hpp ================================================ #pragma once #ifdef __cplusplus extern "C" { #endif #include <3ds/result.h> #include <3ds/services/cfgu.h> #ifdef __cplusplus } #endif namespace devilution { namespace n3ds { class CFGUService { public: CFGUService() { Result res = cfguInit(); isInitialized = R_SUCCEEDED(res); } ~CFGUService() { cfguExit(); } bool IsInitialized() { return isInitialized; } private: bool isInitialized; }; } // namespace n3ds } // namespace devilution ================================================ FILE: Source/platform/ctr/display.cpp ================================================ #include "platform/ctr/display.hpp" #include #include uint32_t Get3DSScalingFlag(bool fitToScreen, int width, int height) { if (fitToScreen) return SDL_FULLSCREEN; if (width * 3 < height * 5) return SDL_FITHEIGHT; return SDL_FITWIDTH; } ================================================ FILE: Source/platform/ctr/display.hpp ================================================ #pragma once #include uint32_t Get3DSScalingFlag(bool fitToScreen, int width, int height); ================================================ FILE: Source/platform/ctr/keyboard.cpp ================================================ #include #include #include "platform/ctr/keyboard.h" #include "utils/utf8.hpp" constexpr size_t MAX_TEXT_LENGTH = 255; struct vkbdEvent { std::string_view hintText; std::string_view inText; char *outText; size_t maxLength; }; static vkbdEvent events[16]; static int eventCount = 0; void ctr_vkbdInput(std::string_view hintText, std::string_view inText, char *outText, size_t maxLength) { if (eventCount >= sizeof(events)) return; vkbdEvent &event = events[eventCount]; event.hintText = hintText; event.inText = inText; event.outText = outText; event.maxLength = maxLength; eventCount++; } void ctr_vkbdFlush() { for (int i = 0; i < eventCount; i++) { vkbdEvent &event = events[i]; SwkbdState swkbd; swkbdInit(&swkbd, SWKBD_TYPE_WESTERN, 2, MAX_TEXT_LENGTH); swkbdSetValidation(&swkbd, SWKBD_NOTEMPTY_NOTBLANK, 0, 0); // swkbdSetInitialText stores the pointer to the c-string, only copying it when swkbdInputText is called. Need to // ensure it has a valid null-terminated string until that point. std::string initialText { event.inText }; swkbdSetInitialText(&swkbd, initialText.c_str()); // swkbdSetHintText copies from the c-string immediately so we can use the output buffer to save a malloc char mybuf[MAX_TEXT_LENGTH + 1]; devilution::CopyUtf8(mybuf, event.hintText, sizeof(mybuf)); swkbdSetHintText(&swkbd, mybuf); memset(mybuf, 0, sizeof(mybuf)); SwkbdButton button = swkbdInputText(&swkbd, mybuf, sizeof(mybuf)); if (button == SWKBD_BUTTON_CONFIRM) { devilution::CopyUtf8(event.outText, mybuf, event.maxLength); } } eventCount = 0; } ================================================ FILE: Source/platform/ctr/keyboard.h ================================================ #pragma once #include #include <3ds.h> /** * @brief Queues a request for user input for the next call to ctr_vkbdFlush() * @see ctr_vkdbFlush() * @param title Label for the input * @param inText Optional text to prefil the input field * @param outText Pointer to a buffer to receive user input * @param maxLength Size of the buffer */ void ctr_vkbdInput(std::string_view title, std::string_view inText, char *outText, size_t maxLength); /** * @brief Processes pending requests for user input */ void ctr_vkbdFlush(); ================================================ FILE: Source/platform/ctr/locale.cpp ================================================ #include "platform/ctr/locale.hpp" #include <3ds.h> #include "platform/ctr/cfgu_service.hpp" namespace devilution { namespace n3ds { std::string GetLocale() { CFGUService cfguService; if (!cfguService.IsInitialized()) return ""; u8 language; Result res = CFGU_GetSystemLanguage(&language); if (!R_SUCCEEDED(res)) return ""; switch (language) { case CFG_LANGUAGE_JP: return "ja"; case CFG_LANGUAGE_EN: return "en"; case CFG_LANGUAGE_FR: return "fr"; case CFG_LANGUAGE_DE: return "de"; case CFG_LANGUAGE_IT: return "it"; case CFG_LANGUAGE_ES: return "es"; case CFG_LANGUAGE_ZH: return "zh_CN"; case CFG_LANGUAGE_KO: return "ko"; case CFG_LANGUAGE_NL: return "nl"; case CFG_LANGUAGE_PT: return "pt_BR"; case CFG_LANGUAGE_RU: return "ru"; case CFG_LANGUAGE_TW: return "zh_TW"; default: return ""; } } } // namespace n3ds } // namespace devilution ================================================ FILE: Source/platform/ctr/locale.hpp ================================================ #pragma once #include namespace devilution { namespace n3ds { std::string GetLocale(); } // namespace n3ds } // namespace devilution ================================================ FILE: Source/platform/ctr/messagebox.cpp ================================================ #include <3ds.h> #include #include "utils/sdl2_to_1_2_backports.h" #include "utils/str_cat.hpp" int SDL_ShowSimpleMessageBox(Uint32 flags, const char *title, const char *message, SDL_Surface *window) { if (SDL_ShowCursor(SDL_DISABLE) <= -1) SDL_Log("%s", SDL_GetError()); bool init = !gspHasGpuRight(); auto text = devilution::StrCat(title, "\n\n", message); if (init) gfxInitDefault(); errorConf error; errorInit(&error, ERROR_TEXT, CFG_LANGUAGE_EN); errorText(&error, text.c_str()); errorDisp(&error); if (init) gfxExit(); return 0; } ================================================ FILE: Source/platform/ctr/random.cpp ================================================ #include <3ds.h> #include #include #include static const char *randombytes_ctrrandom_implementation_name() { return "ctrrandom"; } static bool randombytes_ctrrandom_tryfill(void *const buf, const size_t size) { Result res; if (!psGetSessionHandle()) { res = psInit(); if (!R_SUCCEEDED(res)) return false; } res = PS_GenerateRandomBytes(buf, size); return R_SUCCEEDED(res); } static uint32_t randombytes_ctrrandom() { uint32_t num; if (!randombytes_ctrrandom_tryfill(&num, sizeof(uint32_t))) sodium_misuse(); return num; } static void randombytes_ctrrandom_buf(void *const buf, const size_t size) { if (!randombytes_ctrrandom_tryfill(buf, size)) sodium_misuse(); } struct randombytes_implementation randombytes_ctrrandom_implementation = { randombytes_ctrrandom_implementation_name, randombytes_ctrrandom, nullptr, nullptr, randombytes_ctrrandom_buf, nullptr }; void randombytes_ctrrandom_init() { randombytes_set_implementation(&randombytes_ctrrandom_implementation); } ================================================ FILE: Source/platform/ctr/random.hpp ================================================ #pragma once void randombytes_ctrrandom_init(); ================================================ FILE: Source/platform/ctr/sockets.cpp ================================================ #include "platform/ctr/sockets.hpp" #include #include // This header must be included before any 3DS code // because 3DS SDK defines a macro with the same name // as an fmt template parameter in some versions of fmt. // See https://github.com/fmtlib/fmt/issues/3632 #include #include <3ds.h> #include "utils/log.hpp" namespace devilution { constexpr auto SOC_ALIGN = 0x1000; constexpr auto SOC_BUFFERSIZE = 0x100000; static u32 *socBuffer; static bool initialized; static bool waitForWifi() { // 100 ms constexpr s64 sleepNano = 100 * 1000 * 1000; // 5 sec constexpr int loopCount = 5 * 1000 / 100; uint32_t wifi = 0; for (int i = 0; i < loopCount; ++i) { if (R_SUCCEEDED(ACU_GetWifiStatus(&wifi)) && wifi) return true; svcSleepThread(sleepNano); } return false; } void n3ds_socExit() { if (socBuffer == nullptr) return; socExit(); free(socBuffer); socBuffer = nullptr; } void n3ds_socInit() { if (!waitForWifi()) { LogError("n3ds_socInit: Wifi off"); return; } socBuffer = (u32 *)memalign(SOC_ALIGN, SOC_BUFFERSIZE); if (socBuffer == nullptr) { LogError("n3ds_socInit: memalign() failed"); return; } Result result = socInit(socBuffer, SOC_BUFFERSIZE); if (!R_SUCCEEDED(result)) { LogError("n3ds_socInit: socInit() failed"); free(socBuffer); return; } if (!initialized) atexit([]() { n3ds_socExit(); }); initialized = true; } } // namespace devilution ================================================ FILE: Source/platform/ctr/sockets.hpp ================================================ #pragma once namespace devilution { void n3ds_socInit(); void n3ds_socExit(); } // namespace devilution ================================================ FILE: Source/platform/ctr/system.cpp ================================================ #include <3ds.h> #include #include #include "platform/ctr/cfgu_service.hpp" #include "platform/ctr/random.hpp" #include "platform/ctr/sockets.hpp" #include "platform/ctr/system.h" using namespace devilution; bool shouldDisableBacklight; aptHookCookie cookie; void aptHookFunc(APT_HookType hookType, void *param) { switch (hookType) { case APTHOOK_ONSUSPEND: ctr_lcd_backlight_on(); break; case APTHOOK_ONSLEEP: break; case APTHOOK_ONRESTORE: ctr_lcd_backlight_off(); break; case APTHOOK_ONWAKEUP: ctr_lcd_backlight_off(); break; case APTHOOK_ONEXIT: ctr_lcd_backlight_on(); break; default: break; } } void ctr_lcd_backlight_on() { if (!shouldDisableBacklight) return; gspLcdInit(); GSPLCD_PowerOnBacklight(GSPLCD_SCREEN_BOTTOM); gspLcdExit(); } void ctr_lcd_backlight_off() { if (!shouldDisableBacklight) return; gspLcdInit(); GSPLCD_PowerOffBacklight(GSPLCD_SCREEN_BOTTOM); gspLcdExit(); } bool ctr_check_dsp() { FILE *dsp = fopen("sdmc:/3ds/dspfirm.cdc", "r"); if (dsp == NULL) { gfxInitDefault(); errorConf error; errorInit(&error, ERROR_TEXT, CFG_LANGUAGE_EN); errorText(&error, "Cannot find DSP firmware!\n\n\"sdmc:/3ds/dspfirm.cdc\"\n\nRun \'DSP1\' at least once to\ndump your DSP firmware."); errorDisp(&error); gfxExit(); return false; } fclose(dsp); return true; } bool ctr_is_n3ds() { bool isN3DS; Result res = APT_CheckNew3DS(&isN3DS); return R_SUCCEEDED(res) && isN3DS; } bool ctr_should_disable_backlight() { n3ds::CFGUService cfguService; if (!cfguService.IsInitialized()) return false; u8 model; Result res = CFGU_GetSystemModel(&model); if (!R_SUCCEEDED(res)) return false; return model != CFG_MODEL_2DS; } void ctr_sys_init() { if (ctr_check_dsp() == false) exit(0); aptHook(&cookie, aptHookFunc, NULL); if (ctr_is_n3ds()) osSetSpeedupEnable(true); shouldDisableBacklight = ctr_should_disable_backlight(); ctr_lcd_backlight_off(); atexit([]() { ctr_lcd_backlight_on(); }); romfsInit(); atexit([]() { romfsExit(); }); acInit(); atexit([]() { acExit(); }); n3ds_socInit(); atexit([]() { n3ds_socExit(); }); #ifdef PACKET_ENCRYPTION randombytes_ctrrandom_init(); atexit([]() { if (psGetSessionHandle()) psExit(); }); #endif } ================================================ FILE: Source/platform/ctr/system.h ================================================ #pragma once void ctr_lcd_backlight_on(); void ctr_lcd_backlight_off(); bool ctr_check_dsp(); void ctr_sys_init(); ================================================ FILE: Source/platform/ios/ios_paths.h ================================================ #pragma once #ifdef __cplusplus extern "C" { #endif extern char *IOSGetPrefPath(); #ifdef __cplusplus } #endif ================================================ FILE: Source/platform/ios/ios_paths.m ================================================ #import #include "ios_paths.h" #ifdef __cplusplus extern "C" { #endif char *IOSGetPrefPath() { @autoreleasepool { NSArray *array = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); if ([array count] > 0) { NSString *str = [array objectAtIndex:0]; str = [str stringByAppendingString:@"/"]; const char *base = [str fileSystemRepresentation]; char *copy = malloc(strlen(base) + 1); strcpy(copy, base); return copy; } return ""; } } #ifdef __cplusplus } #endif ================================================ FILE: Source/platform/locale.cpp ================================================ #include "locale.hpp" #include #include #include #ifdef __ANDROID__ #include #include #elif defined(__vita__) #include #include #include #elif defined(__3DS__) #include "platform/ctr/locale.hpp" #elif defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) // Suppress definitions of `min` and `max` macros by : #define NOMINMAX 1 #define WIN32_LEAN_AND_MEAN // clang-format off #include #include // clang-format on #elif defined(__APPLE__) and defined(USE_COREFOUNDATION) #include #else #include #endif namespace devilution { namespace { #if (defined(_WIN32) && WINVER >= 0x0600 && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR)) || (defined(__APPLE__) && defined(USE_COREFOUNDATION)) std::string IetfToPosix(std::string_view langCode) { /* * Handle special case for simplified/traditional Chinese. IETF/BCP-47 specifies that only the script should be * used to discriminate languages when the region doesn't add additional value. For chinese scripts zh-Hans is * preferred over zh-Hans-CN (but platforms may include the region identifier anyway). POSIX locales don't use * script in the same way so we need to convert these back to lang_region. */ if (langCode.substr(0, 7) == "zh-Hans") { return "zh_CN"; } if (langCode.substr(0, 7) == "zh-Hant") { return "zh_TW"; } std::string posixLangCode { langCode }; // if a region is included in the locale do a simple transformation to the expected POSIX style. std::replace(posixLangCode.begin(), posixLangCode.end(), '-', '_'); return posixLangCode; } #endif } // namespace std::vector GetLocales() { std::vector locales {}; #ifdef __ANDROID__ JNIEnv *env = (JNIEnv *)SDL_AndroidGetJNIEnv(); jobject activity = (jobject)SDL_AndroidGetActivity(); jclass clazz(env->GetObjectClass(activity)); jmethodID method_id = env->GetMethodID(clazz, "getLocale", "()Ljava/lang/String;"); jstring jLocale = (jstring)env->CallObjectMethod(activity, method_id); const char *cLocale = env->GetStringUTFChars(jLocale, nullptr); locales.emplace_back(cLocale); env->ReleaseStringUTFChars(jLocale, cLocale); env->DeleteLocalRef(jLocale); env->DeleteLocalRef(activity); env->DeleteLocalRef(clazz); #elif defined(__vita__) int32_t language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US; // default to english const char *vita_locales[] = { "ja_JP", "en_US", "fr_FR", "es_ES", "de_DE", "it_IT", "nl_NL", "pt_PT", "ru_RU", "ko_KR", "zh_TW", "zh_CN", "fi_FI", "sv_SE", "da_DK", "no_NO", "pl_PL", "pt_BR", "en_GB", "tr_TR", }; SceAppUtilInitParam initParam; SceAppUtilBootParam bootParam; memset(&initParam, 0, sizeof(SceAppUtilInitParam)); memset(&bootParam, 0, sizeof(SceAppUtilBootParam)); sceAppUtilInit(&initParam, &bootParam); sceAppUtilSystemParamGetInt(SCE_SYSTEM_PARAM_ID_LANG, &language); if (language < 0 || language > SCE_SYSTEM_PARAM_LANG_TURKISH) language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US; // default to english locales.emplace_back(vita_locales[language]); sceAppUtilShutdown(); #elif defined(__ORBIS__) // use default #elif defined(__3DS__) locales.push_back(n3ds::GetLocale()); #elif defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #if WINVER >= 0x0600 auto wideCharToUtf8 = [](PWSTR wideString) { // WideCharToMultiByte potentially leaves the buffer unterminated, default initialise here as a workaround char utf8Buffer[16] {}; // Fetching up to 10 characters to allow for script tags WideCharToMultiByte(CP_UTF8, 0, wideString, 10, utf8Buffer, sizeof(utf8Buffer), nullptr, nullptr); // Windows NLS functions return IETF/BCP-47 locale codes (or potentially arbitrary custom locales) but devX // uses posix format codes, return the expected format return std::move(IetfToPosix(utf8Buffer)); }; WCHAR localeBuffer[LOCALE_NAME_MAX_LENGTH]; if (GetUserDefaultLocaleName(localeBuffer, sizeof(localeBuffer)) != 0) { // Found a user default locale convert from WIN32's default of UTF16 to UTF8 and add to the list locales.push_back(wideCharToUtf8(localeBuffer)); } ULONG numberOfLanguages; ULONG bufferSize = sizeof(localeBuffer); GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, localeBuffer, &bufferSize); PWSTR bufferOffset = localeBuffer; for (unsigned i = 0; i < numberOfLanguages; i++) { // Found one (or more) user preferred UI language(s), add these to the list to check as well locales.push_back(wideCharToUtf8(bufferOffset)); // GetUserPreferredUILanguages returns a null separated list of strings, need to increment past the null terminating // the current string. bufferOffset += lstrlenW(localeBuffer) + 1; } #else // Fallback method for older versions of windows, this is deprecated since Vista char localeBuffer[LOCALE_NAME_MAX_LENGTH]; // Deliberately not using the unicode versions here as the information retrieved should be represented in ASCII/single // byte UTF8 codepoints. if (GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO639LANGNAME, localeBuffer, LOCALE_NAME_MAX_LENGTH) != 0) { std::string locale { localeBuffer }; if (GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME, localeBuffer, LOCALE_NAME_MAX_LENGTH) != 0) { locale.append("_"); locale.append(localeBuffer); } locales.push_back(std::move(locale)); } #endif #elif defined(__APPLE__) && defined(USE_COREFOUNDATION) && DARWIN_MAJOR_VERSION >= 9 // Get the user's language list (in order of preference) CFArrayRef languages = CFLocaleCopyPreferredLanguages(); CFIndex numLanguages = CFArrayGetCount(languages); for (CFIndex i = 0; i < numLanguages; i++) { auto language = static_cast(CFArrayGetValueAtIndex(languages, i)); char buffer[16]; if (CFStringGetCString(language, buffer, sizeof(buffer), kCFStringEncodingUTF8)) { // Convert to the posix format expected by callers locales.push_back(IetfToPosix(buffer)); } } CFRelease(languages); #else constexpr auto svOrEmpty = [](const char *cString) -> std::string_view { return cString != nullptr ? cString : ""; }; std::string_view languages = svOrEmpty(std::getenv("LANGUAGE")); if (languages.empty()) { languages = svOrEmpty(std::getenv("LANG")); if (languages.empty()) { // Ideally setlocale with a POSIX defined constant should never return NULL, but... #ifdef LC_MESSAGES languages = svOrEmpty(setlocale(LC_MESSAGES, nullptr)); #else languages = svOrEmpty(setlocale(LC_CTYPE, nullptr)); #endif } if (!languages.empty()) locales.emplace_back(languages.substr(0, languages.find_first_of("."))); } else { do { const size_t separatorPos = languages.find_first_of(":"); if (separatorPos != 0) locales.emplace_back(std::string(languages.substr(0, separatorPos))); if (separatorPos != languages.npos) languages.remove_prefix(separatorPos + 1); else break; } while (true); } #endif return locales; } } // namespace devilution ================================================ FILE: Source/platform/locale.hpp ================================================ #pragma once #include #include namespace devilution { /** * @brief Returns a list of preferred languages based on user/system configuration. * @return 0 or more POSIX locale codes (language code with optional region identifier) */ std::vector GetLocales(); } // namespace devilution ================================================ FILE: Source/platform/macos_sdl1/SDL_filesystem.m ================================================ /* Simple DirectMedia Layer Copyright (C) 1997-2024 Sam Lantinga This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. An altered version based on: https://github.com/libsdl-org/SDL/blob/3c142abcb2b0b0ad7e08b096ea8d9a1a1e1af1ef/src/filesystem/cocoa/SDL_sysfilesystem.m Modifications: 1. Changes to compile with gcc (@autoreleasepool -> NSAutoreleasePool) 2. Targets SDL-1.2 rather than SDL2 (SDL_InvalidParamError -> SDL_SetError) */ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* System dependent filesystem routines */ #include #include #include #include char *SDL_GetBasePath(void) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSBundle *bundle = [NSBundle mainBundle]; const char *baseType = [[[bundle infoDictionary] objectForKey:@"SDL_FILESYSTEM_BASE_DIR_TYPE"] UTF8String]; const char *base = NULL; char *retval = NULL; if (baseType == NULL) { baseType = "resource"; } if (SDL_strcasecmp(baseType, "bundle") == 0) { base = [[bundle bundlePath] fileSystemRepresentation]; } else if (SDL_strcasecmp(baseType, "parent") == 0) { base = [[[bundle bundlePath] stringByDeletingLastPathComponent] fileSystemRepresentation]; } else { /* this returns the exedir for non-bundled and the resourceDir for bundled apps */ base = [[bundle resourcePath] fileSystemRepresentation]; } if (base) { const size_t len = SDL_strlen(base) + 2; retval = (char *)SDL_malloc(len); if (retval == NULL) { SDL_OutOfMemory(); } else { SDL_snprintf(retval, len, "%s/", base); } } [pool drain]; return retval; } char *SDL_GetPrefPath(const char *org, const char *app) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; char *retval = NULL; NSArray *array; if (!app) { SDL_SetError("SDL_GetPrefPath: app argument cannot be null"); return NULL; } if (!org) { org = ""; } #if !TARGET_OS_TV array = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); #else /* tvOS does not have persistent local storage! * The only place on-device where we can store data is * a cache directory that the OS can empty at any time. * * It's therefore very likely that save data will be erased * between sessions. If you want your app's save data to * actually stick around, you'll need to use iCloud storage. */ { static SDL_bool shown = SDL_FALSE; if (!shown) { shown = SDL_TRUE; SDL_LogCritical(SDL_LOG_CATEGORY_SYSTEM, "tvOS does not have persistent local storage! Use iCloud storage if you want your data to persist between sessions.\n"); } } array = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); #endif /* !TARGET_OS_TV */ if ([array count] > 0) { /* we only want the first item in the list. */ NSString *str = [array objectAtIndex:0]; const char *base = [str fileSystemRepresentation]; if (base) { const size_t len = SDL_strlen(base) + SDL_strlen(org) + SDL_strlen(app) + 4; retval = (char *)SDL_malloc(len); if (retval == NULL) { SDL_OutOfMemory(); } else { char *ptr; if (*org) { SDL_snprintf(retval, len, "%s/%s/%s/", base, org, app); } else { SDL_snprintf(retval, len, "%s/%s/", base, app); } for (ptr = retval + 1; *ptr; ptr++) { if (*ptr == '/') { *ptr = '\0'; mkdir(retval, 0700); *ptr = '/'; } } mkdir(retval, 0700); } } } [pool drain]; return retval; } /* vi: set ts=4 sw=4 expandtab: */ ================================================ FILE: Source/platform/switch/CMakeLists.txt ================================================ include(functions/devilutionx_library) add_devilutionx_object_library(libdevilutionx_switch romfs.cpp network.cpp keyboard.cpp docking.cpp asio/pause.c asio/net/if.c asio/sys/signal.c ) if(NOT NONET) if(NOT DISABLE_TCP) target_link_libraries(libdevilutionx_switch PUBLIC asio) endif() if(PACKET_ENCRYPTION) target_sources(libdevilutionx_switch PRIVATE random.cpp) target_link_libraries(libdevilutionx_switch PUBLIC sodium) endif() endif() target_link_libraries(libdevilutionx_switch PUBLIC DevilutionX::SDL libdevilutionx_log ) ================================================ FILE: Source/platform/switch/asio/include/errno.h ================================================ #pragma once #include_next #define ESHUTDOWN (__ELASTERROR + 1) ================================================ FILE: Source/platform/switch/asio/include/net/if.h ================================================ #ifndef _NET_IF_H #define _NET_IF_H 1 #define IF_NAMESIZE 16 struct if_nameindex { unsigned int if_index; char *if_name; }; #ifdef __cplusplus extern "C" { #endif unsigned int if_nametoindex(const char *__ifname); char *if_indextoname(unsigned int __ifindex, char *__ifname); struct if_nameindex *if_nameindex(); void if_freenameindex(struct if_nameindex *__ptr); #ifdef __cplusplus } #endif #endif ================================================ FILE: Source/platform/switch/asio/include/netinet/in.h ================================================ #pragma once #include_next struct ipv6_mreq { struct in6_addr ipv6mr_multiaddr; unsigned ipv6mr_interface; }; ================================================ FILE: Source/platform/switch/asio/include/sys/uio.h ================================================ #ifndef _SYS_UIO_H #define _SYS_UIO_H 1 #include #include ssize_t readv(int __fd, const struct iovec *__iovec, int __count); ssize_t writev(int __fd, const struct iovec *__iovec, int __count); #endif ================================================ FILE: Source/platform/switch/asio/include/sys/un.h ================================================ #ifndef _SYS_UN_H #define _SYS_UN_H 1 #include #ifndef _SA_FAMILY_T_DECLARED typedef __sa_family_t sa_family_t; #define _SA_FAMILY_T_DECLARED #endif struct sockaddr_un { sa_family_t sun_family; char sun_path[108]; }; #endif ================================================ FILE: Source/platform/switch/asio/net/if.c ================================================ #include #include #include unsigned int if_nametoindex(const char *__ifname) { return ENOTSUP; } char *if_indextoname(unsigned int __ifindex, char *__ifname) { return NULL; } struct if_nameindex *if_nameindex() { return NULL; } void if_freenameindex(struct if_nameindex *__ptr) { } ================================================ FILE: Source/platform/switch/asio/pause.c ================================================ #include int pause(void) { errno = ENOSYS; return -1; } ================================================ FILE: Source/platform/switch/asio/sys/signal.c ================================================ #include #include int pthread_sigmask(int, const sigset_t *, sigset_t *) { return ENOTSUP; } ================================================ FILE: Source/platform/switch/docking.cpp ================================================ #include "platform/switch/docking.h" #include #include #include #include "utils/display.h" namespace devilution { namespace { enum class OperationMode : int8_t { Handheld, Docked, Uninitialized = -1 }; } /** * @brief Do a manual window resize when docking/undocking the Switch */ void HandleDocking() { static OperationMode currentMode = OperationMode::Uninitialized; // keep track of docked or handheld mode OperationMode newMode; switch (appletGetOperationMode()) { case AppletOperationMode_Console: newMode = OperationMode::Docked; break; case AppletOperationMode_Handheld: default: newMode = OperationMode::Handheld; } if (currentMode != newMode) { int display_width; int display_height; // docked mode has changed, update window size if (newMode == OperationMode::Docked) { display_width = 1920; display_height = 1080; } else { display_width = 1280; display_height = 720; } currentMode = newMode; // remove leftover-garbage on screen. Need to perform three clears to ensure all buffers get cleared, otherwise // the display flickers showing a stale frame at certain refresh rates/dock modes. for (auto i = 0; i < 3; i++) { SDL_RenderClear(renderer); SDL_RenderPresent(renderer); } SDL_SetWindowSize(ghMainWnd, display_width, display_height); } } } // namespace devilution ================================================ FILE: Source/platform/switch/docking.h ================================================ #pragma once namespace devilution { void HandleDocking(); } // namespace devilution ================================================ FILE: Source/platform/switch/keyboard.cpp ================================================ #include "platform/switch/keyboard.h" #include #include #include #include #include "utils/utf8.hpp" static void switch_keyboard_get(std::string_view guide_text, std::string_view initial_text, char *buf, unsigned buf_len) { Result rc = 0; SwkbdConfig kbd; rc = swkbdCreate(&kbd, 0); if (R_SUCCEEDED(rc)) { swkbdConfigMakePresetDefault(&kbd); // swkbConfigSetGuide/InitialText both copy the input string. They expect a null terminated c-string but we're // getting a string view which may be a substring of a larger std::string/c-string, so we copy to a temporary // null-terminated string first for safety if (!guide_text.empty()) { std::string textCopy { guide_text }; swkbdConfigSetGuideText(&kbd, textCopy.c_str()); } if (!initial_text.empty()) { std::string textCopy { initial_text }; swkbdConfigSetInitialText(&kbd, textCopy.c_str()); } swkbdConfigSetStringLenMax(&kbd, buf_len); rc = swkbdShow(&kbd, buf, buf_len); swkbdClose(&kbd); } } static int get_utf8_character_bytes(const uint8_t *uc) { if (uc[0] < 0x80) { return 1; } else if ((uc[0] & 0xe0) == 0xc0 && (uc[1] & 0xc0) == 0x80) { return 2; } else if ((uc[0] & 0xf0) == 0xe0 && (uc[1] & 0xc0) == 0x80 && (uc[2] & 0xc0) == 0x80) { return 3; } else if ((uc[0] & 0xf8) == 0xf0 && (uc[1] & 0xc0) == 0x80 && (uc[2] & 0xc0) == 0x80 && (uc[3] & 0xc0) == 0x80) { return 4; } else { return 1; } } static void switch_create_and_push_sdlkey_event(uint32_t event_type, SDL_Scancode scan, SDL_Keycode key) { SDL_Event event; event.type = event_type; event.key.keysym.scancode = scan; event.key.keysym.sym = key; event.key.keysym.mod = 0; SDL_PushEvent(&event); } void switch_start_text_input(std::string_view guide_text, std::string_view initial_text, unsigned max_length) { char text[max_length] = { '\0' }; switch_keyboard_get(guide_text, initial_text, text, sizeof(text)); for (int i = 0; i < 600; i++) { switch_create_and_push_sdlkey_event(SDL_KEYDOWN, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE); switch_create_and_push_sdlkey_event(SDL_KEYUP, SDL_SCANCODE_BACKSPACE, SDLK_BACKSPACE); } for (int i = 0; i < 600; i++) { switch_create_and_push_sdlkey_event(SDL_KEYDOWN, SDL_SCANCODE_DELETE, SDLK_DELETE); switch_create_and_push_sdlkey_event(SDL_KEYUP, SDL_SCANCODE_DELETE, SDLK_DELETE); } if (text[0] == '\0') { devilution::CopyUtf8(text, initial_text, sizeof(text)); } const uint8_t *utf8_text = (uint8_t *)text; for (int i = 0; i < sizeof(text) && utf8_text[i];) { int bytes_in_char = get_utf8_character_bytes(&utf8_text[i]); SDL_Event textinput_event; textinput_event.type = SDL_TEXTINPUT; for (int n = 0; n < bytes_in_char; n++) { textinput_event.text.text[n] = text[i + n]; } textinput_event.text.text[bytes_in_char] = 0; SDL_PushEvent(&textinput_event); i += bytes_in_char; } } ================================================ FILE: Source/platform/switch/keyboard.h ================================================ #pragma once #include /** * @brief Prompts the user for text input, pushes the user-provided text to the event queue, then returns * @param guide_text Hint text to display to the user if the input is empty * @param initial_text An optional prefilled value for the input * @param max_length How many bytes of input to accept from the user (certain characters will take multiple bytes) */ void switch_start_text_input(std::string_view guide_text, std::string_view initial_text, unsigned max_length); ================================================ FILE: Source/platform/switch/network.cpp ================================================ #include "platform/switch/network.h" #include #include #include static int nxlink_sock = -1; // for stdio on Switch void switch_enable_network() { // enable network and stdio on Switch socketInitializeDefault(); // enable error messages via nxlink on Switch nxlink_sock = nxlinkStdio(); atexit(switch_disable_network); } void switch_disable_network() { // disable network and stdio on Switch if (nxlink_sock != -1) close(nxlink_sock); socketExit(); } ================================================ FILE: Source/platform/switch/network.h ================================================ #pragma once void switch_enable_network(); void switch_disable_network(); ================================================ FILE: Source/platform/switch/random.cpp ================================================ #include "platform/switch/random.hpp" #include #include #include extern "C" { #include #include } static const char *randombytes_switchrandom_implementation_name() { return "switchrandom"; } static bool randombytes_switchrandom_tryfill(void *const buf, const size_t size) { Result res; Service *csrngService = csrngGetServiceSession(); if (!serviceIsActive(csrngService)) { res = csrngInitialize(); if (!R_SUCCEEDED(res)) return false; } res = csrngGetRandomBytes(buf, size); return R_SUCCEEDED(res); } static uint32_t randombytes_switchrandom() { uint32_t num; if (!randombytes_switchrandom_tryfill(&num, sizeof(uint32_t))) sodium_misuse(); return num; } static void randombytes_switchrandom_buf(void *const buf, const size_t size) { if (!randombytes_switchrandom_tryfill(buf, size)) sodium_misuse(); } struct randombytes_implementation randombytes_switchrandom_implementation = { randombytes_switchrandom_implementation_name, randombytes_switchrandom, nullptr, nullptr, randombytes_switchrandom_buf, nullptr }; void randombytes_switchrandom_init() { randombytes_set_implementation(&randombytes_switchrandom_implementation); atexit(csrngExit); } ================================================ FILE: Source/platform/switch/random.hpp ================================================ #pragma once void randombytes_switchrandom_init(); ================================================ FILE: Source/platform/switch/romfs.cpp ================================================ #include "platform/switch/romfs.hpp" #include extern "C" { #include } void switch_romfs_init() { Result res = romfsInit(); if (R_SUCCEEDED(res)) atexit([]() { romfsExit(); }); } ================================================ FILE: Source/platform/switch/romfs.hpp ================================================ #pragma once void switch_romfs_init(); ================================================ FILE: Source/platform/vita/CMakeLists.txt ================================================ include(functions/devilutionx_library) add_devilutionx_object_library(libdevilutionx_vita network.cpp keyboard.cpp touch.cpp ) if(NOT NONET) if(NOT DISABLE_TCP) target_link_libraries(libdevilutionx_vita PUBLIC asio) endif() if(PACKET_ENCRYPTION) target_sources(libdevilutionx_vita PRIVATE random.cpp) target_link_libraries(libdevilutionx_vita PUBLIC sodium) endif() endif() target_link_libraries(libdevilutionx_vita PUBLIC DevilutionX::SDL ScePower_stub SceAppUtil_stub SceNet_stub SceNetCtl_stub libdevilutionx_options ) ================================================ FILE: Source/platform/vita/keyboard.cpp ================================================ #include "platform/vita/keyboard.h" #include #include #include #include #include static void utf16_to_utf8(const uint16_t *src, uint8_t *dst) { for (int i = 0; src[i]; i++) { if ((src[i] & 0xFF80) == 0) { *(dst++) = src[i] & 0xFF; } else if ((src[i] & 0xF800) == 0) { *(dst++) = ((src[i] >> 6) & 0xFF) | 0xC0; *(dst++) = (src[i] & 0x3F) | 0x80; } else if ((src[i] & 0xFC00) == 0xD800 && (src[i + 1] & 0xFC00) == 0xDC00) { *(dst++) = (((src[i] + 64) >> 8) & 0x3) | 0xF0; *(dst++) = (((src[i] >> 2) + 16) & 0x3F) | 0x80; *(dst++) = ((src[i] >> 4) & 0x30) | 0x80 | ((src[i + 1] << 2) & 0xF); *(dst++) = (src[i + 1] & 0x3F) | 0x80; i += 1; } else { *(dst++) = ((src[i] >> 12) & 0xF) | 0xE0; *(dst++) = ((src[i] >> 6) & 0x3F) | 0x80; *(dst++) = (src[i] & 0x3F) | 0x80; } } *dst = '\0'; } static void utf8_to_utf16(const uint8_t *src, size_t src_size, uint16_t *dst) { for (size_t i = 0; i < src_size && src[i];) { if ((src[i] & 0xE0) == 0xE0) { if (i + 2 >= src_size) { break; } *(dst++) = ((src[i] & 0x0F) << 12) | ((src[i + 1] & 0x3F) << 6) | (src[i + 2] & 0x3F); i += 3; } else if ((src[i] & 0xC0) == 0xC0) { if (i + 1 >= src_size) { break; } *(dst++) = ((src[i] & 0x1F) << 6) | (src[i + 1] & 0x3F); i += 2; } else { *(dst++) = src[i]; i += 1; } } *dst = '\0'; } static int vita_input_thread(void *ime_buffer) { while (1) { // update IME status. Terminate, if finished SceCommonDialogStatus dialogStatus = sceImeDialogGetStatus(); if (dialogStatus == SCE_COMMON_DIALOG_STATUS_FINISHED) { uint8_t utf8_buffer[SCE_IME_DIALOG_MAX_TEXT_LENGTH]; SceImeDialogResult result; SDL_memset(&result, 0, sizeof(SceImeDialogResult)); sceImeDialogGetResult(&result); // Convert UTF16 to UTF8 utf16_to_utf8((SceWChar16 *)ime_buffer, utf8_buffer); // send sdl event SDL_Event event; event.text.type = SDL_TEXTINPUT; SDL_utf8strlcpy(event.text.text, (const char *)utf8_buffer, SDL_arraysize(event.text.text)); SDL_PushEvent(&event); sceImeDialogTerm(); break; } } return 0; } static int vita_keyboard_get(std::string_view guide_text, std::string_view initial_text, unsigned max_len, SceWChar16 *buf) { SceWChar16 title[SCE_IME_DIALOG_MAX_TITLE_LENGTH]; SceWChar16 text[SCE_IME_DIALOG_MAX_TEXT_LENGTH]; SceInt32 res; SDL_memset(&title, 0, sizeof(title)); SDL_memset(&text, 0, sizeof(text)); utf8_to_utf16(reinterpret_cast(guide_text.data()), guide_text.size(), title); utf8_to_utf16(reinterpret_cast(initial_text.data()), initial_text.size(), text); SceImeDialogParam param; sceImeDialogParamInit(¶m); param.supportedLanguages = 0; param.languagesForced = SCE_FALSE; param.type = SCE_IME_TYPE_DEFAULT; param.option = 0; param.textBoxMode = SCE_IME_DIALOG_TEXTBOX_MODE_WITH_CLEAR; param.maxTextLength = max_len; param.title = title; param.initialText = text; param.inputTextBuffer = buf; res = sceImeDialogInit(¶m); if (res < 0) { return 0; } return 1; } void vita_start_text_input(std::string_view guide_text, std::string_view initial_text, unsigned max_length) { SceWChar16 ime_buffer[SCE_IME_DIALOG_MAX_TEXT_LENGTH]; if (vita_keyboard_get(guide_text, initial_text, max_length, ime_buffer)) { SDL_CreateThread(vita_input_thread, "vita_input_thread", (void *)ime_buffer); } } ================================================ FILE: Source/platform/vita/keyboard.h ================================================ #pragma once #include void vita_start_text_input(std::string_view guide_text, std::string_view initial_text, unsigned max_length); ================================================ FILE: Source/platform/vita/network.cpp ================================================ #include "platform/vita/network.h" #include #include #include #include #include #include void vita_enable_network() { SceNetInitParam param; static char memory[64 * 1024]; int ret; ret = sceSysmoduleLoadModule(SCE_SYSMODULE_NET); if (ret < 0) { return; } param.memory = memory; param.size = sizeof(memory); param.flags = 0; ret = sceNetInit(¶m); if (ret < 0) { return; } ret = sceNetCtlInit(); if (ret < 0) { return; } } ================================================ FILE: Source/platform/vita/network.h ================================================ #pragma once void vita_enable_network(); ================================================ FILE: Source/platform/vita/random.cpp ================================================ #include #include #include #include static const char *randombytes_vitarandom_implementation_name() { return "vitarandom"; } static uint32_t randombytes_vitarandom() { uint32_t num; sceKernelGetRandomNumber(&num, sizeof(uint32_t)); return num; } static void randombytes_vitarandom_buf(void *const buf, const size_t size) { sceKernelGetRandomNumber(buf, size); } struct randombytes_implementation randombytes_vitarandom_implementation = { randombytes_vitarandom_implementation_name, randombytes_vitarandom, nullptr, nullptr, randombytes_vitarandom_buf, nullptr }; void randombytes_vitarandom_init() { randombytes_set_implementation(&randombytes_vitarandom_implementation); } ================================================ FILE: Source/platform/vita/random.hpp ================================================ #pragma once void randombytes_vitarandom_init(); ================================================ FILE: Source/platform/vita/touch.cpp ================================================ #include "platform/vita/touch.h" #include #include #include "options.h" #include "utils/display.h" #include "utils/ui_fwd.h" namespace devilution { namespace { #define TOUCH_PORT_MAX_NUM 1 #define NO_TOUCH (-1) // finger id setting if finger is not touching the screen int visible_width; int visible_height; int x_borderwidth; int y_borderwidth; template inline T clip(T v, T amin, T amax) { if (v < amin) return amin; if (v > amax) return amax; return v; } void SetMouseMotionEvent(SDL_Event *event, int32_t x, int32_t y, int32_t xrel, int32_t yrel) { event->type = SDL_MOUSEMOTION; event->motion.x = x; event->motion.y = y; event->motion.xrel = xrel; event->motion.yrel = yrel; event->motion.which = SDL_TOUCH_MOUSEID; } bool touch_initialized = false; unsigned int simulated_click_start_time[TOUCH_PORT_MAX_NUM][2]; // initiation time of last simulated left or right click (zero if no click) bool direct_touch = true; // pointer jumps to finger Point Mouse; // always reflects current mouse position uint32_t IgnoreEvent = SDL_USEREVENT; // custom event type to signal events that should be ignored enum { // clang-format off MaxNumFingers = 3, // number of fingers to track per panel MaxTapTime = 250, // taps longer than this will not result in mouse click events MaxTapMotionDistance = 10, // max distance finger motion in Vita screen pixels to be considered a tap SimulatedClickDuration = 50, // time in ms how long simulated mouse clicks should be // clang-format on }; struct Touch { int id; // -1: not touching uint32_t timeLastDown; int lastX; // last known screen coordinates int lastY; // last known screen coordinates float lastDownX; // SDL touch coordinates when last pressed down float lastDownY; // SDL touch coordinates when last pressed down }; Touch finger[TOUCH_PORT_MAX_NUM][MaxNumFingers]; // keep track of finger status enum DraggingType : uint8_t { DragNone, DragTwoFinger, DragThreeFinger, }; DraggingType multi_finger_dragging[TOUCH_PORT_MAX_NUM]; // keep track whether we are currently drag-and-dropping void InitTouch() { for (int port = 0; port < TOUCH_PORT_MAX_NUM; port++) { for (int i = 0; i < MaxNumFingers; i++) { finger[port][i].id = NO_TOUCH; } multi_finger_dragging[port] = DragNone; } for (auto &port : simulated_click_start_time) { for (unsigned int &time : port) { time = 0; } } SDL_DisplayMode current; SDL_GetCurrentDisplayMode(0, ¤t); visible_height = current.h; visible_width = (current.h * devilution::gnScreenWidth) / devilution::gnScreenHeight; x_borderwidth = (current.w - visible_width) / 2; y_borderwidth = (current.h - visible_height) / 2; IgnoreEvent = SDL_RegisterEvents(1); } void SetMouseButtonEvent(SDL_Event &event, uint32_t type, uint8_t button, Point position) { event.type = type; event.button.button = button; if (type == SDL_MOUSEBUTTONDOWN) { event.button.state = SDL_PRESSED; } else { event.button.state = SDL_RELEASED; } event.button.x = position.x; event.button.y = position.y; } void PreprocessFingerDown(SDL_Event *event) { // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; // id (for multitouch) SDL_FingerID id = event->tfinger.fingerId; int x = Mouse.x; int y = Mouse.y; if (direct_touch) { x = static_cast(event->tfinger.x * visible_width) + x_borderwidth; y = static_cast(event->tfinger.y * visible_height) + y_borderwidth; devilution::OutputToLogical(&x, &y); } // make sure each finger is not reported down multiple times for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id != id) { continue; } finger[port][i].id = NO_TOUCH; } // we need the timestamps to decide later if the user performed a short tap (click) // or a long tap (drag) // we also need the last coordinates for each finger to keep track of dragging for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id != NO_TOUCH) { continue; } finger[port][i].id = id; finger[port][i].timeLastDown = event->tfinger.timestamp; finger[port][i].lastDownX = event->tfinger.x; finger[port][i].lastDownY = event->tfinger.y; finger[port][i].lastX = x; finger[port][i].lastY = y; break; } } void PreprocessBackFingerDown(SDL_Event *event) { // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; if (port != 1) return; event->type = SDL_CONTROLLERAXISMOTION; event->caxis.value = 32767; event->caxis.which = 0; if (event->tfinger.x <= 0.5) { ; event->caxis.axis = SDL_CONTROLLER_AXIS_TRIGGERLEFT; } else { event->caxis.axis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; } } void PreprocessBackFingerUp(SDL_Event *event) { // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; if (port != 1) return; event->type = SDL_CONTROLLERAXISMOTION; event->caxis.value = 0; event->caxis.which = 0; if (event->tfinger.x <= 0.5) { event->caxis.axis = SDL_CONTROLLER_AXIS_TRIGGERLEFT; } else { event->caxis.axis = SDL_CONTROLLER_AXIS_TRIGGERRIGHT; } } void PreprocessFingerUp(SDL_Event *event) { // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; // id (for multitouch) SDL_FingerID id = event->tfinger.fingerId; // find out how many fingers were down before this event int numFingersDown = 0; for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id >= 0) { numFingersDown++; } } int x = Mouse.x; int y = Mouse.y; for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id != id) { continue; } finger[port][i].id = NO_TOUCH; if (multi_finger_dragging[port] == DragNone) { if ((event->tfinger.timestamp - finger[port][i].timeLastDown) > MaxTapTime) { continue; } // short (tfinger.x * devilution::GetOutputSurface()->w) - (finger[port][i].lastDownX * devilution::GetOutputSurface()->w)); float yrel = ((event->tfinger.y * devilution::GetOutputSurface()->h) - (finger[port][i].lastDownY * devilution::GetOutputSurface()->h)); auto maxRSquared = static_cast(MaxTapMotionDistance * MaxTapMotionDistance); if ((xrel * xrel + yrel * yrel) >= maxRSquared) { continue; } if (numFingersDown != 2 && numFingersDown != 1) { continue; } Uint8 simulatedButton = 0; if (numFingersDown == 2) { simulatedButton = SDL_BUTTON_RIGHT; // need to raise the button later simulated_click_start_time[port][1] = event->tfinger.timestamp; } else if (numFingersDown == 1) { simulatedButton = SDL_BUTTON_LEFT; // need to raise the button later simulated_click_start_time[port][0] = event->tfinger.timestamp; if (direct_touch) { x = static_cast(event->tfinger.x * visible_width) + x_borderwidth; y = static_cast(event->tfinger.y * visible_height) + y_borderwidth; devilution::OutputToLogical(&x, &y); } } SetMouseButtonEvent(*event, SDL_MOUSEBUTTONDOWN, simulatedButton, { x, y }); event->button.which = SDL_TOUCH_MOUSEID; } else if (numFingersDown == 1) { // when dragging, and the last finger is lifted, the drag is over Uint8 simulatedButton = 0; if (multi_finger_dragging[port] == DragThreeFinger) { simulatedButton = SDL_BUTTON_RIGHT; } else { simulatedButton = SDL_BUTTON_LEFT; } SetMouseButtonEvent(*event, SDL_MOUSEBUTTONUP, simulatedButton, { x, y }); event->button.which = SDL_TOUCH_MOUSEID; multi_finger_dragging[port] = DragNone; } } } void PreprocessFingerMotion(SDL_Event *event) { // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; // id (for multitouch) SDL_FingerID id = event->tfinger.fingerId; // find out how many fingers were down before this event int numFingersDown = 0; for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id >= 0) { numFingersDown++; } } if (numFingersDown == 0) { return; } if (numFingersDown >= 1) { int x = Mouse.x; int y = Mouse.y; if (direct_touch) { x = static_cast(event->tfinger.x * visible_width) + x_borderwidth; y = static_cast(event->tfinger.y * visible_height) + y_borderwidth; devilution::OutputToLogical(&x, &y); } else { // for relative mode, use the pointer speed setting constexpr float SpeedFactor = 1.25F; // convert touch events to relative mouse pointer events // Whenever an SDL_event involving the mouse is processed, x = static_cast(Mouse.x + (event->tfinger.dx * SpeedFactor * devilution::GetOutputSurface()->w)); y = static_cast(Mouse.y + (event->tfinger.dy * SpeedFactor * devilution::GetOutputSurface()->h)); } x = clip(x, 0, devilution::GetOutputSurface()->w); y = clip(y, 0, devilution::GetOutputSurface()->h); int xrel = x - Mouse.x; int yrel = y - Mouse.y; // update the current finger's coordinates so we can track it later for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id != id) continue; finger[port][i].lastX = x; finger[port][i].lastY = y; } // If we are starting a multi-finger drag, start holding down the mouse button if (numFingersDown >= 2 && multi_finger_dragging[port] == DragNone) { // only start a multi-finger drag if at least two fingers have been down long enough int numFingersDownlong = 0; for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id == NO_TOUCH) { continue; } if (event->tfinger.timestamp - finger[port][i].timeLastDown > MaxTapTime) { numFingersDownlong++; } } if (numFingersDownlong >= 2) { Point mouseDown = Mouse; if (direct_touch) { for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id == id) { uint32_t earliestTime = finger[port][i].timeLastDown; for (int j = 0; j < MaxNumFingers; j++) { if (finger[port][j].id >= 0 && (i != j)) { if (finger[port][j].timeLastDown < earliestTime) { mouseDown.x = finger[port][j].lastX; mouseDown.y = finger[port][j].lastY; earliestTime = finger[port][j].timeLastDown; } } } break; } } } Uint8 simulatedButton = 0; if (numFingersDownlong == 2) { simulatedButton = SDL_BUTTON_LEFT; multi_finger_dragging[port] = DragTwoFinger; } else { simulatedButton = SDL_BUTTON_RIGHT; multi_finger_dragging[port] = DragThreeFinger; } SDL_Event ev; SetMouseButtonEvent(ev, SDL_MOUSEBUTTONDOWN, simulatedButton, mouseDown); ev.button.which = SDL_TOUCH_MOUSEID; SDL_PushEvent(&ev); } } if (xrel == 0 && yrel == 0) { return; } // check if this is the "oldest" finger down (or the only finger down) // otherwise it will not affect mouse motion bool updatePointer = true; if (numFingersDown > 1) { for (int i = 0; i < MaxNumFingers; i++) { if (finger[port][i].id != id) { continue; } for (int j = 0; j < MaxNumFingers; j++) { if (finger[port][j].id == NO_TOUCH || (j == i)) { continue; } if (finger[port][j].timeLastDown < finger[port][i].timeLastDown) { updatePointer = false; } } } } if (!updatePointer) { return; } SetMouseMotionEvent(event, x, y, xrel, yrel); } } void PreprocessEvents(SDL_Event *event) { // Supported touch gestures: // left mouse click: single finger short tap // right mouse click: second finger short tap while first finger is still down // pointer motion: single finger drag // left button drag and drop: dual finger drag // right button drag and drop: triple finger drag if (event->type != SDL_FINGERDOWN && event->type != SDL_FINGERUP && event->type != SDL_FINGERMOTION) { return; } // front (0) or back (1) panel SDL_TouchID port = event->tfinger.touchId; if (port != 0) { if (devilution::GetOptions().Controller.bRearTouch) { switch (event->type) { case SDL_FINGERDOWN: PreprocessBackFingerDown(event); break; case SDL_FINGERUP: PreprocessBackFingerUp(event); break; } } return; } switch (event->type) { case SDL_FINGERDOWN: PreprocessFingerDown(event); break; case SDL_FINGERUP: PreprocessFingerUp(event); break; case SDL_FINGERMOTION: PreprocessFingerMotion(event); break; } } } // namespace void HandleTouchEvent(SDL_Event *event, Point mousePosition) { Mouse = mousePosition; if (!touch_initialized) { InitTouch(); touch_initialized = true; } PreprocessEvents(event); if (event->type == SDL_FINGERDOWN || event->type == SDL_FINGERUP || event->type == SDL_FINGERMOTION) { event->type = IgnoreEvent; } } void FinishSimulatedMouseClicks(Point mousePosition) { Mouse = mousePosition; for (auto &port : simulated_click_start_time) { for (int i = 0; i < 2; i++) { if (port[i] == 0) { continue; } Uint32 currentTime = SDL_GetTicks(); if (currentTime - port[i] < SimulatedClickDuration) { continue; } int simulatedButton; if (i == 0) { simulatedButton = SDL_BUTTON_LEFT; } else { simulatedButton = SDL_BUTTON_RIGHT; } SDL_Event ev; SetMouseButtonEvent(ev, SDL_MOUSEBUTTONUP, simulatedButton, Mouse); ev.button.which = SDL_TOUCH_MOUSEID; SDL_PushEvent(&ev); port[i] = 0; } } } } // namespace devilution ================================================ FILE: Source/platform/vita/touch.h ================================================ #pragma once #ifdef __vita__ #include #include "engine/point.hpp" namespace devilution { void HandleTouchEvent(SDL_Event *event, Point mousePosition); void FinishSimulatedMouseClicks(Point mousePosition); } // namespace devilution #endif ================================================ FILE: Source/player.cpp ================================================ /** * @file player.cpp * * Implementation of player functionality, leveling, actions, creation, loading, etc. */ #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "control/control.hpp" #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "dead.h" #ifdef _DEBUG #include "debug.h" #endif #include "engine/backbuffer_state.hpp" #include "engine/load_cl2.hpp" #include "engine/load_file.hpp" #include "engine/points_in_rectangle_range.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/trn.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "gamemenu.h" #include "headless_mode.hpp" #include "help.h" #include "inv_iterators.hpp" #include "levels/tile_properties.hpp" #include "levels/trigs.h" #include "lighting.h" #include "loadsave.h" #include "lua/lua_event.hpp" #include "minitext.h" #include "missiles.h" #include "monster.h" #include "nthread.h" #include "objects.h" #include "options.h" #include "player.h" #include "qol/autopickup.h" #include "qol/stash.h" #include "spells.h" #include "stores.h" #include "towners.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/log.hpp" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" namespace devilution { uint8_t MyPlayerId; Player *MyPlayer; std::vector Players; Player *InspectPlayer; bool MyPlayerIsDead; namespace { struct DirectionSettings { Direction dir; PLR_MODE walkMode; }; void UpdatePlayerLightOffset(Player &player) { if (player.lightId == NO_LIGHT) return; const WorldTileDisplacement offset = player.position.CalculateWalkingOffset(player._pdir, player.AnimInfo); ChangeLightOffset(player.lightId, offset.screenToLight()); } void WalkInDirection(Player &player, const DirectionSettings &walkParams) { player.occupyTile(player.position.future, true); player.position.temp = player.position.tile + walkParams.dir; } constexpr std::array WalkSettings { { // clang-format off { Direction::South, PM_WALK_SOUTHWARDS }, { Direction::SouthWest, PM_WALK_SOUTHWARDS }, { Direction::West, PM_WALK_SIDEWAYS }, { Direction::NorthWest, PM_WALK_NORTHWARDS }, { Direction::North, PM_WALK_NORTHWARDS }, { Direction::NorthEast, PM_WALK_NORTHWARDS }, { Direction::East, PM_WALK_SIDEWAYS }, { Direction::SouthEast, PM_WALK_SOUTHWARDS } // clang-format on } }; bool PlrDirOK(const Player &player, Direction dir) { const Point position = player.position.tile; const Point futurePosition = position + dir; if (futurePosition.x < 0 || !PosOkPlayer(player, futurePosition)) { return false; } if (dir == Direction::East) { return !IsTileSolid(position + Direction::SouthEast); } if (dir == Direction::West) { return !IsTileSolid(position + Direction::SouthWest); } return true; } void HandleWalkMode(Player &player, Direction dir) { const auto &dirModeParams = WalkSettings[static_cast(dir)]; SetPlayerOld(player); if (!PlrDirOK(player, dir)) { return; } player._pdir = dir; // The player's tile position after finishing this movement action player.position.future = player.position.tile + dirModeParams.dir; WalkInDirection(player, dirModeParams); player.tempDirection = dirModeParams.dir; player._pmode = dirModeParams.walkMode; } void StartWalkAnimation(Player &player, Direction dir, bool pmWillBeCalled) { int8_t skippedFrames = -2; if (leveltype == DTYPE_TOWN && sgGameInitInfo.bRunInTown != 0) skippedFrames = 2; if (pmWillBeCalled) skippedFrames += 1; NewPlrAnim(player, player_graphic::Walk, dir, AnimationDistributionFlags::ProcessAnimationPending, skippedFrames); } /** * @brief Start moving a player to a new tile */ void StartWalk(Player &player, Direction dir, bool pmWillBeCalled) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } StartWalkAnimation(player, dir, pmWillBeCalled); HandleWalkMode(player, dir); } void ClearStateVariables(Player &player) { player.position.temp = { 0, 0 }; player.tempDirection = Direction::South; player.queuedSpell.spellLevel = 0; } void StartAttack(Player &player, Direction d, bool includesFirstFrame) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } int8_t skippedAnimationFrames = 0; const auto flags = player._pIFlags; // If the first frame is not included in vanilla, the skip logic for the first frame will not be executed. // This will result in a different and slower attack speed. if (HasAnyOf(flags, ItemSpecialEffect::FastestAttack)) { // If the fastest attack logic is trigger frames in vanilla two frames are skipped, so missing the first frame reduces the skip logic by two frames. skippedAnimationFrames = includesFirstFrame ? 4 : 2; } else if (HasAnyOf(flags, ItemSpecialEffect::FasterAttack)) { skippedAnimationFrames = includesFirstFrame ? 3 : 2; } else if (HasAnyOf(flags, ItemSpecialEffect::FastAttack)) { skippedAnimationFrames = includesFirstFrame ? 2 : 1; } else if (HasAnyOf(flags, ItemSpecialEffect::QuickAttack)) { skippedAnimationFrames = includesFirstFrame ? 1 : 0; } auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending; if (player._pmode == PM_ATTACK) animationFlags = static_cast(animationFlags | AnimationDistributionFlags::RepeatedAction); NewPlrAnim(player, player_graphic::Attack, d, animationFlags, skippedAnimationFrames, player._pAFNum); player._pmode = PM_ATTACK; FixPlayerLocation(player, d); SetPlayerOld(player); } void StartRangeAttack(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord cy, bool includesFirstFrame) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } int8_t skippedAnimationFrames = 0; const auto flags = player._pIFlags; if (!gbIsHellfire) { if (includesFirstFrame && HasAnyOf(flags, ItemSpecialEffect::QuickAttack | ItemSpecialEffect::FastAttack)) { skippedAnimationFrames += 1; } if (HasAnyOf(flags, ItemSpecialEffect::FastAttack)) { skippedAnimationFrames += 1; } } auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending; if (player._pmode == PM_RATTACK) animationFlags = static_cast(animationFlags | AnimationDistributionFlags::RepeatedAction); NewPlrAnim(player, player_graphic::Attack, d, animationFlags, skippedAnimationFrames, player._pAFNum); player._pmode = PM_RATTACK; FixPlayerLocation(player, d); SetPlayerOld(player); player.position.temp = WorldTilePosition { cx, cy }; } player_graphic GetPlayerGraphicForSpell(SpellID spellId) { switch (GetSpellData(spellId).type()) { case MagicType::Fire: return player_graphic::Fire; case MagicType::Lightning: return player_graphic::Lightning; default: return player_graphic::Magic; } } void StartSpell(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord cy) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } // Checks conditions for spell again, because initial check was done when spell was queued and the parameters could be changed meanwhile bool isValid = false; switch (player.queuedSpell.spellType) { case SpellType::Skill: case SpellType::Spell: isValid = CheckSpell(player, player.queuedSpell.spellId, player.queuedSpell.spellType, true) == SpellCheckResult::Success; break; case SpellType::Scroll: isValid = CanUseScroll(player, player.queuedSpell.spellId); break; case SpellType::Charges: isValid = CanUseStaff(player, player.queuedSpell.spellId); break; default: break; } if (!isValid) return; auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending; if (player._pmode == PM_SPELL) animationFlags = static_cast(animationFlags | AnimationDistributionFlags::RepeatedAction); NewPlrAnim(player, GetPlayerGraphicForSpell(player.queuedSpell.spellId), d, animationFlags, 0, player._pSFNum); PlaySfxLoc(GetSpellData(player.queuedSpell.spellId).sSFX, player.position.tile); player._pmode = PM_SPELL; FixPlayerLocation(player, d); SetPlayerOld(player); player.position.temp = WorldTilePosition { cx, cy }; player.queuedSpell.spellLevel = player.GetSpellLevel(player.queuedSpell.spellId); player.executedSpell = player.queuedSpell; } void RespawnDeadItem(Item &&itm, Point target) { if (ActiveItemCount >= MAXITEMS) return; const int ii = AllocateItem(); Item &item = Items[ii]; dItem[target.x][target.y] = ii + 1; item = itm; item.position = target; RespawnItem(item, true); NetSendCmdPItem(false, CMD_SPAWNITEM, target, item); } void DeadItem(Player &player, Item &&item, Displacement direction) { if (item.isEmpty()) return; const Point playerTile = player.position.tile; if (direction != Displacement { 0, 0 }) { const Point target = playerTile + direction; if (ItemSpaceOk(target)) { RespawnDeadItem(std::move(item), target); return; } } std::optional dropPoint = FindClosestValidPosition(ItemSpaceOk, playerTile, 1, 50); if (dropPoint) { RespawnDeadItem(std::move(item), *dropPoint); } } int DropGold(Player &player, int amount, bool skipFullStacks) { for (int i = 0; i < player._pNumInv && amount > 0; i++) { Item &item = player.InvList[i]; if (item._itype != ItemType::Gold || (skipFullStacks && item._ivalue == MaxGold)) continue; if (amount < item._ivalue) { Item goldItem; MakeGoldStack(goldItem, amount); DeadItem(player, std::move(goldItem), { 0, 0 }); item._ivalue -= amount; return 0; } amount -= item._ivalue; DeadItem(player, std::move(item), { 0, 0 }); player.RemoveInvItem(i); i = -1; } return amount; } void DropHalfPlayersGold(Player &player) { const int remainingGold = DropGold(player, player._pGold / 2, true); if (remainingGold > 0) { DropGold(player, remainingGold, false); } player._pGold /= 2; } void InitLevelChange(Player &player) { const Player &myPlayer = *MyPlayer; RemoveEnemyReferences(player); RemovePlrMissiles(player); player.pManaShield = false; player.wReflections = 0; if (&player != MyPlayer) { // share info about your manashield when another player joins the level if (myPlayer.pManaShield) NetSendCmd(true, CMD_SETSHIELD); // share info about your reflect charges when another player joins the level NetSendCmdParam1(true, CMD_SETREFLECT, myPlayer.wReflections); } else if (qtextflag) { qtextflag = false; stream_stop(); } FixPlrWalkTags(player); SetPlayerOld(player); if (&player == MyPlayer) { player.occupyTile(player.position.tile, false); } else { player._pLvlVisited[player.plrlevel] = true; } ClrPlrPath(player); player.destAction = ACTION_NONE; player._pLvlChanging = true; if (&player == MyPlayer) { player.pLvlLoad = 10; } } /** * @brief Continue movement towards new tile */ bool DoWalk(Player &player) { // Play walking sound effect on certain animation frames if (*GetOptions().Audio.walkingSound && (leveltype != DTYPE_TOWN || sgGameInitInfo.bRunInTown == 0)) { if (player.AnimInfo.currentFrame == 0 || player.AnimInfo.currentFrame == 4) { PlaySfxLoc(SfxID::Walk, player.position.tile); } } if (!player.AnimInfo.isLastFrame()) { // We didn't reach new tile so update player's "sub-tile" position UpdatePlayerLightOffset(player); return false; } // We reached the new tile -> update the player's tile position dPlayer[player.position.tile.x][player.position.tile.y] = 0; player.position.tile = player.position.temp; // dPlayer is set here for backwards compatibility; without it, the player would be invisible if loaded from a vanilla save. player.occupyTile(player.position.tile, false); // Update the coordinates for lighting and vision entries for the player if (leveltype != DTYPE_TOWN) { ChangeLightXY(player.lightId, player.position.tile); ChangeVisionXY(player.getId(), player.position.tile); } StartStand(player, player.tempDirection); ClearStateVariables(player); // Reset the "sub-tile" position of the player's light entry to 0 if (leveltype != DTYPE_TOWN) { ChangeLightOffset(player.lightId, { 0, 0 }); } AutoPickup(player); return true; } bool WeaponDecay(Player &player, int ii) { if (!player.InvBody[ii].isEmpty() && player.InvBody[ii]._iClass == ICLASS_WEAPON && HasAnyOf(player.InvBody[ii]._iDamAcFlags, ItemSpecialEffectHf::Decay)) { player.InvBody[ii]._iPLDam -= 5; if (player.InvBody[ii]._iPLDam <= -100) { RemoveEquipment(player, static_cast(ii), true); CalcPlrInv(player, true); return true; } CalcPlrInv(player, true); } return false; } bool DamageWeapon(Player &player, unsigned damageFrequency) { if (&player != MyPlayer) { return false; } if (WeaponDecay(player, INVLOC_HAND_LEFT)) return true; if (WeaponDecay(player, INVLOC_HAND_RIGHT)) return true; if (!FlipCoin(damageFrequency)) { return false; } if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._iClass == ICLASS_WEAPON) { if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) { return false; } player.InvBody[INVLOC_HAND_LEFT]._iDurability--; if (player.InvBody[INVLOC_HAND_LEFT]._iDurability <= 0) { RemoveEquipment(player, INVLOC_HAND_LEFT, true); CalcPlrInv(player, true); return true; } } if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._iClass == ICLASS_WEAPON) { if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == DUR_INDESTRUCTIBLE) { return false; } player.InvBody[INVLOC_HAND_RIGHT]._iDurability--; if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) { RemoveEquipment(player, INVLOC_HAND_RIGHT, true); CalcPlrInv(player, true); return true; } } if (player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == DUR_INDESTRUCTIBLE) { return false; } player.InvBody[INVLOC_HAND_RIGHT]._iDurability--; if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) { RemoveEquipment(player, INVLOC_HAND_RIGHT, true); CalcPlrInv(player, true); return true; } } if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield) { if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) { return false; } player.InvBody[INVLOC_HAND_LEFT]._iDurability--; if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == 0) { RemoveEquipment(player, INVLOC_HAND_LEFT, true); CalcPlrInv(player, true); return true; } } return false; } bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false) { int hper = 0; if (!monster.isPossibleToHit()) return false; if (adjacentDamage) { if (player.getCharacterLevel() > 20) hper -= 30; else hper -= (35 - player.getCharacterLevel()) * 2; } int hit = GenerateRnd(100); if (monster.mode == MonsterMode::Petrified) { hit = 0; } hper += player.GetMeleePiercingToHit() - player.CalculateArmorPierce(monster.armorClass, true); hper = std::clamp(hper, 5, 95); if (monster.tryLiftGargoyle()) return true; if (hit >= hper) { #ifdef _DEBUG if (!DebugGodMode) #endif return false; } if (gbIsHellfire && HasAllOf(player._pIFlags, ItemSpecialEffect::FireDamage | ItemSpecialEffect::LightningDamage)) { // Fixed off by 1 error from Hellfire const int midam = RandomIntBetween(player._pIFMinDam, player._pIFMaxDam); AddMissile(player.position.tile, player.position.temp, player._pdir, MissileID::SpectralArrow, TARGET_MONSTERS, player, midam, 0); } const int mind = player._pIMinDam; const int maxd = player._pIMaxDam; int dam = RandomIntBetween(mind, maxd); dam += dam * player._pIBonusDam / 100; dam += player._pIBonusDamMod; int dam2 = dam << 6; dam += player._pDamageMod; const ClassAttributes &classAttributes = GetClassAttributes(player._pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) { if (GenerateRnd(100) < player.getCharacterLevel()) { dam *= 2; } } ItemType phanditype = ItemType::None; if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Sword || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Sword) { phanditype = ItemType::Sword; } if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Mace || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Mace) { phanditype = ItemType::Mace; } switch (monster.data().monsterClass) { case MonsterClass::Undead: if (phanditype == ItemType::Sword) { dam -= dam / 2; } else if (phanditype == ItemType::Mace) { dam += dam / 2; } break; case MonsterClass::Animal: if (phanditype == ItemType::Mace) { dam -= dam / 2; } else if (phanditype == ItemType::Sword) { dam += dam / 2; } break; case MonsterClass::Demon: if (HasAnyOf(player._pIFlags, ItemSpecialEffect::TripleDemonDamage)) { dam *= 3; } break; } if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Devastation) && GenerateRnd(100) < 5) { dam *= 3; } if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Doppelganger) && monster.type().type != MT_DIABLO && !monster.isUnique() && GenerateRnd(100) < 10) { AddDoppelganger(monster); } dam <<= 6; if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Jesters)) { int r = GenerateRnd(201); if (r >= 100) r = 100 + (r - 100) * 5; dam = dam * r / 100; } if (adjacentDamage) dam >>= 2; if (&player == MyPlayer) { if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Peril)) { dam2 += player._pIGetHit << 6; if (dam2 >= 0) { ApplyPlrDamage(DamageType::Physical, player, 0, 1, dam2); } dam *= 2; } #ifdef _DEBUG if (DebugGodMode) { dam = monster.hitPoints; /* ensure monster is killed with one hit */ } #endif ApplyMonsterDamage(DamageType::Physical, monster, dam); } int skdam = 0; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::RandomStealLife)) { skdam = GenerateRnd(dam / 8); player._pHitPoints += skdam; if (player._pHitPoints > player._pMaxHP) { player._pHitPoints = player._pMaxHP; } player._pHPBase += skdam; if (player._pHPBase > player._pMaxHPBase) { player._pHPBase = player._pMaxHPBase; } RedrawComponent(PanelDrawComponent::Health); } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana3 | ItemSpecialEffect::StealMana5) && HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana3)) { skdam = 3 * dam / 100; } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana5)) { skdam = 5 * dam / 100; } player._pMana += skdam; if (player._pMana > player._pMaxMana) { player._pMana = player._pMaxMana; } player._pManaBase += skdam; if (player._pManaBase > player._pMaxManaBase) { player._pManaBase = player._pMaxManaBase; } RedrawComponent(PanelDrawComponent::Mana); } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife3 | ItemSpecialEffect::StealLife5)) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife3)) { skdam = 3 * dam / 100; } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife5)) { skdam = 5 * dam / 100; } player._pHitPoints += skdam; if (player._pHitPoints > player._pMaxHP) { player._pHitPoints = player._pMaxHP; } player._pHPBase += skdam; if (player._pHPBase > player._pMaxHPBase) { player._pHPBase = player._pMaxHPBase; } RedrawComponent(PanelDrawComponent::Health); } if (monster.hasNoLife()) { M_StartKill(monster, player); } else { if (monster.mode != MonsterMode::Petrified && HasAnyOf(player._pIFlags, ItemSpecialEffect::Knockback)) M_GetKnockback(monster, player.position.tile); M_StartHit(monster, player, dam); } return true; } bool PlrHitPlr(Player &attacker, Player &target) { if (target._pInvincible) { return false; } if (HasAnyOf(target._pSpellFlags, SpellFlag::Etherealize)) { return false; } const int hit = GenerateRnd(100); int hper = attacker.GetMeleeToHit() - target.GetArmor(); hper = std::clamp(hper, 5, 95); int blk = 100; if ((target._pmode == PM_STAND || target._pmode == PM_ATTACK) && target._pBlockFlag) { blk = GenerateRnd(100); } int blkper = target.GetBlockChance() - (attacker.getCharacterLevel() * 2); blkper = std::clamp(blkper, 0, 100); if (hit >= hper) { return false; } if (blk < blkper) { const Direction dir = GetDirection(target.position.tile, attacker.position.tile); StartPlrBlock(target, dir); return true; } const int mind = attacker._pIMinDam; const int maxd = attacker._pIMaxDam; int dam = RandomIntBetween(mind, maxd); dam += (dam * attacker._pIBonusDam) / 100; dam += attacker._pIBonusDamMod + attacker._pDamageMod; const ClassAttributes &classAttributes = GetClassAttributes(attacker._pClass); if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) { if (GenerateRnd(100) < attacker.getCharacterLevel()) { dam *= 2; } } const int skdam = dam << 6; if (HasAnyOf(attacker._pIFlags, ItemSpecialEffect::RandomStealLife)) { const int tac = GenerateRnd(skdam / 8); attacker._pHitPoints += tac; if (attacker._pHitPoints > attacker._pMaxHP) { attacker._pHitPoints = attacker._pMaxHP; } attacker._pHPBase += tac; if (attacker._pHPBase > attacker._pMaxHPBase) { attacker._pHPBase = attacker._pMaxHPBase; } RedrawComponent(PanelDrawComponent::Health); } if (&attacker == MyPlayer) { NetSendCmdDamage(true, target, skdam, DamageType::Physical); } StartPlrHit(target, skdam, false); return true; } bool PlrHitObj(const Player &player, Object &targetObject) { if (targetObject.IsBreakable()) { BreakObject(player, targetObject); return true; } return false; } bool DoAttack(Player &player) { if (player.AnimInfo.currentFrame == player._pAFNum - 2) { PlaySfxLoc(SfxID::Swing, player.position.tile); } bool didhit = false; if (player.AnimInfo.currentFrame == player._pAFNum - 1) { Point position = player.position.tile + player._pdir; Monster *monster = FindMonsterAtPosition(position); if (monster != nullptr) { if (CanTalkToMonst(*monster)) { player.position.temp.x = 0; /** @todo Looks to be irrelevant, probably just remove it */ return false; } } if (!gbIsHellfire || !HasAllOf(player._pIFlags, ItemSpecialEffect::FireDamage | ItemSpecialEffect::LightningDamage)) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FireDamage)) { AddMissile(position, { 1, 0 }, Direction::South, MissileID::WeaponExplosion, TARGET_MONSTERS, player, 0, 0); } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::LightningDamage)) { AddMissile(position, { 2, 0 }, Direction::South, MissileID::WeaponExplosion, TARGET_MONSTERS, player, 0, 0); } } if (monster != nullptr) { didhit = PlrHitMonst(player, *monster); } else if (PlayerAtPosition(position) != nullptr && !player.friendlyMode) { didhit = PlrHitPlr(player, *PlayerAtPosition(position)); } else { Object *object = FindObjectAtPosition(position, false); if (object != nullptr) { didhit = PlrHitObj(player, *object); } } if (player.CanCleave()) { // playing as a class/weapon with cleave position = player.position.tile + Right(player._pdir); monster = FindMonsterAtPosition(position); if (monster != nullptr) { if (!CanTalkToMonst(*monster) && monster->position.old == position) { if (PlrHitMonst(player, *monster, true)) didhit = true; } } position = player.position.tile + Left(player._pdir); monster = FindMonsterAtPosition(position); if (monster != nullptr) { if (!CanTalkToMonst(*monster) && monster->position.old == position) { if (PlrHitMonst(player, *monster, true)) didhit = true; } } } if (didhit && DamageWeapon(player, 30)) { StartStand(player, player._pdir); ClearStateVariables(player); return true; } } if (player.AnimInfo.isLastFrame()) { StartStand(player, player._pdir); ClearStateVariables(player); return true; } return false; } bool DoRangeAttack(Player &player) { int arrows = 0; if (player.AnimInfo.currentFrame == player._pAFNum - 1) { arrows = 1; } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::MultipleArrows) && player.AnimInfo.currentFrame == player._pAFNum + 1) { arrows = 2; } for (int arrow = 0; arrow < arrows; arrow++) { int xoff = 0; int yoff = 0; if (arrows != 1) { const int angle = arrow == 0 ? -1 : 1; const int x = player.position.temp.x - player.position.tile.x; if (x != 0) yoff = x < 0 ? angle : -angle; const int y = player.position.temp.y - player.position.tile.y; if (y != 0) xoff = y < 0 ? -angle : angle; } int dmg = 4; MissileID mistype = MissileID::Arrow; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FireArrows)) { mistype = MissileID::FireArrow; } if (HasAnyOf(player._pIFlags, ItemSpecialEffect::LightningArrows)) { mistype = MissileID::LightningArrow; } if (HasAllOf(player._pIFlags, ItemSpecialEffect::FireArrows | ItemSpecialEffect::LightningArrows)) { // Fixed off by 1 error from Hellfire dmg = RandomIntBetween(player._pIFMinDam, player._pIFMaxDam); mistype = MissileID::SpectralArrow; } AddMissile( player.position.tile, player.position.temp + Displacement { xoff, yoff }, player._pdir, mistype, TARGET_MONSTERS, player, dmg, 0); if (arrow == 0 && mistype != MissileID::SpectralArrow) { PlaySfxLoc(arrows != 1 ? SfxID::ShootBow2 : SfxID::ShootBow, player.position.tile); } if (DamageWeapon(player, 40)) { StartStand(player, player._pdir); ClearStateVariables(player); return true; } } if (player.AnimInfo.isLastFrame()) { StartStand(player, player._pdir); ClearStateVariables(player); return true; } return false; } void DamageParryItem(Player &player) { if (&player != MyPlayer) { return; } if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield || player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Staff) { if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) { return; } player.InvBody[INVLOC_HAND_LEFT]._iDurability--; if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == 0) { RemoveEquipment(player, INVLOC_HAND_LEFT, true); CalcPlrInv(player, true); } } if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) { if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability != DUR_INDESTRUCTIBLE) { player.InvBody[INVLOC_HAND_RIGHT]._iDurability--; if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) { RemoveEquipment(player, INVLOC_HAND_RIGHT, true); CalcPlrInv(player, true); } } } } bool DoBlock(Player &player) { if (player.AnimInfo.isLastFrame()) { StartStand(player, player._pdir); ClearStateVariables(player); if (FlipCoin(10)) { DamageParryItem(player); } return true; } return false; } void DamageArmor(Player &player) { if (&player != MyPlayer) { return; } if (player.InvBody[INVLOC_CHEST].isEmpty() && player.InvBody[INVLOC_HEAD].isEmpty()) { return; } bool targetHead = FlipCoin(3); if (!player.InvBody[INVLOC_CHEST].isEmpty() && player.InvBody[INVLOC_HEAD].isEmpty()) { targetHead = false; } if (player.InvBody[INVLOC_CHEST].isEmpty() && !player.InvBody[INVLOC_HEAD].isEmpty()) { targetHead = true; } Item *pi; if (targetHead) { pi = &player.InvBody[INVLOC_HEAD]; } else { pi = &player.InvBody[INVLOC_CHEST]; } if (pi->_iDurability == DUR_INDESTRUCTIBLE) { return; } pi->_iDurability--; if (pi->_iDurability != 0) { return; } if (targetHead) { RemoveEquipment(player, INVLOC_HEAD, true); } else { RemoveEquipment(player, INVLOC_CHEST, true); } CalcPlrInv(player, true); } bool DoSpell(Player &player) { if (player.AnimInfo.currentFrame == player._pSFNum) { CastSpell( player, player.executedSpell.spellId, player.position.tile, player.position.temp, player.executedSpell.spellLevel); if (IsAnyOf(player.executedSpell.spellType, SpellType::Scroll, SpellType::Charges)) { EnsureValidReadiedSpell(player); } } if (player.AnimInfo.isLastFrame()) { StartStand(player, player._pdir); ClearStateVariables(player); return true; } return false; } bool DoGotHit(Player &player) { if (player.AnimInfo.isLastFrame()) { StartStand(player, player._pdir); ClearStateVariables(player); if (!FlipCoin(4)) { DamageArmor(player); } return true; } return false; } bool DoDeath(Player &player) { if (player.AnimInfo.isLastFrame()) { if (player.AnimInfo.tickCounterOfCurrentFrame == 0) { player.AnimInfo.ticksPerFrame = 100; dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; } else if (&player == MyPlayer && player.AnimInfo.tickCounterOfCurrentFrame == 30) { MyPlayerIsDead = true; } } return false; } bool IsPlayerAdjacentToObject(Player &player, Object &object) { const int x = std::abs(player.position.tile.x - object.position.x); int y = std::abs(player.position.tile.y - object.position.y); if (y > 1 && object.position.y >= 1 && FindObjectAtPosition(object.position + Direction::NorthEast) == &object) { // special case for activating a large object from the north-east side y = std::abs(player.position.tile.y - object.position.y + 1); } return x <= 1 && y <= 1; } void TryDisarm(const Player &player, Object &object) { if (&player == MyPlayer) NewCursor(CURSOR_HAND); if (!object._oTrapFlag) { return; } const int trapdisper = 2 * player._pDexterity - 5 * currlevel; if (GenerateRnd(100) > trapdisper) { return; } for (int j = 0; j < ActiveObjectCount; j++) { Object &trap = Objects[ActiveObjects[j]]; if (trap.IsTrap() && FindObjectAtPosition({ trap._oVar1, trap._oVar2 }) == &object) { trap._oVar4 = 1; object._oTrapFlag = false; } } if (object.IsTrappedChest()) { object._oTrapFlag = false; } } void CheckNewPath(Player &player, bool pmWillBeCalled) { int x = 0; int y = 0; Monster *monster; Player *target; Object *object; Item *item; const int targetId = player.destParam1; switch (player.destAction) { case ACTION_ATTACKMON: case ACTION_RATTACKMON: case ACTION_SPELLMON: monster = &Monsters[targetId]; if (monster->hasNoLife()) { player.Stop(); return; } if (player.destAction == ACTION_ATTACKMON) MakePlrPath(player, monster->position.future, false); break; case ACTION_ATTACKPLR: case ACTION_RATTACKPLR: case ACTION_SPELLPLR: target = &Players[targetId]; if (target->hasNoLife()) { player.Stop(); return; } if (player.destAction == ACTION_ATTACKPLR) MakePlrPath(player, target->position.future, false); break; case ACTION_OPERATE: case ACTION_DISARM: case ACTION_OPERATETK: object = &Objects[targetId]; break; case ACTION_PICKUPITEM: case ACTION_PICKUPAITEM: item = &Items[targetId]; break; default: break; } Direction d; if (player.walkpath[0] != WALK_NONE) { if (player._pmode == PM_STAND) { if (&player == MyPlayer) { if (player.destAction == ACTION_ATTACKMON || player.destAction == ACTION_ATTACKPLR) { if (player.destAction == ACTION_ATTACKMON) { x = std::abs(player.position.future.x - monster->position.future.x); y = std::abs(player.position.future.y - monster->position.future.y); d = GetDirection(player.position.future, monster->position.future); } else { x = std::abs(player.position.future.x - target->position.future.x); y = std::abs(player.position.future.y - target->position.future.y); d = GetDirection(player.position.future, target->position.future); } if (x < 2 && y < 2) { ClrPlrPath(player); if (player.destAction == ACTION_ATTACKMON && monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) { TalktoMonster(player, *monster); } else { StartAttack(player, d, pmWillBeCalled); } player.destAction = ACTION_NONE; } } } switch (player.walkpath[0]) { case WALK_N: StartWalk(player, Direction::North, pmWillBeCalled); break; case WALK_NE: StartWalk(player, Direction::NorthEast, pmWillBeCalled); break; case WALK_E: StartWalk(player, Direction::East, pmWillBeCalled); break; case WALK_SE: StartWalk(player, Direction::SouthEast, pmWillBeCalled); break; case WALK_S: StartWalk(player, Direction::South, pmWillBeCalled); break; case WALK_SW: StartWalk(player, Direction::SouthWest, pmWillBeCalled); break; case WALK_W: StartWalk(player, Direction::West, pmWillBeCalled); break; case WALK_NW: StartWalk(player, Direction::NorthWest, pmWillBeCalled); break; } for (size_t j = 1; j < MaxPathLengthPlayer; j++) { player.walkpath[j - 1] = player.walkpath[j]; } player.walkpath[MaxPathLengthPlayer - 1] = WALK_NONE; if (player._pmode == PM_STAND) { StartStand(player, player._pdir); player.destAction = ACTION_NONE; } } return; } if (player.destAction == ACTION_NONE) { return; } if (player._pmode == PM_STAND) { switch (player.destAction) { case ACTION_ATTACK: d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 }); StartAttack(player, d, pmWillBeCalled); break; case ACTION_ATTACKMON: x = std::abs(player.position.tile.x - monster->position.future.x); y = std::abs(player.position.tile.y - monster->position.future.y); if (x <= 1 && y <= 1) { d = GetDirection(player.position.future, monster->position.future); if (monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) { TalktoMonster(player, *monster); } else { StartAttack(player, d, pmWillBeCalled); } } break; case ACTION_ATTACKPLR: x = std::abs(player.position.tile.x - target->position.future.x); y = std::abs(player.position.tile.y - target->position.future.y); if (x <= 1 && y <= 1) { d = GetDirection(player.position.future, target->position.future); StartAttack(player, d, pmWillBeCalled); } break; case ACTION_RATTACK: d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 }); StartRangeAttack(player, d, player.destParam1, player.destParam2, pmWillBeCalled); break; case ACTION_RATTACKMON: d = GetDirection(player.position.future, monster->position.future); if (monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) { TalktoMonster(player, *monster); } else { StartRangeAttack(player, d, monster->position.future.x, monster->position.future.y, pmWillBeCalled); } break; case ACTION_RATTACKPLR: d = GetDirection(player.position.future, target->position.future); StartRangeAttack(player, d, target->position.future.x, target->position.future.y, pmWillBeCalled); break; case ACTION_SPELL: d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 }); StartSpell(player, d, player.destParam1, player.destParam2); break; case ACTION_SPELLWALL: StartSpell(player, static_cast(player.destParam3), player.destParam1, player.destParam2); player.tempDirection = static_cast(player.destParam3); break; case ACTION_SPELLMON: d = GetDirection(player.position.tile, monster->position.future); StartSpell(player, d, monster->position.future.x, monster->position.future.y); break; case ACTION_SPELLPLR: d = GetDirection(player.position.tile, target->position.future); StartSpell(player, d, target->position.future.x, target->position.future.y); break; case ACTION_OPERATE: if (IsPlayerAdjacentToObject(player, *object)) { if (object->_oBreak == 1) { d = GetDirection(player.position.tile, object->position); StartAttack(player, d, pmWillBeCalled); } else { OperateObject(player, *object); } } break; case ACTION_DISARM: if (IsPlayerAdjacentToObject(player, *object)) { if (object->_oBreak == 1) { d = GetDirection(player.position.tile, object->position); StartAttack(player, d, pmWillBeCalled); } else { TryDisarm(player, *object); OperateObject(player, *object); } } break; case ACTION_OPERATETK: if (object->_oBreak != 1) { OperateObject(player, *object); } break; case ACTION_PICKUPITEM: if (&player == MyPlayer) { x = std::abs(player.position.tile.x - item->position.x); y = std::abs(player.position.tile.y - item->position.y); if (x <= 1 && y <= 1 && pcurs == CURSOR_HAND && !item->_iRequest) { NetSendCmdGItem(true, CMD_REQUESTGITEM, player, targetId); item->_iRequest = true; } } break; case ACTION_PICKUPAITEM: if (&player == MyPlayer) { x = std::abs(player.position.tile.x - item->position.x); y = std::abs(player.position.tile.y - item->position.y); if (x <= 1 && y <= 1 && pcurs == CURSOR_HAND) { NetSendCmdGItem(true, CMD_REQUESTAGITEM, player, targetId); } } break; case ACTION_TALK: if (&player == MyPlayer) { HelpFlag = false; TalkToTowner(player, player.destParam1); } break; default: break; } FixPlayerLocation(player, player._pdir); player.destAction = ACTION_NONE; return; } if (player._pmode == PM_ATTACK && player.AnimInfo.currentFrame >= player._pAFNum) { if (player.destAction == ACTION_ATTACK) { d = GetDirection(player.position.future, { player.destParam1, player.destParam2 }); StartAttack(player, d, pmWillBeCalled); player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_ATTACKMON) { x = std::abs(player.position.tile.x - monster->position.future.x); y = std::abs(player.position.tile.y - monster->position.future.y); if (x <= 1 && y <= 1) { d = GetDirection(player.position.future, monster->position.future); StartAttack(player, d, pmWillBeCalled); } player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_ATTACKPLR) { x = std::abs(player.position.tile.x - target->position.future.x); y = std::abs(player.position.tile.y - target->position.future.y); if (x <= 1 && y <= 1) { d = GetDirection(player.position.future, target->position.future); StartAttack(player, d, pmWillBeCalled); } player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_OPERATE) { if (IsPlayerAdjacentToObject(player, *object)) { if (object->_oBreak == 1) { d = GetDirection(player.position.tile, object->position); StartAttack(player, d, pmWillBeCalled); } } } } if (player._pmode == PM_RATTACK && player.AnimInfo.currentFrame >= player._pAFNum) { if (player.destAction == ACTION_RATTACK) { d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 }); StartRangeAttack(player, d, player.destParam1, player.destParam2, pmWillBeCalled); player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_RATTACKMON) { d = GetDirection(player.position.tile, monster->position.future); StartRangeAttack(player, d, monster->position.future.x, monster->position.future.y, pmWillBeCalled); player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_RATTACKPLR) { d = GetDirection(player.position.tile, target->position.future); StartRangeAttack(player, d, target->position.future.x, target->position.future.y, pmWillBeCalled); player.destAction = ACTION_NONE; } } if (player._pmode == PM_SPELL && player.AnimInfo.currentFrame >= player._pSFNum) { if (player.destAction == ACTION_SPELL) { d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 }); StartSpell(player, d, player.destParam1, player.destParam2); player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_SPELLMON) { d = GetDirection(player.position.tile, monster->position.future); StartSpell(player, d, monster->position.future.x, monster->position.future.y); player.destAction = ACTION_NONE; } else if (player.destAction == ACTION_SPELLPLR) { d = GetDirection(player.position.tile, target->position.future); StartSpell(player, d, target->position.future.x, target->position.future.y); player.destAction = ACTION_NONE; } } } bool PlrDeathModeOK(Player &player) { if (&player != MyPlayer) { return true; } if (player._pmode == PM_DEATH) { return true; } if (player._pmode == PM_QUIT) { return true; } if (player._pmode == PM_NEWLVL) { return true; } return false; } void ValidatePlayer() { assert(MyPlayer != nullptr); Player &myPlayer = *MyPlayer; // Player::setCharacterLevel ensures that the player level is within the expected range in case someone has edited their character level in memory myPlayer.setCharacterLevel(myPlayer.getCharacterLevel()); // This lets us catch cases where someone is editing experience directly through memory modification and reset their experience back to the expected cap. if (myPlayer._pExperience > myPlayer.getNextExperienceThreshold()) { myPlayer._pExperience = myPlayer.getNextExperienceThreshold(); if (*GetOptions().Gameplay.experienceBar) { RedrawEverything(); } } int gt = 0; for (int i = 0; i < myPlayer._pNumInv; i++) { if (myPlayer.InvList[i]._itype == ItemType::Gold) { int maxGold = GOLD_MAX_LIMIT; if (gbIsHellfire) { maxGold *= 2; } if (myPlayer.InvList[i]._ivalue > maxGold) { myPlayer.InvList[i]._ivalue = maxGold; } gt += myPlayer.InvList[i]._ivalue; } } if (gt != myPlayer._pGold) myPlayer._pGold = gt; if (myPlayer._pBaseStr > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength)) { myPlayer._pBaseStr = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength); } if (myPlayer._pBaseMag > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic)) { myPlayer._pBaseMag = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic); } if (myPlayer._pBaseDex > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity)) { myPlayer._pBaseDex = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity); } if (myPlayer._pBaseVit > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Vitality)) { myPlayer._pBaseVit = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Vitality); } uint64_t msk = 0; for (size_t b = static_cast(SpellID::Firebolt); b < SpellsData.size(); b++) { if (GetSpellBookLevel((SpellID)b) != -1) { msk |= GetSpellBitmask(static_cast(b)); if (myPlayer._pSplLvl[b] > MaxSpellLevel) myPlayer._pSplLvl[b] = MaxSpellLevel; } } myPlayer._pMemSpells &= msk; myPlayer._pInfraFlag = false; } HeroClass GetPlayerSpriteClass(HeroClass cls) { if (cls == HeroClass::Bard && !HaveBardAssets()) return HeroClass::Rogue; if (cls == HeroClass::Barbarian && !HaveBarbarianAssets()) return HeroClass::Warrior; return cls; } PlayerWeaponGraphic GetPlayerWeaponGraphic(player_graphic graphic, PlayerWeaponGraphic weaponGraphic) { if (leveltype == DTYPE_TOWN && IsAnyOf(graphic, player_graphic::Lightning, player_graphic::Fire, player_graphic::Magic)) { // If the hero doesn't hold the weapon in town then we should use the unarmed animation for casting switch (weaponGraphic) { case PlayerWeaponGraphic::Mace: case PlayerWeaponGraphic::Sword: return PlayerWeaponGraphic::Unarmed; case PlayerWeaponGraphic::SwordShield: case PlayerWeaponGraphic::MaceShield: return PlayerWeaponGraphic::UnarmedShield; default: break; } } return weaponGraphic; } uint16_t GetPlayerSpriteWidth(HeroClass cls, player_graphic graphic, PlayerWeaponGraphic weaponGraphic) { const PlayerSpriteData spriteData = GetPlayerSpriteDataForClass(cls); switch (graphic) { case player_graphic::Stand: return spriteData.stand; case player_graphic::Walk: return spriteData.walk; case player_graphic::Attack: if (weaponGraphic == PlayerWeaponGraphic::Bow) return spriteData.bow; return spriteData.attack; case player_graphic::Hit: return spriteData.swHit; case player_graphic::Block: return spriteData.block; case player_graphic::Lightning: return spriteData.lightning; case player_graphic::Fire: return spriteData.fire; case player_graphic::Magic: return spriteData.magic; case player_graphic::Death: return spriteData.death; } app_fatal("Invalid player_graphic"); } void GetPlayerGraphicsPath(std::string_view path, std::string_view prefix, std::string_view type, char out[256]) { *BufCopy(out, "plrgfx\\", path, "\\", prefix, "\\", prefix, type) = '\0'; } } // namespace void Player::CalcScrolls() { _pScrlSpells = 0; for (const Item &item : InventoryAndBeltPlayerItemsRange { *this }) { if (item.isScroll() && item._iStatFlag) { _pScrlSpells |= GetSpellBitmask(item._iSpell); } } EnsureValidReadiedSpell(*this); } bool Player::CanUseItem(const Item &item) const { if (!IsItemValid(*this, item)) return false; return _pStrength >= item._iMinStr && _pMagic >= item._iMinMag && _pDexterity >= item._iMinDex; } void Player::RemoveInvItem(int iv, bool calcScrolls) { if (this == MyPlayer) { // Locate the first grid index containing this item and notify remote clients for (size_t i = 0; i < InventoryGridCells; i++) { const int8_t itemIndex = InvGrid[i]; if (std::abs(itemIndex) - 1 == iv) { NetSendCmdParam1(false, CMD_DELINVITEMS, static_cast(i)); break; } } } // Iterate through invGrid and remove every reference to item for (int8_t &itemIndex : InvGrid) { if (std::abs(itemIndex) - 1 == iv) { itemIndex = 0; } } InvList[iv].clear(); _pNumInv--; // If the item at the end of inventory array isn't the one removed, shift all following items back one index to retain inventory order. if (_pNumInv > 0 && _pNumInv != iv) { for (int newIndex = iv; newIndex < _pNumInv; newIndex++) { InvList[newIndex] = InvList[newIndex + 1].pop(); } for (int8_t &itemIndex : InvGrid) { if (itemIndex > iv + 1) { // if item was shifted, decrease the index so it's paired with the correct item. itemIndex--; } if (itemIndex < -(iv + 1)) { itemIndex++; // since occupied cells are negative, increment the index to keep it same as as top-left cell for item, only negative. } } } if (calcScrolls) { CalcScrolls(); } } void Player::RemoveSpdBarItem(int iv) { if (this == MyPlayer) { NetSendCmdParam1(false, CMD_DELBELTITEMS, iv); } SpdList[iv].clear(); CalcScrolls(); RedrawEverything(); } [[nodiscard]] uint8_t Player::getId() const { return static_cast(std::distance(&Players[0], this)); } int Player::GetBaseAttributeValue(CharacterAttribute attribute) const { switch (attribute) { case CharacterAttribute::Dexterity: return this->_pBaseDex; case CharacterAttribute::Magic: return this->_pBaseMag; case CharacterAttribute::Strength: return this->_pBaseStr; case CharacterAttribute::Vitality: return this->_pBaseVit; default: app_fatal("Unsupported attribute"); } } int Player::GetCurrentAttributeValue(CharacterAttribute attribute) const { switch (attribute) { case CharacterAttribute::Dexterity: return this->_pDexterity; case CharacterAttribute::Magic: return this->_pMagic; case CharacterAttribute::Strength: return this->_pStrength; case CharacterAttribute::Vitality: return this->_pVitality; default: app_fatal("Unsupported attribute"); } } int Player::GetMaximumAttributeValue(CharacterAttribute attribute) const { const ClassAttributes &attr = getClassAttributes(); switch (attribute) { case CharacterAttribute::Strength: return attr.maxStr; case CharacterAttribute::Magic: return attr.maxMag; case CharacterAttribute::Dexterity: return attr.maxDex; case CharacterAttribute::Vitality: return attr.maxVit; } app_fatal("Unsupported attribute"); } Point Player::GetTargetPosition() const { // clang-format off constexpr int DirectionOffsetX[8] = { 0,-1, 1, 0,-1, 1, 1,-1 }; constexpr int DirectionOffsetY[8] = { -1, 0, 0, 1,-1,-1, 1, 1 }; // clang-format on Point target = position.future; for (auto step : walkpath) { if (step == WALK_NONE) break; if (step > 0) { target.x += DirectionOffsetX[step - 1]; target.y += DirectionOffsetY[step - 1]; } } return target; } int Player::GetPositionPathIndex(Point pos) { constexpr Displacement DirectionOffset[8] = { { 0, -1 }, { -1, 0 }, { 1, 0 }, { 0, 1 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } }; Point target = position.future; int i = 0; for (auto step : walkpath) { if (target == pos) return i; if (step == WALK_NONE) break; if (step > 0) { target += DirectionOffset[step - 1]; } ++i; } return -1; } void Player::Say(HeroSpeech speechId) const { const SfxID soundEffect = GetHeroSound(_pClass, speechId); if (soundEffect == SfxID::None) return; PlaySfxLoc(soundEffect, position.tile); } void Player::SaySpecific(HeroSpeech speechId) const { const SfxID soundEffect = GetHeroSound(_pClass, speechId); if (soundEffect == SfxID::None || effect_is_playing(soundEffect)) return; PlaySfxLoc(soundEffect, position.tile, false); } void Player::Say(HeroSpeech speechId, int delay) const { sfxdelay = delay; sfxdnum = GetHeroSound(_pClass, speechId); } void Player::Stop() { ClrPlrPath(*this); destAction = ACTION_NONE; } bool Player::isWalking() const { return IsAnyOf(_pmode, PM_WALK_NORTHWARDS, PM_WALK_SOUTHWARDS, PM_WALK_SIDEWAYS); } int Player::GetManaShieldDamageReduction() { constexpr uint8_t Max = 7; return 24 - std::min(_pSplLvl[static_cast(SpellID::ManaShield)], Max) * 3; } void Player::RestorePartialLife() { const int wholeHitpoints = _pMaxHP >> 6; int l = ((wholeHitpoints / 8) + GenerateRnd(wholeHitpoints / 4)) << 6; if (IsAnyOf(_pClass, HeroClass::Warrior, HeroClass::Barbarian)) l *= 2; if (IsAnyOf(_pClass, HeroClass::Rogue, HeroClass::Monk, HeroClass::Bard)) l += l / 2; _pHitPoints = std::min(_pHitPoints + l, _pMaxHP); _pHPBase = std::min(_pHPBase + l, _pMaxHPBase); } void Player::RestorePartialMana() { const int wholeManaPoints = _pMaxMana >> 6; int l = ((wholeManaPoints / 8) + GenerateRnd(wholeManaPoints / 4)) << 6; if (_pClass == HeroClass::Sorcerer) l *= 2; if (IsAnyOf(_pClass, HeroClass::Rogue, HeroClass::Monk, HeroClass::Bard)) l += l / 2; if (HasNoneOf(_pIFlags, ItemSpecialEffect::NoMana)) { _pMana = std::min(_pMana + l, _pMaxMana); _pManaBase = std::min(_pManaBase + l, _pMaxManaBase); } } void Player::ReadySpellFromEquipment(inv_body_loc bodyLocation, bool forceSpell) { const Item &item = InvBody[bodyLocation]; if (item._itype == ItemType::Staff && IsValidSpell(item._iSpell) && item._iCharges > 0 && item._iStatFlag) { if (forceSpell || _pRSpell == SpellID::Invalid || _pRSplType == SpellType::Invalid) { _pRSpell = item._iSpell; _pRSplType = SpellType::Charges; RedrawEverything(); } } } player_graphic Player::getGraphic() const { switch (_pmode) { case PM_STAND: case PM_NEWLVL: case PM_QUIT: return player_graphic::Stand; case PM_WALK_NORTHWARDS: case PM_WALK_SOUTHWARDS: case PM_WALK_SIDEWAYS: return player_graphic::Walk; case PM_ATTACK: case PM_RATTACK: return player_graphic::Attack; case PM_BLOCK: return player_graphic::Block; case PM_SPELL: return GetPlayerGraphicForSpell(executedSpell.spellId); case PM_GOTHIT: return player_graphic::Hit; case PM_DEATH: return player_graphic::Death; default: app_fatal("SyncPlrAnim"); } } uint16_t Player::getSpriteWidth() const { if (!HeadlessMode) return (*AnimInfo.sprites)[0].width(); const player_graphic graphic = getGraphic(); const HeroClass cls = GetPlayerSpriteClass(_pClass); const PlayerWeaponGraphic weaponGraphic = GetPlayerWeaponGraphic(graphic, static_cast(_pgfxnum & 0xF)); return GetPlayerSpriteWidth(cls, graphic, weaponGraphic); } void Player::getAnimationFramesAndTicksPerFrame(player_graphic graphics, int8_t &numberOfFrames, int8_t &ticksPerFrame) const { ticksPerFrame = 1; switch (graphics) { case player_graphic::Stand: numberOfFrames = _pNFrames; ticksPerFrame = 4; break; case player_graphic::Walk: numberOfFrames = _pWFrames; break; case player_graphic::Attack: numberOfFrames = _pAFrames; break; case player_graphic::Hit: numberOfFrames = _pHFrames; break; case player_graphic::Lightning: case player_graphic::Fire: case player_graphic::Magic: numberOfFrames = _pSFrames; break; case player_graphic::Death: numberOfFrames = _pDFrames; ticksPerFrame = 2; break; case player_graphic::Block: numberOfFrames = _pBFrames; ticksPerFrame = 3; break; default: app_fatal("Unknown player graphics"); } } void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1, uint16_t wParam2) { // if game is not running don't show a preview if (!gbRunGame || PauseMode != 0 || !gbProcessPlayers) return; // we can only show a preview if our command is executed in the next game tick if (_pmode != PM_STAND) return; std::optional graphic; Direction dir = Direction::South; int minimalWalkDistance = -1; switch (cmdId) { case _cmd_id::CMD_RATTACKID: { const Monster &monster = Monsters[wParam1]; dir = GetDirection(position.future, monster.position.future); graphic = player_graphic::Attack; break; } case _cmd_id::CMD_SPELLID: { const Monster &monster = Monsters[wParam1]; dir = GetDirection(position.future, monster.position.future); graphic = GetPlayerGraphicForSpell(static_cast(wParam2)); break; } case _cmd_id::CMD_ATTACKID: { const Monster &monster = Monsters[wParam1]; point = monster.position.future; minimalWalkDistance = 2; if (!CanTalkToMonst(monster)) { dir = GetDirection(position.future, monster.position.future); graphic = player_graphic::Attack; } break; } case _cmd_id::CMD_RATTACKPID: { const Player &targetPlayer = Players[wParam1]; dir = GetDirection(position.future, targetPlayer.position.future); graphic = player_graphic::Attack; break; } case _cmd_id::CMD_SPELLPID: { const Player &targetPlayer = Players[wParam1]; dir = GetDirection(position.future, targetPlayer.position.future); graphic = GetPlayerGraphicForSpell(static_cast(wParam2)); break; } case _cmd_id::CMD_ATTACKPID: { const Player &targetPlayer = Players[wParam1]; point = targetPlayer.position.future; minimalWalkDistance = 2; dir = GetDirection(position.future, targetPlayer.position.future); graphic = player_graphic::Attack; break; } case _cmd_id::CMD_RATTACKXY: case _cmd_id::CMD_SATTACKXY: dir = GetDirection(position.tile, point); graphic = player_graphic::Attack; break; case _cmd_id::CMD_SPELLXY: dir = GetDirection(position.tile, point); graphic = GetPlayerGraphicForSpell(static_cast(wParam1)); break; case _cmd_id::CMD_SPELLXYD: dir = static_cast(wParam2); graphic = GetPlayerGraphicForSpell(static_cast(wParam1)); break; case _cmd_id::CMD_WALKXY: minimalWalkDistance = 1; break; case _cmd_id::CMD_TALKXY: case _cmd_id::CMD_DISARMXY: case _cmd_id::CMD_OPOBJXY: case _cmd_id::CMD_GOTOGETITEM: case _cmd_id::CMD_GOTOAGETITEM: minimalWalkDistance = 2; break; default: return; } if (minimalWalkDistance >= 0 && position.future != point) { int8_t testWalkPath[MaxPathLengthPlayer]; const int steps = FindPath(CanStep, [this](Point tile) { return PosOkPlayer(*this, tile); }, position.future, point, testWalkPath, MaxPathLengthPlayer); if (steps == 0) { // Can't walk to desired location => stand still return; } if (steps >= minimalWalkDistance) { graphic = player_graphic::Walk; switch (testWalkPath[0]) { case WALK_N: dir = Direction::North; break; case WALK_NE: dir = Direction::NorthEast; break; case WALK_E: dir = Direction::East; break; case WALK_SE: dir = Direction::SouthEast; break; case WALK_S: dir = Direction::South; break; case WALK_SW: dir = Direction::SouthWest; break; case WALK_W: dir = Direction::West; break; case WALK_NW: dir = Direction::NorthWest; break; } if (!PlrDirOK(*this, dir)) return; } } if (!graphic || HeadlessMode) return; LoadPlrGFX(*this, *graphic); const ClxSpriteList sprites = AnimationData[static_cast(*graphic)].spritesForDirection(dir); if (!previewCelSprite || *previewCelSprite != sprites[0]) { previewCelSprite = sprites[0]; progressToNextGameTickWhenPreviewWasSet = ProgressToNextGameTick; } } void Player::setCharacterLevel(uint8_t level) { this->_pLevel = std::clamp(level, 1U, getMaxCharacterLevel()); } uint8_t Player::getMaxCharacterLevel() const { return GetMaximumCharacterLevel(); } uint32_t Player::getNextExperienceThreshold() const { return GetNextExperienceThresholdForLevel(this->getCharacterLevel()); } int32_t Player::calculateBaseLife() const { const ClassAttributes &attr = getClassAttributes(); return attr.adjLife + (attr.lvlLife * getCharacterLevel()) + (attr.chrLife * _pBaseVit); } int32_t Player::calculateBaseMana() const { const ClassAttributes &attr = getClassAttributes(); return attr.adjMana + (attr.lvlMana * getCharacterLevel()) + (attr.chrMana * _pBaseMag); } void Player::occupyTile(Point tilePosition, bool isMoving) const { int16_t id = this->getId(); id += 1; dPlayer[tilePosition.x][tilePosition.y] = isMoving ? -id : id; } bool Player::isLevelOwnedByLocalClient() const { for (const Player &other : Players) { if (!other.plractive) continue; if (other._pLvlChanging) continue; if (other._pmode == PM_NEWLVL) continue; if (other.plrlevel != this->plrlevel) continue; if (other.plrIsOnSetLevel != this->plrIsOnSetLevel) continue; if (&other == MyPlayer && gbBufferMsgs != 0) continue; return &other == MyPlayer; } return false; } Player *PlayerAtPosition(Point position, bool ignoreMovingPlayers /*= false*/) { if (!InDungeonBounds(position)) return nullptr; auto playerIndex = dPlayer[position.x][position.y]; if (playerIndex == 0 || (ignoreMovingPlayers && playerIndex < 0)) return nullptr; return &Players[std::abs(playerIndex) - 1]; } ClxSprite GetPlayerPortraitSprite(Player &player) { const bool inDungeon = (player.plrlevel != 0); const HeroClass cls = GetPlayerSpriteClass(player._pClass); const PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(player_graphic::Stand, static_cast(player._pgfxnum & 0xF)); const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls); const char *path = spriteData.classPath.c_str(); std::string_view szCel = inDungeon ? "as" : "st"; player_graphic graphic = player_graphic::Stand; if (player.hasNoLife()) { if (animWeaponId == PlayerWeaponGraphic::Unarmed) { szCel = "dt"; graphic = player_graphic::Death; } } const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; char pszName[256]; GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName); const std::string spritePath { pszName }; // Check to see if the sprite has updated. if (player.PartyInfoSpriteLocations[inDungeon] != spritePath) { // The sprite has changed so store the new location player.PartyInfoSpriteLocations[inDungeon] = spritePath; player.PartyInfoSprites[inDungeon] = std::nullopt; // And now load the new sprite and store it const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId); player.PartyInfoSprites[inDungeon] = LoadCl2Sheet(pszName, animationWidth); } const ClxSpriteList spriteList = (*player.PartyInfoSprites[inDungeon])[static_cast(Direction::South)]; return spriteList[(graphic == player_graphic::Stand) ? 0 : spriteList.numSprites() - 1]; } bool IsPlayerUnarmed(Player &player) { const PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(player_graphic::Stand, static_cast(player._pgfxnum & 0xF)); return animWeaponId == PlayerWeaponGraphic::Unarmed; } void LoadPlrGFX(Player &player, player_graphic graphic) { if (HeadlessMode) return; auto &animationData = player.AnimationData[static_cast(graphic)]; if (animationData.sprites) return; const HeroClass cls = GetPlayerSpriteClass(player._pClass); PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(graphic, static_cast(player._pgfxnum & 0xF)); const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls); const char *path = spriteData.classPath.c_str(); std::string_view szCel; switch (graphic) { case player_graphic::Stand: szCel = "as"; if (leveltype == DTYPE_TOWN) szCel = "st"; break; case player_graphic::Walk: szCel = "aw"; if (leveltype == DTYPE_TOWN) szCel = "wl"; break; case player_graphic::Attack: if (leveltype == DTYPE_TOWN) return; szCel = "at"; break; case player_graphic::Hit: if (leveltype == DTYPE_TOWN) return; szCel = "ht"; break; case player_graphic::Lightning: szCel = "lm"; break; case player_graphic::Fire: szCel = "fm"; break; case player_graphic::Magic: szCel = "qm"; break; case player_graphic::Death: // Only one Death animation exists, for unarmed characters animWeaponId = PlayerWeaponGraphic::Unarmed; szCel = "dt"; break; case player_graphic::Block: if (leveltype == DTYPE_TOWN) return; if (!player._pBlockFlag) return; szCel = "bl"; break; default: app_fatal("PLR:2"); } const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast(animWeaponId)] }; char pszName[256]; GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName); const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId); animationData.sprites = LoadCl2Sheet(pszName, animationWidth); std::optional> graphicTRN = GetPlayerGraphicTRN(pszName); if (graphicTRN) { ClxApplyTrans(*animationData.sprites, graphicTRN->data()); } std::optional> classTRN = GetClassTRN(player); if (classTRN) { ClxApplyTrans(*animationData.sprites, classTRN->data()); } } void InitPlayerGFX(Player &player) { if (HeadlessMode) return; ResetPlayerGFX(player); if (player.hasNoLife()) { player._pgfxnum &= ~0xFU; LoadPlrGFX(player, player_graphic::Death); return; } for (size_t i = 0; i < enum_size::value; i++) { auto graphic = static_cast(i); if (graphic == player_graphic::Death) continue; LoadPlrGFX(player, graphic); } } void ResetPlayerGFX(Player &player) { player.AnimInfo.sprites = std::nullopt; if (!gbRunGame) { player.PartyInfoSprites[0] = std::nullopt; player.PartyInfoSprites[1] = std::nullopt; } for (PlayerAnimationData &animData : player.AnimationData) { animData.sprites = std::nullopt; } } void NewPlrAnim(Player &player, player_graphic graphic, Direction dir, AnimationDistributionFlags flags /*= AnimationDistributionFlags::None*/, int8_t numSkippedFrames /*= 0*/, int8_t distributeFramesBeforeFrame /*= 0*/) { LoadPlrGFX(player, graphic); OptionalClxSpriteList sprites; int previewShownGameTickFragments = 0; if (!HeadlessMode) { sprites = player.AnimationData[static_cast(graphic)].spritesForDirection(dir); if (player.previewCelSprite && (*sprites)[0] == *player.previewCelSprite && !player.isWalking()) { previewShownGameTickFragments = std::clamp(AnimationInfo::baseValueFraction - player.progressToNextGameTickWhenPreviewWasSet, 0, AnimationInfo::baseValueFraction); } } int8_t numberOfFrames; int8_t ticksPerFrame; player.getAnimationFramesAndTicksPerFrame(graphic, numberOfFrames, ticksPerFrame); player.AnimInfo.setNewAnimation(sprites, numberOfFrames, ticksPerFrame, flags, numSkippedFrames, distributeFramesBeforeFrame, static_cast(previewShownGameTickFragments)); } void SetPlrAnims(Player &player) { const HeroClass pc = player._pClass; const PlayerAnimData &plrAtkAnimData = GetPlayerAnimDataForClass(pc); auto gn = static_cast(player._pgfxnum & 0xFU); if (leveltype == DTYPE_TOWN) { player._pNFrames = plrAtkAnimData.townIdleFrames; player._pWFrames = plrAtkAnimData.townWalkingFrames; } else { player._pNFrames = plrAtkAnimData.idleFrames; player._pWFrames = plrAtkAnimData.walkingFrames; player._pHFrames = plrAtkAnimData.recoveryFrames; player._pBFrames = plrAtkAnimData.blockingFrames; switch (gn) { case PlayerWeaponGraphic::Unarmed: player._pAFrames = plrAtkAnimData.unarmedFrames; player._pAFNum = plrAtkAnimData.unarmedActionFrame; break; case PlayerWeaponGraphic::UnarmedShield: player._pAFrames = plrAtkAnimData.unarmedShieldFrames; player._pAFNum = plrAtkAnimData.unarmedShieldActionFrame; break; case PlayerWeaponGraphic::Sword: player._pAFrames = plrAtkAnimData.swordFrames; player._pAFNum = plrAtkAnimData.swordActionFrame; break; case PlayerWeaponGraphic::SwordShield: player._pAFrames = plrAtkAnimData.swordShieldFrames; player._pAFNum = plrAtkAnimData.swordShieldActionFrame; break; case PlayerWeaponGraphic::Bow: player._pAFrames = plrAtkAnimData.bowFrames; player._pAFNum = plrAtkAnimData.bowActionFrame; break; case PlayerWeaponGraphic::Axe: player._pAFrames = plrAtkAnimData.axeFrames; player._pAFNum = plrAtkAnimData.axeActionFrame; break; case PlayerWeaponGraphic::Mace: player._pAFrames = plrAtkAnimData.maceFrames; player._pAFNum = plrAtkAnimData.maceActionFrame; break; case PlayerWeaponGraphic::MaceShield: player._pAFrames = plrAtkAnimData.maceShieldFrames; player._pAFNum = plrAtkAnimData.maceShieldActionFrame; break; case PlayerWeaponGraphic::Staff: player._pAFrames = plrAtkAnimData.staffFrames; player._pAFNum = plrAtkAnimData.staffActionFrame; break; } } player._pDFrames = plrAtkAnimData.deathFrames; player._pSFrames = plrAtkAnimData.castingFrames; player._pSFNum = plrAtkAnimData.castingActionFrame; const int armorGraphicIndex = player._pgfxnum & ~0xFU; if (IsAnyOf(pc, HeroClass::Warrior, HeroClass::Barbarian)) { if (gn == PlayerWeaponGraphic::Bow && leveltype != DTYPE_TOWN) player._pNFrames = 8; if (armorGraphicIndex > 0) player._pDFrames = 15; } } /** * @param player The player reference. * @param c The hero class. */ void CreatePlayer(Player &player, HeroClass c) { player = {}; SetRndSeed(SDL_GetTicks()); player.setCharacterLevel(1); player._pClass = c; const ClassAttributes &attr = player.getClassAttributes(); player._pBaseStr = attr.baseStr; player._pStrength = player._pBaseStr; player._pBaseMag = attr.baseMag; player._pMagic = player._pBaseMag; player._pBaseDex = attr.baseDex; player._pDexterity = player._pBaseDex; player._pBaseVit = attr.baseVit; player._pVitality = player._pBaseVit; player._pHitPoints = player.calculateBaseLife(); player._pMaxHP = player._pHitPoints; player._pHPBase = player._pHitPoints; player._pMaxHPBase = player._pHitPoints; player._pMana = player.calculateBaseMana(); player._pMaxMana = player._pMana; player._pManaBase = player._pMana; player._pMaxManaBase = player._pMana; player._pExperience = 0; player._pArmorClass = 0; player._pLightRad = 10; player._pInfraFlag = false; for (uint8_t &spellLevel : player._pSplLvl) { spellLevel = 0; } player._pSpellFlags = SpellFlag::None; player._pRSplType = SpellType::Invalid; // Initializing the hotkey bindings to no selection std::fill(player._pSplHotKey, player._pSplHotKey + NumHotkeys, SpellID::Invalid); // CreatePlrItems calls AutoEquip which will overwrite the player graphic if required player._pgfxnum = static_cast(PlayerWeaponGraphic::Unarmed); for (bool &levelVisited : player._pLvlVisited) { levelVisited = false; } for (int i = 0; i < 10; i++) { player._pSLvlVisited[i] = false; } player._pLvlChanging = false; player.pTownWarps = 0; player.pLvlLoad = 0; player.pManaShield = false; player.pDamAcFlags = ItemSpecialEffectHf::None; player.wReflections = 0; InitDungMsgs(player); CreatePlrItems(player); SetRndSeed(0); } int CalcStatDiff(Player &player) { int diff = 0; for (auto attribute : enum_values()) { diff += player.GetMaximumAttributeValue(attribute); diff -= player.GetBaseAttributeValue(attribute); } return diff; } void NextPlrLevel(Player &player) { player.setCharacterLevel(player.getCharacterLevel() + 1); CalcPlrInv(player, true); if (CalcStatDiff(player) < 5) { player._pStatPts = CalcStatDiff(player); } else { player._pStatPts += 5; } const int hp = player.getClassAttributes().lvlLife; player._pMaxHP += hp; player._pHitPoints = player._pMaxHP; player._pMaxHPBase += hp; player._pHPBase = player._pMaxHPBase; if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } const int mana = player.getClassAttributes().lvlMana; player._pMaxMana += mana; player._pMaxManaBase += mana; if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) { player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; } if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Mana); } if (ControlMode != ControlTypes::KeyboardAndMouse) FocusOnCharInfo(); CalcPlrInv(player, true); PlaySFX(SfxID::ItemArmor); PlaySFX(SfxID::ItemSign); } void Player::_addExperience(uint32_t experience, int levelDelta) { if (this != MyPlayer || hasNoLife()) return; if (isMaxCharacterLevel()) { return; } // Adjust xp based on difference between the players current level and the target level (usually a monster level) uint32_t clampedExp = static_cast(std::clamp(static_cast(experience * (1 + levelDelta / 10.0)), 0, std::numeric_limits::max())); // Prevent power leveling if (gbIsMultiplayer) { // for low level characters experience gain is capped to 1/20 of current levels xp // for high level characters experience gain is capped to 200 * current level - this is a smaller value than 1/20 of the exp needed for the next level after level 5. clampedExp = std::min({ clampedExp, /* level 1-5: */ getNextExperienceThreshold() / 20U, /* level 6-50: */ 200U * getCharacterLevel() }); } lua::OnPlayerGainExperience(this, clampedExp); const uint32_t maxExperience = GetNextExperienceThresholdForLevel(getMaxCharacterLevel()); // ensure we only add enough experience to reach the max experience cap so we don't overflow _pExperience += std::min(clampedExp, maxExperience - _pExperience); if (*GetOptions().Gameplay.experienceBar) { RedrawEverything(); } // Increase player level if applicable while (!isMaxCharacterLevel() && _pExperience >= getNextExperienceThreshold()) { // NextPlrLevel increments character level which changes the next experience threshold NextPlrLevel(*this); } NetSendCmdParam1(false, CMD_PLRLEVEL, getCharacterLevel()); } void AddPlrMonstExper(int lvl, unsigned exp, char pmask) { unsigned totplrs = 0; for (size_t i = 0; i < Players.size(); i++) { if (((1 << i) & pmask) != 0) { totplrs++; } } if (totplrs != 0) { const unsigned e = exp / totplrs; if ((pmask & (1 << MyPlayerId)) != 0) MyPlayer->addExperience(e, lvl); } } void InitPlayer(Player &player, bool firstTime) { if (firstTime) { player._pRSplType = SpellType::Invalid; player._pRSpell = SpellID::Invalid; if (&player == MyPlayer) LoadHotkeys(); player._pSBkSpell = SpellID::Invalid; player.queuedSpell.spellId = player._pRSpell; player.queuedSpell.spellType = player._pRSplType; player.pManaShield = false; player.wReflections = 0; } player.lightId = NO_LIGHT; if (player.isOnActiveLevel()) { SetPlrAnims(player); ClearStateVariables(player); if (!player.hasNoLife()) { player._pmode = PM_STAND; NewPlrAnim(player, player_graphic::Stand, Direction::South); player.AnimInfo.currentFrame = GenerateRnd(player._pNFrames - 1); player.AnimInfo.tickCounterOfCurrentFrame = GenerateRnd(3); } else { player._pgfxnum &= ~0xFU; player._pmode = PM_DEATH; NewPlrAnim(player, player_graphic::Death, Direction::South); player.AnimInfo.currentFrame = player.AnimInfo.numberOfFrames - 2; } player._pdir = Direction::South; if (&player == MyPlayer && (!firstTime || leveltype != DTYPE_TOWN)) { player.position.tile = ViewPosition; } SetPlayerOld(player); player.walkpath[0] = WALK_NONE; player.destAction = ACTION_NONE; if (&player == MyPlayer) { player.lightId = AddLight(player.position.tile, player._pLightRad); ChangeLightXY(player.lightId, player.position.tile); // fix for a bug where old light is still visible at the entrance after reentering level } ActivateVision(player.position.tile, player._pLightRad, player.getId()); } player._pAblSpells = GetSpellBitmask(GetPlayerStartingLoadoutForClass(player._pClass).skill); player._pInvincible = false; if (&player == MyPlayer) { MyPlayerIsDead = false; } } void InitMultiView() { assert(MyPlayer != nullptr); ViewPosition = MyPlayer->position.tile; } void PlrClrTrans(Point position) { for (int i = position.y - 1; i <= position.y + 1; i++) { for (int j = position.x - 1; j <= position.x + 1; j++) { TransList[dTransVal[j][i]] = false; } } } void PlrDoTrans(Point position) { if (IsNoneOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS, DTYPE_CRYPT)) { TransList[1] = true; return; } for (int i = position.y - 1; i <= position.y + 1; i++) { for (int j = position.x - 1; j <= position.x + 1; j++) { if (IsTileNotSolid({ j, i }) && dTransVal[j][i] != 0) { TransList[dTransVal[j][i]] = true; } } } } void SetPlayerOld(Player &player) { player.position.old = player.position.tile; } void FixPlayerLocation(Player &player, Direction bDir) { player.position.future = player.position.tile; player._pdir = bDir; if (&player == MyPlayer) { ViewPosition = player.position.tile; } ChangeLightXY(player.lightId, player.position.tile); ChangeVisionXY(player.getId(), player.position.tile); } void StartStand(Player &player, Direction dir) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } NewPlrAnim(player, player_graphic::Stand, dir); player._pmode = PM_STAND; FixPlayerLocation(player, dir); FixPlrWalkTags(player); player.occupyTile(player.position.tile, false); SetPlayerOld(player); } void StartPlrBlock(Player &player, Direction dir) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } PlaySfxLoc(SfxID::ItemSword, player.position.tile); int8_t skippedAnimationFrames = 0; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastBlock)) { skippedAnimationFrames = (player._pBFrames - 2); // ISPL_FASTBLOCK means we cancel the animation if frame 2 was shown } NewPlrAnim(player, player_graphic::Block, dir, AnimationDistributionFlags::SkipsDelayOfLastFrame, skippedAnimationFrames); player._pmode = PM_BLOCK; FixPlayerLocation(player, dir); SetPlayerOld(player); } /** * @todo Figure out why clearing player.position.old sometimes fails */ void FixPlrWalkTags(const Player &player) { for (int y = 0; y < MAXDUNY; y++) { for (int x = 0; x < MAXDUNX; x++) { if (PlayerAtPosition({ x, y }) == &player) dPlayer[x][y] = 0; } } } void StartPlrHit(Player &player, int dam, bool forcehit) { if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) { SyncPlrKill(player, DeathReason::Unknown); return; } player.Say(HeroSpeech::ArghClang); RedrawComponent(PanelDrawComponent::Health); if (player._pClass == HeroClass::Barbarian) { if (dam >> 6 < player.getCharacterLevel() + player.getCharacterLevel() / 4 && !forcehit) { return; } } else if (dam >> 6 < player.getCharacterLevel() && !forcehit) { return; } const Direction pd = player._pdir; int8_t skippedAnimationFrames = 0; if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastestHitRecovery)) { skippedAnimationFrames = 3; } else if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FasterHitRecovery)) { skippedAnimationFrames = 2; } else if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastHitRecovery)) { skippedAnimationFrames = 1; } else { skippedAnimationFrames = 0; } NewPlrAnim(player, player_graphic::Hit, pd, AnimationDistributionFlags::None, skippedAnimationFrames); player._pmode = PM_GOTHIT; FixPlayerLocation(player, pd); FixPlrWalkTags(player); player.occupyTile(player.position.tile, false); SetPlayerOld(player); } #if defined(__clang__) || defined(__GNUC__) __attribute__((no_sanitize("shift-base"))) #endif void StartPlayerKill(Player &player, DeathReason deathReason) { if (player.hasNoLife() && player._pmode == PM_DEATH) { return; } if (&player == MyPlayer) { NetSendCmdParam1(true, CMD_PLRDEAD, static_cast(deathReason)); gamemenu_off(); } const bool dropGold = !gbIsMultiplayer || !(player.isOnLevel(16) || player.isOnArenaLevel()); const bool dropItems = dropGold && deathReason == DeathReason::MonsterOrTrap; const bool dropEar = dropGold && deathReason == DeathReason::Player; player.Say(HeroSpeech::AuughUh); // Are the current animations item dependent? if (player._pgfxnum != 0) { if (dropItems) { // Ensure death animation show the player without weapon and armor, because they drop on death player._pgfxnum = 0; } else { // Death animation aren't weapon specific, so always use the unarmed animations player._pgfxnum &= ~0xFU; } ResetPlayerGFX(player); SetPlrAnims(player); } NewPlrAnim(player, player_graphic::Death, player._pdir); player._pBlockFlag = false; player._pmode = PM_DEATH; player._pInvincible = true; SetPlayerHitPoints(player, 0); if (&player != MyPlayer && dropItems) { // Ensure that items are removed for remote players // The dropped items will be synced separately (by the remote client) for (Item &item : player.InvBody) { item.clear(); } CalcPlrInv(player, false); } if (player.isOnActiveLevel()) { FixPlayerLocation(player, player._pdir); FixPlrWalkTags(player); dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer; SetPlayerOld(player); // Only generate drops once (for the local player) // For remote players we get separated sync messages (by the remote client) if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); if (!player.HoldItem.isEmpty()) { DeadItem(player, std::move(player.HoldItem), { 0, 0 }); NewCursor(CURSOR_HAND); } if (dropGold) { DropHalfPlayersGold(player); } if (dropEar) { Item ear; InitializeItem(ear, IDI_EAR); CopyUtf8(ear._iName, fmt::format(fmt::runtime("Ear of {:s}"), player._pName), sizeof(ear._iName)); CopyUtf8(ear._iIName, player._pName, ItemNameLength); switch (player._pClass) { case HeroClass::Sorcerer: ear._iCurs = ICURS_EAR_SORCERER; break; case HeroClass::Warrior: ear._iCurs = ICURS_EAR_WARRIOR; break; case HeroClass::Rogue: case HeroClass::Monk: case HeroClass::Bard: case HeroClass::Barbarian: ear._iCurs = ICURS_EAR_ROGUE; break; default: break; } ear._iCreateInfo = player._pName[0] << 8 | player._pName[1]; ear._iSeed = player._pName[2] << 24 | player._pName[3] << 16 | player._pName[4] << 8 | player._pName[5]; ear._ivalue = player.getCharacterLevel(); if (FindGetItem(ear._iSeed, IDI_EAR, ear._iCreateInfo) == -1) { DeadItem(player, std::move(ear), { 0, 0 }); } } if (dropItems) { Direction pdd = player._pdir; for (Item &item : player.InvBody) { pdd = Left(pdd); DeadItem(player, item.pop(), Displacement(pdd)); } CalcPlrInv(player, false); } } } SetPlayerHitPoints(player, 0); } void StripTopGold(Player &player) { for (Item &item : InventoryPlayerItemsRange { player }) { if (item._itype != ItemType::Gold) continue; if (item._ivalue <= MaxGold) continue; Item excessGold; MakeGoldStack(excessGold, item._ivalue - MaxGold); item._ivalue = MaxGold; if (GoldAutoPlace(player, excessGold)) continue; if (!player.HoldItem.isEmpty() && ActiveItemCount + 1 >= MAXITEMS) continue; DeadItem(player, std::move(excessGold), { 0, 0 }); } player._pGold = CalculateGold(player); if (player.HoldItem.isEmpty()) return; if (AutoEquip(player, player.HoldItem, false)) return; if (CanFitItemInInventory(player, player.HoldItem)) return; if (AutoPlaceItemInBelt(player, player.HoldItem)) return; const std::optional itemTile = FindAdjacentPositionForItem(player.position.tile, player._pdir); if (itemTile) return; DeadItem(player, std::move(player.HoldItem), { 0, 0 }); NewCursor(CURSOR_HAND); } void ApplyPlrDamage(DamageType damageType, Player &player, int dam, int minHP /*= 0*/, int frac /*= 0*/, DeathReason deathReason /*= DeathReason::MonsterOrTrap*/) { int totalDamage = (dam << 6) + frac; if (&player == MyPlayer && !player.hasNoLife()) { lua::OnPlayerTakeDamage(&player, totalDamage, static_cast(damageType)); } if (totalDamage > 0 && player.pManaShield && HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) { const uint8_t manaShieldLevel = player._pSplLvl[static_cast(SpellID::ManaShield)]; if (manaShieldLevel > 0) { totalDamage += totalDamage / -player.GetManaShieldDamageReduction(); } if (&player == MyPlayer) RedrawComponent(PanelDrawComponent::Mana); if (player._pMana >= totalDamage) { player._pMana -= totalDamage; player._pManaBase -= totalDamage; totalDamage = 0; } else { totalDamage -= player._pMana; if (manaShieldLevel > 0) { totalDamage += totalDamage / (player.GetManaShieldDamageReduction() - 1); } player._pMana = 0; player._pManaBase = player._pMaxManaBase - player._pMaxMana; if (&player == MyPlayer) NetSendCmd(true, CMD_REMSHIELD); } } if (totalDamage == 0) return; RedrawComponent(PanelDrawComponent::Health); player._pHitPoints -= totalDamage; player._pHPBase -= totalDamage; if (player._pHitPoints > player._pMaxHP) { player._pHitPoints = player._pMaxHP; player._pHPBase = player._pMaxHPBase; } const int minHitPoints = minHP << 6; if (player._pHitPoints < minHitPoints) { SetPlayerHitPoints(player, minHitPoints); } if (player.hasNoLife()) { SyncPlrKill(player, deathReason); } } void SyncPlrKill(Player &player, DeathReason deathReason) { SetPlayerHitPoints(player, 0); StartPlayerKill(player, deathReason); } void RemovePlrMissiles(const Player &player) { if (leveltype != DTYPE_TOWN) { Monster *golem; while ((golem = FindGolemForPlayer(player)) != nullptr) { KillGolem(*golem); } } for (auto &missile : Missiles) { if (missile._mitype == MissileID::StoneCurse && &Players[missile._misource] == &player) { Monsters[missile.var2].mode = static_cast(missile.var1); } } } #if defined(__clang__) || defined(__GNUC__) __attribute__((no_sanitize("shift-base"))) #endif void StartNewLvl(Player &player, interface_mode fom, int lvl) { InitLevelChange(player); switch (fom) { case WM_DIABNEXTLVL: case WM_DIABPREVLVL: case WM_DIABRTNLVL: case WM_DIABTOWNWARP: player.setLevel(lvl); break; case WM_DIABSETLVL: if (&player == MyPlayer) setlvlnum = (_setlevels)lvl; player.setLevel(setlvlnum); break; case WM_DIABTWARPUP: MyPlayer->pTownWarps |= 1 << (leveltype - 2); player.setLevel(lvl); break; case WM_DIABRETOWN: break; default: app_fatal("StartNewLvl"); } if (&player == MyPlayer) { player._pmode = PM_NEWLVL; player._pInvincible = true; SDL_Event event; CustomEventToSdlEvent(event, fom); SDL_PushEvent(&event); if (gbIsMultiplayer) { NetSendCmdParam2(true, CMD_NEWLVL, fom, lvl); } } } void RestartTownLvl(Player &player) { InitLevelChange(player); player.setLevel(0); player._pInvincible = false; SetPlayerHitPoints(player, 64); player._pMana = 0; player._pManaBase = player._pMana - (player._pMaxMana - player._pMaxManaBase); CalcPlrInv(player, false); player._pmode = PM_NEWLVL; if (&player == MyPlayer) { player._pInvincible = true; SDL_Event event; CustomEventToSdlEvent(event, WM_DIABRETOWN); SDL_PushEvent(&event); } } void StartWarpLvl(Player &player, size_t pidx) { InitLevelChange(player); if (gbIsMultiplayer) { if (!player.isOnLevel(0)) { player.setLevel(0); } else { if (Portals[pidx].setlvl) player.setLevel(static_cast<_setlevels>(Portals[pidx].level)); else player.setLevel(Portals[pidx].level); } } if (&player == MyPlayer) { SetCurrentPortal(pidx); player._pmode = PM_NEWLVL; player._pInvincible = true; SDL_Event event; CustomEventToSdlEvent(event, WM_DIABWARPLVL); SDL_PushEvent(&event); } } void ProcessPlayers() { assert(MyPlayer != nullptr); Player &myPlayer = *MyPlayer; if (myPlayer.pLvlLoad > 0) { myPlayer.pLvlLoad--; } if (sfxdelay > 0) { sfxdelay--; if (sfxdelay == 0) { switch (sfxdnum) { case SfxID::Defiler1: InitQTextMsg(TEXT_DEFILER1); break; case SfxID::Defiler2: InitQTextMsg(TEXT_DEFILER2); break; case SfxID::Defiler3: InitQTextMsg(TEXT_DEFILER3); break; case SfxID::Defiler4: InitQTextMsg(TEXT_DEFILER4); break; default: PlaySFX(sfxdnum); } } } ValidatePlayer(); for (size_t pnum = 0; pnum < Players.size(); pnum++) { Player &player = Players[pnum]; if (player.plractive && player.isOnActiveLevel() && (&player == MyPlayer || !player._pLvlChanging)) { if (!PlrDeathModeOK(player) && player.hasNoLife()) { SyncPlrKill(player, DeathReason::Unknown); } if (&player == MyPlayer) { if (HasAnyOf(player._pIFlags, ItemSpecialEffect::DrainLife) && leveltype != DTYPE_TOWN) { ApplyPlrDamage(DamageType::Physical, player, 0, 0, 4); } if (player.pManaShield && HasAnyOf(player._pIFlags, ItemSpecialEffect::NoMana)) { NetSendCmd(true, CMD_REMSHIELD); } } bool tplayer = false; do { switch (player._pmode) { case PM_STAND: case PM_NEWLVL: case PM_QUIT: tplayer = false; break; case PM_WALK_NORTHWARDS: case PM_WALK_SOUTHWARDS: case PM_WALK_SIDEWAYS: tplayer = DoWalk(player); break; case PM_ATTACK: tplayer = DoAttack(player); break; case PM_RATTACK: tplayer = DoRangeAttack(player); break; case PM_BLOCK: tplayer = DoBlock(player); break; case PM_SPELL: tplayer = DoSpell(player); break; case PM_GOTHIT: tplayer = DoGotHit(player); break; case PM_DEATH: tplayer = DoDeath(player); break; } CheckNewPath(player, tplayer); } while (tplayer); player.previewCelSprite = std::nullopt; if (player._pmode != PM_DEATH || player.AnimInfo.tickCounterOfCurrentFrame != 40) player.AnimInfo.processAnimation(); } } } void ClrPlrPath(Player &player) { memset(player.walkpath, WALK_NONE, sizeof(player.walkpath)); } /** * @brief Determines if the target position is clear for the given player to stand on. * * This requires an ID instead of a Player& to compare with the dPlayer lookup table values. * * @param player The player to check. * @param position Dungeon tile coordinates. * @return False if something (other than the player themselves) is blocking the tile. */ bool PosOkPlayer(const Player &player, Point position) { if (!InDungeonBounds(position)) return false; if (!IsTileWalkable(position)) return false; Player *otherPlayer = PlayerAtPosition(position); if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife()) return false; if (dMonster[position.x][position.y] != 0) { if (leveltype == DTYPE_TOWN) { return false; } if (dMonster[position.x][position.y] <= 0) { return false; } if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife()) { return false; } } return true; } void MakePlrPath(Player &player, Point targetPosition, bool endspace) { if (player.position.future == targetPosition) { return; } int path = FindPath(CanStep, [&player](Point position) { return PosOkPlayer(player, position); }, player.position.future, targetPosition, player.walkpath, MaxPathLengthPlayer); if (path == 0) { return; } if (!endspace) { path--; } player.walkpath[path] = WALK_NONE; } void CheckPlrSpell(bool isShiftHeld, SpellID spellID, SpellType spellType) { bool addflag = false; assert(MyPlayer != nullptr); Player &myPlayer = *MyPlayer; if (!IsValidSpell(spellID)) { myPlayer.Say(HeroSpeech::IDontHaveASpellReady); return; } if (ControlMode == ControlTypes::KeyboardAndMouse) { if (pcurs != CURSOR_HAND) return; if (GetMainPanel().contains(MousePosition)) // inside main panel return; if ( (IsLeftPanelOpen() && GetLeftPanel().contains(MousePosition)) // inside left panel || (IsRightPanelOpen() && GetRightPanel().contains(MousePosition)) // inside right panel ) { if (spellID != SpellID::Healing && spellID != SpellID::Identify && spellID != SpellID::ItemRepair && spellID != SpellID::Infravision && spellID != SpellID::StaffRecharge) return; } } if (leveltype == DTYPE_TOWN && !GetSpellData(spellID).isAllowedInTown()) { myPlayer.Say(HeroSpeech::ICantCastThatHere); return; } SpellCheckResult spellcheck = SpellCheckResult::Success; switch (spellType) { case SpellType::Skill: case SpellType::Spell: spellcheck = CheckSpell(*MyPlayer, spellID, spellType, false); addflag = spellcheck == SpellCheckResult::Success; break; case SpellType::Scroll: addflag = pcurs == CURSOR_HAND && CanUseScroll(myPlayer, spellID); break; case SpellType::Charges: addflag = pcurs == CURSOR_HAND && CanUseStaff(myPlayer, spellID); break; case SpellType::Invalid: return; } if (!addflag) { if (spellType == SpellType::Spell) { switch (spellcheck) { case SpellCheckResult::Fail_NoMana: myPlayer.Say(HeroSpeech::NotEnoughMana); break; case SpellCheckResult::Fail_Level0: myPlayer.Say(HeroSpeech::ICantCastThatYet); break; default: myPlayer.Say(HeroSpeech::ICantDoThat); break; } LastPlayerAction = PlayerActionType::None; } return; } const int spellFrom = 0; if (IsWallSpell(spellID)) { LastPlayerAction = PlayerActionType::Spell; const Direction sd = GetDirection(myPlayer.position.tile, cursPosition); NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast(spellID), static_cast(spellType), static_cast(sd), spellFrom); } else if (pcursmonst != -1 && !isShiftHeld) { LastPlayerAction = PlayerActionType::SpellMonsterTarget; NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast(spellID), static_cast(spellType), spellFrom); } else if (PlayerUnderCursor != nullptr && !PlayerUnderCursor->hasNoLife() && !isShiftHeld && !myPlayer.friendlyMode) { LastPlayerAction = PlayerActionType::SpellPlayerTarget; NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast(spellID), static_cast(spellType), spellFrom); } else { Point targetedTile = cursPosition; if (spellID == SpellID::Teleport && myPlayer.executedSpell.spellId == SpellID::Teleport) { // Check if the player is attempting to queue Teleport onto a tile that is currently being targeted with Teleport, or a nearby tile if (cursPosition.WalkingDistance(myPlayer.position.temp) <= 1) { // Get the relative displacement from the player's current position to the cursor position const WorldTileDisplacement relativeMove = cursPosition - static_cast(myPlayer.position.tile); // Target the tile the relative distance away from the player's targeted Teleport tile targetedTile = myPlayer.position.temp + relativeMove; } } LastPlayerAction = PlayerActionType::Spell; NetSendCmdLocParam3(true, CMD_SPELLXY, targetedTile, static_cast(spellID), static_cast(spellType), spellFrom); } } void SyncPlrAnim(Player &player) { const player_graphic graphic = player.getGraphic(); if (!HeadlessMode) player.AnimInfo.sprites = player.AnimationData[static_cast(graphic)].spritesForDirection(player._pdir); } void SyncInitPlrPos(Player &player) { if (!player.isOnActiveLevel()) return; const WorldTileDisplacement offset[9] = { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 }, { 2, 0 }, { 0, 2 }, { 1, 2 }, { 2, 1 }, { 2, 2 } }; const Point position = [&]() { for (int i = 0; i < 8; i++) { Point position = player.position.tile + offset[i]; if (PosOkPlayer(player, position)) return position; } const std::optional nearPosition = FindClosestValidPosition( [&player](Point testPosition) { for (int i = 0; i < numtrigs; i++) { if (trigs[i].position == testPosition) return false; } return PosOkPlayer(player, testPosition) && !PosOkPortal(currlevel, testPosition); }, player.position.tile, 1, // skip the starting tile since that was checked in the previous loop 50); return nearPosition.value_or(Point { 0, 0 }); }(); player.position.tile = position; player.occupyTile(position, false); player.position.future = position; if (&player == MyPlayer) { ViewPosition = position; } } void SyncInitPlr(Player &player) { SetPlrAnims(player); SyncInitPlrPos(player); if (&player != MyPlayer) player.lightId = NO_LIGHT; } void CheckStats(Player &player) { for (auto attribute : enum_values()) { const int maxStatPoint = player.GetMaximumAttributeValue(attribute); switch (attribute) { case CharacterAttribute::Strength: player._pBaseStr = std::clamp(player._pBaseStr, 0, maxStatPoint); break; case CharacterAttribute::Magic: player._pBaseMag = std::clamp(player._pBaseMag, 0, maxStatPoint); break; case CharacterAttribute::Dexterity: player._pBaseDex = std::clamp(player._pBaseDex, 0, maxStatPoint); break; case CharacterAttribute::Vitality: player._pBaseVit = std::clamp(player._pBaseVit, 0, maxStatPoint); break; } } } void ModifyPlrStr(Player &player, int l) { l = std::clamp(l, 0 - player._pBaseStr, player.GetMaximumAttributeValue(CharacterAttribute::Strength) - player._pBaseStr); player._pStrength += l; player._pBaseStr += l; CalcPlrInv(player, true); if (&player == MyPlayer) { NetSendCmdParam1(false, CMD_SETSTR, player._pBaseStr); } } void ModifyPlrMag(Player &player, int l) { l = std::clamp(l, 0 - player._pBaseMag, player.GetMaximumAttributeValue(CharacterAttribute::Magic) - player._pBaseMag); player._pMagic += l; player._pBaseMag += l; int ms = l; ms *= player.getClassAttributes().chrMana; player._pMaxManaBase += ms; player._pMaxMana += ms; if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) { player._pManaBase += ms; player._pMana += ms; } CalcPlrInv(player, true); if (&player == MyPlayer) { NetSendCmdParam1(false, CMD_SETMAG, player._pBaseMag); } } void ModifyPlrDex(Player &player, int l) { l = std::clamp(l, 0 - player._pBaseDex, player.GetMaximumAttributeValue(CharacterAttribute::Dexterity) - player._pBaseDex); player._pDexterity += l; player._pBaseDex += l; CalcPlrInv(player, true); if (&player == MyPlayer) { NetSendCmdParam1(false, CMD_SETDEX, player._pBaseDex); } } void ModifyPlrVit(Player &player, int l) { l = std::clamp(l, 0 - player._pBaseVit, player.GetMaximumAttributeValue(CharacterAttribute::Vitality) - player._pBaseVit); player._pVitality += l; player._pBaseVit += l; int ms = l; ms *= player.getClassAttributes().chrLife; player._pHPBase += ms; player._pMaxHPBase += ms; player._pHitPoints += ms; player._pMaxHP += ms; CalcPlrInv(player, true); if (&player == MyPlayer) { NetSendCmdParam1(false, CMD_SETVIT, player._pBaseVit); } } void SetPlayerHitPoints(Player &player, int val) { player._pHitPoints = val; player._pHPBase = val + player._pMaxHPBase - player._pMaxHP; if (&player == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } } void SetPlrStr(Player &player, int v) { player._pBaseStr = v; CalcPlrInv(player, true); } void SetPlrMag(Player &player, int v) { player._pBaseMag = v; int m = v; m *= player.getClassAttributes().chrMana; player._pMaxManaBase = m; player._pMaxMana = m; CalcPlrInv(player, true); } void SetPlrDex(Player &player, int v) { player._pBaseDex = v; CalcPlrInv(player, true); } void SetPlrVit(Player &player, int v) { player._pBaseVit = v; int hp = v; hp *= player.getClassAttributes().chrLife; player._pHPBase = hp; player._pMaxHPBase = hp; CalcPlrInv(player, true); } void InitDungMsgs(Player &player) { player.pDungMsgs = 0; player.pDungMsgs2 = 0; } enum { // clang-format off DungMsgCathedral = 1 << 0, DungMsgCatacombs = 1 << 1, DungMsgCaves = 1 << 2, DungMsgHell = 1 << 3, DungMsgDiablo = 1 << 4, // clang-format on }; void PlayDungMsgs() { assert(MyPlayer != nullptr); Player &myPlayer = *MyPlayer; if (!setlevel && currlevel == 1 && !myPlayer._pLvlVisited[1] && (myPlayer.pDungMsgs & DungMsgCathedral) == 0) { myPlayer.Say(HeroSpeech::TheSanctityOfThisPlaceHasBeenFouled, 40); myPlayer.pDungMsgs = myPlayer.pDungMsgs | DungMsgCathedral; } else if (!setlevel && currlevel == 5 && !myPlayer._pLvlVisited[5] && (myPlayer.pDungMsgs & DungMsgCatacombs) == 0) { myPlayer.Say(HeroSpeech::TheSmellOfDeathSurroundsMe, 40); myPlayer.pDungMsgs |= DungMsgCatacombs; } else if (!setlevel && currlevel == 9 && !myPlayer._pLvlVisited[9] && (myPlayer.pDungMsgs & DungMsgCaves) == 0) { myPlayer.Say(HeroSpeech::ItsHotDownHere, 40); myPlayer.pDungMsgs |= DungMsgCaves; } else if (!setlevel && currlevel == 13 && !myPlayer._pLvlVisited[13] && (myPlayer.pDungMsgs & DungMsgHell) == 0) { myPlayer.Say(HeroSpeech::IMustBeGettingClose, 40); myPlayer.pDungMsgs |= DungMsgHell; } else if (!setlevel && currlevel == 16 && !myPlayer._pLvlVisited[16] && (myPlayer.pDungMsgs & DungMsgDiablo) == 0) { for (auto &monster : Monsters) { if (monster.type().type != MT_DIABLO) continue; if (monster.hitPoints > 0) { sfxdelay = 40; sfxdnum = SfxID::DiabloGreeting; myPlayer.pDungMsgs |= DungMsgDiablo; } break; } } else if (!setlevel && currlevel == 17 && !myPlayer._pLvlVisited[17] && (myPlayer.pDungMsgs2 & 1) == 0) { sfxdelay = 10; sfxdnum = SfxID::Defiler1; Quests[Q_DEFILER]._qactive = QUEST_ACTIVE; Quests[Q_DEFILER]._qlog = true; Quests[Q_DEFILER]._qmsg = TEXT_DEFILER1; NetSendCmdQuest(true, Quests[Q_DEFILER]); myPlayer.pDungMsgs2 |= 1; } else if (!setlevel && currlevel == 19 && !myPlayer._pLvlVisited[19] && (myPlayer.pDungMsgs2 & 4) == 0) { sfxdelay = 10; sfxdnum = SfxID::Defiler3; myPlayer.pDungMsgs2 |= 4; } else if (!setlevel && currlevel == 21 && !myPlayer._pLvlVisited[21] && (myPlayer.pDungMsgs & 32) == 0) { myPlayer.Say(HeroSpeech::ThisIsAPlaceOfGreatPower, 30); myPlayer.pDungMsgs |= 32; } else if (setlevel && setlvlnum == SL_SKELKING && !gbIsSpawn && !myPlayer._pSLvlVisited[SL_SKELKING] && Quests[Q_SKELKING]._qactive == QUEST_ACTIVE) { sfxdelay = 10; sfxdnum = SfxID::LeoricGreeting; } else { sfxdelay = 0; } } #ifdef BUILD_TESTING bool TestPlayerDoGotHit(Player &player) { return DoGotHit(player); } #endif } // namespace devilution ================================================ FILE: Source/player.h ================================================ /** * @file player.h * * Interface of player functionality, leveling, actions, creation, loading, etc. */ #pragma once #include #include #include #include #include #include "diablo.h" #include "engine/actor_position.hpp" #include "engine/animationinfo.h" #include "engine/clx_sprite.hpp" #include "engine/displacement.hpp" #include "engine/path.h" #include "engine/point.hpp" #include "game_mode.hpp" #include "interfac.h" #include "items.h" #include "items/validation.h" #include "levels/dun_tile.hpp" #include "levels/gendung.h" #include "multi.h" #include "tables/playerdat.hpp" #include "tables/spelldat.h" #include "utils/attributes.h" #include "utils/enum_traits.h" #include "utils/is_of.hpp" namespace devilution { constexpr int InventoryGridCells = 40; constexpr int MaxBeltItems = 8; constexpr int MaxResistance = 75; constexpr uint8_t MaxSpellLevel = 15; constexpr int PlayerNameLength = 32; constexpr size_t NumHotkeys = 12; /** Walking directions */ enum { // clang-format off WALK_NE = 1, WALK_NW = 2, WALK_SE = 3, WALK_SW = 4, WALK_N = 5, WALK_E = 6, WALK_S = 7, WALK_W = 8, WALK_NONE = -1, // clang-format on }; enum class CharacterAttribute : uint8_t { Strength, Magic, Dexterity, Vitality, FIRST = Strength, LAST = Vitality }; // Logical equipment locations enum inv_body_loc : uint8_t { INVLOC_HEAD, INVLOC_RING_LEFT, INVLOC_RING_RIGHT, INVLOC_AMULET, INVLOC_HAND_LEFT, INVLOC_HAND_RIGHT, INVLOC_CHEST, NUM_INVLOC, }; enum class player_graphic : uint8_t { Stand, Walk, Attack, Hit, Lightning, Fire, Magic, Death, Block, LAST = Block }; enum class PlayerWeaponGraphic : uint8_t { Unarmed, UnarmedShield, Sword, SwordShield, Bow, Axe, Mace, MaceShield, Staff, }; enum PLR_MODE : uint8_t { PM_STAND, PM_WALK_NORTHWARDS, PM_WALK_SOUTHWARDS, PM_WALK_SIDEWAYS, PM_ATTACK, PM_RATTACK, PM_BLOCK, PM_GOTHIT, PM_DEATH, PM_SPELL, PM_NEWLVL, PM_QUIT, }; enum action_id : int8_t { // clang-format off ACTION_WALK = -2, // Automatic walk when using gamepad ACTION_NONE = -1, ACTION_ATTACK = 9, ACTION_RATTACK = 10, ACTION_SPELL = 12, ACTION_OPERATE = 13, ACTION_DISARM = 14, ACTION_PICKUPITEM = 15, // put item in hand (inventory screen open) ACTION_PICKUPAITEM = 16, // put item in inventory ACTION_TALK = 17, ACTION_OPERATETK = 18, // operate via telekinesis ACTION_ATTACKMON = 20, ACTION_ATTACKPLR = 21, ACTION_RATTACKMON = 22, ACTION_RATTACKPLR = 23, ACTION_SPELLMON = 24, ACTION_SPELLPLR = 25, ACTION_SPELLWALL = 26, // clang-format on }; enum class SpellFlag : uint8_t { // clang-format off None = 0, Etherealize = 1 << 0, RageActive = 1 << 1, RageCooldown = 1 << 2, // bits 3-7 are unused // clang-format on }; use_enum_as_flags(SpellFlag); /* @brief When the player dies, what is the reason/source why? */ enum class DeathReason { /* @brief Monster or Trap (dungeon) */ MonsterOrTrap, /* @brief Other player or selfkill (for example firewall) */ Player, /* @brief HP is zero but we don't know when or where this happened */ Unknown, }; /** Maps from armor animation to letter used in graphic files. */ constexpr std::array ArmourChar = { 'l', // light 'm', // medium 'h', // heavy }; /** Maps from weapon animation to letter used in graphic files. */ constexpr std::array WepChar = { 'n', // unarmed 'u', // no weapon + shield 's', // sword + no shield 'd', // sword + shield 'b', // bow 'a', // axe 'm', // blunt + no shield 'h', // blunt + shield 't', // staff }; /** * @brief Contains Data (CelSprites) for a player graphic (player_graphic) */ struct PlayerAnimationData { /** * @brief Sprite lists for each of the 8 directions. */ OptionalOwnedClxSpriteSheet sprites; [[nodiscard]] ClxSpriteList spritesForDirection(Direction direction) const { return (*sprites)[static_cast(direction)]; } }; struct SpellCastInfo { SpellID spellId; SpellType spellType; /* @brief Inventory location for scrolls */ int8_t spellFrom; /* @brief Used for spell level */ int spellLevel; }; struct Player { Player() = default; Player(Player &&) noexcept = default; Player &operator=(Player &&) noexcept = default; char _pName[PlayerNameLength]; Item InvBody[NUM_INVLOC]; Item InvList[InventoryGridCells]; Item SpdList[MaxBeltItems]; Item HoldItem; int lightId; int _pNumInv; int _pStrength; int _pBaseStr; int _pMagic; int _pBaseMag; int _pDexterity; int _pBaseDex; int _pVitality; int _pBaseVit; int _pStatPts; int _pDamageMod; int _pHPBase; int _pMaxHPBase; int _pHitPoints; int _pMaxHP; int _pHPPer; int _pManaBase; int _pMaxManaBase; int _pMana; int _pMaxMana; int _pManaPer; int _pIMinDam; int _pIMaxDam; int _pIAC; int _pIBonusDam; int _pIBonusToHit; int _pIBonusAC; int _pIBonusDamMod; int _pIGetHit; int _pIEnAc; int _pIFMinDam; int _pIFMaxDam; int _pILMinDam; int _pILMaxDam; uint32_t _pExperience; PLR_MODE _pmode; int8_t walkpath[MaxPathLengthPlayer]; bool plractive; action_id destAction; int destParam1; int destParam2; int destParam3; int destParam4; int _pGold; /** * @brief Contains Information for current Animation */ AnimationInfo AnimInfo; /** * @brief Contains a optional preview ClxSprite that is displayed until the current command is handled by the game logic */ OptionalClxSprite previewCelSprite; /** * @brief Contains the progress to next game tick when previewCelSprite was set */ int8_t progressToNextGameTickWhenPreviewWasSet; /** @brief Bitmask using item_special_effect */ ItemSpecialEffect _pIFlags; /** * @brief Contains Data (Sprites) for the different Animations */ std::array::value> AnimationData; std::array PartyInfoSprites; std::array PartyInfoSpriteLocations; int8_t _pNFrames; int8_t _pWFrames; int8_t _pAFrames; int8_t _pAFNum; int8_t _pSFrames; int8_t _pSFNum; int8_t _pHFrames; int8_t _pDFrames; int8_t _pBFrames; int8_t InvGrid[InventoryGridCells]; uint8_t plrlevel; bool plrIsOnSetLevel; ActorPosition position; Direction _pdir; // Direction faced by player (direction enum) HeroClass _pClass; private: uint8_t _pLevel = 1; // Use get/setCharacterLevel to ensure this attribute stays within the accepted range public: uint8_t _pgfxnum; // Bitmask indicating what variant of the sprite the player is using. The 3 lower bits define weapon (PlayerWeaponGraphic) and the higher bits define armour (starting with PlayerArmorGraphic) int8_t _pISplLvlAdd; /** @brief Specifies whether players are in non-PvP mode. */ bool friendlyMode = true; /** @brief The next queued spell */ SpellCastInfo queuedSpell; /** @brief The spell that is currently being cast */ SpellCastInfo executedSpell; /* @brief Which spell should be executed with CURSOR_TELEPORT */ SpellID inventorySpell; /* @brief Inventory location for scrolls with CURSOR_TELEPORT */ int8_t spellFrom; SpellID _pRSpell; SpellType _pRSplType; SpellID _pSBkSpell; uint8_t _pSplLvl[64]; /** @brief Bitmask of staff spell */ uint64_t _pISpells; /** @brief Bitmask of learned spells */ uint64_t _pMemSpells; /** @brief Bitmask of abilities */ uint64_t _pAblSpells; /** @brief Bitmask of spells available via scrolls */ uint64_t _pScrlSpells; SpellFlag _pSpellFlags; SpellID _pSplHotKey[NumHotkeys]; SpellType _pSplTHotKey[NumHotkeys]; bool _pBlockFlag; bool _pInvincible; int8_t _pLightRad; /** @brief True when the player is transitioning between levels */ bool _pLvlChanging; int8_t _pArmorClass; int8_t _pMagResist; int8_t _pFireResist; int8_t _pLghtResist; bool _pInfraFlag; /** Player's direction when ending movement. Also used for casting direction of SpellID::FireWall. */ Direction tempDirection; bool _pLvlVisited[NUMLEVELS]; bool _pSLvlVisited[NUMLEVELS]; // only 10 used item_misc_id _pOilType; uint8_t pTownWarps; uint8_t pDungMsgs; uint8_t pLvlLoad; bool pManaShield; uint8_t pDungMsgs2; bool pOriginalCathedral; uint8_t pDiabloKillLevel; uint16_t wReflections; ItemSpecialEffectHf pDamAcFlags; [[nodiscard]] std::string_view name() const { return _pName; } /** * @brief Convenience function to get the base stats/bonuses for this player's class */ [[nodiscard]] const ClassAttributes &getClassAttributes() const { return GetClassAttributes(_pClass); } [[nodiscard]] const PlayerCombatData &getPlayerCombatData() const { return GetPlayerCombatDataForClass(_pClass); } [[nodiscard]] const PlayerData &getPlayerData() const { return GetPlayerDataForClass(_pClass); } /** * @brief Gets the translated name for the character's class */ [[nodiscard]] std::string_view getClassName() const { return _(getPlayerData().className); } [[nodiscard]] int getBaseToBlock() const { return getPlayerCombatData().baseToBlock; } void CalcScrolls(); bool CanUseItem(const Item &item) const; bool CanCleave() { switch (_pClass) { case HeroClass::Warrior: case HeroClass::Rogue: case HeroClass::Sorcerer: return false; case HeroClass::Monk: return isEquipped(ItemType::Staff); case HeroClass::Bard: return InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Sword && InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Sword; case HeroClass::Barbarian: return isEquipped(ItemType::Axe) || (!isEquipped(ItemType::Shield) && (isEquipped(ItemType::Mace, true) || isEquipped(ItemType::Sword, true))); default: return false; } } bool isEquipped(ItemType itemType, bool isTwoHanded = false) { switch (itemType) { case ItemType::Sword: case ItemType::Axe: case ItemType::Bow: case ItemType::Mace: case ItemType::Shield: case ItemType::Staff: return (InvBody[INVLOC_HAND_LEFT]._itype == itemType && (!isTwoHanded || InvBody[INVLOC_HAND_LEFT]._iLoc == ILOC_TWOHAND)) || (InvBody[INVLOC_HAND_RIGHT]._itype == itemType && (!isTwoHanded || InvBody[INVLOC_HAND_LEFT]._iLoc == ILOC_TWOHAND)); case ItemType::LightArmor: case ItemType::MediumArmor: case ItemType::HeavyArmor: return InvBody[INVLOC_CHEST]._itype == itemType; case ItemType::Helm: return InvBody[INVLOC_HEAD]._itype == itemType; case ItemType::Ring: return InvBody[INVLOC_RING_LEFT]._itype == itemType || InvBody[INVLOC_RING_RIGHT]._itype == itemType; case ItemType::Amulet: return InvBody[INVLOC_AMULET]._itype == itemType; default: return false; } } /** * @brief Remove an item from player inventory * @param iv invList index of item to be removed * @param calcScrolls If true, CalcScrolls() gets called after removing item */ void RemoveInvItem(int iv, bool calcScrolls = true); /** * @brief Returns the network identifier for this player */ [[nodiscard]] uint8_t getId() const; void RemoveSpdBarItem(int iv); /** * @brief Gets the most valuable item out of all the player's items that match the given predicate. * @param itemPredicate The predicate used to match the items. * @return The most valuable item out of all the player's items that match the given predicate, or 'nullptr' in case no * matching items were found. */ template const Item *GetMostValuableItem(const TPredicate &itemPredicate) const { const auto getMostValuableItem = [&itemPredicate](const Item *begin, const Item *end, const Item *mostValuableItem = nullptr) { for (const auto *item = begin; item < end; item++) { if (item->isEmpty() || !itemPredicate(*item)) { continue; } if (mostValuableItem == nullptr || item->_iIvalue > mostValuableItem->_iIvalue) { mostValuableItem = item; } } return mostValuableItem; }; const Item *mostValuableItem = getMostValuableItem(SpdList, SpdList + MaxBeltItems); mostValuableItem = getMostValuableItem(InvBody, InvBody + inv_body_loc::NUM_INVLOC, mostValuableItem); mostValuableItem = getMostValuableItem(InvList, InvList + _pNumInv, mostValuableItem); return mostValuableItem; } /** * @brief Gets the base value of the player's specified attribute. * @param attribute The attribute to retrieve the base value for * @return The base value for the requested attribute. */ int GetBaseAttributeValue(CharacterAttribute attribute) const; /** * @brief Gets the current value of the player's specified attribute. * @param attribute The attribute to retrieve the current value for * @return The current value for the requested attribute. */ int GetCurrentAttributeValue(CharacterAttribute attribute) const; /** * @brief Gets the maximum value of the player's specified attribute. * @param attribute The attribute to retrieve the maximum value for * @return The maximum value for the requested attribute. */ int GetMaximumAttributeValue(CharacterAttribute attribute) const; /** * @brief Get the tile coordinates a player is moving to (if not moving, then it corresponds to current position). */ Point GetTargetPosition() const; /** * @brief Returns the index of the given position in `walkpath`, or -1 if not found. */ int GetPositionPathIndex(Point position); /** * @brief Says a speech line. * @todo BUGFIX Prevent more than one speech to be played at a time (reject new requests). */ void Say(HeroSpeech speechId) const; /** * @brief Says a speech line after a given delay. * @param speechId The speech ID to say. * @param delay Multiple of 50ms wait before starting the speech */ void Say(HeroSpeech speechId, int delay) const; /** * @brief Says a speech line, without random variants. */ void SaySpecific(HeroSpeech speechId) const; /** * @brief Attempts to stop the player from performing any queued up action. If the player is currently walking, his walking will * stop as soon as he reaches the next tile. If any action was queued with the previous command (like targeting a monster, * opening a chest, picking an item up, etc) this action will also be cancelled. */ void Stop(); /** * @brief Is the player currently walking? */ bool isWalking() const; /** * @brief Returns item location taking into consideration barbarian's ability to hold two-handed maces and clubs in one hand. */ item_equip_type GetItemLocation(const Item &item) const { if (_pClass == HeroClass::Barbarian && item._iLoc == ILOC_TWOHAND && IsAnyOf(item._itype, ItemType::Sword, ItemType::Mace)) return ILOC_ONEHAND; return item._iLoc; } /** * @brief Return player's armor value */ int GetArmor() const { return _pIBonusAC + _pIAC + _pDexterity / 5; } /** * @brief Return player's melee to hit value */ int GetMeleeToHit() const { return getCharacterLevel() + _pDexterity / 2 + _pIBonusToHit + getPlayerCombatData().baseMeleeToHit; } /** * @brief Return player's melee to hit value, including armor piercing */ int GetMeleePiercingToHit() const { int hper = GetMeleeToHit(); // in hellfire armor piercing ignores % of enemy armor instead, no way to include it here if (!gbIsHellfire) hper += _pIEnAc; return hper; } /** * @brief Return player's ranged to hit value */ int GetRangedToHit() const { return getCharacterLevel() + _pDexterity + _pIBonusToHit + getPlayerCombatData().baseRangedToHit; } int GetRangedPiercingToHit() const { int hper = GetRangedToHit(); // in hellfire armor piercing ignores % of enemy armor instead, no way to include it here if (!gbIsHellfire) hper += _pIEnAc; return hper; } /** * @brief Return magic hit chance */ int GetMagicToHit() const { return _pMagic + getPlayerCombatData().baseMagicToHit; } /** * @brief Return block chance * @param useLevel - indicate if player's level should be added to block chance (the only case where it isn't is blocking a trap) */ int GetBlockChance(bool useLevel = true) const { int blkper = _pDexterity + getBaseToBlock(); if (useLevel) blkper += getCharacterLevel() * 2; return blkper; } /** * @brief Return reciprocal of the factor for calculating damage reduction due to Mana Shield. * * Valid only for players with Mana Shield spell level greater than zero. */ int GetManaShieldDamageReduction(); /** * @brief Gets the effective spell level for the player, considering item bonuses * @param spell SpellID enum member identifying the spell * @return effective spell level */ int GetSpellLevel(SpellID spell) const { if (spell == SpellID::Invalid || static_cast(spell) >= sizeof(_pSplLvl)) { return 0; } return std::max(_pISplLvlAdd + _pSplLvl[static_cast(spell)], 0); } /** * @brief Return monster armor value after including player's armor piercing % (hellfire only) * @param monsterArmor - monster armor before applying % armor pierce * @param isMelee - indicates if it's melee or ranged combat */ int CalculateArmorPierce(int monsterArmor, bool isMelee) const { int tmac = monsterArmor; if (_pIEnAc > 0) { if (gbIsHellfire) { int pIEnAc = _pIEnAc - 1; if (pIEnAc > 0) tmac >>= pIEnAc; else tmac -= tmac / 4; } if (isMelee && _pClass == HeroClass::Barbarian) { tmac -= monsterArmor / 8; } } if (tmac < 0) tmac = 0; return tmac; } /** * @brief Calculates the players current Hit Points as a percentage of their max HP and stores it for later reference * * The stored value is unused... * @see _pHPPer * @return The players current hit points as a percentage of their maximum (from 0 to 80%) */ int UpdateHitPointPercentage() { if (_pMaxHP <= 0) { // divide by zero guard _pHPPer = 0; } else { // Maximum achievable HP is approximately 1200. Diablo uses fixed point integers where the last 6 bits are // fractional values. This means that we will never overflow HP values normally by doing this multiplication // as the max value is representable in 17 bits and the multiplication result will be at most 23 bits _pHPPer = std::clamp(_pHitPoints * 81 / _pMaxHP, 0, 81); // hp should never be greater than maxHP but just in case } return _pHPPer; } int UpdateManaPercentage() { if (_pMaxMana <= 0) { _pManaPer = 0; } else { _pManaPer = std::clamp(_pMana * 81 / _pMaxMana, 0, 81); } return _pManaPer; } /** * @brief Restores between 1/8 (inclusive) and 1/4 (exclusive) of the players max HP (further adjusted by class). * * This determines a random amount of non-fractional life points to restore then scales the value based on the * player class. Warriors/barbarians get between 1/4 and 1/2 life restored per potion, rogue/monk/bard get 3/16 * to 3/8, and sorcerers get the base amount. */ void RestorePartialLife(); /** * @brief Resets hp to maxHp */ void RestoreFullLife() { _pHitPoints = _pMaxHP; _pHPBase = _pMaxHPBase; } /** * @brief Restores between 1/8 (inclusive) and 1/4 (exclusive) of the players max Mana (further adjusted by class). * * This determines a random amount of non-fractional mana points to restore then scales the value based on the * player class. Sorcerers get between 1/4 and 1/2 mana restored per potion, rogue/monk/bard get 3/16 to 3/8, * and warrior/barbarian get the base amount. However if the player can't use magic due to an equipped item then * they get nothing. */ void RestorePartialMana(); /** * @brief Resets mana to maxMana (if the player can use magic) */ void RestoreFullMana() { if (HasNoneOf(_pIFlags, ItemSpecialEffect::NoMana)) { _pMana = _pMaxMana; _pManaBase = _pMaxManaBase; } } /** * @brief Sets the readied spell to the spell in the specified equipment slot. Does nothing if the item does not have a valid spell. * @param bodyLocation - the body location whose item will be checked for the spell. * @param forceSpell - if true, always change active spell, if false, only when current spell slot is empty */ void ReadySpellFromEquipment(inv_body_loc bodyLocation, bool forceSpell); /** * @brief Does the player currently have a ranged weapon equipped? */ bool UsesRangedWeapon() const { return static_cast(_pgfxnum & 0xF) == PlayerWeaponGraphic::Bow; } bool CanChangeAction() { if (_pmode == PM_STAND) return true; if (_pmode == PM_ATTACK && AnimInfo.currentFrame >= _pAFNum) return true; if (_pmode == PM_RATTACK && AnimInfo.currentFrame >= _pAFNum) return true; if (_pmode == PM_SPELL && AnimInfo.currentFrame >= _pSFNum) return true; if (isWalking() && AnimInfo.isLastFrame()) return true; return false; } [[nodiscard]] player_graphic getGraphic() const; [[nodiscard]] uint16_t getSpriteWidth() const; void getAnimationFramesAndTicksPerFrame(player_graphic graphics, int8_t &numberOfFrames, int8_t &ticksPerFrame) const; [[nodiscard]] ClxSprite currentSprite() const { return previewCelSprite ? *previewCelSprite : AnimInfo.currentSprite(); } [[nodiscard]] Displacement getRenderingOffset(const ClxSprite sprite) const { Displacement offset = { -CalculateSpriteTileCenterX(sprite.width()), 0 }; if (isWalking()) offset += GetOffsetForWalking(AnimInfo, _pdir); return offset; } /** * @brief Updates previewCelSprite according to new requested command * @param cmdId What command is requested * @param point Point for the command * @param wParam1 First Parameter * @param wParam2 Second Parameter */ void UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1, uint16_t wParam2); [[nodiscard]] uint8_t getCharacterLevel() const { return _pLevel; } /** * @brief Sets the character level to the target level or nearest valid value. * @param level New character level, will be clamped to the allowed range */ void setCharacterLevel(uint8_t level); [[nodiscard]] uint8_t getMaxCharacterLevel() const; [[nodiscard]] bool isMaxCharacterLevel() const { return getCharacterLevel() >= getMaxCharacterLevel(); } private: void _addExperience(uint32_t experience, int levelDelta); public: /** * @brief Adds experience to the local player based on the current game mode * @param experience base value to add, this will be adjusted to prevent power leveling in multiplayer games */ void addExperience(uint32_t experience) { _addExperience(experience, 0); } /** * @brief Adds experience to the local player based on the difference between the monster level * and current level, then also applying the power level cap in multiplayer games. * @param experience base value to add, will be scaled up/down by the difference between player and monster level * @param monsterLevel level of the monster that has rewarded this experience */ void addExperience(uint32_t experience, int monsterLevel) { _addExperience(experience, monsterLevel - getCharacterLevel()); } [[nodiscard]] uint32_t getNextExperienceThreshold() const; /** @brief Checks if the player is on the same level as the local player (MyPlayer). */ bool isOnActiveLevel() const { if (setlevel) return isOnLevel(setlvlnum); return isOnLevel(currlevel); } /** @brief Checks if the player is on the corresponding level. */ bool isOnLevel(uint8_t level) const { return !this->plrIsOnSetLevel && this->plrlevel == level; } /** @brief Checks if the player is on the corresponding level. */ bool isOnLevel(_setlevels level) const { return this->plrIsOnSetLevel && this->plrlevel == static_cast(level); } /** @brief Checks if the player is on a arena level. */ bool isOnArenaLevel() const { return plrIsOnSetLevel && IsArenaLevel(static_cast<_setlevels>(plrlevel)); } void setLevel(uint8_t level) { this->plrlevel = level; this->plrIsOnSetLevel = false; } void setLevel(_setlevels level) { this->plrlevel = static_cast(level); this->plrIsOnSetLevel = true; } /** @brief Returns a character's life based on starting life, character level, and base vitality. */ int32_t calculateBaseLife() const; /** @brief Returns a character's mana based on starting mana, character level, and base magic. */ int32_t calculateBaseMana() const; /** * @brief Sets a tile/dPlayer to be occupied by the player * @param position tile to update * @param isMoving specifies whether the player is moving or not (true/moving results in a negative index in dPlayer) */ void occupyTile(Point position, bool isMoving) const; /** @brief Checks if the player level is owned by local client. */ bool isLevelOwnedByLocalClient() const; /** @brief Checks if the player is holding an item of the provided type, and is usable. */ bool isHoldingItem(const ItemType type) const { const Item &leftHandItem = InvBody[INVLOC_HAND_LEFT]; const Item &rightHandItem = InvBody[INVLOC_HAND_RIGHT]; return (type == leftHandItem._itype && leftHandItem._iStatFlag) || (type == rightHandItem._itype && rightHandItem._iStatFlag); } bool hasNoLife() const { return leveltype == DTYPE_TOWN ? false : _pHitPoints >> 6 <= 0; } bool hasNoMana() const { return _pMana >> 6 <= 0; } }; extern DVL_API_FOR_TEST uint8_t MyPlayerId; extern DVL_API_FOR_TEST Player *MyPlayer; extern DVL_API_FOR_TEST std::vector Players; /** @brief What Player items and stats should be displayed? Normally this is identical to MyPlayer but can differ when /inspect was used. */ extern Player *InspectPlayer; /** @brief Do we currently inspect a remote player (/inspect was used)? In this case the (remote) players items and stats can't be modified. */ inline bool IsInspectingPlayer() { return MyPlayer != InspectPlayer; } extern bool MyPlayerIsDead; Player *PlayerAtPosition(Point position, bool ignoreMovingPlayers = false); /** * @brief Get the players current portrait sprite which is used for the party panel. * @param player */ ClxSprite GetPlayerPortraitSprite(Player &player); bool IsPlayerUnarmed(Player &player); void LoadPlrGFX(Player &player, player_graphic graphic); void InitPlayerGFX(Player &player); void ResetPlayerGFX(Player &player); /** * @brief Sets the new Player Animation with all relevant information for rendering * @param player The player to set the animation for * @param graphic What player animation should be displayed * @param dir Direction of the animation * @param numberOfFrames Number of Frames in Animation * @param delayLen Delay after each Animation sequence * @param flags Specifies what special logics are applied to this Animation * @param numSkippedFrames Number of Frames that will be skipped (for example with modifier "faster attack") * @param distributeFramesBeforeFrame Distribute the numSkippedFrames only before this frame */ void NewPlrAnim(Player &player, player_graphic graphic, Direction dir, AnimationDistributionFlags flags = AnimationDistributionFlags::None, int8_t numSkippedFrames = 0, int8_t distributeFramesBeforeFrame = 0); void SetPlrAnims(Player &player); void CreatePlayer(Player &player, HeroClass c); int CalcStatDiff(Player &player); #ifdef _DEBUG void NextPlrLevel(Player &player); #endif void AddPlrMonstExper(int lvl, unsigned int exp, char pmask); void ApplyPlrDamage(DamageType damageType, Player &player, int dam, int minHP = 0, int frac = 0, DeathReason deathReason = DeathReason::MonsterOrTrap); void InitPlayer(Player &player, bool FirstTime); void InitMultiView(); void PlrClrTrans(Point position); void PlrDoTrans(Point position); void SetPlayerOld(Player &player); void FixPlayerLocation(Player &player, Direction bDir); void StartStand(Player &player, Direction dir); void StartPlrBlock(Player &player, Direction dir); void FixPlrWalkTags(const Player &player); void StartPlrHit(Player &player, int dam, bool forcehit); void StartPlayerKill(Player &player, DeathReason deathReason); /** * @brief Strip the top off gold piles that are larger than MaxGold */ void StripTopGold(Player &player); void SyncPlrKill(Player &player, DeathReason deathReason); void RemovePlrMissiles(const Player &player); void StartNewLvl(Player &player, interface_mode fom, int lvl); void RestartTownLvl(Player &player); void StartWarpLvl(Player &player, size_t pidx); void ProcessPlayers(); void ClrPlrPath(Player &player); bool PosOkPlayer(const Player &player, Point position); void MakePlrPath(Player &player, Point targetPosition, bool endspace); void CheckPlrSpell(bool isShiftHeld, SpellID spellID = MyPlayer->_pRSpell, SpellType spellType = MyPlayer->_pRSplType); void SyncPlrAnim(Player &player); void SyncInitPlrPos(Player &player); void SyncInitPlr(Player &player); void CheckStats(Player &player); void ModifyPlrStr(Player &player, int l); void ModifyPlrMag(Player &player, int l); void ModifyPlrDex(Player &player, int l); void ModifyPlrVit(Player &player, int l); void SetPlayerHitPoints(Player &player, int val); void SetPlrStr(Player &player, int v); void SetPlrMag(Player &player, int v); void SetPlrDex(Player &player, int v); void SetPlrVit(Player &player, int v); void InitDungMsgs(Player &player); void PlayDungMsgs(); } // namespace devilution ================================================ FILE: Source/plrmsg.cpp ================================================ /** * @file plrmsg.cpp * * Implementation of functionality for printing the ingame chat messages. */ #include "plrmsg.h" #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "control/control.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "inv.h" #include "qol/chatlog.h" #include "qol/stash.h" #include "utils/algorithm/container.hpp" #include "utils/language.h" #include "utils/utf8.hpp" namespace devilution { namespace { struct PlayerMessage { /** Time message was received */ uint32_t time; /** The default text color */ UiFlags style; /** The text message to display on screen */ std::string text; /** Length of first portion of text that should be rendered in gold */ size_t prefixLength; /** The line height of the text */ int lineHeight; }; std::array Messages; int CountLinesOfText(std::string_view text) { return static_cast(1 + c_count(text, '\n')); } PlayerMessage &GetNextMessage() { std::move_backward(Messages.begin(), Messages.end() - 1, Messages.end()); // Push back older messages return Messages.front(); } } // namespace void DelayPlrMessages(uint32_t delayTime) { for (PlayerMessage &message : Messages) message.time += delayTime; } void EventPlrMsg(std::string_view text, UiFlags style) { PlayerMessage &message = GetNextMessage(); message.style = style; message.time = SDL_GetTicks(); message.text = std::string(text); message.prefixLength = 0; message.lineHeight = GetLineHeight(message.text, GameFont12) + 3; AddMessageToChatLog(text); } void SendPlrMsg(Player &player, std::string_view text) { PlayerMessage &message = GetNextMessage(); const std::string from = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player._pName, player.getCharacterLevel()); message.style = UiFlags::ColorWhite; message.time = SDL_GetTicks(); message.text = from + std::string(text); message.prefixLength = from.size(); message.lineHeight = GetLineHeight(message.text, GameFont12) + 3; AddMessageToChatLog(text, &player); } void InitPlrMsg() { Messages = {}; } void DrawPlrMsg(const Surface &out) { if (ChatLogFlag) return; int x = 10; int y = GetMainPanel().position.y - 13; int width = gnScreenWidth - 20; if (!ChatFlag && IsLeftPanelOpen()) { x += GetLeftPanel().position.x + GetLeftPanel().size.width; width -= GetLeftPanel().size.width; } if (!ChatFlag && IsRightPanelOpen()) width -= gnScreenWidth - GetRightPanel().position.x; if (width < 300) return; width = std::min(540, width); for (const PlayerMessage &message : Messages) { if (message.text.empty()) break; if (!ChatFlag && SDL_GetTicks() - message.time >= 10000) break; std::string text = WordWrapString(message.text, width); const int chatlines = CountLinesOfText(text); y -= message.lineHeight * chatlines; DrawHalfTransparentRectTo(out, x - 3, y, width + 6, message.lineHeight * chatlines); std::array args { DrawStringFormatArg { std::string_view(text.data(), message.prefixLength), UiFlags::ColorWhitegold }, DrawStringFormatArg { std::string_view(text.data() + message.prefixLength, text.size() - message.prefixLength), message.style } }; DrawStringWithColors(out, "{:s}{:s}", args.data(), args.size(), { { x, y }, { width, 0 } }, { .flags = UiFlags::None, .lineHeight = message.lineHeight }); } } } // namespace devilution ================================================ FILE: Source/plrmsg.h ================================================ /** * @file plrmsg.h * * Interface of functionality for printing the ingame chat messages. */ #pragma once #include #include #include #include "DiabloUI/ui_flags.hpp" #include "engine/surface.hpp" #include "player.h" namespace devilution { void DelayPlrMessages(uint32_t delayTime); void EventPlrMsg(std::string_view text, UiFlags style = UiFlags::ColorWhitegold); void SendPlrMsg(Player &player, std::string_view text); void InitPlrMsg(); void DrawPlrMsg(const Surface &out); } // namespace devilution ================================================ FILE: Source/portal.cpp ================================================ /** * @file portal.cpp * * Implementation of functionality for handling town portals. */ #include "portal.h" #include "lighting.h" #include "missiles.h" #include "multi.h" #include "player.h" #include "tables/misdat.h" namespace devilution { /** In-game state of portals. */ Portal Portals[MAXPORTAL]; namespace { /** Current portal number (a portal array index). */ size_t portalindex; /** Coordinate of each player's portal in town. */ Point PortalTownPosition[MAXPORTAL] = { { 57, 40 }, { 59, 40 }, { 61, 40 }, { 63, 40 }, }; } // namespace void InitPortals() { for (auto &portal : Portals) { portal.open = false; } } void SetPortalStats(int i, bool o, Point position, int lvl, dungeon_type lvltype, bool isSetLevel) { Portals[i].open = o; Portals[i].position = position; Portals[i].level = lvl; Portals[i].ltype = lvltype; Portals[i].setlvl = isSetLevel; } void AddPortalMissile(const Player &player, Point position, bool sync) { auto *missile = AddMissile({ 0, 0 }, position, Direction::South, MissileID::TownPortal, TARGET_MONSTERS, player, 0, 0, /*parent=*/nullptr, SfxID::None); if (missile != nullptr) { // Don't show portal opening animation if we sync existing portals if (sync) missile->setFrameGroup(PortalFrame::Idle); if (leveltype != DTYPE_TOWN) missile->_mlid = AddLight(missile->position.tile, 15); } } void SyncPortals() { for (int i = 0; i < MAXPORTAL; i++) { if (!Portals[i].open) continue; const Player &player = Players[i]; if (leveltype == DTYPE_TOWN) AddPortalMissile(player, PortalTownPosition[i], true); else { int lvl = currlevel; if (setlevel) lvl = setlvlnum; if (Portals[i].level == lvl && Portals[i].setlvl == setlevel) AddPortalMissile(player, Portals[i].position, true); } } } void AddPortalInTown(const Player &player) { AddPortalMissile(player, PortalTownPosition[player.getId()], false); } void ActivatePortal(const Player &player, Point position, int lvl, dungeon_type dungeonType, bool isSetLevel) { Portal &portal = Portals[player.getId()]; portal.open = true; if (lvl != 0) { portal.position = position; portal.level = lvl; portal.ltype = dungeonType; portal.setlvl = isSetLevel; } } void DeactivatePortal(const Player &player) { Portals[player.getId()].open = false; } bool PortalOnLevel(const Player &player) { const Portal &portal = Portals[player.getId()]; if (portal.setlvl == setlevel && portal.level == (setlevel ? static_cast(setlvlnum) : currlevel)) return true; return leveltype == DTYPE_TOWN; } void RemovePortalMissile(const Player &player) { const size_t id = player.getId(); Missiles.remove_if([id](Missile &missile) { if (missile._mitype == MissileID::TownPortal && missile._misource == static_cast(id)) { dFlags[missile.position.tile.x][missile.position.tile.y] &= ~DungeonFlag::Missile; if (Portals[id].level != 0) AddUnLight(missile._mlid); return true; } return false; }); } void SetCurrentPortal(size_t p) { portalindex = p; } void GetPortalLevel() { if (leveltype != DTYPE_TOWN) { setlevel = false; currlevel = 0; MyPlayer->setLevel(0); leveltype = DTYPE_TOWN; return; } if (Portals[portalindex].setlvl) { setlevel = true; setlvlnum = (_setlevels)Portals[portalindex].level; currlevel = Portals[portalindex].level; MyPlayer->setLevel(setlvlnum); setlvltype = leveltype = Portals[portalindex].ltype; } else { setlevel = false; currlevel = Portals[portalindex].level; MyPlayer->setLevel(currlevel); leveltype = Portals[portalindex].ltype; } if (portalindex == MyPlayerId) { NetSendCmd(true, CMD_DEACTIVATEPORTAL); DeactivatePortal(*MyPlayer); } } void GetPortalLvlPos() { if (leveltype == DTYPE_TOWN) { ViewPosition = PortalTownPosition[portalindex] + Displacement { 1, 1 }; } else { ViewPosition = Portals[portalindex].position; if (portalindex != MyPlayerId) { ViewPosition.x++; ViewPosition.y++; } } } bool PosOkPortal(int lvl, Point position) { for (auto &portal : Portals) { if (portal.open && portal.level == lvl && ((portal.position == position) || (portal.position == position - Displacement { 1, 1 }))) return true; } return false; } } // namespace devilution ================================================ FILE: Source/portal.h ================================================ /** * @file portal.h * * Interface of functionality for handling town portals. */ #pragma once #include "engine/point.hpp" #include "levels/gendung.h" namespace devilution { // Defined in player.h, forward declared here to allow for functions which operate in the context of a player. struct Player; #define MAXPORTAL 4 struct Portal { bool open; Point position; int level; dungeon_type ltype; bool setlvl; }; extern Portal Portals[MAXPORTAL]; void InitPortals(); void SetPortalStats(int i, bool o, Point position, int lvl, dungeon_type lvltype, bool isSetLevel); void AddPortalMissile(const Player &player, Point position, bool sync); void SyncPortals(); void AddPortalInTown(const Player &player); void ActivatePortal(const Player &player, Point position, int lvl, dungeon_type lvltype, bool sp); void DeactivatePortal(const Player &player); bool PortalOnLevel(const Player &player); void RemovePortalMissile(const Player &player); void SetCurrentPortal(size_t p); void GetPortalLevel(); void GetPortalLvlPos(); bool PosOkPortal(int lvl, Point position); } // namespace devilution ================================================ FILE: Source/portals/validation.cpp ================================================ /** * @file portals/validation.cpp * * Implementation of functions for validation of portal data. */ #include "portals/validation.hpp" #include #include "engine/world_tile.hpp" #include "levels/gendung.h" #include "levels/setmaps.h" #include "quests.h" namespace devilution { namespace { dungeon_type GetQuestLevelType(_setlevels questLevel) { for (const Quest &quest : Quests) { if (quest._qslvl == questLevel) return quest._qlvltype; } return DTYPE_NONE; } dungeon_type GetSetLevelType(_setlevels setLevel) { const bool isArenaLevel = setLevel >= SL_FIRST_ARENA && setLevel <= SL_LAST; return isArenaLevel ? GetArenaLevelType(setLevel) : GetQuestLevelType(setLevel); } } // namespace bool IsPortalDeltaValid(WorldTilePosition location, uint8_t level, uint8_t ltype, bool isOnSetLevel) { if (!InDungeonBounds(location)) return false; const dungeon_type levelType = static_cast(ltype); if (levelType == DTYPE_NONE) return false; if (isOnSetLevel) return levelType == GetSetLevelType(static_cast<_setlevels>(level)); return levelType == GetLevelType(level); } } // namespace devilution ================================================ FILE: Source/portals/validation.hpp ================================================ /** * @file portals/validation.hpp * * Interface of functions for validation of portal data. */ #pragma once #include #include "engine/world_tile.hpp" namespace devilution { bool IsPortalDeltaValid(WorldTilePosition location, uint8_t level, uint8_t levelType, bool isOnSetLevel); } // namespace devilution ================================================ FILE: Source/qol/autopickup.cpp ================================================ /** * @file autopickup.cpp * * QoL feature for automatically picking up gold */ #include "qol/autopickup.h" #include #include "inv_iterators.hpp" #include "options.h" #include "player.h" #include "utils/algorithm/container.hpp" namespace devilution { namespace { bool HasRoomForGold() { for (const int idx : MyPlayer->InvGrid) { // Secondary item cell. No need to check those as we'll go through the main item cells anyway. if (idx < 0) continue; // Empty cell. 1x1 space available. if (idx == 0) return true; // Main item cell. Potentially a gold pile so check it. auto item = MyPlayer->InvList[idx - 1]; if (item._itype == ItemType::Gold && item._ivalue < MaxGold) return true; } return false; } int NumMiscItemsInInv(int iMiscId) { return c_count_if(InventoryAndBeltPlayerItemsRange { *MyPlayer }, [iMiscId](const Item &item) { return item._iMiscId == iMiscId; }); } bool DoPickup(Item item) { if (item._itype == ItemType::Gold && *GetOptions().Gameplay.autoGoldPickup && HasRoomForGold()) return true; if (item._itype == ItemType::Misc && (CanFitItemInInventory(*MyPlayer, item) || AutoPlaceItemInBelt(*MyPlayer, item))) { switch (item._iMiscId) { case IMISC_HEAL: return *GetOptions().Gameplay.numHealPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_FULLHEAL: return *GetOptions().Gameplay.numFullHealPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_MANA: return *GetOptions().Gameplay.numManaPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_FULLMANA: return *GetOptions().Gameplay.numFullManaPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_REJUV: return *GetOptions().Gameplay.numRejuPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_FULLREJUV: return *GetOptions().Gameplay.numFullRejuPotionPickup > NumMiscItemsInInv(item._iMiscId); case IMISC_ELIXSTR: case IMISC_ELIXMAG: case IMISC_ELIXDEX: case IMISC_ELIXVIT: return *GetOptions().Gameplay.autoElixirPickup; case IMISC_OILFIRST: case IMISC_OILOF: case IMISC_OILACC: case IMISC_OILMAST: case IMISC_OILSHARP: case IMISC_OILDEATH: case IMISC_OILSKILL: case IMISC_OILBSMTH: case IMISC_OILFORT: case IMISC_OILPERM: case IMISC_OILHARD: case IMISC_OILIMP: case IMISC_OILLAST: return *GetOptions().Gameplay.autoOilPickup; default: return false; } } return false; } } // namespace void AutoPickup(const Player &player) { if (&player != MyPlayer) return; if (leveltype == DTYPE_TOWN && !*GetOptions().Gameplay.autoPickupInTown) return; for (auto pathDir : PathDirs) { const Point tile = player.position.tile + pathDir; if (dItem[tile.x][tile.y] != 0) { const int itemIndex = dItem[tile.x][tile.y] - 1; auto &item = Items[itemIndex]; if (DoPickup(item)) { NetSendCmdGItem(true, CMD_REQUESTAGITEM, player, itemIndex); item._iRequest = true; } } } } } // namespace devilution ================================================ FILE: Source/qol/autopickup.h ================================================ /** * @file autopickup.h * * QoL feature for automatically picking up gold */ #pragma once #include "player.h" namespace devilution { void AutoPickup(const Player &player); } // namespace devilution ================================================ FILE: Source/qol/chatlog.cpp ================================================ /** * @file chatlog.cpp * * Implementation of the in-game chat log. */ #include #include #include #include #include #include #include "DiabloUI/ui_flags.hpp" #include "automap.h" #include "chatlog.h" #include "control/control.hpp" #include "diablo_msg.hpp" #include "doom.h" #include "engine/render/text_render.hpp" #include "gamemenu.h" #include "help.h" #include "inv.h" #include "minitext.h" #include "stores.h" #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { namespace { struct ColoredText { std::string text; UiFlags color; }; struct MultiColoredText { std::string text; std::vector colors; }; bool UnreadFlag = false; size_t SkipLines; unsigned int MessageCounter = 0; std::vector ChatLogLines; constexpr int PaddingTop = 32; constexpr int PaddingLeft = 32; constexpr int PanelHeight = 297; constexpr int ContentTextWidth = 577; int LineHeight() { return IsSmallFontTall() ? 18 : 14; } int BlankLineHeight() { return 12; } int DividerLineMarginY() { return BlankLineHeight() / 2; } int HeaderHeight() { return PaddingTop + LineHeight() + 2 * BlankLineHeight() + DividerLineMarginY(); } int ContentPaddingY() { return BlankLineHeight(); } int ContentsTextHeight() { return PanelHeight - HeaderHeight() - DividerLineMarginY() - 2 * ContentPaddingY() - BlankLineHeight(); } int NumVisibleLines() { return (ContentsTextHeight() - 1) / LineHeight() + 1; // Ceil } } // namespace bool ChatLogFlag = false; void ToggleChatLog() { if (ChatLogFlag) { ChatLogFlag = false; } else { ActiveStore = TalkID::None; CloseInventory(); CloseCharPanel(); SpellbookFlag = false; SpellSelectFlag = false; if (qtextflag && leveltype == DTYPE_TOWN) { qtextflag = false; stream_stop(); } QuestLogIsOpen = false; HelpFlag = false; CancelCurrentDiabloMsg(); gamemenu_off(); SkipLines = 0; ChatLogFlag = true; doom_close(); } } void AddMessageToChatLog(std::string_view message, Player *player, UiFlags flags) { MessageCounter++; const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); std::string timestamp = localtimeResult != nullptr ? StrCat("[#", MessageCounter, "] ", LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')) : StrCat("[#", MessageCounter, "] "); const size_t oldSize = ChatLogLines.size(); if (player == nullptr) { ChatLogLines.emplace_back(MultiColoredText { "{0} {1}", { { timestamp, UiFlags::ColorRed }, { std::string(message), flags } } }); } else { std::string playerInfo = fmt::format(fmt::runtime(_("{:s} (lvl {:d}): ")), player->_pName, player->getCharacterLevel()); UiFlags nameColor = player == MyPlayer ? UiFlags::ColorWhitegold : UiFlags::ColorBlue; const std::string prefix = timestamp + " - " + playerInfo; const std::string text = WordWrapString(prefix + std::string(message), ContentTextWidth); std::vector lines; std::stringstream ss(text); for (std::string s; getline(ss, s, '\n');) { lines.push_back(s); } for (int i = static_cast(lines.size()) - 1; i >= 1; i--) { ChatLogLines.emplace_back(MultiColoredText { lines[i], {} }); } lines[0].erase(0, prefix.length()); ChatLogLines.emplace_back(MultiColoredText { "{0} - {1}{2}", { { timestamp, UiFlags::ColorRed }, { playerInfo, nameColor }, { lines[0], UiFlags::ColorWhite } } }); } const size_t diff = ChatLogLines.size() - oldSize; // only autoscroll when on top of the log if (SkipLines != 0) { SkipLines += diff; UnreadFlag = true; } } void DrawChatLog(const Surface &out) { DrawSTextHelp(); DrawQTextBack(out); if (SkipLines == 0) { UnreadFlag = false; } const Point uiPosition = GetUIRectangle().position; const int lineHeight = LineHeight(); const int blankLineHeight = BlankLineHeight(); const int sx = uiPosition.x + PaddingLeft; const int sy = uiPosition.y; DrawString(out, fmt::format(fmt::runtime(_("Chat History (Messages: {:d})")), MessageCounter), { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = (UnreadFlag ? UiFlags::ColorRed : UiFlags::ColorWhitegold) | UiFlags::AlignCenter }); const time_t timeResult = time(nullptr); const std::tm *localtimeResult = localtime(&timeResult); if (localtimeResult != nullptr) { const std::string timestamp = StrCat( LeftPad(localtimeResult->tm_hour, 2, '0'), ":", LeftPad(localtimeResult->tm_min, 2, '0'), ":", LeftPad(localtimeResult->tm_sec, 2, '0')); DrawString(out, timestamp, { { sx, sy + PaddingTop + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold }); } const int titleBottom = sy + HeaderHeight(); DrawSLine(out, titleBottom); const int numLines = NumVisibleLines(); const int contentY = titleBottom + DividerLineMarginY() + ContentPaddingY(); for (int i = 0; i < numLines; i++) { if (i + SkipLines >= ChatLogLines.size()) break; const MultiColoredText &text = ChatLogLines[ChatLogLines.size() - (i + SkipLines + 1)]; const std::string_view line = text.text; std::vector args; for (auto &x : text.colors) { args.emplace_back(DrawStringFormatArg { x.text, x.color }); } DrawStringWithColors(out, line, args, { { sx, contentY + i * lineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhite, .lineHeight = lineHeight }); } DrawString(out, _("Press ESC to end or the arrow keys to scroll."), { { sx, contentY + ContentsTextHeight() + ContentPaddingY() + blankLineHeight }, { ContentTextWidth, lineHeight } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter }); } void ChatLogScrollUp() { if (SkipLines > 0) SkipLines--; } void ChatLogScrollDown() { if (SkipLines + NumVisibleLines() < ChatLogLines.size()) SkipLines++; } void ChatLogScrollTop() { SkipLines = 0; } void ChatLogScrollBottom() { SkipLines = ChatLogLines.size() - NumVisibleLines(); } } // namespace devilution ================================================ FILE: Source/qol/chatlog.h ================================================ /** * @file chatlog.h * * Adds Chat log QoL feature */ #pragma once #include "DiabloUI/ui_flags.hpp" #include "engine/surface.hpp" #include "player.h" namespace devilution { extern bool ChatLogFlag; void ToggleChatLog(); void AddMessageToChatLog(std::string_view message, Player *player = nullptr, UiFlags flags = UiFlags::ColorWhite); void DrawChatLog(const Surface &out); void ChatLogScrollUp(); void ChatLogScrollDown(); void ChatLogScrollTop(); void ChatLogScrollBottom(); } // namespace devilution ================================================ FILE: Source/qol/floatingnumbers.cpp ================================================ #include "floatingnumbers.h" #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "engine/render/text_render.hpp" #include "options.h" #include "utils/str_cat.hpp" namespace devilution { namespace { struct FloatingNumber { Point startPos; Displacement startOffset; Displacement endOffset; std::string text; uint32_t time; uint32_t lastMerge; UiFlags style; int id; bool reverseDirection; }; std::deque FloatingQueue; void ClearExpiredNumbers() { while (!FloatingQueue.empty()) { const FloatingNumber &num = FloatingQueue.front(); if (num.time > SDL_GetTicks()) break; FloatingQueue.pop_front(); } } GameFontTables GetGameFontSize(UiFlags flags) { if (HasAnyOf(flags, UiFlags::FontSize30)) return GameFont30; if (HasAnyOf(flags, UiFlags::FontSize24)) return GameFont24; return GameFont12; } } // namespace void AddFloatingNumber(Point pos, Displacement offset, std::string text, UiFlags style, int id, bool reverseDirection) { Displacement endOffset; if (!reverseDirection) endOffset = { 0, -140 }; else endOffset = { 0, 140 }; for (auto &num : FloatingQueue) { if (id != 0 && num.id == id && (SDL_GetTicks() - static_cast(num.lastMerge)) <= 100) { num.text = text; num.lastMerge = SDL_GetTicks(); num.style = style; num.startPos = pos; return; } } FloatingNumber num { pos, offset, endOffset, text, static_cast(SDL_GetTicks() + 2500), static_cast(SDL_GetTicks()), style | UiFlags::Outlined, id, reverseDirection }; FloatingQueue.push_back(num); } void DrawFloatingNumbers(const Surface &out, Point viewPosition, Displacement offset) { for (auto &floatingNum : FloatingQueue) { Displacement worldOffset = viewPosition - floatingNum.startPos; worldOffset = worldOffset.worldToScreen() + offset + Displacement { TILE_WIDTH / 2, -TILE_HEIGHT / 2 } + floatingNum.startOffset; if (*GetOptions().Graphics.zoom) { worldOffset *= 2; } Point screenPosition { worldOffset.deltaX, worldOffset.deltaY }; const int lineWidth = GetLineWidth(floatingNum.text, GetGameFontSize(floatingNum.style)); screenPosition.x -= lineWidth / 2; const uint32_t timeLeft = floatingNum.time - SDL_GetTicks(); const float mul = 1 - (timeLeft / 2500.0f); screenPosition += floatingNum.endOffset * mul; DrawString(out, floatingNum.text, Rectangle { screenPosition, { lineWidth, 0 } }, { .flags = floatingNum.style }); } ClearExpiredNumbers(); } void ClearFloatingNumbers() { srand(static_cast(time(nullptr))); FloatingQueue.clear(); } } // namespace devilution ================================================ FILE: Source/qol/floatingnumbers.h ================================================ /** * @file floatingnumbers.h * * Adds floating numbers QoL feature */ #pragma once #include #include "DiabloUI/ui_flags.hpp" #include "engine/displacement.hpp" #include "engine/point.hpp" #include "engine/surface.hpp" namespace devilution { void AddFloatingNumber(Point pos, Displacement offset, std::string text, UiFlags style, int id = 0, bool reverseDirection = false); void DrawFloatingNumbers(const Surface &out, Point viewPosition, Displacement offset); void ClearFloatingNumbers(); } // namespace devilution ================================================ FILE: Source/qol/itemlabels.cpp ================================================ #include "itemlabels.h" #include #include #include #include #include #include #include #include "control/control.hpp" #include "cursor.h" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "gmenu.h" #include "inv.h" #include "options.h" #include "qol/stash.h" #include "stores.h" #include "utils/algorithm/container.hpp" #include "utils/format_int.hpp" #include "utils/language.h" namespace devilution { namespace { struct ItemLabel { int id, width; Point pos; StringOrView text; }; std::vector labelQueue; bool highlightKeyPressed = false; bool isLabelHighlighted = false; std::array, ITEMTYPES> labelCenterOffsets; const int BorderX = 4; // minimal horizontal space between labels const int BorderY = 2; // minimal vertical space between labels const int MarginX = 2; // horizontal margins between text and edges of the label // Vertical space between the text and the edges of the label. int TextMarginTop() { return IsSmallFontTall() ? 1 : -1; } int TextMarginBottom() { return IsSmallFontTall() ? 1 : 3; } // The total height of the label box. int LabelHeight() { return (IsSmallFontTall() ? 16 : 11) + TextMarginBottom() + TextMarginTop(); } /** * @brief The set of used X coordinates for a certain Y coordinate. */ class UsedX { public: [[nodiscard]] bool contains(int val) const { return c_find(data_, val) != data_.end(); } void insert(int val) { if (!contains(val)) data_.push_back(val); } void clear() { data_.clear(); } private: std::vector data_; }; } // namespace void ToggleItemLabelHighlight() { GetOptions().Gameplay.showItemLabels.SetValue(!*GetOptions().Gameplay.showItemLabels); } void HighlightKeyPressed(bool pressed) { highlightKeyPressed = pressed; } bool IsItemLabelHighlighted() { return isLabelHighlighted; } void ResetItemlabelHighlighted() { isLabelHighlighted = false; } bool IsHighlightingLabelsEnabled() { return !IsPlayerInStore() && highlightKeyPressed != *GetOptions().Gameplay.showItemLabels; } void AddItemToLabelQueue(int id, Point position) { if (!IsHighlightingLabelsEnabled()) return; Item &item = Items[id]; StringOrView textOnGround; if (item._itype == ItemType::Gold) { textOnGround = fmt::format(fmt::runtime(_("{:s} gold")), FormatInteger(item._ivalue)); } else { textOnGround = item.getName(); } int nameWidth = GetLineWidth(textOnGround); nameWidth += MarginX * 2; const int index = ItemCAnimTbl[item._iCurs]; if (!labelCenterOffsets[index]) { const auto [xBegin, xEnd] = ClxMeasureSolidHorizontalBounds((*item.AnimInfo.sprites)[item.AnimInfo.currentFrame]); labelCenterOffsets[index].emplace((xBegin + xEnd) / 2); } position.x += *labelCenterOffsets[index]; position.y -= TILE_HEIGHT; if (*GetOptions().Graphics.zoom) { position *= 2; } position.x -= nameWidth / 2; position.y -= LabelHeight(); labelQueue.push_back(ItemLabel { id, nameWidth, position, std::move(textOnGround) }); } bool IsMouseOverGameArea() { if ((IsRightPanelOpen()) && GetRightPanel().contains(MousePosition)) return false; if ((IsLeftPanelOpen()) && GetLeftPanel().contains(MousePosition)) return false; if (GetMainPanel().contains(MousePosition)) return false; return true; } void DrawItemNameLabels(const Surface &out) { const Surface clippedOut = out.subregionY(0, gnViewportHeight); isLabelHighlighted = false; if (labelQueue.empty()) return; UsedX usedX; const int labelHeight = LabelHeight(); const int labelMarginTop = TextMarginTop(); for (unsigned i = 0; i < labelQueue.size(); ++i) { usedX.clear(); bool canShow; do { canShow = true; for (unsigned j = 0; j < i; ++j) { ItemLabel &a = labelQueue[i]; const ItemLabel &b = labelQueue[j]; if (std::abs(b.pos.y - a.pos.y) < labelHeight + BorderY) { const int widthA = a.width + BorderX + MarginX * 2; const int widthB = b.width + BorderX + MarginX * 2; int newpos = b.pos.x; if (b.pos.x >= a.pos.x && b.pos.x - a.pos.x < widthA) { newpos -= widthA; if (usedX.contains(newpos)) newpos = b.pos.x + widthB; } else if (b.pos.x < a.pos.x && a.pos.x - b.pos.x < widthB) { newpos += widthB; if (usedX.contains(newpos)) newpos = b.pos.x - widthA; } else continue; canShow = false; a.pos.x = newpos; usedX.insert(newpos); } } } while (!canShow); } for (const ItemLabel &label : labelQueue) { const Item &item = Items[label.id]; if (MousePosition.x >= label.pos.x && MousePosition.x < label.pos.x + label.width && MousePosition.y >= label.pos.y && MousePosition.y < label.pos.y + labelHeight) { if (!gmenu_is_active() && PauseMode == 0 && !MyPlayerIsDead && !IsPlayerInStore() && IsMouseOverGameArea() && LastPlayerAction == PlayerActionType::None) { isLabelHighlighted = true; cursPosition = item.position; pcursitem = label.id; } } if (pcursitem == label.id && !IsPlayerInStore()) FillRect(clippedOut, label.pos.x, label.pos.y, label.width, labelHeight, PAL8_BLUE + 6); else DrawHalfTransparentRectTo(clippedOut, label.pos.x, label.pos.y, label.width, labelHeight); DrawString(clippedOut, label.text, { { label.pos.x + MarginX, label.pos.y + labelMarginTop }, { label.width, labelHeight } }, { .flags = item.getTextColor() }); } labelQueue.clear(); } } // namespace devilution ================================================ FILE: Source/qol/itemlabels.h ================================================ /** * @file itemlabels.h * * Adds item labels QoL feature */ #pragma once #include "engine/point.hpp" #include "engine/surface.hpp" namespace devilution { void ToggleItemLabelHighlight(); void HighlightKeyPressed(bool pressed); bool IsItemLabelHighlighted(); void ResetItemlabelHighlighted(); bool IsHighlightingLabelsEnabled(); void AddItemToLabelQueue(int id, Point position); void DrawItemNameLabels(const Surface &out); } // namespace devilution ================================================ FILE: Source/qol/monhealthbar.cpp ================================================ /** * @file monhealthbar.cpp * * Adds monster health bar QoL feature */ #include "monhealthbar.h" #include #include #include "control/control.hpp" #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" #include "options.h" #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { namespace { OptionalOwnedClxSpriteList healthBox; OptionalOwnedClxSpriteList resistance; OptionalOwnedClxSpriteList health; OptionalOwnedClxSpriteList healthBlue; OptionalOwnedClxSpriteList playerExpTags; void OptionEnemyHealthBarChanged() { if (!gbRunGame) return; if (*GetOptions().Gameplay.enemyHealthBar) InitMonsterHealthBar(); else FreeMonsterHealthBar(); } const auto OptionChangeHandler = (GetOptions().Gameplay.enemyHealthBar.SetValueChangedCallback(OptionEnemyHealthBarChanged), true); } // namespace void InitMonsterHealthBar() { if (!*GetOptions().Gameplay.enemyHealthBar) return; healthBox = LoadClx("data\\healthbox.clx"); health = LoadClx("data\\health.clx"); resistance = LoadClx("data\\resistance.clx"); playerExpTags = LoadClx("data\\monstertags.clx"); std::array healthBlueTrn; healthBlueTrn[234] = 185; healthBlueTrn[235] = 186; healthBlueTrn[236] = 187; healthBlue = health->clone(); ClxApplyTrans(*healthBlue, healthBlueTrn.data()); } void FreeMonsterHealthBar() { healthBlue = std::nullopt; playerExpTags = std::nullopt; resistance = std::nullopt; health = std::nullopt; healthBox = std::nullopt; } void DrawMonsterHealthBar(const Surface &out) { if (!*GetOptions().Gameplay.enemyHealthBar) return; if (leveltype == DTYPE_TOWN) return; if (pcursmonst == -1) return; const Monster &monster = Monsters[pcursmonst]; const int width = (*healthBox)[0].width(); const int barWidth = (*health)[0].width(); const int height = (*healthBox)[0].height(); Point position = { (gnScreenWidth - width) / 2, 18 }; if (CanPanelsCoverView()) { if (IsRightPanelOpen()) position.x -= SidePanelSize.width / 2; if (IsLeftPanelOpen()) position.x += SidePanelSize.width / 2; } const int border = 3; int multiplier = 0; int currLife = monster.hitPoints; // lifestealing monsters can reach HP exceeding their max if (monster.hitPoints > monster.maxHitPoints) { multiplier = monster.hitPoints / monster.maxHitPoints; currLife = monster.hitPoints - monster.maxHitPoints * multiplier; if (currLife == 0 && multiplier > 0) { multiplier--; currLife = monster.maxHitPoints; } } RenderClxSprite(out, (*healthBox)[0], position); DrawHalfTransparentRectTo(out, position.x + border, position.y + border, width - (border * 2), height - (border * 2)); const int barProgress = (barWidth * currLife) / monster.maxHitPoints; if (barProgress != 0) { RenderClxSprite( out.subregion(position.x + border + 1, position.y + border + 1, barProgress, height - (border * 2) - 2), (*(multiplier > 0 ? healthBlue : health))[0], { 0, 0 }); } constexpr auto GetBorderColor = [](MonsterClass monsterClass) { switch (monsterClass) { case MonsterClass::Undead: return 248; case MonsterClass::Demon: return 232; case MonsterClass::Animal: return 150; default: app_fatal(StrCat("Invalid monster class: ", static_cast(monsterClass))); } }; if (*GetOptions().Gameplay.showMonsterType) { const Uint8 borderColor = GetBorderColor(monster.data().monsterClass); const int borderWidth = width - (border * 2); UnsafeDrawHorizontalLine(out, { position.x + border, position.y + border }, borderWidth, borderColor); UnsafeDrawHorizontalLine(out, { position.x + border, position.y + height - border - 1 }, borderWidth, borderColor); const int borderHeight = height - (border * 2) - 2; UnsafeDrawVerticalLine(out, { position.x + border, position.y + border + 1 }, borderHeight, borderColor); UnsafeDrawVerticalLine(out, { position.x + width - border - 1, position.y + border + 1 }, borderHeight, borderColor); } UiFlags style = UiFlags::AlignCenter | UiFlags::VerticalCenter; DrawString(out, monster.name(), { position + Displacement { -1, 1 }, { width, height } }, { .flags = style | UiFlags::ColorBlack }); if (monster.isUnique()) style |= UiFlags::ColorWhitegold; else if (monster.leader != Monster::NoLeader) style |= UiFlags::ColorBlue; else style |= UiFlags::ColorWhite; DrawString(out, monster.name(), { position, { width, height } }, { .flags = style }); if (multiplier > 0) DrawString(out, StrCat("x", multiplier), { position, { width - 2, height } }, { .flags = UiFlags::ColorWhite | UiFlags::AlignRight | UiFlags::VerticalCenter }); if (monster.isUnique() || MonsterKillCounts[monster.type().type] >= 15) { const monster_resistance immunes[] = { IMMUNE_MAGIC, IMMUNE_FIRE, IMMUNE_LIGHTNING }; const monster_resistance resists[] = { RESIST_MAGIC, RESIST_FIRE, RESIST_LIGHTNING }; int resOffset = 5; for (size_t i = 0; i < 3; i++) { if ((monster.resistance & immunes[i]) != 0) { RenderClxSprite(out, (*resistance)[i * 2 + 1], position + Displacement { resOffset, height - 6 }); resOffset += (*resistance)[0].width() + 2; } else if ((monster.resistance & resists[i]) != 0) { RenderClxSprite(out, (*resistance)[i * 2], position + Displacement { resOffset, height - 6 }); resOffset += (*resistance)[0].width() + 2; } } } if (Players.size() > 1) { int tagOffset = 5; for (size_t i = 0; i < Players.size(); i++) { if (((1U << i) & monster.whoHit) != 0) { RenderClxSprite(out, (*playerExpTags)[i + 1], position + Displacement { tagOffset, height - 31 }); } else if (Players[i].plractive) { RenderClxSprite(out, (*playerExpTags)[0], position + Displacement { tagOffset, height - 31 }); } tagOffset += (*playerExpTags)[0].width(); } } } } // namespace devilution ================================================ FILE: Source/qol/monhealthbar.h ================================================ /** * @file monhealthbar.h * * Adds monster health bar QoL feature */ #pragma once namespace devilution { struct Surface; void InitMonsterHealthBar(); void FreeMonsterHealthBar(); void DrawMonsterHealthBar(const Surface &out); } // namespace devilution ================================================ FILE: Source/qol/stash.cpp ================================================ #include "qol/stash.h" #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "DiabloUI/text_input.hpp" #include "control/control.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/rectangle.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/size.hpp" #include "headless_mode.hpp" #include "hwcursor.hpp" #include "inv.h" #include "minitext.h" #include "stores.h" #include "utils/display.h" #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" namespace devilution { bool IsStashOpen; StashStruct Stash; bool IsWithdrawGoldOpen; namespace { constexpr unsigned CountStashPages = 100; constexpr unsigned LastStashPage = CountStashPages - 1; char GoldWithdrawText[21]; TextInputCursorState GoldWithdrawCursor; std::optional GoldWithdrawInputState; constexpr Size ButtonSize { 27, 16 }; /** Contains mappings for the buttons in the stash (2 navigation buttons, withdraw gold buttons, 2 navigation buttons) */ constexpr Rectangle StashButtonRect[] = { // clang-format off { { 19, 19 }, ButtonSize }, // 10 left { { 56, 19 }, ButtonSize }, // 1 left { { 93, 19 }, ButtonSize }, // withdraw gold { { 242, 19 }, ButtonSize }, // 1 right { { 279, 19 }, ButtonSize } // 10 right // clang-format on }; OptionalOwnedClxSpriteList StashPanelArt; OptionalOwnedClxSpriteList StashNavButtonArt; /** * @param page The stash page index. * @param position Position to add the item to. * @param stashListIndex The item's StashList index * @param itemSize Size of item */ void AddItemToStashGrid(unsigned page, Point position, uint16_t stashListIndex, Size itemSize) { for (const Point point : PointsInRectangle(Rectangle { position, itemSize })) { Stash.stashGrids[page][point.x][point.y] = stashListIndex + 1; } } std::optional FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) { for (auto point : StashGridRange) { const Rectangle cell { GetStashSlotCoord(point), InventorySlotSizeInPixels + 1 }; if (cell.contains(cursorPosition)) { // When trying to paste into the stash we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. if (itemSize.height <= 1 && itemSize.width <= 1) { // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do return point; } // Otherwise work out how far the central cell is from the top-left cell Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. if (itemSize.width % 2 == 0 && cell.contains(cursorPosition + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left hotPixelCellOffset.deltaX++; } if (itemSize.height % 2 == 0 && cell.contains(cursorPosition + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above hotPixelCellOffset.deltaY++; } // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the stash would otherwise put it out of bounds) point.y = std::clamp(point.y - hotPixelCellOffset.deltaY, 0, StashGridSize.height - itemSize.height); point.x = std::clamp(point.x - hotPixelCellOffset.deltaX, 0, StashGridSize.width - itemSize.width); return point; } } return {}; } bool IsItemAllowedInStash(const Item &item) { return item._iMiscId != IMISC_ARENAPOT; } void CheckStashPaste(Point cursorPosition) { Player &player = *MyPlayer; if (!IsItemAllowedInStash(player.HoldItem)) return; if (player.HoldItem._itype == ItemType::Gold) { if (Stash.gold > std::numeric_limits::max() - player.HoldItem._ivalue) return; Stash.gold += player.HoldItem._ivalue; player.HoldItem.clear(); PlaySFX(SfxID::ItemGold); Stash.dirty = true; NewCursor(CURSOR_HAND); return; } const Size itemSize = GetInventorySize(player.HoldItem); std::optional targetSlot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); if (!targetSlot) return; const Point firstSlot = *targetSlot; // Check that no more than 1 item is replaced by the move StashStruct::StashCell stashIndex = StashStruct::EmptyCell; for (const Point point : PointsInRectangle(Rectangle { firstSlot, itemSize })) { const StashStruct::StashCell iv = Stash.GetItemIdAtPosition(point); if (iv == StashStruct::EmptyCell || stashIndex == iv) continue; if (stashIndex == StashStruct::EmptyCell) { stashIndex = iv; // Found first item continue; } return; // Found a second item } PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); // Need to set the item anchor position to the bottom left so drawing code functions correctly. player.HoldItem.position = firstSlot + Displacement { 0, itemSize.height - 1 }; if (stashIndex == StashStruct::EmptyCell) { Stash.stashList.emplace_back(player.HoldItem.pop()); // stashList will have at most 10 000 items, up to 65 535 are supported with uint16_t indexes stashIndex = static_cast(Stash.stashList.size() - 1); } else { // swap the held item and whatever was in the stash at this position std::swap(Stash.stashList[stashIndex], player.HoldItem); // then clear the space occupied by the old item for (auto &row : Stash.GetCurrentGrid()) { for (auto &itemId : row) { if (itemId - 1 == stashIndex) itemId = 0; } } } // Finally mark the area now occupied by the pasted item in the current page/grid. AddItemToStashGrid(Stash.GetPage(), firstSlot, stashIndex, itemSize); Stash.dirty = true; NewCursor(player.HoldItem); } void CheckStashCut(Point cursorPosition, bool automaticMove) { Player &player = *MyPlayer; CloseGoldWithdraw(); Point slot = InvalidStashPoint; for (auto point : StashGridRange) { const Rectangle cell { GetStashSlotCoord(point), InventorySlotSizeInPixels + 1 }; // check which inventory rectangle the mouse is in, if any if (cell.contains(cursorPosition)) { slot = point; break; } } if (slot == InvalidStashPoint) { return; } Item &holdItem = player.HoldItem; holdItem.clear(); bool automaticallyMoved = false; const bool automaticallyEquipped = false; const StashStruct::StashCell iv = Stash.GetItemIdAtPosition(slot); if (iv != StashStruct::EmptyCell) { holdItem = Stash.stashList[iv]; if (automaticMove) { if (CanBePlacedOnBelt(player, holdItem)) { automaticallyMoved = AutoPlaceItemInBelt(player, holdItem, true, true); } else { automaticallyMoved = AutoEquip(player, holdItem, true, true); } } if (!automaticMove || automaticallyMoved) { Stash.RemoveStashItem(iv); } } if (!holdItem.isEmpty()) { CalcPlrInv(player, true); holdItem._iStatFlag = player.CanUseItem(holdItem); if (automaticallyEquipped) { PlaySFX(ItemInvSnds[ItemCAnimTbl[holdItem._iCurs]]); } else if (!automaticMove || automaticallyMoved) { PlaySFX(SfxID::GrabItem); } if (automaticMove) { if (!automaticallyMoved) { if (CanBePlacedOnBelt(player, holdItem)) { player.SaySpecific(HeroSpeech::IHaveNoRoom); } else { player.SaySpecific(HeroSpeech::ICantDoThat); } } holdItem.clear(); } else { NewCursor(holdItem); } } } void WithdrawGold(Player &player, int amount) { AddGoldToInventory(player, amount); Stash.gold -= amount; Stash.dirty = true; } } // namespace Point GetStashSlotCoord(Point slot) { constexpr int StashNextCell = INV_SLOT_SIZE_PX + 1; // spacing between each cell return GetPanelPosition(UiPanels::Stash, slot * StashNextCell + Displacement { 17, 48 }); } void FreeStashGFX() { StashNavButtonArt = std::nullopt; StashPanelArt = std::nullopt; } void InitStash() { if (!HeadlessMode) { StashPanelArt = LoadClx("data\\stash.clx"); StashNavButtonArt = LoadClx("data\\stashnavbtns.clx"); } } void TransferItemToInventory(Player &player, uint16_t itemId) { if (itemId == StashStruct::EmptyCell) { return; } const Item &item = Stash.stashList[itemId]; if (item.isEmpty()) { return; } if (!AutoPlaceItemInInventory(player, item)) { player.SaySpecific(HeroSpeech::IHaveNoRoom); return; } PlaySFX(ItemInvSnds[ItemCAnimTbl[item._iCurs]]); Stash.RemoveStashItem(itemId); } int StashButtonPressed = -1; void CheckStashButtonRelease(Point mousePosition) { if (StashButtonPressed == -1) return; Rectangle stashButton = StashButtonRect[StashButtonPressed]; stashButton.position = GetPanelPosition(UiPanels::Stash, stashButton.position); if (stashButton.contains(mousePosition)) { switch (StashButtonPressed) { case 0: Stash.PreviousPage(10); break; case 1: Stash.PreviousPage(); break; case 2: StartGoldWithdraw(); break; case 3: Stash.NextPage(); break; case 4: Stash.NextPage(10); break; } } StashButtonPressed = -1; } void CheckStashButtonPress(Point mousePosition) { Rectangle stashButton; for (int i = 0; i < 5; i++) { stashButton = StashButtonRect[i]; stashButton.position = GetPanelPosition(UiPanels::Stash, stashButton.position); if (stashButton.contains(mousePosition)) { StashButtonPressed = i; return; } } StashButtonPressed = -1; } void DrawStash(const Surface &out) { RenderClxSprite(out, (*StashPanelArt)[0], GetPanelPosition(UiPanels::Stash)); if (StashButtonPressed != -1) { const Point stashButton = GetPanelPosition(UiPanels::Stash, StashButtonRect[StashButtonPressed].position); RenderClxSprite(out, (*StashNavButtonArt)[StashButtonPressed], stashButton); } constexpr Displacement offset { 0, INV_SLOT_SIZE_PX - 1 }; for (auto slot : StashGridRange) { const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot); if (itemId == StashStruct::EmptyCell) { continue; // No item in the given slot } const Item &item = Stash.stashList[itemId]; InvDrawSlotBack(out, GetStashSlotCoord(slot) + offset, InventorySlotSizeInPixels, item._iMagical); } for (auto slot : StashGridRange) { const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot); if (itemId == StashStruct::EmptyCell) { continue; // No item in the given slot } const Item &item = Stash.stashList[itemId]; if (item.position != slot) { continue; // Not the first slot of the item } const int frame = item._iCurs + CURSOR_FIRSTITEM; const Point position = GetStashSlotCoord(item.position) + offset; const ClxSprite sprite = GetInvItemSprite(frame); if (pcursstashitem == itemId) { const uint8_t color = GetOutlineColor(item, true); ClxDrawOutline(out, color, position, sprite); } DrawItem(item, out, position, sprite); } const Point position = GetPanelPosition(UiPanels::Stash); const UiFlags style = UiFlags::VerticalCenter | UiFlags::ColorWhite; const int textboxHeight = 13; DrawString(out, StrCat(Stash.GetPage() + 1), { position + Displacement { 132, 0 }, { 57, textboxHeight } }, { .flags = UiFlags::AlignCenter | style }); DrawString(out, FormatInteger(Stash.gold), { position + Displacement { 122, 19 }, { 107, textboxHeight } }, { .flags = UiFlags::AlignRight | style }); } void CheckStashItem(Point mousePosition, bool isShiftHeld, bool isCtrlHeld) { if (!MyPlayer->HoldItem.isEmpty()) { CheckStashPaste(mousePosition); } else if (isCtrlHeld) { TransferItemToInventory(*MyPlayer, pcursstashitem); } else { CheckStashCut(mousePosition, isShiftHeld); } } uint16_t CheckStashHLight(Point mousePosition) { Point slot = InvalidStashPoint; for (auto point : StashGridRange) { const Rectangle cell { GetStashSlotCoord(point), InventorySlotSizeInPixels + 1 }; if (cell.contains(mousePosition)) { slot = point; break; } } if (slot == InvalidStashPoint) return -1; InfoColor = UiFlags::ColorWhite; const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(slot); if (itemId == StashStruct::EmptyCell) { return -1; } const Item &item = Stash.stashList[itemId]; if (item.isEmpty()) { return -1; } InfoColor = item.getTextColor(); InfoString = item.getName(); FloatingInfoString = item.getName(); if (item._iIdentified) { PrintItemDetails(item); } else { PrintItemDur(item); } return itemId; } bool UseStashItem(uint16_t c) { if (MyPlayer->_pInvincible && MyPlayer->hasNoLife()) return true; if (pcurs != CURSOR_HAND) return true; if (IsPlayerInStore()) return true; Item *item = &Stash.stashList[c]; constexpr int SpeechDelay = 10; if (item->IDidx == IDI_MUSHROOM) { MyPlayer->Say(HeroSpeech::NowThatsOneBigMushroom, SpeechDelay); return true; } if (item->IDidx == IDI_FUNGALTM) { PlaySFX(SfxID::ItemBook); MyPlayer->Say(HeroSpeech::ThatDidntDoAnything, SpeechDelay); return true; } if (!item->isUsable()) return false; if (!MyPlayer->CanUseItem(*item)) { MyPlayer->Say(HeroSpeech::ICantUseThisYet); return true; } CloseGoldWithdraw(); if (item->isScroll()) { return true; } if (item->_iMiscId > IMISC_RUNEFIRST && item->_iMiscId < IMISC_RUNELAST && leveltype == DTYPE_TOWN) { return true; } if (item->_iMiscId == IMISC_BOOK) PlaySFX(SfxID::ReadBook); else PlaySFX(ItemInvSnds[ItemCAnimTbl[item->_iCurs]]); UseItem(*MyPlayer, item->_iMiscId, item->_iSpell, -1); if (Stash.stashList[c]._iMiscId == IMISC_MAPOFDOOM) return true; if (Stash.stashList[c]._iMiscId == IMISC_NOTE) { InitQTextMsg(TEXT_BOOK9); CloseInventory(); return true; } Stash.RemoveStashItem(c); return true; } void StashStruct::RemoveStashItem(StashStruct::StashCell iv) { // Iterate through stashGrid and remove every reference to item for (auto &row : Stash.GetCurrentGrid()) { for (StashStruct::StashCell &itemId : row) { if (itemId - 1 == iv) { itemId = 0; } } } if (stashList.empty()) { return; } // If the item at the end of stash array isn't the one we removed, we need to swap its position in the array with the removed item const StashStruct::StashCell lastItemIndex = static_cast(stashList.size() - 1); if (lastItemIndex != iv) { stashList[iv] = stashList[lastItemIndex]; for (auto &[_, grid] : Stash.stashGrids) { for (auto &row : grid) { for (StashStruct::StashCell &itemId : row) { if (itemId == lastItemIndex + 1) { itemId = iv + 1; } } } } } stashList.pop_back(); Stash.dirty = true; } void StashStruct::SetPage(unsigned newPage) { page = std::min(newPage, LastStashPage); dirty = true; } void StashStruct::NextPage(unsigned offset) { if (page <= LastStashPage) { page += std::min(offset, LastStashPage - page); } else { page = LastStashPage; } dirty = true; } void StashStruct::PreviousPage(unsigned offset) { if (page <= LastStashPage) { page -= std::min(offset, page); } else { page = LastStashPage; } dirty = true; } void StashStruct::RefreshItemStatFlags() { for (auto &item : Stash.stashList) { item.updateRequiredStatsCacheForPlayer(*MyPlayer); } } void StartGoldWithdraw() { CloseGoldDrop(); if (ChatFlag) ResetChat(); const Point start = GetPanelPosition(UiPanels::Stash, { 67, 128 }); SDL_Rect rect = MakeSdlRect(start.x, start.y, 180, 20); SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0); IsWithdrawGoldOpen = true; GoldWithdrawText[0] = '\0'; GoldWithdrawInputState.emplace(NumberInputState::Options { .textOptions { .value = GoldWithdrawText, .cursor = &GoldWithdrawCursor, .maxLength = sizeof(GoldWithdrawText) - 1, }, .min = 0, .max = std::min(RoomForGold(), Stash.gold), }); SDLC_StartTextInput(ghMainWnd); } void WithdrawGoldKeyPress(SDL_Keycode vkey) { Player &myPlayer = *MyPlayer; if (myPlayer.hasNoLife()) { CloseGoldWithdraw(); return; } switch (vkey) { case SDLK_RETURN: case SDLK_KP_ENTER: if (const int value = GoldWithdrawInputState->value(); value != 0) { WithdrawGold(myPlayer, value); PlaySFX(SfxID::ItemGold); } CloseGoldWithdraw(); break; case SDLK_ESCAPE: CloseGoldWithdraw(); break; default: break; } } void DrawGoldWithdraw(const Surface &out) { if (!IsWithdrawGoldOpen) { return; } const std::string_view amountText = GoldWithdrawText; const TextInputCursorState &cursor = GoldWithdrawCursor; const int dialogX = 30; ClxDraw(out, GetPanelPosition(UiPanels::Stash, { dialogX, 178 }), (*GoldBoxBuffer)[0]); // Pre-wrap the string at spaces, otherwise DrawString would hard wrap in the middle of words const std::string wrapped = WordWrapString(_("How many gold pieces do you want to withdraw?"), 200); // The split gold dialog is roughly 4 lines high, but we need at least one line for the player to input an amount. // Using a clipping region 50 units high (approx 3 lines with a lineheight of 17) to ensure there is enough room left // for the text entered by the player. DrawString(out, wrapped, { GetPanelPosition(UiPanels::Stash, { dialogX + 31, 75 }), { 200, 50 } }, { .flags = UiFlags::ColorWhitegold | UiFlags::AlignCenter, .lineHeight = 17 }); // Even a ten digit amount of gold only takes up about half a line. There's no need to wrap or clip text here so we // use the Point form of DrawString. DrawString(out, amountText, GetPanelPosition(UiPanels::Stash, { dialogX + 37, 128 }), { .flags = UiFlags::ColorWhite | UiFlags::PentaCursor, .cursorPosition = static_cast(cursor.position), .highlightRange = { static_cast(cursor.selection.begin), static_cast(cursor.selection.end) }, }); } void CloseGoldWithdraw() { if (!IsWithdrawGoldOpen) return; SDLC_StopTextInput(ghMainWnd); IsWithdrawGoldOpen = false; GoldWithdrawInputState = std::nullopt; } bool HandleGoldWithdrawTextInputEvent(const SDL_Event &event) { return HandleNumberInputEvent(event, *GoldWithdrawInputState); } bool AutoPlaceItemInStash(Player &player, const Item &item, bool persistItem) { if (!IsItemAllowedInStash(item)) return false; if (item._itype == ItemType::Gold) { if (Stash.gold > std::numeric_limits::max() - item._ivalue) return false; if (persistItem) { Stash.gold += item._ivalue; Stash.dirty = true; } return true; } const Size itemSize = GetInventorySize(item); // Try to add the item to the current active page and if it's not possible move forward for (unsigned pageCounter = 0; pageCounter < CountStashPages; pageCounter++) { unsigned pageIndex = Stash.GetPage() + pageCounter; // Wrap around if needed if (pageIndex >= CountStashPages) pageIndex -= CountStashPages; // Search all possible position in stash grid for (auto stashPosition : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10 - (itemSize.width - 1), 10 - (itemSize.height - 1) } })) { // Check that all needed slots are free bool isSpaceFree = true; for (auto itemPoint : PointsInRectangle(Rectangle { stashPosition, itemSize })) { const uint16_t iv = Stash.stashGrids[pageIndex][itemPoint.x][itemPoint.y]; if (iv != 0) { isSpaceFree = false; break; } } if (!isSpaceFree) continue; if (persistItem) { Stash.stashList.push_back(item); const uint16_t stashIndex = static_cast(Stash.stashList.size() - 1); Stash.stashList[stashIndex].position = stashPosition + Displacement { 0, itemSize.height - 1 }; AddItemToStashGrid(pageIndex, stashPosition, stashIndex, itemSize); Stash.dirty = true; } return true; } } return false; } } // namespace devilution ================================================ FILE: Source/qol/stash.h ================================================ /** * @file qol/stash.h * * Interface of player stash. */ #pragma once #include #include #include #include "engine/point.hpp" #include "engine/points_in_rectangle_range.hpp" #include "items.h" namespace devilution { class StashStruct { public: using StashCell = uint16_t; using StashGrid = std::array, 10>; static constexpr StashCell EmptyCell = -1; void RemoveStashItem(StashCell iv); ankerl::unordered_dense::map stashGrids; std::vector stashList; int gold; bool dirty = false; unsigned GetPage() const { return page; } StashGrid &GetCurrentGrid() { return stashGrids[GetPage()]; } /** * @brief Returns the 0-based index of the item at the specified position, or EmptyCell if no item occupies that slot * @param gridPosition x,y coordinate of the current stash page * @return a value which can be used to index into stashList or StashStruct::EmptyCell */ StashCell GetItemIdAtPosition(Point gridPosition) { // Because StashCell is an unsigned type we can let this underflow return GetCurrentGrid()[gridPosition.x][gridPosition.y] - 1; } bool IsItemAtPosition(Point gridPosition) { return GetItemIdAtPosition(gridPosition) != EmptyCell; } void SetPage(unsigned newPage); void NextPage(unsigned offset = 1); void PreviousPage(unsigned offset = 1); /** @brief Updates _iStatFlag for all stash items. */ void RefreshItemStatFlags(); private: /** Current Page */ unsigned page; }; constexpr Point InvalidStashPoint { -1, -1 }; extern bool IsStashOpen; extern StashStruct Stash; extern bool IsWithdrawGoldOpen; extern int WithdrawGoldValue; inline constexpr Size StashGridSize { 10, 10 }; inline constexpr PointsInRectangle StashGridRange { { { 0, 0 }, StashGridSize } }; Point GetStashSlotCoord(Point slot); void InitStash(); void FreeStashGFX(); void TransferItemToInventory(Player &player, uint16_t itemId); /** * @brief Render the inventory panel to the given buffer. */ void DrawStash(const Surface &out); void CheckStashItem(Point mousePosition, bool isShiftHeld = false, bool isCtrlHeld = false); bool UseStashItem(uint16_t cii); uint16_t CheckStashHLight(Point mousePosition); void CheckStashButtonRelease(Point mousePosition); void CheckStashButtonPress(Point mousePosition); void StartGoldWithdraw(); void WithdrawGoldKeyPress(SDL_Keycode vkey); void DrawGoldWithdraw(const Surface &out); void CloseGoldWithdraw(); bool HandleGoldWithdrawTextInputEvent(const SDL_Event &event); /** * @brief Checks whether the given item can be placed on the specified player's stash. * If 'persistItem' is 'True', the item is also placed in the inventory. * @param player The player to check. * @param item The item to be checked. * @param persistItem Pass 'True' to actually place the item in the inventory. The default is 'False'. * @return 'True' in case the item can be placed on the player's inventory and 'False' otherwise. */ bool AutoPlaceItemInStash(Player &player, const Item &item, bool persistItem); } // namespace devilution ================================================ FILE: Source/qol/xpbar.cpp ================================================ /** * @file xpbar.cpp * * Adds XP bar QoL feature */ #include "xpbar.h" #include #include #include #include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/load_clx.hpp" #include "engine/point.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "game_mode.hpp" #include "options.h" #include "tables/playerdat.hpp" #include "utils/format_int.hpp" #include "utils/language.h" namespace devilution { namespace { constexpr int BarWidth = 307; using ColorGradient = std::array; constexpr ColorGradient GoldGradient = { 0xCF, 0xCE, 0xCD, 0xCC, 0xCB, 0xCA, 0xC9, 0xC8, 0xC7, 0xC6, 0xC5, 0xC4 }; constexpr ColorGradient SilverGradient = { 0xFE, 0xFD, 0xFC, 0xFB, 0xFA, 0xF9, 0xF8, 0xF7, 0xF6, 0xF5, 0xF4, 0xF3 }; constexpr int BackWidth = 313; constexpr int BackHeight = 9; OptionalOwnedClxSpriteList xpbarArt; void DrawBar(const Surface &out, Point screenPosition, int width, const ColorGradient &gradient) { UnsafeDrawHorizontalLine(out, screenPosition + Displacement { 0, 1 }, width, gradient[gradient.size() * 3 / 4 - 1]); UnsafeDrawHorizontalLine(out, screenPosition + Displacement { 0, 2 }, width, gradient[gradient.size() - 1]); UnsafeDrawHorizontalLine(out, screenPosition + Displacement { 0, 3 }, width, gradient[gradient.size() / 2 - 1]); } void DrawEndCap(const Surface &out, Point point, int idx, const ColorGradient &gradient) { out.SetPixel({ point.x, point.y + 1 }, gradient[idx * 3 / 4]); out.SetPixel({ point.x, point.y + 2 }, gradient[idx]); out.SetPixel({ point.x, point.y + 3 }, gradient[idx / 2]); } void OptionExperienceBarChanged() { if (!gbRunGame) return; if (*GetOptions().Gameplay.experienceBar) InitXPBar(); else FreeXPBar(); } const auto OptionChangeHandler = (GetOptions().Gameplay.experienceBar.SetValueChangedCallback(OptionExperienceBarChanged), true); } // namespace void InitXPBar() { if (*GetOptions().Gameplay.experienceBar) { xpbarArt = LoadClx("data\\xpbar.clx"); } } void FreeXPBar() { xpbarArt = std::nullopt; } void DrawXPBar(const Surface &out) { if (!*GetOptions().Gameplay.experienceBar || ChatFlag) return; const Player &player = *MyPlayer; const Rectangle &mainPanel = GetMainPanel(); const Point back = { mainPanel.position.x + mainPanel.size.width / 2 - 155, mainPanel.position.y + mainPanel.size.height - 11 }; const Point position = back + Displacement { 3, 2 }; RenderClxSprite(out, (*xpbarArt)[0], back); if (player.isMaxCharacterLevel()) { // Draw a nice golden bar for max level characters. DrawBar(out, position, BarWidth, GoldGradient); return; } const uint8_t charLevel = player.getCharacterLevel(); const uint64_t prevXp = GetNextExperienceThresholdForLevel(charLevel - 1); if (player._pExperience < prevXp) return; const uint64_t prevXpDelta1 = player._pExperience - prevXp; const uint64_t prevXpDelta = GetNextExperienceThresholdForLevel(charLevel) - prevXp; const uint64_t fullBar = BarWidth * prevXpDelta1 / prevXpDelta; // Figure out how much to fill the last pixel of the XP bar, to make it gradually appear with gained XP const uint64_t onePx = prevXpDelta / BarWidth + 1; const uint64_t lastFullPx = fullBar * prevXpDelta / BarWidth; const uint64_t fade = (prevXpDelta1 - lastFullPx) * (SilverGradient.size() - 1) / onePx; // Draw beginning of bar full brightness DrawBar(out, position, static_cast(fullBar), SilverGradient); // End pixels appear gradually DrawEndCap(out, position + Displacement { static_cast(fullBar), 0 }, static_cast(fade), SilverGradient); } bool CheckXPBarInfo() { if (!*GetOptions().Gameplay.experienceBar) return false; const Rectangle &mainPanel = GetMainPanel(); const int backX = mainPanel.position.x + mainPanel.size.width / 2 - 155; const int backY = mainPanel.position.y + mainPanel.size.height - 11; if (MousePosition.x < backX || MousePosition.x >= backX + BackWidth || MousePosition.y < backY || MousePosition.y >= backY + BackHeight) return false; const Player &player = *MyPlayer; const uint8_t charLevel = player.getCharacterLevel(); AddInfoBoxString(fmt::format(fmt::runtime(_("Level {:d}")), charLevel)); if (player.isMaxCharacterLevel()) { // Show a maximum level indicator for max level players. InfoColor = UiFlags::ColorWhitegold; AddInfoBoxString(fmt::format(fmt::runtime(_("Experience: {:s}")), FormatInteger(player._pExperience))); AddInfoBoxString(_("Maximum Level")); return true; } InfoColor = UiFlags::ColorWhite; AddInfoBoxString(fmt::format(fmt::runtime(_("Experience: {:s}")), FormatInteger(player._pExperience))); const uint32_t nextExperienceThreshold = player.getNextExperienceThreshold(); AddInfoBoxString(fmt::format(fmt::runtime(_("Next Level: {:s}")), FormatInteger(nextExperienceThreshold))); AddInfoBoxString(fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(nextExperienceThreshold - player._pExperience), charLevel + 1)); return true; } } // namespace devilution ================================================ FILE: Source/qol/xpbar.h ================================================ /** * @file xpbar.h * * Adds XP bar QoL feature */ #pragma once namespace devilution { struct Surface; void InitXPBar(); void FreeXPBar(); void DrawXPBar(const Surface &out); bool CheckXPBarInfo(); } // namespace devilution ================================================ FILE: Source/quests/validation.cpp ================================================ /** * @file quests/validation.cpp * * Implementation of functions for validation of quest data. */ #include "quests/validation.hpp" #include #include "quests.h" #include "tables/objdat.h" #include "tables/textdat.h" #include "utils/is_of.hpp" namespace devilution { bool IsQuestDeltaValid(quest_id qidx, quest_state qstate, uint8_t qlog, int16_t qmsg) { if (IsNoneOf(qlog, 0, 1)) return false; if (qmsg < 0 || static_cast(qmsg) >= Speeches.size()) return false; switch (qstate) { case QUEST_NOTAVAIL: case QUEST_INIT: case QUEST_ACTIVE: case QUEST_DONE: return true; case QUEST_HIVE_TEASE1: case QUEST_HIVE_TEASE2: case QUEST_HIVE_ACTIVE: return qidx == Q_JERSEY; case QUEST_HIVE_DONE: return IsAnyOf(qidx, Q_FARMER, Q_JERSEY); default: return false; } } } // namespace devilution ================================================ FILE: Source/quests/validation.hpp ================================================ /** * @file quests/validation.hpp * * Interface of functions for validation of quest data. */ #pragma once #include #include "quests.h" #include "tables/objdat.h" namespace devilution { bool IsQuestDeltaValid(quest_id qidx, quest_state qstate, uint8_t qlog, int16_t qmsg); } // namespace devilution ================================================ FILE: Source/quests.cpp ================================================ /** * @file quests.cpp * * Implementation of functionality for handling quests. */ #include "quests.h" #include #include #include "DiabloUI/ui_flags.hpp" #include "control/control.hpp" #include "cursor.h" #include "data/file.hpp" #include "data/record_reader.hpp" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/text_render.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "levels/gendung.h" #include "levels/town.h" #include "levels/trigs.h" #include "minitext.h" #include "missiles.h" #include "monster.h" #include "options.h" #include "panels/ui_panels.hpp" #include "stores.h" #include "tables/townerdat.hpp" #include "towners.h" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/utf8.hpp" #ifdef _DEBUG #include "debug.h" #endif namespace devilution { bool QuestLogIsOpen; OptionalOwnedClxSpriteList pQLogCel; /** Contains the quests of the current game. */ Quest Quests[MAXQUESTS]; Point ReturnLvlPosition; dungeon_type ReturnLevelType; int ReturnLevel; /** Contains the data related to each quest_id. */ std::vector QuestsData; namespace { int WaterDone; /** Indices of quests to display in quest log window. `FirstFinishedQuest` are active quests the rest are completed */ quest_id EncounteredQuests[MAXQUESTS]; /** Overall number of EncounteredQuests entries */ int EncounteredQuestCount; /** First (nonselectable) finished quest in list */ int FirstFinishedQuest; /** Currently selected quest list item */ int SelectedQuest; constexpr Rectangle InnerPanel { { 32, 26 }, { 280, 300 } }; constexpr int LineHeight = 12; constexpr int MaxSpacing = LineHeight * 2; int ListYOffset; int LineSpacing; /** The number of pixels to move finished quest, to separate them from the active ones */ int FinishedQuestOffset; const char *const QuestTriggerNames[5] = { N_(/* TRANSLATORS: Quest Map*/ "King Leoric's Tomb"), N_(/* TRANSLATORS: Quest Map*/ "The Chamber of Bone"), N_(/* TRANSLATORS: Quest Map*/ "Maze"), N_(/* TRANSLATORS: Quest Map*/ "A Dark Passage"), N_(/* TRANSLATORS: Quest Map*/ "Unholy Altar") }; /** * @brief There is no reason to run this, the room has already had a proper sector assigned */ void DrawButcher() { const Point position = SetPiece.position.megaToWorld() + Displacement { 3, 3 }; DRLG_RectTrans({ position, { 7, 7 } }); } void DrawSkelKing(quest_id q, Point position) { Quests[q].position = position.megaToWorld() + Displacement { 12, 7 }; } void DrawWarLord(Point position) { auto dunData = LoadFileInMem("levels\\l4data\\warlord2.dun"); SetPiece = { position, GetDunSize(dunData.get()) }; PlaceDunTiles(dunData.get(), position, 6); } void DrawSChamber(quest_id q, Point position) { auto dunData = LoadFileInMem("levels\\l2data\\bonestr1.dun"); SetPiece = { position, GetDunSize(dunData.get()) }; PlaceDunTiles(dunData.get(), position, 3); Quests[q].position = position.megaToWorld() + Displacement { 6, 7 }; } void DrawLTBanner(Point position) { auto dunData = LoadFileInMem("levels\\l1data\\banner1.dun"); const WorldTileSize size = GetDunSize(dunData.get()); SetPiece = { position, size }; const uint16_t *tileLayer = &dunData[2]; for (WorldTileCoord j = 0; j < size.height; j++) { for (WorldTileCoord i = 0; i < size.width; i++) { auto tileId = static_cast(Swap16LE(tileLayer[j * size.width + i])); if (tileId != 0) { pdungeon[position.x + i][position.y + j] = tileId; } } } } /** * Close outer wall */ void DrawBlind(Point position) { dungeon[position.x][position.y + 1] = 154; dungeon[position.x + 10][position.y + 8] = 154; } void DrawBlood(Point position) { auto dunData = LoadFileInMem("levels\\l2data\\blood2.dun"); SetPiece = { position, GetDunSize(dunData.get()) }; PlaceDunTiles(dunData.get(), position, 0); } int QuestLogMouseToEntry() { Rectangle innerArea = InnerPanel; innerArea.position += Displacement(GetLeftPanel().position.x, GetLeftPanel().position.y); if (!innerArea.contains(MousePosition) || (EncounteredQuestCount == 0)) return -1; const int y = MousePosition.y - innerArea.position.y; for (int i = 0; i < FirstFinishedQuest; i++) { if ((y >= ListYOffset + i * LineSpacing) && (y < ListYOffset + i * LineSpacing + LineHeight)) { return i; } } return -1; } void PrintQLString(const Surface &out, int x, int y, std::string_view str, bool marked, bool disabled = false) { const int width = GetLineWidth(str); x += std::max((257 - width) / 2, 0); if (marked) { ClxDraw(out, GetPanelPosition(UiPanels::Quest, { x - 20, y + 13 }), (*pSPentSpn2Cels)[PentSpn2Spin()]); } DrawString(out, str, { GetPanelPosition(UiPanels::Quest, { x, y }), { 257, 0 } }, { .flags = disabled ? UiFlags::ColorWhitegold : UiFlags::ColorWhite }); if (marked) { ClxDraw(out, GetPanelPosition(UiPanels::Quest, { x + width + 7, y + 13 }), (*pSPentSpn2Cels)[PentSpn2Spin()]); } } std::array PureWaterPalette; void StartPWaterPurify() { PlaySfxLoc(SfxID::QuestDone, MyPlayer->position.tile); LoadFileInMem("levels\\l3data\\l3pwater.pal", PureWaterPalette); UpdatePWaterPalette(); WaterDone = 32; } } // namespace void InitQuests() { SetTownerQuestDialog(TOWN_HEALER, Q_MUSHROOM, TEXT_NONE); SetTownerQuestDialog(TOWN_WITCH, Q_MUSHROOM, TEXT_MUSH9); QuestLogIsOpen = false; WaterDone = 0; int q = 0; for (auto &quest : Quests) { quest._qidx = static_cast(q); auto &questData = QuestsData[q]; q++; quest._qactive = QUEST_NOTAVAIL; quest.position = { 0, 0 }; quest._qlvltype = questData._qlvlt; quest._qslvl = questData._qslvl; quest._qvar1 = 0; quest._qvar2 = 0; quest._qlog = false; quest._qmsg = questData._qdmsg; if (!UseMultiplayerQuests()) { quest._qlevel = questData._qdlvl; quest._qactive = QUEST_INIT; } else if (!questData.isSinglePlayerOnly) { quest._qlevel = questData._qdmultlvl; quest._qactive = QUEST_INIT; } } if (!UseMultiplayerQuests() && *GetOptions().Gameplay.randomizeQuests) { // Quests are set from the seed used to generate level 15. InitialiseQuestPools(DungeonSeeds[15], Quests); } if (gbIsSpawn) { for (auto &quest : Quests) { quest._qactive = QUEST_NOTAVAIL; } } if (Quests[Q_SKELKING]._qactive == QUEST_NOTAVAIL) Quests[Q_SKELKING]._qvar2 = 2; if (Quests[Q_ROCK]._qactive == QUEST_NOTAVAIL) Quests[Q_ROCK]._qvar2 = 2; Quests[Q_LTBANNER]._qvar1 = 1; if (UseMultiplayerQuests()) Quests[Q_BETRAYER]._qvar1 = 2; // In multiplayer items spawn during level generation to avoid desyncs if (gbIsMultiplayer && Quests[Q_MUSHROOM]._qactive == QUEST_INIT) Quests[Q_MUSHROOM]._qvar1 = QS_TOMESPAWNED; } void InitialiseQuestPools(uint32_t seed, Quest quests[]) { DiabloGenerator rng(seed); quests[rng.pickRandomlyAmong({ Q_SKELKING, Q_PWATER })]._qactive = QUEST_NOTAVAIL; if (seed == 988045466) { // If someone starts a new game at 1977-12-28 19:44:42 UTC or 2087-02-18 22:43:02 UTC // vanilla Diablo ends up reading QuestGroup1[-2] here. Due to the way the data segment // is laid out (at least in 1.09) this ends up reading the address of the string // "A Dark Passage" and trying to write to Quests[*8] which lands in read-only memory. // The proper result would've been to mark The Butcher unavailable but instead nothing happens. rng.discardRandomValues(1); } else { quests[rng.pickRandomlyAmong({ Q_BUTCHER, Q_LTBANNER, Q_GARBUD })]._qactive = QUEST_NOTAVAIL; } quests[rng.pickRandomlyAmong({ Q_BLIND, Q_ROCK, Q_BLOOD })]._qactive = QUEST_NOTAVAIL; quests[rng.pickRandomlyAmong({ Q_MUSHROOM, Q_ZHAR, Q_ANVIL })]._qactive = QUEST_NOTAVAIL; quests[rng.pickRandomlyAmong({ Q_VEIL, Q_WARLORD })]._qactive = QUEST_NOTAVAIL; } void CheckQuests() { if (gbIsSpawn) return; auto &quest = Quests[Q_BETRAYER]; if (quest.IsAvailable() && UseMultiplayerQuests() && quest._qvar1 == 2) { AddObject(OBJ_ALTBOY, SetPiece.position.megaToWorld() + Displacement { 4, 6 }); quest._qvar1 = 3; NetSendCmdQuest(true, quest); } if (UseMultiplayerQuests()) { return; } if (currlevel == quest._qlevel && !setlevel && quest._qvar1 >= 2 && (quest._qactive == QUEST_ACTIVE || quest._qactive == QUEST_DONE) && (quest._qvar2 == 0 || quest._qvar2 == 2)) { // Spawn a portal at the quest trigger location AddMissile(quest.position, quest.position, Direction::South, MissileID::RedPortal, TARGET_MONSTERS, *MyPlayer, 0, 0); quest._qvar2 = 1; if (quest._qactive == QUEST_ACTIVE && quest._qvar1 == 2) { quest._qvar1 = 3; } } if (quest._qactive == QUEST_DONE && setlevel && setlvlnum == SL_VILEBETRAYER && quest._qvar2 == 4) { const Point portalLocation { 35, 32 }; AddMissile(portalLocation, portalLocation, Direction::South, MissileID::RedPortal, TARGET_MONSTERS, *MyPlayer, 0, 0); quest._qvar2 = 3; } if (setlevel) { Quest &poisonWater = Quests[Q_PWATER]; if (setlvlnum == poisonWater._qslvl && poisonWater._qactive != QUEST_INIT && leveltype == poisonWater._qlvltype && ActiveMonsterCount == 4 && poisonWater._qactive != QUEST_DONE) { poisonWater._qactive = QUEST_DONE; poisonWater._qlog = true; // even if the player skips talking to Pepin completely they should at least notice the water being purified once they cleanse the level NetSendCmdQuest(true, poisonWater); StartPWaterPurify(); } } else if (MyPlayer->_pmode == PM_STAND) { for (auto ¤tQuest : Quests) { if (currlevel == currentQuest._qlevel && currentQuest._qslvl != 0 && currentQuest._qactive != QUEST_NOTAVAIL && MyPlayer->position.tile == currentQuest.position && (currentQuest._qidx != Q_BETRAYER || currentQuest._qvar1 >= 3)) { if (currentQuest._qlvltype != DTYPE_NONE) { setlvltype = currentQuest._qlvltype; } StartNewLvl(*MyPlayer, WM_DIABSETLVL, currentQuest._qslvl); } } } } bool ForceQuests() { if (gbIsSpawn) return false; if (UseMultiplayerQuests()) { return false; } for (auto &quest : Quests) { if (quest._qidx != Q_BETRAYER && currlevel == quest._qlevel && quest._qslvl != 0) { const int ql = quest._qslvl - 1; if (EntranceBoundaryContains(quest.position, cursPosition)) { InfoString = fmt::format(fmt::runtime(_(/* TRANSLATORS: Used for Quest Portals. {:s} is a Map Name */ "To {:s}")), _(QuestTriggerNames[ql])); cursPosition = quest.position; return true; } } } return false; } void CheckQuestKill(const Monster &monster, bool sendmsg) { if (gbIsSpawn) return; const Player &myPlayer = *MyPlayer; if (monster.type().type == MT_SKING) { auto &quest = Quests[Q_SKELKING]; quest._qactive = QUEST_DONE; myPlayer.Say(HeroSpeech::RestWellLeoricIllFindYourSon, 30); if (sendmsg) NetSendCmdQuest(true, quest); } else if (monster.type().type == MT_CLEAVER) { auto &quest = Quests[Q_BUTCHER]; quest._qactive = QUEST_DONE; myPlayer.Say(HeroSpeech::TheSpiritsOfTheDeadAreNowAvenged, 30); if (sendmsg) NetSendCmdQuest(true, quest); } else if (monster.uniqueType == UniqueMonsterType::Garbud) { //"Gharbad the Weak" Quests[Q_GARBUD]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_GARBUD]); myPlayer.Say(HeroSpeech::ImNotImpressed, 30); } else if (monster.uniqueType == UniqueMonsterType::Zhar) { //"Zhar the Mad" Quests[Q_ZHAR]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_ZHAR]); myPlayer.Say(HeroSpeech::ImSorryDidIBreakYourConcentration, 30); } else if (monster.uniqueType == UniqueMonsterType::Lazarus) { //"Arch-Bishop Lazarus" auto &betrayerQuest = Quests[Q_BETRAYER]; betrayerQuest._qactive = QUEST_DONE; myPlayer.Say(HeroSpeech::YourMadnessEndsHereBetrayer, 30); betrayerQuest._qvar1 = 7; auto &diabloQuest = Quests[Q_DIABLO]; diabloQuest._qactive = QUEST_ACTIVE; if (UseMultiplayerQuests()) { for (WorldTileCoord j = 0; j < MAXDUNY; j++) { for (WorldTileCoord i = 0; i < MAXDUNX; i++) { if (dPiece[i][j] == 369) { trigs[numtrigs].position = { i, j }; trigs[numtrigs]._tmsg = WM_DIABNEXTLVL; numtrigs++; } } } } else { InitVPTriggers(); betrayerQuest._qvar2 = 4; AddMissile({ 35, 32 }, { 35, 32 }, Direction::South, MissileID::RedPortal, TARGET_MONSTERS, myPlayer, 0, 0); } if (sendmsg) { NetSendCmdQuest(true, betrayerQuest); NetSendCmdQuest(true, diabloQuest); } } else if (monster.uniqueType == UniqueMonsterType::WarlordOfBlood) { Quests[Q_WARLORD]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_WARLORD]); myPlayer.Say(HeroSpeech::YourReignOfPainHasEnded, 30); } } void DRLG_CheckQuests(Point position) { for (auto &quest : Quests) { if (quest.IsAvailable()) { switch (quest._qidx) { case Q_BUTCHER: DrawButcher(); break; case Q_LTBANNER: DrawLTBanner(position); break; case Q_BLIND: DrawBlind(position); break; case Q_BLOOD: DrawBlood(position); break; case Q_WARLORD: DrawWarLord(position); break; case Q_SKELKING: DrawSkelKing(quest._qidx, position); break; case Q_SCHAMB: DrawSChamber(quest._qidx, position); break; default: break; } } } } int GetMapReturnLevel() { switch (setlvlnum) { case SL_SKELKING: return Quests[Q_SKELKING]._qlevel; case SL_BONECHAMB: return Quests[Q_SCHAMB]._qlevel; case SL_POISONWATER: return Quests[Q_PWATER]._qlevel; case SL_VILEBETRAYER: return Quests[Q_BETRAYER]._qlevel; default: return 0; } } Point GetMapReturnPosition() { #ifdef _DEBUG if (!TestMapPath.empty()) return ViewPosition; #endif switch (setlvlnum) { case SL_SKELKING: return Quests[Q_SKELKING].position + Direction::SouthEast; case SL_BONECHAMB: return Quests[Q_SCHAMB].position + Direction::SouthEast; case SL_POISONWATER: return Quests[Q_PWATER].position + Direction::SouthWest; case SL_VILEBETRAYER: return Quests[Q_BETRAYER].position + Direction::South; default: return GetTowner(TOWN_DRUNK)->position + Direction::SouthEast; } } void LoadPWaterPalette() { if (!setlevel || setlvlnum != Quests[Q_PWATER]._qslvl || Quests[Q_PWATER]._qactive == QUEST_INIT || leveltype != Quests[Q_PWATER]._qlvltype) return; if (Quests[Q_PWATER]._qactive == QUEST_DONE) LoadPaletteAndInitBlending("levels\\l3data\\l3pwater.pal"); else LoadPaletteAndInitBlending("levels\\l3data\\l3pfoul.pal"); } void UpdatePWaterPalette() { if (WaterDone > 0) { // `WaterDone` is in [1, 32], so `colorIndex` is in [0, 31]. const unsigned colorIndex = 32 - WaterDone; SetLogicalPaletteColor(colorIndex, PureWaterPalette[colorIndex].toSDL()); WaterDone--; return; } palette_update_caves(); } void ResyncMPQuests() { if (gbIsSpawn) return; auto &kingQuest = Quests[Q_SKELKING]; if (kingQuest._qactive == QUEST_INIT && currlevel >= kingQuest._qlevel - 1 && currlevel <= kingQuest._qlevel + 1) { kingQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, kingQuest); } auto &butcherQuest = Quests[Q_BUTCHER]; if (butcherQuest._qactive == QUEST_INIT && currlevel >= butcherQuest._qlevel - 1 && currlevel <= butcherQuest._qlevel + 1) { butcherQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, butcherQuest); } auto &betrayerQuest = Quests[Q_BETRAYER]; if (betrayerQuest._qactive == QUEST_INIT && currlevel == betrayerQuest._qlevel - 1) { betrayerQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, betrayerQuest); } if (betrayerQuest.IsAvailable()) AddObject(OBJ_ALTBOY, SetPiece.position.megaToWorld() + Displacement { 4, 6 }); auto &cryptQuest = Quests[Q_GRAVE]; if (cryptQuest._qactive == QUEST_INIT && currlevel == cryptQuest._qlevel - 1) { cryptQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, cryptQuest); } auto &defilerQuest = Quests[Q_DEFILER]; if (defilerQuest._qactive == QUEST_INIT && currlevel == defilerQuest._qlevel - 1) { defilerQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, defilerQuest); } auto &nakrulQuest = Quests[Q_NAKRUL]; if (nakrulQuest._qactive == QUEST_INIT && currlevel == nakrulQuest._qlevel - 1) { nakrulQuest._qactive = QUEST_ACTIVE; NetSendCmdQuest(true, nakrulQuest); } } void ResyncQuests() { if (gbIsSpawn) return; LoadingMapObjects = true; if (Quests[Q_LTBANNER].IsAvailable()) { Monster *snotSpill = FindUniqueMonster(UniqueMonsterType::SnotSpill); if (Quests[Q_LTBANNER]._qvar1 == 1) { ObjChangeMapResync( SetPiece.position.x + SetPiece.size.width - 2, SetPiece.position.y + SetPiece.size.height - 2, SetPiece.position.x + SetPiece.size.width + 1, SetPiece.position.y + SetPiece.size.height + 1); } if (Quests[Q_LTBANNER]._qvar1 == 2) { ObjChangeMapResync( SetPiece.position.x + SetPiece.size.width - 2, SetPiece.position.y + SetPiece.size.height - 2, SetPiece.position.x + SetPiece.size.width + 1, SetPiece.position.y + SetPiece.size.height + 1); ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + (SetPiece.size.width / 2) + 2, SetPiece.position.y + (SetPiece.size.height / 2) - 2); for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans({ SetPiece.position, WorldTileSize(SetPiece.size.width / 2 + 4, SetPiece.size.height / 2) }); TransVal = tren; if (gbIsMultiplayer && snotSpill != nullptr && snotSpill->talkMsg != TEXT_BANNER12) { snotSpill->goal = MonsterGoal::Inquiring; snotSpill->talkMsg = Quests[Q_LTBANNER]._qactive == QUEST_DONE ? TEXT_BANNER12 : TEXT_BANNER11; snotSpill->flags |= MFLAG_QUEST_COMPLETE; } } if (Quests[Q_LTBANNER]._qvar1 == 3) { ObjChangeMapResync(SetPiece.position.x, SetPiece.position.y, SetPiece.position.x + SetPiece.size.width + 1, SetPiece.position.y + SetPiece.size.height + 1); for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); auto tren = TransVal; TransVal = 9; DRLG_MRectTrans({ SetPiece.position, WorldTileSize(SetPiece.size.width / 2 + 4, SetPiece.size.height / 2) }); TransVal = tren; if (gbIsMultiplayer && snotSpill != nullptr) { snotSpill->goal = MonsterGoal::Normal; snotSpill->flags |= MFLAG_QUEST_COMPLETE; snotSpill->talkMsg = TEXT_NONE; snotSpill->activeForTicks = UINT8_MAX; RedoPlayerVision(); } } } if (currlevel == Quests[Q_MUSHROOM]._qlevel && !setlevel) { if (Quests[Q_MUSHROOM]._qactive == QUEST_INIT && Quests[Q_MUSHROOM]._qvar1 == QS_INIT) { SpawnQuestItem(IDI_FUNGALTM, { 0, 0 }, 5, SelectionRegion::Bottom, true); Quests[Q_MUSHROOM]._qvar1 = QS_TOMESPAWNED; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); } else { if (Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE) { if (Quests[Q_MUSHROOM]._qvar1 >= QS_MUSHGIVEN) { SetTownerQuestDialog(TOWN_WITCH, Q_MUSHROOM, TEXT_NONE); SetTownerQuestDialog(TOWN_HEALER, Q_MUSHROOM, TEXT_MUSH3); } else if (Quests[Q_MUSHROOM]._qvar1 >= QS_BRAINGIVEN) { SetTownerQuestDialog(TOWN_HEALER, Q_MUSHROOM, TEXT_NONE); } } } } if (currlevel == Quests[Q_VEIL]._qlevel + 1 && Quests[Q_VEIL]._qactive == QUEST_ACTIVE && Quests[Q_VEIL]._qvar1 == 0 && !gbIsMultiplayer) { Quests[Q_VEIL]._qvar1 = 1; SpawnQuestItem(IDI_GLDNELIX, { 0, 0 }, 5, SelectionRegion::Bottom, true); NetSendCmdQuest(true, Quests[Q_VEIL]); } if (setlevel && setlvlnum == SL_VILEBETRAYER) { if (Quests[Q_BETRAYER]._qvar1 >= 4) ObjChangeMapResync(1, 11, 20, 18); if (Quests[Q_BETRAYER]._qvar1 >= 6) { ObjChangeMapResync(1, 18, 20, 24); if (gbIsMultiplayer) { Monster *lazarus = FindUniqueMonster(UniqueMonsterType::Lazarus); if (lazarus != nullptr) { // Ensure lazarus starts attacking again after returning to the level lazarus->goal = MonsterGoal::Normal; lazarus->talkMsg = TEXT_NONE; } } } if (Quests[Q_BETRAYER]._qvar1 >= 7) InitVPTriggers(); for (int i = 0; i < ActiveObjectCount; i++) SyncObjectAnim(Objects[ActiveObjects[i]]); } if (currlevel == Quests[Q_BETRAYER]._qlevel && !setlevel && (Quests[Q_BETRAYER]._qvar2 == 1 || Quests[Q_BETRAYER]._qvar2 >= 3) && (Quests[Q_BETRAYER]._qactive == QUEST_ACTIVE || Quests[Q_BETRAYER]._qactive == QUEST_DONE)) { Quests[Q_BETRAYER]._qvar2 = 2; NetSendCmdQuest(true, Quests[Q_BETRAYER]); } if (currlevel == Quests[Q_DIABLO]._qlevel && !setlevel && Quests[Q_DIABLO]._qactive == QUEST_ACTIVE && gbIsMultiplayer) { const Point posPentagram = Quests[Q_DIABLO].position; ObjChangeMapResync(posPentagram.x, posPentagram.y, posPentagram.x + 5, posPentagram.y + 5); InitL4Triggers(); } if (currlevel == 0 && Quests[Q_PWATER]._qactive == QUEST_DONE && gbIsMultiplayer) { CleanTownFountain(); } if (Quests[Q_GARBUD].IsAvailable() && gbIsMultiplayer) { Monster *garbud = FindUniqueMonster(UniqueMonsterType::Garbud); if (garbud != nullptr && Quests[Q_GARBUD]._qvar1 != QS_GHARBAD_INIT) { switch (Quests[Q_GARBUD]._qvar1) { case QS_GHARBAD_FIRST_ITEM_READY: garbud->goal = MonsterGoal::Inquiring; break; case QS_GHARBAD_FIRST_ITEM_SPAWNED: garbud->talkMsg = TEXT_GARBUD2; garbud->flags |= MFLAG_QUEST_COMPLETE; garbud->goal = MonsterGoal::Talking; break; case QS_GHARBAD_SECOND_ITEM_NEARLY_DONE: garbud->talkMsg = TEXT_GARBUD3; garbud->flags |= MFLAG_QUEST_COMPLETE; garbud->goal = MonsterGoal::Inquiring; break; case QS_GHARBAD_SECOND_ITEM_READY: garbud->talkMsg = TEXT_GARBUD4; garbud->flags |= MFLAG_QUEST_COMPLETE; garbud->goal = MonsterGoal::Inquiring; break; case QS_GHARBAD_ATTACKING: garbud->talkMsg = TEXT_NONE; garbud->flags |= MFLAG_QUEST_COMPLETE; garbud->goal = MonsterGoal::Normal; garbud->activeForTicks = UINT8_MAX; break; } } } if (Quests[Q_ZHAR].IsAvailable() && gbIsMultiplayer) { Monster *zhar = FindUniqueMonster(UniqueMonsterType::Zhar); if (zhar != nullptr && Quests[Q_ZHAR]._qvar1 != QS_ZHAR_INIT) { zhar->flags |= MFLAG_QUEST_COMPLETE; switch (Quests[Q_ZHAR]._qvar1) { case QS_ZHAR_ITEM_SPAWNED: zhar->goal = MonsterGoal::Talking; break; case QS_ZHAR_ANGRY: zhar->talkMsg = TEXT_ZHAR2; zhar->goal = MonsterGoal::Inquiring; break; case QS_ZHAR_ATTACKING: zhar->talkMsg = TEXT_NONE; zhar->goal = MonsterGoal::Normal; zhar->activeForTicks = UINT8_MAX; break; } } } if (Quests[Q_WARLORD].IsAvailable() && gbIsMultiplayer) { Monster *warlord = FindUniqueMonster(UniqueMonsterType::WarlordOfBlood); if (warlord != nullptr && Quests[Q_WARLORD]._qvar1 == QS_WARLORD_ATTACKING) { warlord->activeForTicks = UINT8_MAX; warlord->talkMsg = TEXT_NONE; warlord->goal = MonsterGoal::Normal; } } if (Quests[Q_VEIL].IsAvailable() && gbIsMultiplayer) { Monster *lachdan = FindUniqueMonster(UniqueMonsterType::Lachdan); if (lachdan != nullptr) { switch (Quests[Q_VEIL]._qvar2) { case QS_VEIL_EARLY_RETURN: lachdan->talkMsg = TEXT_VEIL10; lachdan->goal = MonsterGoal::Inquiring; break; case QS_VEIL_ITEM_SPAWNED: if (lachdan->talkMsg == TEXT_VEIL11) break; lachdan->talkMsg = TEXT_VEIL11; lachdan->flags |= MFLAG_QUEST_COMPLETE; lachdan->goal = MonsterGoal::Inquiring; break; } } } LoadingMapObjects = false; } void DrawQuestLog(const Surface &out) { const int l = QuestLogMouseToEntry(); if (l >= 0) { SelectedQuest = l; } const auto x = InnerPanel.position.x; ClxDraw(out, GetPanelPosition(UiPanels::Quest, { 0, 351 }), (*pQLogCel)[0]); int y = InnerPanel.position.y + ListYOffset; for (int i = 0; i < EncounteredQuestCount; i++) { if (i == FirstFinishedQuest) { y += FinishedQuestOffset; } PrintQLString(out, x, y, _(QuestsData[EncounteredQuests[i]]._qlstr), i == SelectedQuest, i >= FirstFinishedQuest); y += LineSpacing; } } void StartQuestlog() { auto sortQuestIdx = [](int a, int b) { return QuestsData[a].questBookOrder < QuestsData[b].questBookOrder; }; EncounteredQuestCount = 0; for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && quest._qlog) { EncounteredQuests[EncounteredQuestCount] = quest._qidx; EncounteredQuestCount++; } } FirstFinishedQuest = EncounteredQuestCount; for (auto &quest : Quests) { if (quest._qactive == QUEST_DONE || quest._qactive == QUEST_HIVE_DONE) { EncounteredQuests[EncounteredQuestCount] = quest._qidx; EncounteredQuestCount++; } } std::sort(&EncounteredQuests[0], &EncounteredQuests[FirstFinishedQuest], sortQuestIdx); std::sort(&EncounteredQuests[FirstFinishedQuest], &EncounteredQuests[EncounteredQuestCount], sortQuestIdx); const bool twoBlocks = FirstFinishedQuest != 0 && FirstFinishedQuest < EncounteredQuestCount; ListYOffset = 0; FinishedQuestOffset = !twoBlocks ? 0 : LineHeight / 2; const int overallMinHeight = EncounteredQuestCount * LineHeight + FinishedQuestOffset; const int space = InnerPanel.size.height; if (EncounteredQuestCount > 0) { const int additionalSpace = space - overallMinHeight; int addLineSpacing = additionalSpace / EncounteredQuestCount; addLineSpacing = std::min(MaxSpacing - LineHeight, addLineSpacing); LineSpacing = LineHeight + addLineSpacing; if (twoBlocks) { int additionalSepSpace = additionalSpace - (addLineSpacing * EncounteredQuestCount); additionalSepSpace = std::min(LineHeight, additionalSepSpace); FinishedQuestOffset = std::max(4, additionalSepSpace); } const int overallHeight = EncounteredQuestCount * LineSpacing + FinishedQuestOffset; ListYOffset += (space - overallHeight) / 2; } SelectedQuest = FirstFinishedQuest == 0 ? -1 : 0; QuestLogIsOpen = true; } void QuestlogUp() { if (FirstFinishedQuest == 0) { SelectedQuest = -1; } else { SelectedQuest--; if (SelectedQuest < 0) { SelectedQuest = FirstFinishedQuest - 1; } PlaySFX(SfxID::MenuMove); } } void QuestlogDown() { if (FirstFinishedQuest == 0) { SelectedQuest = -1; } else { SelectedQuest++; if (SelectedQuest == FirstFinishedQuest) { SelectedQuest = 0; } PlaySFX(SfxID::MenuMove); } } void QuestlogEnter() { PlaySFX(SfxID::MenuSelect); if (EncounteredQuestCount != 0 && SelectedQuest >= 0 && SelectedQuest < FirstFinishedQuest) InitQTextMsg(Quests[EncounteredQuests[SelectedQuest]]._qmsg); QuestLogIsOpen = false; } void QuestlogESC() { const int l = QuestLogMouseToEntry(); if (l != -1) { QuestlogEnter(); } } void SetMultiQuest(int q, quest_state s, bool log, int v1, int v2, int16_t qmsg) { if (gbIsSpawn) return; auto &quest = Quests[q]; const quest_state oldQuestState = quest._qactive; if (quest._qactive != QUEST_DONE) { if (s > quest._qactive || (IsAnyOf(s, QUEST_ACTIVE, QUEST_DONE) && IsAnyOf(quest._qactive, QUEST_HIVE_TEASE1, QUEST_HIVE_TEASE2, QUEST_HIVE_ACTIVE))) quest._qactive = s; if (log) quest._qlog = true; } if (v1 > quest._qvar1) quest._qvar1 = v1; quest._qvar2 = v2; quest._qmsg = static_cast<_speech_id>(qmsg); if (!UseMultiplayerQuests()) { // Ensure that changes on another client is also updated on our own ResyncQuests(); const bool questGotCompleted = oldQuestState != QUEST_DONE && quest._qactive == QUEST_DONE; // Ensure that water also changes for remote players if (quest._qidx == Q_PWATER && questGotCompleted && MyPlayer->isOnLevel(quest._qslvl)) StartPWaterPurify(); if (quest._qidx == Q_GIRL && questGotCompleted && MyPlayer->isOnLevel(0)) UpdateGirlAnimAfterQuestComplete(); if (quest._qidx == Q_JERSEY && questGotCompleted && MyPlayer->isOnLevel(0)) UpdateCowFarmerAnimAfterQuestComplete(); } } bool UseMultiplayerQuests() { return sgGameInitInfo.fullQuests == 0; } bool Quest::IsAvailable() const { if (setlevel) return false; if (currlevel != _qlevel) return false; if (_qactive == QUEST_NOTAVAIL) return false; if (QuestsData[_qidx].isSinglePlayerOnly && UseMultiplayerQuests()) return false; return true; } namespace { void LoadQuestDatFromFile(DataFile &dataFile, std::string_view filename) { dataFile.skipHeaderOrDie(filename); QuestsData.reserve(QuestsData.size() + dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; QuestData &quest = QuestsData.emplace_back(); reader.readInt("qdlvl", quest._qdlvl); reader.readInt("qdmultlvl", quest._qdmultlvl); reader.read("qlvlt", quest._qlvlt, ParseDungeonType); reader.readInt("bookOrder", quest.questBookOrder); reader.readInt("qdrnd", quest._qdrnd); reader.read("qslvl", quest._qslvl, ParseSetLevel); reader.readBool("isSinglePlayerOnly", quest.isSinglePlayerOnly); reader.read("qdmsg", quest._qdmsg, ParseSpeechId); reader.readString("qlstr", quest._qlstr); } } } // namespace void LoadQuestData() { const std::string_view filename = "txtdata\\quests\\questdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); QuestsData.clear(); LoadQuestDatFromFile(dataFile, filename); QuestsData.shrink_to_fit(); } } // namespace devilution ================================================ FILE: Source/quests.h ================================================ /** * @file quests.cpp * * Interface of functionality for handling quests. */ #pragma once #include #include "engine/clx_sprite.hpp" #include "engine/point.hpp" #include "engine/surface.hpp" #include "levels/gendung.h" #include "monster.h" #include "panels/info_box.hpp" #include "tables/objdat.h" #include "tables/textdat.h" #include "utils/attributes.h" namespace devilution { #define MAXQUESTS 24 /** States of the mushroom quest */ enum { QS_INIT, QS_TOMESPAWNED, QS_TOMEGIVEN, QS_MUSHSPAWNED, QS_MUSHPICKED, QS_MUSHGIVEN, QS_BRAINSPAWNED, QS_BRAINGIVEN, }; /** @brief States of the gharbad the week quest for multiplayer sync */ enum { QS_GHARBAD_INIT, QS_GHARBAD_FIRST_ITEM_READY, QS_GHARBAD_FIRST_ITEM_SPAWNED, QS_GHARBAD_SECOND_ITEM_NEARLY_DONE, QS_GHARBAD_SECOND_ITEM_READY, QS_GHARBAD_ATTACKING, }; /** @brief States of Zhar the Mad quest for multiplayer sync */ enum { QS_ZHAR_INIT, QS_ZHAR_ITEM_SPAWNED, QS_ZHAR_ANGRY, QS_ZHAR_ATTACKING, }; /** @brief States of the Warlord of Blood quest */ enum { QS_WARLORD_INIT, QS_WARLORD_STEELTOME_READ, QS_WARLORD_TALKING, /** @brief State only added for multiplayer quests. Doesn't affect vanilla compatibility. */ QS_WARLORD_ATTACKING, }; /** @brief States of Lachdanan quest for multiplayer sync */ enum { QS_VEIL_INIT, QS_VEIL_EARLY_RETURN, QS_VEIL_ITEM_SPAWNED, }; enum quest_state : uint8_t { QUEST_NOTAVAIL, // quest did not spawn this game QUEST_INIT, // quest has spawned, waiting to trigger QUEST_ACTIVE, // quest is currently in progress QUEST_DONE, // quest log closed and finished QUEST_HIVE_TEASE1 = 7, QUEST_HIVE_TEASE2, QUEST_HIVE_ACTIVE, QUEST_HIVE_DONE, QUEST_INVALID = 0xFF, }; struct Quest { quest_id _qidx; quest_state _qactive; uint8_t _qlevel; Point position; dungeon_type _qlvltype; _setlevels _qslvl; bool _qlog; _speech_id _qmsg; uint8_t _qvar1; uint8_t _qvar2; bool IsAvailable() const; }; struct QuestData { uint8_t _qdlvl; int8_t _qdmultlvl; dungeon_type _qlvlt; int8_t questBookOrder; uint8_t _qdrnd; _setlevels _qslvl; bool isSinglePlayerOnly; _speech_id _qdmsg; std::string _qlstr; }; extern bool QuestLogIsOpen; extern OptionalOwnedClxSpriteList pQLogCel; extern DVL_API_FOR_TEST Quest Quests[MAXQUESTS]; extern Point ReturnLvlPosition; extern dungeon_type ReturnLevelType; extern int ReturnLevel; void InitQuests(); /** * @brief Deactivates quests from each quest pool at random to provide variety for single player games * @param seed The seed used to control which quests are deactivated * @param quests The available quest list, this function will make some of them inactive by the time it returns */ void InitialiseQuestPools(uint32_t seed, Quest quests[]); void CheckQuests(); bool ForceQuests(); void CheckQuestKill(const Monster &monster, bool sendmsg); void DRLG_CheckQuests(Point position); int GetMapReturnLevel(); Point GetMapReturnPosition(); void LoadPWaterPalette(); void UpdatePWaterPalette(); void ResyncMPQuests(); void ResyncQuests(); void DrawQuestLog(const Surface &out); void StartQuestlog(); void QuestlogUp(); void QuestlogDown(); void QuestlogEnter(); void QuestlogESC(); void SetMultiQuest(int q, quest_state s, bool log, int v1, int v2, int16_t qmsg); bool UseMultiplayerQuests(); /* rdata */ extern std::vector QuestsData; void LoadQuestData(); } // namespace devilution ================================================ FILE: Source/quick_messages.cpp ================================================ #include "quick_messages.hpp" #include #include "utils/language.h" namespace devilution { std::array QuickMessages = { QuickMessage { "QuickMessage1", N_("I need help! Come here!") }, QuickMessage { "QuickMessage2", N_("Follow me.") }, QuickMessage { "QuickMessage3", N_("Here's something for you.") }, QuickMessage { "QuickMessage4", N_("Now you DIE!") }, QuickMessage { "QuickMessage5", N_("Heal yourself!") }, QuickMessage { "QuickMessage6", N_("Watch out!") }, QuickMessage { "QuickMessage7", N_("Thanks.") }, QuickMessage { "QuickMessage8", N_("Retreat!") }, QuickMessage { "QuickMessage9", N_("Sorry.") }, QuickMessage { "QuickMessage10", N_("I'm waiting.") }, }; } // namespace devilution ================================================ FILE: Source/quick_messages.hpp ================================================ #pragma once #include namespace devilution { struct QuickMessage { /** Config variable names for quick message */ const char *const key; /** Default quick message */ const char *const message; }; extern std::array QuickMessages; } // namespace devilution ================================================ FILE: Source/restrict.cpp ================================================ /** * @file restrict.cpp * * Implementation of functionality for checking if the game will be able run on the system. */ #include #include "appfat.h" #include "utils/file_util.h" #include "utils/paths.h" #ifdef USE_SDL3 #include #else #include #endif #include "utils/sdl_compat.h" namespace devilution { void ReadOnlyTest() { const std::string path = paths::PrefPath() + "Diablo1ReadOnlyTest.foo"; SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "w"); if (file == nullptr) { DirErrorDlg(paths::PrefPath()); } SDL_CloseIO(file); RemoveFile(path.c_str()); } } // namespace devilution ================================================ FILE: Source/restrict.h ================================================ /** * @file restrict.h * * Interface of functionality for checking if the game will be able run on the system. */ #pragma once namespace devilution { /** * @brief Check that we have write access to the game install folder */ void ReadOnlyTest(); } // namespace devilution ================================================ FILE: Source/sha.cpp ================================================ #include "sha.h" #include #include namespace devilution { // NOTE: Diablo's "SHA1" is different from actual SHA1 in that it uses arithmetic // right shifts (sign bit extension). namespace { /** * Diablo-"SHA1" circular left shift, portable version. */ uint32_t SHA1CircularShift(uint32_t word, size_t bits) { // The SHA-like algorithm as originally implemented treated word as a signed value and used arithmetic right shifts // (sign-extending). This results in the high 32-`bits` bits being set to 1. if ((word & (1 << 31)) != 0) return (0xFFFFFFFF << bits) | (word >> (32 - bits)); return (word << bits) | (word >> (32 - bits)); } void SHA1ProcessMessageBlock(SHA1Context *context) { std::uint32_t w[80]; memcpy(w, context->buffer, BlockSize * sizeof(uint32_t)); for (int i = 16; i < 80; i++) { w[i] = w[i - 16] ^ w[i - 14] ^ w[i - 8] ^ w[i - 3]; } std::uint32_t a = context->state[0]; std::uint32_t b = context->state[1]; std::uint32_t c = context->state[2]; std::uint32_t d = context->state[3]; std::uint32_t e = context->state[4]; for (int i = 0; i < 20; i++) { const std::uint32_t temp = SHA1CircularShift(a, 5) + ((b & c) | ((~b) & d)) + e + w[i] + 0x5A827999; e = d; d = c; c = SHA1CircularShift(b, 30); b = a; a = temp; } for (int i = 20; i < 40; i++) { const std::uint32_t temp = SHA1CircularShift(a, 5) + (b ^ c ^ d) + e + w[i] + 0x6ED9EBA1; e = d; d = c; c = SHA1CircularShift(b, 30); b = a; a = temp; } for (int i = 40; i < 60; i++) { const std::uint32_t temp = SHA1CircularShift(a, 5) + ((b & c) | (b & d) | (c & d)) + e + w[i] + 0x8F1BBCDC; e = d; d = c; c = SHA1CircularShift(b, 30); b = a; a = temp; } for (int i = 60; i < 80; i++) { const std::uint32_t temp = SHA1CircularShift(a, 5) + (b ^ c ^ d) + e + w[i] + 0xCA62C1D6; e = d; d = c; c = SHA1CircularShift(b, 30); b = a; a = temp; } context->state[0] += a; context->state[1] += b; context->state[2] += c; context->state[3] += d; context->state[4] += e; } } // namespace void SHA1Result(SHA1Context &context, uint32_t messageDigest[SHA1HashSize]) { memcpy(messageDigest, context.state, sizeof(context.state)); } void SHA1Calculate(SHA1Context &context, const uint32_t data[BlockSize]) { memcpy(&context.buffer[0], data, BlockSize * sizeof(uint32_t)); SHA1ProcessMessageBlock(&context); } } // namespace devilution ================================================ FILE: Source/sha.h ================================================ /** * @file sha.cpp * * Interface of functionality for calculating X-SHA-1 (a flawed implementation of SHA-1). */ #pragma once #include #include namespace devilution { constexpr size_t BlockSize = 16; constexpr size_t SHA1HashSize = 5; struct SHA1Context { uint32_t state[SHA1HashSize] = { 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0 }; uint32_t buffer[BlockSize]; }; void SHA1Result(SHA1Context &context, uint32_t messageDigest[SHA1HashSize]); void SHA1Calculate(SHA1Context &context, const uint32_t data[BlockSize]); } // namespace devilution ================================================ FILE: Source/sound_effect_enums.h ================================================ #pragma once #include #include namespace devilution { enum class HeroSpeech : uint8_t { ChamberOfBoneLore, HorazonsSanctumLore, GolemSpellLore, HorazonsCreatureOfFlameLore, MortaVespaGaieaInnuminoEvegeenJatanLuaGraton, GrimspikeLieutenantOfBelialLore, HorazonsJournal, YourDeathWillBeAvenged, RestInPeaceMyFriend, ValorLore, HallsOfTheBlindLore, WarlordOfBloodLore, ICantUseThisYet, ICantCarryAnymore, IHaveNoRoom, WhereWouldIPutThis, NoWay, NotAChance, IdNeverUseThis, IdHaveToEquipThat, ICantMoveThis, ICantMoveThisYet, ICantOpenThis, ICantOpenThisYet, ICantLiftThis, ICantLiftThisYet, ICantCastThatHere, ICantCastThatYet, ThatDidntDoAnything, ICanAlreadyDoThat, IDontNeedThat, IDontNeedToDoThat, IDontWantThat, IDontHaveASpellReady, NotEnoughMana, ThatWouldKillMe, ICantDoThat, No, Yes, ThatWontWork, ThatWontWorkHere, ThatWontWorkYet, ICantGetThereFromHere, ItsTooHeavy, ItsTooBig, JustWhatIWasLookingFor, IveGotABadFeelingAboutThis, GotMilk, ImNotThirsty, ImNoMilkmaid, ICouldBlowUpTheWholeVillage, YepThatsACowAlright, TooUghHeavy, InSpirituSanctum, PraedictumOtium, EfficioObitusUtInimicus, TheEnchantmentIsGone, OhTooEasy, BackToTheGrave, TimeToDie, ImNotImpressed, ImSorryDidIBreakYourConcentration, VengeanceIsMine, Die, Yeah, Ah, Phew, Argh, ArghClang, Aaaaargh, OofAh, HeavyBreathing, Oh, Wow, ThankTheLight, WhatWasThat, MmHmm, Hmm, UhHuh, TheSpiritsOfTheDeadAreNowAvenged, TheTownIsSafeFromTheseFoulSpawn, RestWellLeoricIllFindYourSon, YourMadnessEndsHereBetrayer, YoullLureNoMoreMenToTheirDeaths, ReturnToHeavenWarriorOfLight, ICanSeeWhyTheyFearThisWeapon, ThisMustBeWhatGriswoldWanted, INeedToGetThisToLachdanan, INeedToGetThisToGriswold, IveNeverBeenHereBefore, MayTheSpiritOfArkaineProtectMe, ThisIsAPlaceOfGreatPower, ThisBladeMustBeDestroyed, YourReignOfPainHasEnded, NowThatsOneBigMushroom, TheSmellOfDeathSurroundsMe, TheSanctityOfThisPlaceHasBeenFouled, ItsHotDownHere, IMustBeGettingClose, MaybeItsLockedFromTheInside, LooksLikeItsRustedShut, MaybeTheresAnotherWay, AuughUh, LAST = AuughUh }; enum class SfxID : int16_t { Walk, ShootBow, CastSpell, Swing, Swing2, WarriorDeath, QuestDone, BarrelExpload, BarrelBreak, ChestOpen, DoorClose, DoorOpen, ItemAnvilFlip, ItemAxeFlip, ItemBloodStoneFlip, ItemBodyPartFlip, ItemBookFlip, ItemBowFlip, ItemCapFlip, ItemArmorFlip, ItemLeatherFlip, ItemMushroomFlip, ItemPotionFlip, ItemRingFlip, ItemRockFlip, ItemScrollFlip, ItemShieldFlip, ItemSignFlip, ItemStaffFlip, ItemSwordFlip, ItemGold, ItemAnvil, ItemAxe, ItemBloodStone, ItemBodyPart, ItemBook, ItemBow, ItemCap, GrabItem, ItemArmor, ItemLeather, ItemMushroom, ItemPotion, ItemRing, ItemRock, ItemScroll, ItemShield, ItemSign, ItemStaff, ItemSword, OperateLever, OperateShrine, OperateShrine1, ReadBook, Sarcophagus, MenuMove, MenuSelect, TriggerTrap, CastFire, CastLightning, CastSkill, SpellEnd, CastHealing, SpellRepair, SpellAcid, SpellAcid1, SpellApocalypse, SpellBloodStar, SpellBloodStarHit, SpellBoneSpirit, SpellBoneSpiritHit, OperateCaldron, SpellChargedBolt, SpellDoomSerpents, // Unused SpellLightningHit, SpellElemental, SpellEtherealize, SpellFirebolt, SpellFireHit, SpellFlameWave, OperateFountain, SpellGolem, OperateGoatShrine, SpellGuardian, SpellHolyBolt, SpellInfravision, SpellInvisibility, // Unused SpellLightning, SpellManaShield, SpellNova, SpellPuddle, SpellResurrect, SpellStoneCurse, SpellPortal, SpellInferno, SpellTrapDisarm, SpellTeleport, SpellFireWall, Gillian1, Gillian2, Gillian3, Gillian4, Gillian5, Gillian6, Gillian7, Gillian8, Gillian9, Gillian10, Gillian11, Gillian12, Gillian13, Gillian14, Gillian15, Gillian16, Gillian17, Gillian18, Gillian19, Gillian20, Gillian21, Gillian22, Gillian23, Gillian24, Gillian25, Gillian26, Gillian27, Gillian28, Gillian29, Gillian30, Gillian31, Gillian32, Gillian33, Gillian34, Gillian35, Gillian36, Gillian37, Gillian38, Gillian39, Gillian40, Griswold1, Griswold2, Griswold3, Griswold4, Griswold5, Griswold6, Griswold7, Griswold8, Griswold9, Griswold10, Griswold12, Griswold13, Griswold14, Griswold15, Griswold16, Griswold17, Griswold18, Griswold19, Griswold20, Griswold21, Griswold22, Griswold23, Griswold24, Griswold25, Griswold26, Griswold27, Griswold28, Griswold29, Griswold30, Griswold31, Griswold32, Griswold33, Griswold34, Griswold35, Griswold36, Griswold37, Griswold38, Griswold39, Griswold40, Griswold41, Griswold42, Griswold43, Griswold44, Griswold45, Griswold46, Griswold47, Griswold48, Griswold49, Griswold50, Griswold51, Griswold52, Griswold53, Griswold55, Griswold56, Cow1, Cow2, WoundedTownsmanOld, // Unused Farnham1, Farnham2, Farnham3, Farnham4, Farnham5, Farnham6, Farnham7, Farnham8, Farnham9, Farnham10, Farnham11, Farnham12, Farnham13, Farnham14, Farnham15, Farnham16, Farnham17, Farnham18, Farnham19, Farnham20, Farnham21, Farnham22, Farnham23, Farnham24, Farnham25, Farnham26, Farnham27, Farnham28, Farnham29, Farnham30, Farnham31, Farnham32, Farnham33, Farnham34, Farnham35, Pepin1, Pepin2, Pepin3, Pepin4, Pepin5, Pepin6, Pepin7, Pepin8, Pepin9, Pepin10, Pepin11, Pepin12, Pepin13, Pepin14, Pepin15, Pepin16, Pepin17, Pepin18, Pepin19, Pepin20, Pepin21, Pepin22, Pepin23, Pepin24, Pepin25, Pepin26, Pepin27, Pepin28, Pepin29, Pepin30, Pepin31, Pepin32, Pepin33, Pepin34, Pepin35, Pepin36, Pepin37, Pepin38, Pepin39, Pepin40, Pepin41, Pepin42, Pepin43, Pepin44, Pepin45, Pepin46, Pepin47, Wirt1, Wirt2, Wirt3, Wirt4, Wirt5, Wirt6, Wirt7, Wirt8, Wirt9, Wirt10, Wirt11, Wirt12, Wirt13, Wirt14, Wirt15, Wirt16, Wirt17, Wirt18, Wirt19, Wirt20, Wirt21, Wirt22, Wirt23, Wirt24, Wirt25, Wirt26, Wirt27, Wirt28, Wirt29, Wirt30, Wirt31, Wirt32, Wirt33, Wirt34, Wirt35, Wirt36, Wirt37, Wirt38, Wirt39, Wirt40, Wirt41, Wirt42, Wirt43, Tremain0, // Unused Tremain1, // Unused Tremain2, // Unused Tremain3, // Unused Tremain4, // Unused Tremain5, // Unused Tremain6, // Unused Tremain7, // Unused Cain0, Cain1, Cain2, Cain3, Cain4, Cain5, Cain6, Cain7, Cain8, Cain9, Cain10, Cain11, Cain12, Cain13, Cain14, Cain15, Cain16, Cain17, Cain18, Cain19, Cain20, Cain21, Cain22, Cain23, Cain24, Cain25, Cain26, Cain27, Cain28, Cain29, Cain30, Cain31, Cain33, Cain34, Cain35, Cain36, Cain37, Cain38, Ogden0, Ogden1, Ogden2, Ogden3, Ogden4, Ogden5, Ogden6, Ogden7, Ogden8, Ogden9, Ogden10, Ogden11, Ogden12, Ogden13, Ogden14, Ogden15, Ogden16, Ogden17, Ogden18, Ogden19, Ogden20, Ogden21, Ogden22, Ogden23, Ogden24, Ogden25, Ogden26, Ogden27, Ogden28, Ogden29, Ogden30, Ogden31, Ogden32, Ogden33, Ogden34, Ogden35, Ogden36, Ogden37, Ogden38, Ogden39, Ogden40, Ogden41, Ogden43, Ogden44, Ogden45, Adria1, Adria2, Adria3, Adria4, Adria5, Adria6, Adria7, Adria8, Adria9, Adria10, Adria11, Adria12, Adria13, Adria14, Adria15, Adria16, Adria17, Adria18, Adria19, Adria20, Adria21, Adria22, Adria23, Adria24, Adria25, Adria26, Adria27, Adria28, Adria29, Adria30, Adria31, Adria32, Adria33, Adria34, Adria35, Adria36, Adria37, Adria38, Adria39, Adria40, Adria41, Adria42, Adria43, Adria44, Adria45, Adria46, Adria47, Adria48, Adria49, Adria50, WoundedTownsman, Sorceror1, Sorceror2, Sorceror3, Sorceror4, Sorceror5, Sorceror6, Sorceror7, Sorceror8, Sorceror9, Sorceror10, Sorceror11, Sorceror12, Sorceror13, Sorceror14, Sorceror15, Sorceror16, Sorceror17, Sorceror18, Sorceror19, Sorceror20, Sorceror21, Sorceror22, Sorceror23, Sorceror24, Sorceror25, Sorceror26, Sorceror27, Sorceror28, Sorceror29, Sorceror30, Sorceror31, Sorceror32, Sorceror33, Sorceror34, Sorceror35, Sorceror36, Sorceror37, Sorceror38, Sorceror39, Sorceror40, Sorceror41, Sorceror42, Sorceror43, Sorceror44, Sorceror45, Sorceror46, Sorceror47, Sorceror48, Sorceror49, Sorceror50, Sorceror51, Sorceror52, Sorceror53, Sorceror54, Sorceror55, Sorceror56, Sorceror57, Sorceror58, Sorceror59, Sorceror60, Sorceror61, Sorceror62, Sorceror63, Sorceror64, Sorceror65, Sorceror66, Sorceror67, Sorceror68, Sorceror69, Sorceror69b, Sorceror70, Sorceror71, Sorceror72, Sorceror73, Sorceror74, Sorceror75, Sorceror76, Sorceror77, Sorceror78, Sorceror79, Sorceror80, Sorceror81, Sorceror82, Sorceror83, Sorceror84, Sorceror85, Sorceror86, Sorceror87, Sorceror88, Sorceror89, Sorceror90, Sorceror91, Sorceror92, Sorceror93, Sorceror94, Sorceror95, Sorceror96, Sorceror97, Sorceror98, Sorceror99, Sorceror100, Sorceror101, Sorceror102, Rogue1, Rogue2, Rogue3, Rogue4, Rogue5, Rogue6, Rogue7, Rogue8, Rogue9, Rogue10, Rogue11, Rogue12, Rogue13, Rogue14, Rogue15, Rogue16, Rogue17, Rogue18, Rogue19, Rogue20, Rogue21, Rogue22, Rogue23, Rogue24, Rogue25, Rogue26, Rogue27, Rogue28, Rogue29, Rogue30, Rogue31, Rogue32, Rogue33, Rogue34, Rogue35, Rogue36, Rogue37, Rogue38, Rogue39, Rogue40, Rogue41, Rogue42, Rogue43, Rogue44, Rogue45, Rogue46, Rogue47, Rogue48, Rogue49, Rogue50, Rogue51, Rogue52, Rogue53, Rogue54, Rogue55, Rogue56, Rogue57, Rogue58, Rogue59, Rogue60, Rogue61, Rogue62, Rogue63, Rogue64, Rogue65, Rogue66, Rogue67, Rogue68, Rogue69, Rogue69b, Rogue70, Rogue71, Rogue72, Rogue73, Rogue74, Rogue75, Rogue76, Rogue77, Rogue78, Rogue79, Rogue80, Rogue81, Rogue82, Rogue83, Rogue84, Rogue85, Rogue86, Rogue87, Rogue88, Rogue89, Rogue90, Rogue91, Rogue92, Rogue93, Rogue94, Rogue95, Rogue96, Rogue97, Rogue98, Rogue99, Rogue100, Rogue101, Rogue102, Warrior1, Warrior2, Warrior3, Warrior4, Warrior5, Warrior6, Warrior7, Warrior8, Warrior9, Warrior10, Warrior11, Warrior12, Warrior13, Warrior14, Warrior14b, Warrior14c, Warrior15, Warrior15b, Warrior15c, Warrior16, Warrior16b, Warrior16c, Warrior17, Warrior18, Warrior19, Warrior20, Warrior21, Warrior22, Warrior23, Warrior24, Warrior25, Warrior26, Warrior27, Warrior28, Warrior29, Warrior30, Warrior31, Warrior32, Warrior33, Warrior34, Warrior35, Warrior36, Warrior37, Warrior38, Warrior39, Warrior40, Warrior41, Warrior42, Warrior43, Warrior44, Warrior45, Warrior46, Warrior47, Warrior48, Warrior49, Warrior50, Warrior51, Warrior52, Warrior53, Warrior54, Warrior55, Warrior56, Warrior57, Warrior58, Warrior59, Warrior60, Warrior61, Warrior62, Warrior63, Warrior64, Warrior65, Warrior66, Warrior67, Warrior68, Warrior69, Warrior69b, Warrior70, Warrior71, Warrior72, Warrior73, Warrior74, Warrior75, Warrior76, Warrior77, Warrior78, Warrior79, Warrior80, Warrior81, Warrior82, Warrior83, Warrior84, Warrior85, Warrior86, Warrior87, Warrior88, Warrior89, Warrior90, Warrior91, Warrior92, Warrior93, Warrior94, Warrior95, Warrior96b, Warrior97, Warrior98, Warrior99, Warrior100, Warrior101, Warrior102, Narrator1, Narrator2, Narrator3, Narrator4, Narrator5, Narrator6, Narrator7, Narrator8, Narrator9, DiabloGreeting, ButcherGreeting, Gharbad1, Gharbad2, Gharbad3, Gharbad4, Lachdanan1, Lachdanan2, Lachdanan3, LazarusGreeting, LeoricGreeting, Snotspill1, Snotspill2, Snotspill3, Warlord, Zhar1, Zhar2, DiabloDeath, ShootBow2, ShootFireballBow, PodExpload, PodPop, UrnExpload, UrnBreak, BigExplosion, SpellLightningWall, Pig, Monk1, Monk8, Monk9, Monk10, Monk11, Monk12, Monk13, Monk14, Monk15, Monk16, Monk24, Monk27, Monk29, Monk34, Monk35, Monk43, Monk46, Monk49, Monk50, Monk52, Monk54, Monk55, Monk56, Monk61, Monk62, Monk68, Monk69, Monk69b, Monk70, Monk71, Monk79, Monk80, Monk82, Monk83, Monk87, Monk88, Monk89, Monk91, Monk92, Monk94, Monk95, Monk96, Monk97, Monk98, Monk99, Farmer1, Farmer2, Farmer2a, Farmer3, Farmer4, Farmer5, Farmer6, Farmer7, Farmer8, Farmer9, Celia1, Celia2, Celia3, Celia4, Defiler1, Defiler2, Defiler3, Defiler4, Defiler8, Defiler6, Defiler7, NaKrul1, NaKrul2, NaKrul3, NaKrul4, NaKrul5, NaKrul6, NarratorHF3, CompleteNut1, CompleteNut2, CompleteNut3, CompleteNut4, CompleteNut4a, CompleteNut5, CompleteNut6, CompleteNut7, CompleteNut8, CompleteNut9, CompleteNut10, CompleteNut11, CompleteNut12, NarratorHF6, NarratorHF7, NarratorHF8, NarratorHF5, NarratorHF9, NarratorHF4, CryptDoorOpen, CryptDoorClose, AccessibilityWeapon, AccessibilityArmor, AccessibilityGold, AccessibilityPotion, AccessibilityScroll, AccessibilityChest, AccessibilityDoor, AccessibilityStairs, AccessibilityMonster, AccessibilityInteract, LAST = AccessibilityInteract, None = -1, }; enum sfx_flag : uint8_t { // clang-format off sfx_STREAM = 1 << 0, sfx_MISC = 1 << 1, sfx_UI = 1 << 2, sfx_MONK = 1 << 3, sfx_ROGUE = 1 << 4, sfx_WARRIOR = 1 << 5, sfx_SORCERER = 1 << 6, // clang-format on }; } // namespace devilution template <> struct magic_enum::customize::enum_range { static constexpr int min = static_cast(devilution::SfxID::None); static constexpr int max = static_cast(devilution::SfxID::LAST); }; ================================================ FILE: Source/spells.cpp ================================================ /** * @file spells.cpp * * Implementation of functionality for casting player spells. */ #include "spells.h" #include "control/control.hpp" #include "cursor.h" #ifdef _DEBUG #include "debug.h" #endif #include "engine/backbuffer_state.hpp" #include "engine/point.hpp" #include "engine/random.hpp" #include "engine/world_tile.hpp" #include "game_mode.hpp" #include "gamemenu.h" #include "inv.h" #include "missiles.h" namespace devilution { namespace { /** * @brief Gets a value indicating whether the player's current readied spell is a valid spell. Readied spells can be * invalidaded in a few scenarios where the spell comes from items, for example (like dropping the only scroll that * provided the spell). * @param player The player whose readied spell is to be checked. * @return 'true' when the readied spell is currently valid, and 'false' otherwise. */ bool IsReadiedSpellValid(const Player &player) { switch (player._pRSplType) { case SpellType::Skill: case SpellType::Spell: case SpellType::Invalid: return true; case SpellType::Charges: return (player._pISpells & GetSpellBitmask(player._pRSpell)) != 0; case SpellType::Scroll: return (player._pScrlSpells & GetSpellBitmask(player._pRSpell)) != 0; default: return false; } } /** * @brief Clears the current player's readied spell selection. * @note Will force a UI redraw in case the values actually change, so that the new spell reflects on the bottom panel. * @param player The player whose readied spell is to be cleared. */ void ClearReadiedSpell(Player &player) { if (player._pRSpell != SpellID::Invalid) { player._pRSpell = SpellID::Invalid; RedrawEverything(); } if (player._pRSplType != SpellType::Invalid) { player._pRSplType = SpellType::Invalid; RedrawEverything(); } } } // namespace bool IsValidSpell(SpellID spl) { return spl > SpellID::Null && static_cast(spl) < SpellsData.size(); } bool IsValidSpellFrom(int spellFrom) { if (spellFrom == 0) return true; if (spellFrom >= INVITEM_INV_FIRST && spellFrom <= INVITEM_INV_LAST) return true; if (spellFrom >= INVITEM_BELT_FIRST && spellFrom <= INVITEM_BELT_LAST) return true; return false; } bool IsWallSpell(SpellID spl) { return spl == SpellID::FireWall || spl == SpellID::LightningWall; } bool TargetsMonster(SpellID id) { return id == SpellID::Fireball || id == SpellID::FireWall || id == SpellID::Inferno || id == SpellID::Lightning || id == SpellID::StoneCurse || id == SpellID::FlameWave; } int GetManaAmount(const Player &player, SpellID sn) { int ma; // mana amount // mana adjust int adj = 0; // spell level const int sl = std::max(player.GetSpellLevel(sn) - 1, 0); if (sl > 0) { adj = sl * GetSpellData(sn).sManaAdj; } if (sn == SpellID::Firebolt) { adj /= 2; } if (sn == SpellID::Resurrect && sl > 0) { adj = sl * (GetSpellData(SpellID::Resurrect).sManaCost / 8); } if (sn == SpellID::Healing || sn == SpellID::HealOther) { ma = (GetSpellData(SpellID::Healing).sManaCost + 2 * player.getCharacterLevel() - adj); } else if (GetSpellData(sn).sManaCost == 255) { ma = (player._pMaxManaBase >> 6) - adj; } else { ma = (GetSpellData(sn).sManaCost - adj); } ma = std::max(ma, 0); ma <<= 6; if (gbIsHellfire && player._pClass == HeroClass::Sorcerer) { ma /= 2; } else if (player._pClass == HeroClass::Rogue || player._pClass == HeroClass::Monk || player._pClass == HeroClass::Bard) { ma -= ma / 4; } if (GetSpellData(sn).sMinMana > ma >> 6) { ma = GetSpellData(sn).sMinMana << 6; } return ma; } void ConsumeSpell(Player &player, SpellID sn) { switch (player.executedSpell.spellType) { case SpellType::Skill: case SpellType::Invalid: break; case SpellType::Scroll: ConsumeScroll(player); break; case SpellType::Charges: ConsumeStaffCharge(player); break; case SpellType::Spell: #ifdef _DEBUG if (DebugGodMode) break; #endif int ma = GetManaAmount(player, sn); player._pMana -= ma; player._pManaBase -= ma; RedrawComponent(PanelDrawComponent::Mana); break; } if (sn == SpellID::BloodStar) { ApplyPlrDamage(DamageType::Physical, player, 5); } if (sn == SpellID::BoneSpirit) { ApplyPlrDamage(DamageType::Physical, player, 6); } } void EnsureValidReadiedSpell(Player &player) { if (!IsReadiedSpellValid(player)) { ClearReadiedSpell(player); } } SpellCheckResult CheckSpell(const Player &player, SpellID sn, SpellType st, bool manaonly) { #ifdef _DEBUG if (DebugGodMode) return SpellCheckResult::Success; #endif if (!manaonly && pcurs != CURSOR_HAND) { return SpellCheckResult::Fail_Busy; } if (st == SpellType::Skill) { return SpellCheckResult::Success; } if (player.GetSpellLevel(sn) <= 0) { return SpellCheckResult::Fail_Level0; } if (player._pMana < GetManaAmount(player, sn) || HasAnyOf(player._pIFlags, ItemSpecialEffect::NoMana)) { return SpellCheckResult::Fail_NoMana; } return SpellCheckResult::Success; } void CastSpell(Player &player, SpellID spl, WorldTilePosition src, WorldTilePosition dst, int spllvl) { Direction dir = player._pdir; if (IsWallSpell(spl)) { dir = player.tempDirection; } bool fizzled = false; const SpellData &spellData = GetSpellData(spl); for (size_t i = 0; i < sizeof(spellData.sMissiles) / sizeof(spellData.sMissiles[0]) && spellData.sMissiles[i] != MissileID::Null; i++) { Missile *missile = AddMissile(src, dst, dir, spellData.sMissiles[i], TARGET_MONSTERS, player, 0, spllvl); fizzled |= (missile == nullptr); } if (spl == SpellID::ChargedBolt) { for (int i = (spllvl / 2) + 3; i > 0; i--) { Missile *missile = AddMissile(src, dst, dir, MissileID::ChargedBolt, TARGET_MONSTERS, player, 0, spllvl); fizzled |= (missile == nullptr); } } if (!fizzled) { ConsumeSpell(player, spl); } } void SpawnResurrectBeam(Player &caster, Player &target) { AddMissile( target.position.tile, target.position.tile, Direction::South, MissileID::ResurrectBeam, TARGET_MONSTERS, caster.getId(), 0, 0); } void ApplyResurrect(Player &target) { if (&target == MyPlayer) { MyPlayerIsDead = false; gamemenu_off(); RedrawComponent(PanelDrawComponent::Health); RedrawComponent(PanelDrawComponent::Mana); } ClrPlrPath(target); target.destAction = ACTION_NONE; target._pInvincible = false; SyncInitPlrPos(target); int hp = 10 << 6; if (target._pMaxHPBase < (10 << 6)) { hp = target._pMaxHPBase; } SetPlayerHitPoints(target, hp); target._pHPBase = target._pHitPoints + (target._pMaxHPBase - target._pMaxHP); // CODEFIX: does the same stuff as SetPlayerHitPoints above, can be removed target._pMana = 0; target._pManaBase = target._pMana + (target._pMaxManaBase - target._pMaxMana); target._pmode = PM_STAND; CalcPlrInv(target, true); if (target.isOnActiveLevel()) { StartStand(target, target._pdir); } } void DoHealOther(const Player &caster, Player &target) { if (target.hasNoLife()) { return; } int hp = (GenerateRnd(10) + 1) << 6; for (unsigned i = 0; i < caster.getCharacterLevel(); i++) { hp += (GenerateRnd(4) + 1) << 6; } for (int i = 0; i < caster.GetSpellLevel(SpellID::HealOther); i++) { hp += (GenerateRnd(6) + 1) << 6; } if (caster._pClass == HeroClass::Warrior || caster._pClass == HeroClass::Barbarian) { hp *= 2; } else if (caster._pClass == HeroClass::Rogue || caster._pClass == HeroClass::Bard) { hp += hp / 2; } else if (caster._pClass == HeroClass::Monk) { hp *= 3; } target._pHitPoints = std::min(target._pHitPoints + hp, target._pMaxHP); target._pHPBase = std::min(target._pHPBase + hp, target._pMaxHPBase); if (&target == MyPlayer) { RedrawComponent(PanelDrawComponent::Health); } } int GetSpellBookLevel(SpellID s) { if (gbIsSpawn) { switch (s) { case SpellID::StoneCurse: case SpellID::Guardian: case SpellID::Golem: case SpellID::Elemental: case SpellID::BloodStar: case SpellID::BoneSpirit: return -1; default: break; } } if (static_cast(s) >= SpellsData.size()) { return -1; } return GetSpellData(s).sBookLvl; } int GetSpellStaffLevel(SpellID s) { if (gbIsSpawn) { switch (s) { case SpellID::StoneCurse: case SpellID::Guardian: case SpellID::Golem: case SpellID::Apocalypse: case SpellID::Elemental: case SpellID::BloodStar: case SpellID::BoneSpirit: return -1; default: break; } } if (static_cast(s) >= SpellsData.size()) { return -1; } return GetSpellData(s).sStaffLvl; } } // namespace devilution ================================================ FILE: Source/spells.h ================================================ /** * @file spells.h * * Interface of functionality for casting player spells. */ #pragma once #include #include "engine/world_tile.hpp" #include "player.h" namespace devilution { enum class SpellCheckResult : uint8_t { Success, Fail_NoMana, Fail_Level0, Fail_Busy, }; bool IsValidSpell(SpellID spl); bool IsValidSpellFrom(int spellFrom); bool IsWallSpell(SpellID spl); bool TargetsMonster(SpellID id); int GetManaAmount(const Player &player, SpellID sn); void ConsumeSpell(Player &player, SpellID sn); SpellCheckResult CheckSpell(const Player &player, SpellID sn, SpellType st, bool manaonly); /** * @brief Ensures the player's current readied spell is a valid selection for the character. If the current selection is * incompatible with the player's items and spell (for example, if the player does not currently have access to the spell), * the selection is cleared. * @note Will force a UI redraw in case the values actually change, so that the new spell reflects on the bottom panel. * @param player The player whose readied spell is to be checked. */ void EnsureValidReadiedSpell(Player &player); void CastSpell(Player &player, SpellID spl, WorldTilePosition src, WorldTilePosition dst, int spllvl); void SpawnResurrectBeam(Player &caster, Player &target); void ApplyResurrect(Player &target); void DoHealOther(const Player &caster, Player &target); int GetSpellBookLevel(SpellID s); int GetSpellStaffLevel(SpellID s); /** * @brief Gets a value that represents the specified spellID in 64bit bitmask format. * For example: * - spell ID 1: 0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000.0001 * - spell ID 43: 0000.0000.0000.0000.0000.0100.0000.0000.0000.0000.0000.0000.0000.0000.0000.0000 * @param spellId The id of the spell to get a bitmask for. * @return A 64bit bitmask representation for the specified spell. */ constexpr uint64_t GetSpellBitmask(SpellID spellId) { return 1ULL << (static_cast(spellId) - 1); } } // namespace devilution ================================================ FILE: Source/stores.cpp ================================================ /** * @file stores.cpp * * Implementation of functionality for stores and towner dialogs. */ #include "stores.h" #include #include #include #include #include "controls/control_mode.hpp" #include "controls/plrctrls.h" #include "cursor.h" #include "engine/backbuffer_state.hpp" #include "engine/random.hpp" #include "engine/render/clx_render.hpp" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" #include "engine/trn.hpp" #include "game_mode.hpp" #include "lua/lua_event.hpp" #include "minitext.h" #include "multi.h" #include "options.h" #include "panels/info_box.hpp" #include "qol/stash.h" #include "tables/townerdat.hpp" #include "towners.h" #include "utils/format_int.hpp" #include "utils/language.h" #include "utils/str_cat.hpp" #include "utils/utf8.hpp" namespace devilution { TalkID ActiveStore; int CurrentItemIndex; int8_t PlayerItemIndexes[48]; Item PlayerItems[48]; StaticVector SmithItems; int PremiumItemCount; int PremiumItemLevel; StaticVector PremiumItems; StaticVector HealerItems; StaticVector WitchItems; int BoyItemLevel; Item BoyItem; namespace { /** The current towner being interacted with */ _talker_id TownerId; /** Is the current dialog full size */ bool IsTextFullSize; /** Number of text lines in the current dialog */ int NumTextLines; /** Remember currently selected text line from TextLine while displaying a dialog */ int OldTextLine; /** Currently selected text line from TextLine */ int CurrentTextLine; struct STextStruct { enum Type : uint8_t { Label, Divider, Selectable, }; std::string text; int _sval; int y; UiFlags flags; Type type; uint8_t _sx; uint8_t _syoff; int cursId; bool cursIndent; [[nodiscard]] bool isDivider() const { return type == Divider; } [[nodiscard]] bool isSelectable() const { return type == Selectable; } [[nodiscard]] bool hasText() const { return !text.empty(); } }; /** Text lines */ STextStruct TextLine[NumStoreLines]; /** Whether to render the player's gold amount in the top left */ bool RenderGold; /** Does the current panel have a scrollbar */ bool HasScrollbar; /** Remember last scroll position */ int OldScrollPos; /** Scroll position */ int ScrollPos; /** Next scroll position */ int NextScrollPos; /** Previous scroll position */ int PreviousScrollPos; /** Countdown for the push state of the scroll up button */ int8_t CountdownScrollUp; /** Countdown for the push state of the scroll down button */ int8_t CountdownScrollDown; /** Remember current store while displaying a dialog */ TalkID OldActiveStore; /** Temporary item used to hold the item being traded */ Item TempItem; /** Maps from towner IDs to NPC names. */ const char *const TownerNames[] = { N_("Griswold"), N_("Pepin"), "", N_("Ogden"), N_("Cain"), N_("Farnham"), N_("Adria"), N_("Gillian"), N_("Wirt"), }; constexpr int PaddingTop = 32; // For most languages, line height is always 12. // This includes blank lines and divider line. constexpr int SmallLineHeight = 12; constexpr int SmallTextHeight = 12; // For larger small fonts (Chinese and Japanese), text lines are // taller and overflow. // We space out blank lines a bit more to give space to 3-line store items. constexpr int LargeLineHeight = SmallLineHeight + 1; constexpr int LargeTextHeight = 18; /** * The line index with the Back / Leave button. * This is a special button that is always the last line. * * For lists with a scrollbar, it is not selectable (mouse-only). */ int BackButtonLine() { if (IsSmallFontTall()) { return HasScrollbar ? 21 : 20; } return 22; } int LineHeight() { return IsSmallFontTall() ? LargeLineHeight : SmallLineHeight; } int TextHeight() { return IsSmallFontTall() ? LargeTextHeight : SmallTextHeight; } void CalculateLineHeights() { TextLine[0].y = 0; if (IsSmallFontTall()) { for (int i = 1; i < NumStoreLines; ++i) { // Space out consecutive text lines, unless they are both selectable (never the case currently). if (TextLine[i].hasText() && TextLine[i - 1].hasText() && !(TextLine[i].isSelectable() && TextLine[i - 1].isSelectable())) { TextLine[i].y = TextLine[i - 1].y + LargeTextHeight; } else { TextLine[i].y = i * LargeLineHeight; } } } else { for (int i = 1; i < NumStoreLines; ++i) { TextLine[i].y = i * SmallLineHeight; } } } void DrawSTextBack(const Surface &out) { const Point uiPosition = GetUIRectangle().position; ClxDraw(out, { uiPosition.x + 320 + 24, 327 + uiPosition.y }, (*pSTextBoxCels)[0]); DrawHalfTransparentRectTo(out, uiPosition.x + 347, uiPosition.y + 28, 265, 297); } void DrawSSlider(const Surface &out, int y1, int y2) { const Point uiPosition = GetUIRectangle().position; int yd1 = y1 * 12 + 44 + uiPosition.y; const int yd2 = y2 * 12 + 44 + uiPosition.y; if (CountdownScrollUp != -1) ClxDraw(out, { uiPosition.x + 601, yd1 }, (*pSTextSlidCels)[11]); else ClxDraw(out, { uiPosition.x + 601, yd1 }, (*pSTextSlidCels)[9]); if (CountdownScrollDown != -1) ClxDraw(out, { uiPosition.x + 601, yd2 }, (*pSTextSlidCels)[10]); else ClxDraw(out, { uiPosition.x + 601, yd2 }, (*pSTextSlidCels)[8]); yd1 += 12; int yd3 = yd1; for (; yd3 < yd2; yd3 += 12) { ClxDraw(out, { uiPosition.x + 601, yd3 }, (*pSTextSlidCels)[13]); } if (CurrentTextLine == BackButtonLine()) yd3 = OldTextLine; else yd3 = CurrentTextLine; if (CurrentItemIndex > 1) yd3 = 1000 * (ScrollPos + ((yd3 - PreviousScrollPos) / 4)) / (CurrentItemIndex - 1) * (y2 * 12 - y1 * 12 - 24) / 1000; else yd3 = 0; ClxDraw(out, { uiPosition.x + 601, (y1 + 1) * 12 + 44 + uiPosition.y + yd3 }, (*pSTextSlidCels)[12]); } void AddSLine(size_t y) { TextLine[y]._sx = 0; TextLine[y]._syoff = 0; TextLine[y].text.clear(); TextLine[y].text.shrink_to_fit(); TextLine[y].type = STextStruct::Divider; TextLine[y].cursId = -1; TextLine[y].cursIndent = false; } void AddSTextVal(size_t y, int val) { TextLine[y]._sval = val; } void AddSText(uint8_t x, size_t y, std::string_view text, UiFlags flags, bool sel, int cursId = -1, bool cursIndent = false) { TextLine[y]._sx = x; TextLine[y]._syoff = 0; TextLine[y].text.clear(); TextLine[y].text.append(text); TextLine[y].flags = flags; TextLine[y].type = sel ? STextStruct::Selectable : STextStruct::Label; TextLine[y].cursId = cursId; TextLine[y].cursIndent = cursIndent; } void AddOptionsBackButton() { const int line = BackButtonLine(); AddSText(0, line, _("Back"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); TextLine[line]._syoff = IsSmallFontTall() ? 0 : 6; } void AddItemListBackButton(bool selectable = false) { const int line = BackButtonLine(); const std::string_view text = _("Back"); if (!selectable && IsSmallFontTall()) { AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignRight, selectable); } else { AddSLine(line - 1); AddSText(0, line, text, UiFlags::ColorWhite | UiFlags::AlignCenter, selectable); TextLine[line]._syoff = 6; } } void PrintStoreItem(const Item &item, int l, UiFlags flags, bool cursIndent = false) { std::string productLine; if (item._iIdentified) { if (item._iMagical != ITEM_QUALITY_UNIQUE) { if (item._iPrePower != -1) { productLine.append(PrintItemPower(item._iPrePower, item)); } } if (item._iSufPower != -1) { if (!productLine.empty()) productLine.append(_(", ")); productLine.append(PrintItemPower(item._iSufPower, item)); } } if (item._iMiscId == IMISC_STAFF && item._iMaxCharges != 0) { if (!productLine.empty()) productLine.append(_(", ")); productLine.append(fmt::format(fmt::runtime(_("Charges: {:d}/{:d}")), item._iCharges, item._iMaxCharges)); } if (!productLine.empty()) { AddSText(40, l, productLine, flags, false, -1, cursIndent); l++; productLine.clear(); } if (item._itype != ItemType::Misc) { if (item._iClass == ICLASS_WEAPON) productLine = fmt::format(fmt::runtime(_("Damage: {:d}-{:d} ")), item._iMinDam, item._iMaxDam); else if (item._iClass == ICLASS_ARMOR) productLine = fmt::format(fmt::runtime(_("Armor: {:d} ")), item._iAC); if (item._iMaxDur != DUR_INDESTRUCTIBLE && item._iMaxDur != 0) productLine += fmt::format(fmt::runtime(_("Dur: {:d}/{:d}")), item._iDurability, item._iMaxDur); else productLine.append(_("Indestructible")); } int8_t str = item._iMinStr; uint8_t mag = item._iMinMag; int8_t dex = item._iMinDex; if (str != 0 || mag != 0 || dex != 0) { if (!productLine.empty()) productLine.append(_(", ")); productLine.append(_("Required:")); if (str != 0) productLine.append(fmt::format(fmt::runtime(_(" {:d} Str")), str)); if (mag != 0) productLine.append(fmt::format(fmt::runtime(_(" {:d} Mag")), mag)); if (dex != 0) productLine.append(fmt::format(fmt::runtime(_(" {:d} Dex")), dex)); } AddSText(40, l++, productLine, flags, false, -1, cursIndent); } bool StoreAutoPlace(Item &item, bool persistItem) { Player &player = *MyPlayer; if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) { return true; } if (AutoPlaceItemInBelt(player, item, persistItem, true)) { return true; } if (persistItem) { return AutoPlaceItemInInventory(player, item, true); } return CanFitItemInInventory(player, item); } void ScrollVendorStore(std::span itemData, int storeLimit, int idx, int selling = true) { ClearSText(5, 21); PreviousScrollPos = 5; for (int l = 5; l < 20 && idx < storeLimit; l += 4) { const Item &item = itemData[idx]; const UiFlags itemColor = item.getTextColorWithStatCheck(); AddSText(20, l, item.getName(), itemColor, true, item._iCurs, true); AddSTextVal(l, item._iIdentified ? item._iIvalue : item._ivalue); PrintStoreItem(item, l + 1, itemColor, true); NextScrollPos = l; idx++; } if (selling) { if (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable() && CurrentTextLine != BackButtonLine()) CurrentTextLine = NextScrollPos; } else { NumTextLines = std::max(static_cast(storeLimit) - 4, 0); } } void StartSmith() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 3, _("Blacksmith's shop"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 7, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 10, _("Talk to Griswold"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 12, _("Buy basic items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 14, _("Buy premium items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 16, _("Sell items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 18, _("Repair items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 20, _("Leave the shop"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void ScrollSmithBuy(int idx) { ScrollVendorStore(SmithItems, static_cast(SmithItems.size()), idx); } uint32_t TotalPlayerGold() { return MyPlayer->_pGold + Stash.gold; } // TODO: Change `_iIvalue` to be unsigned instead of passing `int` here. bool PlayerCanAfford(int price) { return TotalPlayerGold() >= static_cast(price); } void StartSmithBuy() { IsTextFullSize = true; HasScrollbar = true; ScrollPos = 0; RenderGold = true; AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithBuy(ScrollPos); AddItemListBackButton(); CurrentItemIndex = 0; for (Item &item : SmithItems) { item._iStatFlag = MyPlayer->CanUseItem(item); CurrentItemIndex++; } NumTextLines = std::max(CurrentItemIndex - 4, 0); } void ScrollSmithPremiumBuy(int boughtitems) { int idx = 0; for (; boughtitems != 0; idx++) { if (!PremiumItems[idx].isEmpty()) boughtitems--; } ScrollVendorStore(PremiumItems, static_cast(PremiumItems.size()), idx); } bool StartSmithPremiumBuy() { CurrentItemIndex = 0; for (Item &item : PremiumItems) { item._iStatFlag = MyPlayer->CanUseItem(item); CurrentItemIndex++; } if (CurrentItemIndex == 0) { StartStore(TalkID::Smith); CurrentTextLine = 14; return false; } IsTextFullSize = true; HasScrollbar = true; ScrollPos = 0; RenderGold = true; AddSText(20, 1, _("I have these premium items for sale:"), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(); NumTextLines = std::max(CurrentItemIndex - 4, 0); ScrollSmithPremiumBuy(ScrollPos); return true; } bool SmithSellOk(int i) { Item *pI; if (i >= 0) { pI = &MyPlayer->InvList[i]; } else { pI = &MyPlayer->SpdList[-(i + 1)]; } if (pI->isEmpty()) return false; if (pI->_iMiscId > IMISC_OILFIRST && pI->_iMiscId < IMISC_OILLAST) return true; if (pI->_itype == ItemType::Misc) return false; if (pI->_itype == ItemType::Gold) return false; if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell))) return false; if (pI->_iClass == ICLASS_QUEST) return false; if (pI->IDidx == IDI_LAZSTAFF) return false; return true; } void ScrollSmithSell(int idx) { ScrollVendorStore(PlayerItems, CurrentItemIndex, idx, false); } void StartSmithSell() { IsTextFullSize = true; bool sellOk = false; CurrentItemIndex = 0; for (auto &item : PlayerItems) { item.clear(); } const Player &myPlayer = *MyPlayer; for (int8_t i = 0; i < myPlayer._pNumInv; i++) { if (CurrentItemIndex >= 48) break; if (SmithSellOk(i)) { sellOk = true; PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; PlayerItemIndexes[CurrentItemIndex] = i; CurrentItemIndex++; } } for (int i = 0; i < MaxBeltItems; i++) { if (CurrentItemIndex >= 48) break; if (SmithSellOk(-(i + 1))) { sellOk = true; PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i]; if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; PlayerItemIndexes[CurrentItemIndex] = -(i + 1); CurrentItemIndex++; } } if (!sellOk) { HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(/*selectable=*/true); return; } HasScrollbar = true; ScrollPos = 0; NumTextLines = myPlayer._pNumInv; RenderGold = true; AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithSell(ScrollPos); AddItemListBackButton(); } bool SmithRepairOk(int i) { const Player &myPlayer = *MyPlayer; const Item &item = myPlayer.InvList[i]; if (item.isEmpty()) return false; if (item._itype == ItemType::Misc) return false; if (item._itype == ItemType::Gold) return false; if (item._iDurability == item._iMaxDur) return false; if (item._iMaxDur == DUR_INDESTRUCTIBLE) return false; return true; } void StartSmithRepair() { IsTextFullSize = true; CurrentItemIndex = 0; for (auto &item : PlayerItems) { item.clear(); } Player &myPlayer = *MyPlayer; auto &helmet = myPlayer.InvBody[INVLOC_HEAD]; if (!helmet.isEmpty() && helmet._iDurability != helmet._iMaxDur) { AddStoreHoldRepair(&helmet, -1); } auto &armor = myPlayer.InvBody[INVLOC_CHEST]; if (!armor.isEmpty() && armor._iDurability != armor._iMaxDur) { AddStoreHoldRepair(&armor, -2); } auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; if (!leftHand.isEmpty() && leftHand._iDurability != leftHand._iMaxDur) { AddStoreHoldRepair(&leftHand, -3); } auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT]; if (!rightHand.isEmpty() && rightHand._iDurability != rightHand._iMaxDur) { AddStoreHoldRepair(&rightHand, -4); } for (int i = 0; i < myPlayer._pNumInv; i++) { if (CurrentItemIndex >= 48) break; if (SmithRepairOk(i)) { AddStoreHoldRepair(&myPlayer.InvList[i], i); } } if (CurrentItemIndex == 0) { HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("You have nothing to repair."), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(/*selectable=*/true); return; } HasScrollbar = true; ScrollPos = 0; NumTextLines = myPlayer._pNumInv; RenderGold = true; AddSText(20, 1, _("Repair which item?"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithSell(ScrollPos); AddItemListBackButton(); } void StartWitch() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("Witch's shack"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Adria"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 14, _("Buy items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 16, _("Sell items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 18, _("Recharge staves"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 20, _("Leave the shack"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void ScrollWitchBuy(int idx) { ScrollVendorStore(WitchItems, static_cast(WitchItems.size()), idx); } void WitchBookLevel(Item &bookItem) { if (bookItem._iMiscId != IMISC_BOOK) return; bookItem._iMinMag = GetSpellData(bookItem._iSpell).minInt; uint8_t spellLevel = MyPlayer->_pSplLvl[static_cast(bookItem._iSpell)]; while (spellLevel > 0) { bookItem._iMinMag += 20 * bookItem._iMinMag / 100; spellLevel--; if (bookItem._iMinMag + 20 * bookItem._iMinMag / 100 > 255) { bookItem._iMinMag = 255; spellLevel = 0; } } } void StartWitchBuy() { IsTextFullSize = true; HasScrollbar = true; ScrollPos = 0; NumTextLines = 20; RenderGold = true; AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollWitchBuy(ScrollPos); AddItemListBackButton(); CurrentItemIndex = 0; for (Item &item : WitchItems) { WitchBookLevel(item); item._iStatFlag = MyPlayer->CanUseItem(item); CurrentItemIndex++; } NumTextLines = std::max(CurrentItemIndex - 4, 0); } bool WitchSellOk(int i) { Item *pI; bool rv = false; if (i >= 0) pI = &MyPlayer->InvList[i]; else pI = &MyPlayer->SpdList[-(i + 1)]; if (pI->_itype == ItemType::Misc) rv = true; if (pI->_iMiscId > 29 && pI->_iMiscId < 41) rv = false; if (pI->_iClass == ICLASS_QUEST) rv = false; if (pI->_itype == ItemType::Staff && (!gbIsHellfire || IsValidSpell(pI->_iSpell))) rv = true; if (pI->IDidx >= IDI_FIRSTQUEST && pI->IDidx <= IDI_LASTQUEST) rv = false; if (pI->IDidx == IDI_LAZSTAFF) rv = false; return rv; } void StartWitchSell() { IsTextFullSize = true; bool sellok = false; CurrentItemIndex = 0; for (auto &item : PlayerItems) { item.clear(); } const Player &myPlayer = *MyPlayer; for (int i = 0; i < myPlayer._pNumInv; i++) { if (CurrentItemIndex >= 48) break; if (WitchSellOk(i)) { sellok = true; PlayerItems[CurrentItemIndex] = myPlayer.InvList[i]; if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; PlayerItemIndexes[CurrentItemIndex] = i; CurrentItemIndex++; } } for (int i = 0; i < MaxBeltItems; i++) { if (CurrentItemIndex >= 48) break; if (!myPlayer.SpdList[i].isEmpty() && WitchSellOk(-(i + 1))) { sellok = true; PlayerItems[CurrentItemIndex] = myPlayer.SpdList[i]; if (PlayerItems[CurrentItemIndex]._iMagical != ITEM_QUALITY_NORMAL && PlayerItems[CurrentItemIndex]._iIdentified) PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._iIvalue; PlayerItems[CurrentItemIndex]._ivalue = std::max(PlayerItems[CurrentItemIndex]._ivalue / 4, 1); PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; PlayerItemIndexes[CurrentItemIndex] = -(i + 1); CurrentItemIndex++; } } if (!sellok) { HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("You have nothing I want."), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(/*selectable=*/true); return; } HasScrollbar = true; ScrollPos = 0; NumTextLines = myPlayer._pNumInv; RenderGold = true; AddSText(20, 1, _("Which item is for sale?"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithSell(ScrollPos); AddItemListBackButton(); } bool WitchRechargeOk(int i) { const auto &item = MyPlayer->InvList[i]; if (item._itype == ItemType::Staff && item._iCharges != item._iMaxCharges) { return true; } if ((item._iMiscId == IMISC_UNIQUE || item._iMiscId == IMISC_STAFF) && item._iCharges < item._iMaxCharges) { return true; } return false; } void AddStoreHoldRecharge(Item itm, int8_t i) { PlayerItems[CurrentItemIndex] = itm; PlayerItems[CurrentItemIndex]._ivalue += GetSpellData(itm._iSpell).staffCost(); PlayerItems[CurrentItemIndex]._ivalue = PlayerItems[CurrentItemIndex]._ivalue * (PlayerItems[CurrentItemIndex]._iMaxCharges - PlayerItems[CurrentItemIndex]._iCharges) / (PlayerItems[CurrentItemIndex]._iMaxCharges * 2); PlayerItems[CurrentItemIndex]._iIvalue = PlayerItems[CurrentItemIndex]._ivalue; PlayerItemIndexes[CurrentItemIndex] = i; CurrentItemIndex++; } void StartWitchRecharge() { IsTextFullSize = true; bool rechargeok = false; CurrentItemIndex = 0; for (auto &item : PlayerItems) { item.clear(); } const Player &myPlayer = *MyPlayer; const auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; if ((leftHand._itype == ItemType::Staff || leftHand._iMiscId == IMISC_UNIQUE) && leftHand._iCharges != leftHand._iMaxCharges) { rechargeok = true; AddStoreHoldRecharge(leftHand, -1); } for (int i = 0; i < myPlayer._pNumInv; i++) { if (CurrentItemIndex >= 48) break; if (WitchRechargeOk(i)) { rechargeok = true; AddStoreHoldRecharge(myPlayer.InvList[i], i); } } if (!rechargeok) { HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("You have nothing to recharge."), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(/*selectable=*/true); return; } HasScrollbar = true; ScrollPos = 0; NumTextLines = myPlayer._pNumInv; RenderGold = true; AddSText(20, 1, _("Recharge which item?"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithSell(ScrollPos); AddItemListBackButton(); } void StoreNoMoney() { StartStore(OldActiveStore); HasScrollbar = false; IsTextFullSize = true; RenderGold = true; ClearSText(5, 23); AddSText(0, 14, _("You do not have enough gold"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } void StoreNoRoom() { StartStore(OldActiveStore); HasScrollbar = false; ClearSText(5, 23); AddSText(0, 14, _("You do not have enough room in inventory"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } void StoreConfirm(Item &item) { StartStore(OldActiveStore); HasScrollbar = false; ClearSText(5, 23); const UiFlags itemColor = item.getTextColorWithStatCheck(); AddSText(20, 8, item.getName(), itemColor, false); AddSTextVal(8, item._iIvalue); PrintStoreItem(item, 9, itemColor); std::string_view prompt; switch (OldActiveStore) { case TalkID::BoyBuy: prompt = _("Do we have a deal?"); break; case TalkID::StorytellerIdentify: prompt = _("Are you sure you want to identify this item?"); break; case TalkID::HealerBuy: case TalkID::SmithPremiumBuy: case TalkID::WitchBuy: case TalkID::SmithBuy: prompt = _("Are you sure you want to buy this item?"); break; case TalkID::WitchRecharge: prompt = _("Are you sure you want to recharge this item?"); break; case TalkID::SmithSell: case TalkID::WitchSell: prompt = _("Are you sure you want to sell this item?"); break; case TalkID::SmithRepair: prompt = _("Are you sure you want to repair this item?"); break; default: app_fatal(StrCat("Unknown store dialog ", static_cast(OldActiveStore))); } AddSText(0, 15, prompt, UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddSText(0, 18, _("Yes"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 20, _("No"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } void StartBoy() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("Wirt the Peg-legged boy"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSLine(5); if (!BoyItem.isEmpty()) { AddSText(0, 8, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 12, _("I have something for sale,"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 14, _("but it will cost 50 gold"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 16, _("just to take a look. "), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 18, _("What have you got?"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 20, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } else { AddSText(0, 12, _("Talk to Wirt"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } } void SStartBoyBuy() { IsTextFullSize = true; HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("I have this item for sale:"), UiFlags::ColorWhitegold, false); AddSLine(3); BoyItem._iStatFlag = MyPlayer->CanUseItem(BoyItem); const UiFlags itemColor = BoyItem.getTextColorWithStatCheck(); AddSText(20, 10, BoyItem.getName(), itemColor, true, BoyItem._iCurs, true); if (gbIsHellfire) AddSTextVal(10, BoyItem._iIvalue - (BoyItem._iIvalue / 4)); else AddSTextVal(10, BoyItem._iIvalue + (BoyItem._iIvalue / 2)); PrintStoreItem(BoyItem, 11, itemColor, true); { // Add a Leave button. Unlike the other item list back buttons, // this one has different text and different layout in LargerSmallFont locales. const int line = BackButtonLine(); AddSLine(line - 1); AddSText(0, line, _("Leave"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); TextLine[line]._syoff = 6; } } void HealPlayer() { Player &myPlayer = *MyPlayer; if (myPlayer._pHitPoints != myPlayer._pMaxHP) { PlaySFX(SfxID::CastHealing); } myPlayer._pHitPoints = myPlayer._pMaxHP; myPlayer._pHPBase = myPlayer._pMaxHPBase; RedrawComponent(PanelDrawComponent::Health); } void StartHealer() { HealPlayer(); IsTextFullSize = false; HasScrollbar = false; AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 3, _("Healer's home"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Pepin"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 14, _("Buy items"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 18, _("Leave Healer's home"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void ScrollHealerBuy(int idx) { ScrollVendorStore(HealerItems, static_cast(HealerItems.size()), idx); } void StartHealerBuy() { IsTextFullSize = true; HasScrollbar = true; ScrollPos = 0; RenderGold = true; AddSText(20, 1, _("I have these items for sale:"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollHealerBuy(ScrollPos); AddItemListBackButton(); CurrentItemIndex = 0; for (Item &item : HealerItems) { item._iStatFlag = MyPlayer->CanUseItem(item); CurrentItemIndex++; } NumTextLines = std::max(CurrentItemIndex - 4, 0); } void StartStoryteller() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("The Town Elder"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Cain"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 14, _("Identify an item"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); } bool IdItemOk(Item *i) { if (i->isEmpty()) { return false; } if (i->_iMagical == ITEM_QUALITY_NORMAL) { return false; } return !i->_iIdentified; } void AddStoreHoldId(Item itm, int8_t i) { PlayerItems[CurrentItemIndex] = itm; PlayerItems[CurrentItemIndex]._ivalue = 100; PlayerItems[CurrentItemIndex]._iIvalue = 100; PlayerItemIndexes[CurrentItemIndex] = i; CurrentItemIndex++; } void StartStorytellerIdentify() { bool idok = false; IsTextFullSize = true; CurrentItemIndex = 0; for (auto &item : PlayerItems) { item.clear(); } Player &myPlayer = *MyPlayer; auto &helmet = myPlayer.InvBody[INVLOC_HEAD]; if (IdItemOk(&helmet)) { idok = true; AddStoreHoldId(helmet, -1); } auto &armor = myPlayer.InvBody[INVLOC_CHEST]; if (IdItemOk(&armor)) { idok = true; AddStoreHoldId(armor, -2); } auto &leftHand = myPlayer.InvBody[INVLOC_HAND_LEFT]; if (IdItemOk(&leftHand)) { idok = true; AddStoreHoldId(leftHand, -3); } auto &rightHand = myPlayer.InvBody[INVLOC_HAND_RIGHT]; if (IdItemOk(&rightHand)) { idok = true; AddStoreHoldId(rightHand, -4); } auto &leftRing = myPlayer.InvBody[INVLOC_RING_LEFT]; if (IdItemOk(&leftRing)) { idok = true; AddStoreHoldId(leftRing, -5); } auto &rightRing = myPlayer.InvBody[INVLOC_RING_RIGHT]; if (IdItemOk(&rightRing)) { idok = true; AddStoreHoldId(rightRing, -6); } auto &amulet = myPlayer.InvBody[INVLOC_AMULET]; if (IdItemOk(&amulet)) { idok = true; AddStoreHoldId(amulet, -7); } for (int i = 0; i < myPlayer._pNumInv; i++) { if (CurrentItemIndex >= 48) break; auto &item = myPlayer.InvList[i]; if (IdItemOk(&item)) { idok = true; AddStoreHoldId(item, i); } } if (!idok) { HasScrollbar = false; RenderGold = true; AddSText(20, 1, _("You have nothing to identify."), UiFlags::ColorWhitegold, false); AddSLine(3); AddItemListBackButton(/*selectable=*/true); return; } HasScrollbar = true; ScrollPos = 0; NumTextLines = myPlayer._pNumInv; RenderGold = true; AddSText(20, 1, _("Identify which item?"), UiFlags::ColorWhitegold, false); AddSLine(3); ScrollSmithSell(ScrollPos); AddItemListBackButton(); } void StartStorytellerIdentifyShow(Item &item) { StartStore(OldActiveStore); HasScrollbar = false; ClearSText(5, 23); const UiFlags itemColor = item.getTextColorWithStatCheck(); AddSText(0, 7, _("This item is:"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddSText(20, 11, item.getName(), itemColor, false); PrintStoreItem(item, 12, itemColor); AddSText(0, 18, _("Done"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); } void StartTalk() { int la; IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, fmt::format(fmt::runtime(_("Talk to {:s}")), _(TownerNames[TownerId])), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSLine(5); if (gbIsSpawn) { AddSText(0, 10, fmt::format(fmt::runtime(_("Talking to {:s}")), _(TownerNames[TownerId])), UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddSText(0, 12, _("is not available"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddSText(0, 14, _("in the shareware"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddSText(0, 16, _("version"), UiFlags::ColorWhite | UiFlags::AlignCenter, false); AddOptionsBackButton(); return; } int sn = 0; for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) sn++; } if (sn > 6) { sn = 14 - (sn / 2); la = 1; } else { sn = 15 - sn; la = 2; } const int sn2 = sn - 2; for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) { AddSText(0, sn, _(QuestsData[quest._qidx]._qlstr), UiFlags::ColorWhite | UiFlags::AlignCenter, true); sn += la; } } AddSText(0, sn2, _("Gossip"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddOptionsBackButton(); } void StartTavern() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 1, _("Welcome to the"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 3, _("Rising Sun"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Ogden"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 18, _("Leave the tavern"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void StartBarmaid() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("Gillian"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Gillian"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 14, _("Access Storage"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSText(0, 18, _("Say goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void StartDrunk() { IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("Farnham the Drunk"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 9, _("Would you like to:"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); AddSText(0, 12, _("Talk to Farnham"), UiFlags::ColorBlue | UiFlags::AlignCenter, true); AddSText(0, 18, _("Say Goodbye"), UiFlags::ColorWhite | UiFlags::AlignCenter, true); AddSLine(5); CurrentItemIndex = 20; } void SmithEnter() { switch (CurrentTextLine) { case 10: TownerId = TOWN_SMITH; OldTextLine = 10; OldActiveStore = TalkID::Smith; StartStore(TalkID::Gossip); break; case 12: StartStore(TalkID::SmithBuy); break; case 14: StartStore(TalkID::SmithPremiumBuy); break; case 16: StartStore(TalkID::SmithSell); break; case 18: StartStore(TalkID::SmithRepair); break; case 20: ActiveStore = TalkID::None; break; } } /** * @brief Purchases an item from the smith. */ void SmithBuyItem(Item &item) { TakePlrsMoney(item._iIvalue); if (item._iMagical == ITEM_QUALITY_NORMAL) item._iIdentified = false; StoreAutoPlace(item, true); int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); SmithItems.erase(SmithItems.begin() + idx); CalcPlrInv(*MyPlayer, true); } void SmithBuyEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Smith); CurrentTextLine = 12; return; } OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; OldActiveStore = TalkID::SmithBuy; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(SmithItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } if (!StoreAutoPlace(SmithItems[idx], false)) { StartStore(TalkID::NoRoom); return; } TempItem = SmithItems[idx]; StartStore(TalkID::Confirm); } /** * @brief Purchases a premium item from the smith. */ void SmithBuyPItem(Item &item) { TakePlrsMoney(item._iIvalue); if (item._iMagical == ITEM_QUALITY_NORMAL) item._iIdentified = false; StoreAutoPlace(item, true); int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); ReplacePremium(*MyPlayer, idx); } void SmithPremiumBuyEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Smith); CurrentTextLine = 14; return; } OldActiveStore = TalkID::SmithPremiumBuy; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(PremiumItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } if (!StoreAutoPlace(PremiumItems[idx], false)) { StartStore(TalkID::NoRoom); return; } TempItem = PremiumItems[idx]; StartStore(TalkID::Confirm); } bool StoreGoldFit(Item &item) { const int cost = item._iIvalue; const Size itemSize = GetInventorySize(item); const int itemRoomForGold = itemSize.width * itemSize.height * MaxGold; if (cost <= itemRoomForGold) { return true; } return cost <= itemRoomForGold + RoomForGold(); } /** * @brief Sells an item from the player's inventory or belt. */ void StoreSellItem() { Player &myPlayer = *MyPlayer; int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); if (PlayerItemIndexes[idx] >= 0) myPlayer.RemoveInvItem(PlayerItemIndexes[idx]); else myPlayer.RemoveSpdBarItem(-(PlayerItemIndexes[idx] + 1)); const int cost = PlayerItems[idx]._iIvalue; CurrentItemIndex--; if (idx != CurrentItemIndex) { while (idx < CurrentItemIndex) { PlayerItems[idx] = PlayerItems[idx + 1]; PlayerItemIndexes[idx] = PlayerItemIndexes[idx + 1]; idx++; } } AddGoldToInventory(myPlayer, cost); myPlayer._pGold += cost; } void SmithSellEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Smith); CurrentTextLine = 16; return; } OldTextLine = CurrentTextLine; OldActiveStore = TalkID::SmithSell; OldScrollPos = ScrollPos; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!StoreGoldFit(PlayerItems[idx])) { StartStore(TalkID::NoRoom); return; } TempItem = PlayerItems[idx]; StartStore(TalkID::Confirm); } /** * @brief Repairs an item in the player's inventory or body in the smith. */ void SmithRepairItem(int price) { const int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); PlayerItems[idx]._iDurability = PlayerItems[idx]._iMaxDur; const int8_t i = PlayerItemIndexes[idx]; Player &myPlayer = *MyPlayer; if (i < 0) { if (i == -1) myPlayer.InvBody[INVLOC_HEAD]._iDurability = myPlayer.InvBody[INVLOC_HEAD]._iMaxDur; if (i == -2) myPlayer.InvBody[INVLOC_CHEST]._iDurability = myPlayer.InvBody[INVLOC_CHEST]._iMaxDur; if (i == -3) myPlayer.InvBody[INVLOC_HAND_LEFT]._iDurability = myPlayer.InvBody[INVLOC_HAND_LEFT]._iMaxDur; if (i == -4) myPlayer.InvBody[INVLOC_HAND_RIGHT]._iDurability = myPlayer.InvBody[INVLOC_HAND_RIGHT]._iMaxDur; TakePlrsMoney(price); return; } myPlayer.InvList[i]._iDurability = myPlayer.InvList[i]._iMaxDur; TakePlrsMoney(price); } void SmithRepairEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Smith); CurrentTextLine = 18; return; } OldActiveStore = TalkID::SmithRepair; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } TempItem = PlayerItems[idx]; StartStore(TalkID::Confirm); } void WitchEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_WITCH; OldActiveStore = TalkID::Witch; StartStore(TalkID::Gossip); break; case 14: StartStore(TalkID::WitchBuy); break; case 16: StartStore(TalkID::WitchSell); break; case 18: StartStore(TalkID::WitchRecharge); break; case 20: ActiveStore = TalkID::None; break; } } /** * @brief Purchases an item from the witch. */ void WitchBuyItem(Item &item) { int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); if (idx < 3) item._iSeed = AdvanceRndSeed(); TakePlrsMoney(item._iIvalue); StoreAutoPlace(item, true); if (idx >= 3) { WitchItems.erase(WitchItems.begin() + idx); } CalcPlrInv(*MyPlayer, true); } void WitchBuyEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Witch); CurrentTextLine = 14; return; } OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; OldActiveStore = TalkID::WitchBuy; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(WitchItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } if (!StoreAutoPlace(WitchItems[idx], false)) { StartStore(TalkID::NoRoom); return; } TempItem = WitchItems[idx]; StartStore(TalkID::Confirm); } void WitchSellEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Witch); CurrentTextLine = 16; return; } OldTextLine = CurrentTextLine; OldActiveStore = TalkID::WitchSell; OldScrollPos = ScrollPos; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!StoreGoldFit(PlayerItems[idx])) { StartStore(TalkID::NoRoom); return; } TempItem = PlayerItems[idx]; StartStore(TalkID::Confirm); } /** * @brief Recharges an item in the player's inventory or body in the witch. */ void WitchRechargeItem(int price) { const int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); PlayerItems[idx]._iCharges = PlayerItems[idx]._iMaxCharges; Player &myPlayer = *MyPlayer; const int8_t i = PlayerItemIndexes[idx]; if (i < 0) { myPlayer.InvBody[INVLOC_HAND_LEFT]._iCharges = myPlayer.InvBody[INVLOC_HAND_LEFT]._iMaxCharges; NetSendCmdChItem(true, INVLOC_HAND_LEFT); } else { myPlayer.InvList[i]._iCharges = myPlayer.InvList[i]._iMaxCharges; NetSyncInvItem(myPlayer, i); } TakePlrsMoney(price); CalcPlrInv(myPlayer, true); } void WitchRechargeEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Witch); CurrentTextLine = 18; return; } OldActiveStore = TalkID::WitchRecharge; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } TempItem = PlayerItems[idx]; StartStore(TalkID::Confirm); } void BoyEnter() { if (!BoyItem.isEmpty() && CurrentTextLine == 18) { if (!PlayerCanAfford(50)) { OldActiveStore = TalkID::Boy; OldTextLine = 18; OldScrollPos = ScrollPos; StartStore(TalkID::NoMoney); } else { TakePlrsMoney(50); StartStore(TalkID::BoyBuy); } return; } if ((CurrentTextLine != 8 && !BoyItem.isEmpty()) || (CurrentTextLine != 12 && BoyItem.isEmpty())) { ActiveStore = TalkID::None; return; } TownerId = TOWN_PEGBOY; OldActiveStore = TalkID::Boy; OldTextLine = CurrentTextLine; StartStore(TalkID::Gossip); } void BoyBuyItem(Item &item, int itemPrice) { TakePlrsMoney(itemPrice); StoreAutoPlace(item, true); item.clear(); OldActiveStore = TalkID::Boy; CalcPlrInv(*MyPlayer, true); OldTextLine = 12; } /** * @brief Purchases an item from the healer. */ void HealerBuyItem(Item &item) { int idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); if (!gbIsMultiplayer) { if (idx < 2) item._iSeed = AdvanceRndSeed(); } else { if (idx < 3) item._iSeed = AdvanceRndSeed(); } TakePlrsMoney(item._iIvalue); if (item._iMagical == ITEM_QUALITY_NORMAL) item._iIdentified = false; StoreAutoPlace(item, true); if (!gbIsMultiplayer) { if (idx < 2) return; } else { if (idx < 3) return; } idx = OldScrollPos + ((OldTextLine - PreviousScrollPos) / 4); HealerItems.erase(HealerItems.begin() + idx); CalcPlrInv(*MyPlayer, true); } void BoyBuyEnter() { if (CurrentTextLine != 10) { ActiveStore = TalkID::None; return; } OldActiveStore = TalkID::BoyBuy; OldScrollPos = ScrollPos; OldTextLine = 10; int price = BoyItem._iIvalue; if (gbIsHellfire) price -= BoyItem._iIvalue / 4; else price += BoyItem._iIvalue / 2; if (!PlayerCanAfford(price)) { StartStore(TalkID::NoMoney); return; } if (!StoreAutoPlace(BoyItem, false)) { StartStore(TalkID::NoRoom); return; } TempItem = BoyItem; TempItem._iIvalue = price; StartStore(TalkID::Confirm); } void StorytellerIdentifyItem(Item &item) { Player &myPlayer = *MyPlayer; const int8_t idx = PlayerItemIndexes[((OldTextLine - PreviousScrollPos) / 4) + OldScrollPos]; if (idx < 0) { if (idx == -1) myPlayer.InvBody[INVLOC_HEAD]._iIdentified = true; if (idx == -2) myPlayer.InvBody[INVLOC_CHEST]._iIdentified = true; if (idx == -3) myPlayer.InvBody[INVLOC_HAND_LEFT]._iIdentified = true; if (idx == -4) myPlayer.InvBody[INVLOC_HAND_RIGHT]._iIdentified = true; if (idx == -5) myPlayer.InvBody[INVLOC_RING_LEFT]._iIdentified = true; if (idx == -6) myPlayer.InvBody[INVLOC_RING_RIGHT]._iIdentified = true; if (idx == -7) myPlayer.InvBody[INVLOC_AMULET]._iIdentified = true; } else { myPlayer.InvList[idx]._iIdentified = true; } item._iIdentified = true; TakePlrsMoney(item._iIvalue); CalcPlrInv(myPlayer, true); } void ConfirmEnter(Item &item) { if (CurrentTextLine == 18) { switch (OldActiveStore) { case TalkID::SmithBuy: SmithBuyItem(item); break; case TalkID::SmithSell: case TalkID::WitchSell: StoreSellItem(); break; case TalkID::SmithRepair: SmithRepairItem(item._iIvalue); break; case TalkID::WitchBuy: WitchBuyItem(item); break; case TalkID::WitchRecharge: WitchRechargeItem(item._iIvalue); break; case TalkID::BoyBuy: BoyBuyItem(BoyItem, item._iIvalue); break; case TalkID::HealerBuy: HealerBuyItem(item); break; case TalkID::StorytellerIdentify: StorytellerIdentifyItem(item); StartStore(TalkID::StorytellerIdentifyShow); return; case TalkID::SmithPremiumBuy: SmithBuyPItem(item); break; default: break; } } StartStore(OldActiveStore); if (CurrentTextLine == BackButtonLine()) return; CurrentTextLine = OldTextLine; ScrollPos = std::min(OldScrollPos, NumTextLines); while (CurrentTextLine != -1 && !TextLine[CurrentTextLine].isSelectable()) { CurrentTextLine--; } } void HealerEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_HEALER; OldActiveStore = TalkID::Healer; StartStore(TalkID::Gossip); break; case 14: StartStore(TalkID::HealerBuy); break; case 18: ActiveStore = TalkID::None; break; } } void HealerBuyEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Healer); CurrentTextLine = 14; return; } OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; OldActiveStore = TalkID::HealerBuy; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(HealerItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } if (!StoreAutoPlace(HealerItems[idx], false)) { StartStore(TalkID::NoRoom); return; } TempItem = HealerItems[idx]; StartStore(TalkID::Confirm); } void StorytellerEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_STORY; OldActiveStore = TalkID::Storyteller; StartStore(TalkID::Gossip); break; case 14: StartStore(TalkID::StorytellerIdentify); break; case 18: ActiveStore = TalkID::None; break; } } void StorytellerIdentifyEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(TalkID::Storyteller); CurrentTextLine = 14; return; } OldActiveStore = TalkID::StorytellerIdentify; OldTextLine = CurrentTextLine; OldScrollPos = ScrollPos; const int idx = ScrollPos + ((CurrentTextLine - PreviousScrollPos) / 4); if (!PlayerCanAfford(PlayerItems[idx]._iIvalue)) { StartStore(TalkID::NoMoney); return; } TempItem = PlayerItems[idx]; StartStore(TalkID::Confirm); } void TalkEnter() { if (CurrentTextLine == BackButtonLine()) { StartStore(OldActiveStore); CurrentTextLine = OldTextLine; return; } int sn = 0; for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) sn++; } int la = 2; if (sn > 6) { sn = 14 - (sn / 2); la = 1; } else { sn = 15 - sn; } if (CurrentTextLine == sn - 2) { Towner *target = GetTowner(TownerId); assert(target != nullptr); InitQTextMsg(target->gossip); return; } for (auto &quest : Quests) { if (quest._qactive == QUEST_ACTIVE && GetTownerQuestDialog(TownerId, quest._qidx) != TEXT_NONE && quest._qlog) { if (sn == CurrentTextLine) { InitQTextMsg(GetTownerQuestDialog(TownerId, quest._qidx)); } sn += la; } } } void TavernEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_TAVERN; OldActiveStore = TalkID::Tavern; StartStore(TalkID::Gossip); break; case 18: ActiveStore = TalkID::None; break; } } void BarmaidEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_BMAID; OldActiveStore = TalkID::Barmaid; StartStore(TalkID::Gossip); break; case 14: ActiveStore = TalkID::None; IsStashOpen = true; Stash.RefreshItemStatFlags(); invflag = true; if (ControlMode != ControlTypes::KeyboardAndMouse) { if (pcurs == CURSOR_DISARM) NewCursor(CURSOR_HAND); FocusOnInventory(); } break; case 18: ActiveStore = TalkID::None; break; } } void DrunkEnter() { switch (CurrentTextLine) { case 12: OldTextLine = 12; TownerId = TOWN_DRUNK; OldActiveStore = TalkID::Drunk; StartStore(TalkID::Gossip); break; case 18: ActiveStore = TalkID::None; break; } } int TakeGold(Player &player, int cost, bool skipMaxPiles) { for (int i = 0; i < player._pNumInv; i++) { auto &item = player.InvList[i]; if (item._itype != ItemType::Gold || (skipMaxPiles && item._ivalue == MaxGold)) continue; if (cost < item._ivalue) { item._ivalue -= cost; SetPlrHandGoldCurs(player.InvList[i]); return 0; } cost -= item._ivalue; player.RemoveInvItem(i); i = -1; } return cost; } void DrawSelector(const Surface &out, const Rectangle &rect, std::string_view text, UiFlags flags) { const int lineWidth = GetLineWidth(text); int x1 = rect.position.x - 20; if (HasAnyOf(flags, UiFlags::AlignCenter)) x1 += (rect.size.width - lineWidth) / 2; ClxDraw(out, { x1, rect.position.y + 13 }, (*pSPentSpn2Cels)[PentSpn2Spin()]); int x2 = rect.position.x + rect.size.width + 5; if (HasAnyOf(flags, UiFlags::AlignCenter)) x2 = rect.position.x + (rect.size.width - lineWidth) / 2 + lineWidth + 5; ClxDraw(out, { x2, rect.position.y + 13 }, (*pSPentSpn2Cels)[PentSpn2Spin()]); } } // namespace void AddStoreHoldRepair(Item *itm, int8_t i) { Item *item; int v; item = &PlayerItems[CurrentItemIndex]; PlayerItems[CurrentItemIndex] = *itm; const int due = item->_iMaxDur - item->_iDurability; if (item->_iMagical != ITEM_QUALITY_NORMAL && item->_iIdentified) { v = 30 * item->_iIvalue * due / (item->_iMaxDur * 100 * 2); if (v == 0) return; } else { v = item->_ivalue * due / (item->_iMaxDur * 2); v = std::max(v, 1); } item->_iIvalue = v; item->_ivalue = v; PlayerItemIndexes[CurrentItemIndex] = i; CurrentItemIndex++; } void InitStores() { ClearSText(0, NumStoreLines); ActiveStore = TalkID::None; IsTextFullSize = false; HasScrollbar = false; PremiumItemCount = 0; PremiumItemLevel = 1; SmithItems.clear(); WitchItems.clear(); HealerItems.clear(); PremiumItems.clear(); BoyItem.clear(); BoyItemLevel = 0; } void SetupTownStores() { const Player &myPlayer = *MyPlayer; int l = myPlayer.getCharacterLevel() / 2; if (!gbIsMultiplayer) { l = 0; for (int i = 0; i < NUMLEVELS; i++) { if (myPlayer._pLvlVisited[i]) l = i; } } l = std::clamp(l + 2, 6, 16); SpawnSmith(l); SpawnWitch(l); SpawnHealer(l); SpawnBoy(myPlayer.getCharacterLevel()); SpawnPremium(myPlayer); } void FreeStoreMem() { if (*GetOptions().Gameplay.showItemGraphicsInStores) { FreeHalfSizeItemSprites(); } ActiveStore = TalkID::None; for (STextStruct &entry : TextLine) { entry.text.clear(); entry.text.shrink_to_fit(); } } void PrintSString(const Surface &out, int margin, int line, std::string_view text, UiFlags flags, int price, int cursId, bool cursIndent) { const Point uiPosition = GetUIRectangle().position; int sx = uiPosition.x + 32 + margin; if (!IsTextFullSize) { sx += 320; } const int sy = uiPosition.y + PaddingTop + TextLine[line].y + TextLine[line]._syoff; int width = IsTextFullSize ? 575 : 255; if (HasScrollbar && line >= 4 && line <= 20) { width -= 9; // Space for the selector } width -= margin * 2; const Rectangle rect { { sx, sy }, { width, 0 } }; // Space reserved for item graphic is based on the size of 2x3 cursor sprites constexpr int CursWidth = INV_SLOT_SIZE_PX * 2; constexpr int HalfCursWidth = CursWidth / 2; if (*GetOptions().Gameplay.showItemGraphicsInStores && cursId >= 0) { const Size size = GetInvItemSize(static_cast(CURSOR_FIRSTITEM) + cursId); const bool useHalfSize = size.width > INV_SLOT_SIZE_PX || size.height > INV_SLOT_SIZE_PX; const bool useRed = HasAnyOf(flags, UiFlags::ColorRed); const ClxSprite sprite = useHalfSize ? (useRed ? GetHalfSizeItemSpriteRed(cursId) : GetHalfSizeItemSprite(cursId)) : GetInvItemSprite(static_cast(CURSOR_FIRSTITEM) + cursId); const Point position { rect.position.x + (HalfCursWidth - sprite.width()) / 2, rect.position.y + (TextHeight() * 3 + sprite.height()) / 2 }; if (useHalfSize || !useRed) { ClxDraw(out, position, sprite); } else { ClxDrawTRN(out, position, sprite, GetInfravisionTRN()); } } if (*GetOptions().Gameplay.showItemGraphicsInStores && cursIndent) { const Rectangle textRect { { rect.position.x + HalfCursWidth + 8, rect.position.y }, { rect.size.width - HalfCursWidth + 8, rect.size.height } }; DrawString(out, text, textRect, { .flags = flags }); } else { DrawString(out, text, rect, { .flags = flags }); } if (price > 0) DrawString(out, FormatInteger(price), rect, { .flags = flags | UiFlags::AlignRight }); if (CurrentTextLine == line) { DrawSelector(out, rect, text, flags); } } void DrawSLine(const Surface &out, int sy) { const Point uiPosition = GetUIRectangle().position; int sx = 26; int width = 587; if (!IsTextFullSize) { sx += SidePanelSize.width; width -= SidePanelSize.width; } uint8_t *src = out.at(uiPosition.x + sx, uiPosition.y + 25); uint8_t *dst = out.at(uiPosition.x + sx, sy); for (int i = 0; i < 3; i++, src += out.pitch(), dst += out.pitch()) memcpy(dst, src, width); } void DrawSTextHelp() { CurrentTextLine = -1; IsTextFullSize = true; } void ClearSText(int s, int e) { for (int i = s; i < e; i++) { TextLine[i]._sx = 0; TextLine[i]._syoff = 0; TextLine[i].text.clear(); TextLine[i].text.shrink_to_fit(); TextLine[i].flags = UiFlags::None; TextLine[i].type = STextStruct::Label; TextLine[i]._sval = 0; } } void StartStore(TalkID s) { if (*GetOptions().Gameplay.showItemGraphicsInStores) { CreateHalfSizeItemSprites(); } SpellbookFlag = false; CloseInventory(); CloseCharPanel(); RenderGold = false; QuestLogIsOpen = false; CloseGoldDrop(); ClearSText(0, NumStoreLines); ReleaseStoreBtn(); // Fire StoreOpened Lua event for main store entries switch (s) { case TalkID::Smith: lua::StoreOpened("griswold"); break; case TalkID::Witch: lua::StoreOpened("adria"); break; case TalkID::Boy: lua::StoreOpened("wirt"); break; case TalkID::Healer: lua::StoreOpened("pepin"); break; case TalkID::Storyteller: lua::StoreOpened("cain"); break; case TalkID::Tavern: lua::StoreOpened("ogden"); break; case TalkID::Drunk: lua::StoreOpened("farnham"); break; case TalkID::Barmaid: lua::StoreOpened("gillian"); break; default: break; } switch (s) { case TalkID::Smith: StartSmith(); break; case TalkID::SmithBuy: { if (!SmithItems.empty()) StartSmithBuy(); else { ActiveStore = TalkID::SmithBuy; OldTextLine = 12; StoreESC(); return; } break; } case TalkID::SmithSell: StartSmithSell(); break; case TalkID::SmithRepair: StartSmithRepair(); break; case TalkID::Witch: StartWitch(); break; case TalkID::WitchBuy: if (CurrentItemIndex > 0) StartWitchBuy(); break; case TalkID::WitchSell: StartWitchSell(); break; case TalkID::WitchRecharge: StartWitchRecharge(); break; case TalkID::NoMoney: StoreNoMoney(); break; case TalkID::NoRoom: StoreNoRoom(); break; case TalkID::Confirm: StoreConfirm(TempItem); break; case TalkID::Boy: StartBoy(); break; case TalkID::BoyBuy: SStartBoyBuy(); break; case TalkID::Healer: StartHealer(); break; case TalkID::Storyteller: StartStoryteller(); break; case TalkID::HealerBuy: if (CurrentItemIndex > 0) StartHealerBuy(); break; case TalkID::StorytellerIdentify: StartStorytellerIdentify(); break; case TalkID::SmithPremiumBuy: if (!StartSmithPremiumBuy()) return; break; case TalkID::Gossip: StartTalk(); break; case TalkID::StorytellerIdentifyShow: StartStorytellerIdentifyShow(TempItem); break; case TalkID::Tavern: StartTavern(); break; case TalkID::Drunk: StartDrunk(); break; case TalkID::Barmaid: StartBarmaid(); break; case TalkID::None: break; } CurrentTextLine = -1; for (int i = 0; i < NumStoreLines; i++) { if (TextLine[i].isSelectable()) { CurrentTextLine = i; break; } } ActiveStore = s; } void DrawSText(const Surface &out) { if (!IsTextFullSize) DrawSTextBack(out); else DrawQTextBack(out); if (HasScrollbar) { switch (ActiveStore) { case TalkID::SmithBuy: ScrollSmithBuy(ScrollPos); break; case TalkID::SmithSell: case TalkID::SmithRepair: case TalkID::WitchSell: case TalkID::WitchRecharge: case TalkID::StorytellerIdentify: ScrollSmithSell(ScrollPos); break; case TalkID::WitchBuy: ScrollWitchBuy(ScrollPos); break; case TalkID::HealerBuy: ScrollHealerBuy(ScrollPos); break; case TalkID::SmithPremiumBuy: ScrollSmithPremiumBuy(ScrollPos); break; default: break; } } CalculateLineHeights(); const Point uiPosition = GetUIRectangle().position; for (int i = 0; i < NumStoreLines; i++) { if (TextLine[i].isDivider()) DrawSLine(out, uiPosition.y + PaddingTop + TextLine[i].y + TextHeight() / 2); else if (TextLine[i].hasText()) PrintSString(out, TextLine[i]._sx, i, TextLine[i].text, TextLine[i].flags, TextLine[i]._sval, TextLine[i].cursId, TextLine[i].cursIndent); } if (RenderGold) { PrintSString(out, 28, 1, fmt::format(fmt::runtime(_("Your gold: {:s}")), FormatInteger(TotalPlayerGold())).c_str(), UiFlags::ColorWhitegold | UiFlags::AlignRight); } if (HasScrollbar) DrawSSlider(out, 4, 20); } void StoreESC() { if (qtextflag) { qtextflag = false; if (leveltype == DTYPE_TOWN) stream_stop(); return; } switch (ActiveStore) { case TalkID::Smith: case TalkID::Witch: case TalkID::Boy: case TalkID::BoyBuy: case TalkID::Healer: case TalkID::Storyteller: case TalkID::Tavern: case TalkID::Drunk: case TalkID::Barmaid: ActiveStore = TalkID::None; break; case TalkID::Gossip: StartStore(OldActiveStore); CurrentTextLine = OldTextLine; break; case TalkID::SmithBuy: StartStore(TalkID::Smith); CurrentTextLine = 12; break; case TalkID::SmithPremiumBuy: StartStore(TalkID::Smith); CurrentTextLine = 14; break; case TalkID::SmithSell: StartStore(TalkID::Smith); CurrentTextLine = 16; break; case TalkID::SmithRepair: StartStore(TalkID::Smith); CurrentTextLine = 18; break; case TalkID::WitchBuy: StartStore(TalkID::Witch); CurrentTextLine = 14; break; case TalkID::WitchSell: StartStore(TalkID::Witch); CurrentTextLine = 16; break; case TalkID::WitchRecharge: StartStore(TalkID::Witch); CurrentTextLine = 18; break; case TalkID::HealerBuy: StartStore(TalkID::Healer); CurrentTextLine = 14; break; case TalkID::StorytellerIdentify: StartStore(TalkID::Storyteller); CurrentTextLine = 14; break; case TalkID::StorytellerIdentifyShow: StartStore(TalkID::StorytellerIdentify); break; case TalkID::NoMoney: case TalkID::NoRoom: case TalkID::Confirm: StartStore(OldActiveStore); CurrentTextLine = OldTextLine; ScrollPos = OldScrollPos; break; case TalkID::None: break; } } void StoreUp() { PlaySFX(SfxID::MenuMove); if (CurrentTextLine == -1) { return; } if (HasScrollbar) { if (CurrentTextLine == PreviousScrollPos) { if (ScrollPos != 0) ScrollPos--; return; } CurrentTextLine--; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == 0) CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; } return; } if (CurrentTextLine == 0) CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == 0) CurrentTextLine = NumStoreLines - 1; else CurrentTextLine--; } } void StoreDown() { PlaySFX(SfxID::MenuMove); if (CurrentTextLine == -1) { return; } if (HasScrollbar) { if (CurrentTextLine == NextScrollPos) { if (ScrollPos < NumTextLines) ScrollPos++; return; } CurrentTextLine++; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; } return; } if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; while (!TextLine[CurrentTextLine].isSelectable()) { if (CurrentTextLine == NumStoreLines - 1) CurrentTextLine = 0; else CurrentTextLine++; } } void StorePrior() { PlaySFX(SfxID::MenuMove); if (CurrentTextLine != -1 && HasScrollbar) { if (CurrentTextLine == PreviousScrollPos) { ScrollPos = std::max(ScrollPos - 4, 0); } else { CurrentTextLine = PreviousScrollPos; } } } void StoreNext() { PlaySFX(SfxID::MenuMove); if (CurrentTextLine != -1 && HasScrollbar) { if (CurrentTextLine == NextScrollPos) { if (ScrollPos < NumTextLines) ScrollPos += 4; if (ScrollPos > NumTextLines) ScrollPos = NumTextLines; } else { CurrentTextLine = NextScrollPos; } } } void TakePlrsMoney(int cost) { Player &myPlayer = *MyPlayer; myPlayer._pGold -= std::min(cost, myPlayer._pGold); cost = TakeGold(myPlayer, cost, true); if (cost != 0) { cost = TakeGold(myPlayer, cost, false); } Stash.gold -= cost; Stash.dirty = true; } void StoreEnter() { if (qtextflag) { qtextflag = false; if (leveltype == DTYPE_TOWN) stream_stop(); return; } PlaySFX(SfxID::MenuSelect); switch (ActiveStore) { case TalkID::Smith: SmithEnter(); break; case TalkID::SmithPremiumBuy: SmithPremiumBuyEnter(); break; case TalkID::SmithBuy: SmithBuyEnter(); break; case TalkID::SmithSell: SmithSellEnter(); break; case TalkID::SmithRepair: SmithRepairEnter(); break; case TalkID::Witch: WitchEnter(); break; case TalkID::WitchBuy: WitchBuyEnter(); break; case TalkID::WitchSell: WitchSellEnter(); break; case TalkID::WitchRecharge: WitchRechargeEnter(); break; case TalkID::NoMoney: case TalkID::NoRoom: StartStore(OldActiveStore); CurrentTextLine = OldTextLine; ScrollPos = OldScrollPos; break; case TalkID::Confirm: ConfirmEnter(TempItem); break; case TalkID::Boy: BoyEnter(); break; case TalkID::BoyBuy: BoyBuyEnter(); break; case TalkID::Healer: HealerEnter(); break; case TalkID::Storyteller: StorytellerEnter(); break; case TalkID::HealerBuy: HealerBuyEnter(); break; case TalkID::StorytellerIdentify: StorytellerIdentifyEnter(); break; case TalkID::Gossip: TalkEnter(); break; case TalkID::StorytellerIdentifyShow: StartStore(TalkID::StorytellerIdentify); break; case TalkID::Drunk: DrunkEnter(); break; case TalkID::Tavern: TavernEnter(); break; case TalkID::Barmaid: BarmaidEnter(); break; case TalkID::None: break; } } void CheckStoreBtn() { const Point uiPosition = GetUIRectangle().position; const Rectangle windowRect { { uiPosition.x + 344, uiPosition.y + PaddingTop - 7 }, { 271, 303 } }; const Rectangle windowRectFull { { uiPosition.x + 24, uiPosition.y + PaddingTop - 7 }, { 591, 303 } }; if (!IsTextFullSize) { if (!windowRect.contains(MousePosition)) { while (IsPlayerInStore()) StoreESC(); } } else { if (!windowRectFull.contains(MousePosition)) { while (IsPlayerInStore()) StoreESC(); } } if (qtextflag) { qtextflag = false; if (leveltype == DTYPE_TOWN) stream_stop(); } else if (CurrentTextLine != -1) { const int relativeY = MousePosition.y - (uiPosition.y + PaddingTop); if (HasScrollbar && MousePosition.x > 600 + uiPosition.x) { // Scroll bar is always measured in terms of the small line height. const int y = relativeY / SmallLineHeight; if (y == 4) { if (CountdownScrollUp <= 0) { StoreUp(); CountdownScrollUp = 10; } else { CountdownScrollUp--; } } if (y == 20) { if (CountdownScrollDown <= 0) { StoreDown(); CountdownScrollDown = 10; } else { CountdownScrollDown--; } } return; } int y = relativeY / LineHeight(); // Large small fonts draw beyond LineHeight. Check if the click was on the overflow text. if (IsSmallFontTall() && y > 0 && y < NumStoreLines && TextLine[y - 1].hasText() && !TextLine[y].hasText() && relativeY < TextLine[y - 1].y + LargeTextHeight) { --y; } if (y >= 5) { if (y >= BackButtonLine() + 1) y = BackButtonLine(); if (HasScrollbar && y <= 20 && !TextLine[y].isSelectable()) { if (TextLine[y - 2].isSelectable()) { y -= 2; } else if (TextLine[y - 1].isSelectable()) { y--; } } if (TextLine[y].isSelectable() || (HasScrollbar && y == BackButtonLine())) { CurrentTextLine = y; StoreEnter(); } } } } void ReleaseStoreBtn() { CountdownScrollUp = -1; CountdownScrollDown = -1; } bool IsPlayerInStore() { return ActiveStore != TalkID::None; } } // namespace devilution ================================================ FILE: Source/stores.h ================================================ /** * @file stores.h * * Interface of functionality for stores and towner dialogs. */ #pragma once #include #include #include "DiabloUI/ui_flags.hpp" #include "control/control.hpp" #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" #include "game_mode.hpp" #include "utils/attributes.h" #include "utils/static_vector.hpp" namespace devilution { constexpr int NumSmithBasicItems = 19; constexpr int NumSmithBasicItemsHf = 24; constexpr int NumSmithItems = 6; constexpr int NumSmithItemsHf = 15; constexpr int NumHealerItems = 17; constexpr int NumHealerItemsHf = 19; constexpr int NumHealerPinnedItems = 2; constexpr int NumHealerPinnedItemsMp = 3; constexpr int NumWitchItems = 17; constexpr int NumWitchItemsHf = 24; constexpr int NumWitchPinnedItems = 3; constexpr int NumStoreLines = 104; enum class TalkID : uint8_t { None, Smith, SmithBuy, SmithSell, SmithRepair, Witch, WitchBuy, WitchSell, WitchRecharge, NoMoney, NoRoom, Confirm, Boy, BoyBuy, Healer, Storyteller, HealerBuy, StorytellerIdentify, SmithPremiumBuy, Gossip, StorytellerIdentifyShow, Tavern, Drunk, Barmaid, }; /** Currently active store */ extern TalkID ActiveStore; /** Current index into PlayerItemIndexes/PlayerItems */ extern DVL_API_FOR_TEST int CurrentItemIndex; /** Map of inventory items being presented in the store */ extern int8_t PlayerItemIndexes[48]; /** Copies of the players items as presented in the store */ extern DVL_API_FOR_TEST Item PlayerItems[48]; /** Items sold by Griswold */ extern DVL_API_FOR_TEST StaticVector SmithItems; /** Number of premium items for sale by Griswold */ extern DVL_API_FOR_TEST int PremiumItemCount; /** Base level of current premium items sold by Griswold */ extern DVL_API_FOR_TEST int PremiumItemLevel; /** Premium items sold by Griswold */ extern DVL_API_FOR_TEST StaticVector PremiumItems; /** Items sold by Pepin */ extern DVL_API_FOR_TEST StaticVector HealerItems; /** Items sold by Adria */ extern DVL_API_FOR_TEST StaticVector WitchItems; /** Current level of the item sold by Wirt */ extern int BoyItemLevel; /** Current item sold by Wirt */ extern Item BoyItem; void AddStoreHoldRepair(Item *itm, int8_t i); /** Clears premium items sold by Griswold and Wirt. */ void InitStores(); /** Spawns items sold by vendors, including premium items sold by Griswold and Wirt. */ void SetupTownStores(); void FreeStoreMem(); void PrintSString(const Surface &out, int margin, int line, std::string_view text, UiFlags flags, int price = 0, int cursId = -1, bool cursIndent = false); void DrawSLine(const Surface &out, int sy); void DrawSTextHelp(); void ClearSText(int s, int e); void StartStore(TalkID s); void DrawSText(const Surface &out); void StoreESC(); void StoreUp(); void StoreDown(); void StorePrior(); void StoreNext(); void TakePlrsMoney(int cost); void StoreEnter(); void CheckStoreBtn(); void ReleaseStoreBtn(); bool IsPlayerInStore(); } // namespace devilution ================================================ FILE: Source/storm/storm_net.cpp ================================================ #include "storm/storm_net.hpp" #include #include #include #ifndef NONET #include #include #include #include "utils/sdl_mutex.h" #endif #include "dvlnet/abstract_net.h" #include "engine/demomode.h" #include "headless_mode.hpp" #include "menu.h" #include "multi.h" #include "options.h" #include "utils/log.hpp" #include "utils/stubs.h" #include "utils/utf8.hpp" namespace devilution { namespace { std::unique_ptr dvlnet_inst; bool GameIsPublic = {}; #ifndef NONET SdlMutex storm_net_mutex; #endif } // namespace bool SNetReceiveMessage(uint8_t *senderplayerid, void **data, size_t *databytes) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetReceiveMessage(senderplayerid, data, databytes); } bool SNetSendMessage(uint8_t playerID, void *data, size_t databytes) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetSendMessage(playerID, data, databytes); } bool SNetReceiveTurns(int arraysize, char **arraydata, size_t *arraydatabytes, uint32_t *arrayplayerstatus) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif if (arraysize != MAX_PLRS) UNIMPLEMENTED(); return dvlnet_inst->SNetReceiveTurns(arraydata, arraydatabytes, arrayplayerstatus); } bool SNetSendTurn(char *data, size_t databytes) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetSendTurn(data, databytes); } void SNetGetProviderCaps(struct _SNETCAPS *caps) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif dvlnet_inst->SNetGetProviderCaps(caps); } bool SNetUnregisterEventHandler(event_type evtype) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif if (dvlnet_inst == nullptr) return true; return dvlnet_inst->SNetUnregisterEventHandler(evtype); } bool SNetRegisterEventHandler(event_type evtype, SEVTHANDLER func) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetRegisterEventHandler(evtype, func); } bool SNetDestroy() { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif dvlnet_inst = nullptr; return true; } bool SNetDropPlayer(uint8_t playerid, leaveinfo_t flags) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetDropPlayer(playerid, flags); } bool SNetLeaveGame(leaveinfo_t type) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif if (dvlnet_inst == nullptr) return true; if (!IsLoopback) { std::string upperGameName = GameName; std::transform(upperGameName.begin(), upperGameName.end(), upperGameName.begin(), ::toupper); const std::string reasonDescription = DescribeLeaveReason(type); LogInfo("Leaving {} multiplayer game '{}' (reason: {})", ConnectionNames[provider], upperGameName, reasonDescription); } return dvlnet_inst->SNetLeaveGame(type); } /** * @brief Called by engine for single, called by ui for multi * @param provider BNET, IPXN, MODM, SCBL or UDPN * @param gameData The game data */ bool SNetInitializeProvider(uint32_t provider, struct GameData *gameData) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif dvlnet_inst = net::abstract_net::MakeNet(provider); return (HeadlessMode && !demo::IsRunning()) || mainmenu_select_hero_dialog(gameData); } /** * @brief Called by engine for single, called by ui for multi */ bool SNetCreateGame(const char *pszGameName, const char *pszGamePassword, char *gameTemplateData, int gameTemplateSize, int *playerID) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif if (gameTemplateSize != sizeof(GameData)) ABORT(); net::buffer_t gameInitInfo(gameTemplateData, gameTemplateData + gameTemplateSize); dvlnet_inst->setup_gameinfo(std::move(gameInitInfo)); std::string defaultName; if (pszGameName == nullptr) { defaultName = dvlnet_inst->make_default_gamename(); pszGameName = defaultName.c_str(); } GameName = pszGameName; if (pszGamePassword != nullptr) DvlNet_SetPassword(pszGamePassword); else DvlNet_ClearPassword(); const int createdPlayerId = dvlnet_inst->create(pszGameName); if (createdPlayerId == -1) return false; *playerID = createdPlayerId; if (!IsLoopback) { std::string upperGameName = GameName; std::transform(upperGameName.begin(), upperGameName.end(), upperGameName.begin(), ::toupper); const char *privacy = GameIsPublic ? "public" : "private"; if (gameTemplateData != nullptr && gameTemplateSize >= static_cast(sizeof(GameData))) { const GameData *gameData = reinterpret_cast(gameTemplateData); LogInfo("Created {} {} multiplayer game '{}' (player id: {}, seed: {})", privacy, ConnectionNames[provider], upperGameName, createdPlayerId, FormatGameSeed(gameData->gameSeed)); } else { LogInfo("Created {} {} multiplayer game '{}' (player id: {}, seed unavailable)", privacy, ConnectionNames[provider], upperGameName, createdPlayerId); } } return true; } bool SNetJoinGame(char *pszGameName, char *pszGamePassword, int *playerID) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif if (pszGameName != nullptr) GameName = pszGameName; if (pszGamePassword != nullptr) DvlNet_SetPassword(pszGamePassword); else DvlNet_ClearPassword(); const int joinedPlayerId = dvlnet_inst->join(pszGameName); if (joinedPlayerId == -1) return false; *playerID = joinedPlayerId; // Join message with seed will be logged in NetInit after game data is synchronized return true; } /** * @brief Is this the mirror image of SNetGetTurnsInTransit? */ bool SNetGetOwnerTurnsWaiting(uint32_t *turns) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetGetOwnerTurnsWaiting(turns); } bool SNetGetTurnsInTransit(uint32_t *turns) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return dvlnet_inst->SNetGetTurnsInTransit(turns); } /** * @brief engine calls this only once with argument 1 */ bool SNetSetBasePlayer(int /*unused*/) { #ifndef NONET std::lock_guard lg(storm_net_mutex); #endif return true; } void DvlNet_ProcessNetworkPackets() { return dvlnet_inst->process_network_packets(); } bool DvlNet_SendInfoRequest() { return dvlnet_inst->send_info_request(); } void DvlNet_ClearGamelist() { return dvlnet_inst->clear_gamelist(); } std::vector DvlNet_GetGamelist() { return dvlnet_inst->get_gamelist(); } void DvlNet_SetPassword(std::string pw) { GameIsPublic = false; GamePassword = pw; dvlnet_inst->setup_password(std::move(pw)); } void DvlNet_ClearPassword() { GameIsPublic = true; GamePassword.clear(); dvlnet_inst->clear_password(); } bool DvlNet_IsPublicGame() { return GameIsPublic; } DvlNetLatencies DvlNet_GetLatencies(uint8_t playerId) { return dvlnet_inst->get_latencies(playerId); } } // namespace devilution ================================================ FILE: Source/storm/storm_net.hpp ================================================ #pragma once #include #include #include #include #include #include "multi.h" namespace devilution { using net::leaveinfo_t; enum conn_type : uint8_t { SELCONN_ZT, SELCONN_TCP, SELCONN_LOOPBACK, }; enum event_type : uint8_t { EVENT_TYPE_PLAYER_CREATE_GAME, EVENT_TYPE_PLAYER_LEAVE_GAME, EVENT_TYPE_PLAYER_MESSAGE, }; extern const char *ConnectionNames[]; extern int provider; struct _SNETCAPS { uint32_t size; uint32_t flags; uint32_t maxmessagesize; uint32_t maxqueuesize; uint32_t maxplayers; uint32_t bytessec; uint32_t latencyms; uint32_t defaultturnssec; uint32_t defaultturnsintransit; }; struct _SNETEVENT { uint32_t eventid; // native-endian uint32_t playerid; // native-endian void *data; // little-endian size_t databytes; // native-endian }; struct DvlNetLatencies { uint32_t echoLatency; std::optional providerLatency; std::optional isRelayed; }; #define PS_CONNECTED 0x10000 #define PS_TURN_ARRIVED 0x20000 #define PS_ACTIVE 0x40000 bool SNetCreateGame(const char *pszGameName, const char *pszGamePassword, char *GameTemplateData, int GameTemplateSize, int *playerID); bool SNetDestroy(); /* SNetDropPlayer @ 106 * * Drops a player from the current game. * * playerid: The player ID for the player to be dropped. * flags: * * Returns true if the function was called successfully and false otherwise. */ bool SNetDropPlayer(uint8_t playerid, leaveinfo_t flags); /* SNetGetTurnsInTransit @ 115 * * Retrieves the number of turns (buffers) that have been queued * before sending them over the network. * * turns: A pointer to an integer that will receive the value. * * Returns true if the function was called successfully and false otherwise. */ bool SNetGetTurnsInTransit(uint32_t *turns); bool SNetJoinGame(char *gameName, char *gamePassword, int *playerid); /* SNetLeaveGame @ 119 * * Notifies Storm that the player has left the game. Storm will * notify all connected peers through the network provider. * * type: The leave type. It doesn't appear to be important, no documentation available. * * Returns true if the function was called successfully and false otherwise. */ bool SNetLeaveGame(leaveinfo_t type); bool SNetReceiveMessage(uint8_t *senderplayerid, void **data, size_t *databytes); bool SNetReceiveTurns(int arraysize, char **arraydata, size_t *arraydatabytes, uint32_t *arrayplayerstatus); typedef void (*SEVTHANDLER)(struct _SNETEVENT *); /* SNetSendMessage @ 127 * * Sends a message to a player given their player ID. Network message * is sent using class 01 and is retrieved by the other client using * SNetReceiveMessage(). * * playerID: The player index of the player to receive the data. * Conversely, this field can be one of the following constants: * SNPLAYER_ALL | Sends the message to all players, including oneself. * SNPLAYER_OTHERS | Sends the message to all players, except for oneself. * data: A pointer to the data. * databytes: The amount of bytes that the data pointer contains. * * Returns true if the function was called successfully and false otherwise. */ bool SNetSendMessage(uint8_t playerID, void *data, size_t databytes); // Macro values to target specific players constexpr uint8_t SNPLAYER_OTHERS = 0xFF; /* SNetSendTurn @ 128 * * Sends a turn (data packet) to all players in the game. Network data * is sent using class 02 and is retrieved by the other client using * SNetReceiveTurns(). * * data: A pointer to the data. * databytes: The amount of bytes that the data pointer contains. * * Returns true if the function was called successfully and false otherwise. */ bool SNetSendTurn(char *data, size_t databytes); bool SNetGetOwnerTurnsWaiting(uint32_t *); bool SNetUnregisterEventHandler(event_type); bool SNetRegisterEventHandler(event_type, SEVTHANDLER); bool SNetSetBasePlayer(int); bool SNetInitializeProvider(uint32_t provider, struct GameData *gameData); void SNetGetProviderCaps(struct _SNETCAPS *); void DvlNet_ProcessNetworkPackets(); bool DvlNet_SendInfoRequest(); void DvlNet_ClearGamelist(); std::vector DvlNet_GetGamelist(); void DvlNet_SetPassword(std::string pw); void DvlNet_ClearPassword(); bool DvlNet_IsPublicGame(); DvlNetLatencies DvlNet_GetLatencies(uint8_t playerId); } // namespace devilution ================================================ FILE: Source/storm/storm_svid.cpp ================================================ #include "storm/storm_svid.h" #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #ifndef NOSOUND #include #endif #else #include #ifndef NOSOUND #include "utils/aulib.hpp" #include "utils/push_aulib_decoder.h" #endif #endif #include #include "engine/assets.hpp" #include "engine/dx.h" #include "engine/palette.h" #include "options.h" #include "utils/display.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_wrap.h" namespace devilution { namespace { #ifndef NOSOUND #ifdef USE_SDL3 SDL_AudioStream *SVidAudioStream; bool SVidAutoStreamEnabled; #else std::optional SVidAudioStream; PushAulibDecoder *SVidAudioDecoder; #endif std::uint8_t SVidAudioDepth; std::unique_ptr SVidAudioBuffer; #endif // Smacker's atomic time unit is a one hundred thousand's of a second (i.e. 0.01 millisecond, or 10 microseconds). // We use SDL ticks for timing, which have millisecond resolution. // There are 100 Smacker time units in a millisecond. constexpr uint64_t SmackerTimeUnit = 100; constexpr uint64_t TimeMsToSmk(uint64_t ms) { return ms * SmackerTimeUnit; } constexpr uint64_t TimeSmkToMs(uint64_t time) { return time / SmackerTimeUnit; }; uint64_t GetTicksSmk() { #if SDL_VERSION_ATLEAST(2, 0, 18) && !defined(USE_SDL3) return TimeMsToSmk(SDL_GetTicks64()); #else return TimeMsToSmk(SDL_GetTicks()); #endif } uint32_t SVidWidth, SVidHeight; bool SVidLoop; SmackerHandle SVidHandle; std::unique_ptr SVidFrameBuffer; SDLPaletteUniquePtr SVidPalette; SDLSurfaceUniquePtr SVidSurface; // The end of the current frame (time in SMK time units from the start of the program). uint64_t SVidFrameEnd; // The length of a frame in SMK time units. uint32_t SVidFrameLength; bool IsLandscapeFit(unsigned long srcW, unsigned long srcH, unsigned long dstW, unsigned long dstH) { return srcW * dstH > dstW * srcH; } #ifdef USE_SDL1 // Whether we've changed the video mode temporarily for SVid. // If true, we must restore it once the video has finished playing. bool IsSVidVideoMode = false; // Set the video mode close to the SVid resolution while preserving aspect ratio. void TrySetVideoModeToSVidForSDL1() { const SDL_Surface *display = SDL_GetVideoSurface(); #if defined(SDL1_VIDEO_MODE_SVID_FLAGS) const int flags = SDL1_VIDEO_MODE_SVID_FLAGS; #elif defined(SDL1_VIDEO_MODE_FLAGS) const int flags = SDL1_VIDEO_MODE_FLAGS; #else const int flags = display->flags; #endif #ifdef SDL1_FORCE_SVID_VIDEO_MODE IsSVidVideoMode = true; #else IsSVidVideoMode = (flags & (SDL_FULLSCREEN | SDL_NOFRAME)) != 0; #endif if (!IsSVidVideoMode) return; int w; int h; if (IsLandscapeFit(SVidWidth, SVidHeight, display->w, display->h)) { w = SVidWidth; h = SVidWidth * display->h / display->w; } else { w = SVidHeight * display->w / display->h; h = SVidHeight; } #ifndef SDL1_FORCE_SVID_VIDEO_MODE if (!SDL_VideoModeOK(w, h, /*bpp=*/display->format->BitsPerPixel, flags)) { IsSVidVideoMode = false; // Get available fullscreen/hardware modes SDL_Rect **modes = SDL_ListModes(nullptr, flags); // Check is there are any modes available. if (modes == reinterpret_cast(0) || modes == reinterpret_cast(-1)) { return; } // Search for a usable video mode bool found = false; for (int i = 0; modes[i]; i++) { if (modes[i]->w == w || modes[i]->h == h) { found = true; break; } } if (!found) return; IsSVidVideoMode = true; } #endif SetVideoMode(w, h, display->format->BitsPerPixel, flags); } #endif #ifndef NOSOUND // Returns the volume scaled to [0.0F, 1.0F] range. float GetVolume() { return static_cast(*GetOptions().Audio.soundVolume - VOLUME_MIN) / -VOLUME_MIN; } bool ShouldPushAudioData() { #ifdef USE_SDL3 return SVidAudioStream != nullptr && SVidAutoStreamEnabled; #else return SVidAudioStream && SVidAudioStream->isPlaying(); #endif } #endif bool SVidLoadNextFrame() { if (Smacker_GetCurrentFrameNum(SVidHandle) >= Smacker_GetNumFrames(SVidHandle)) { if (!SVidLoop) { return false; } Smacker_Rewind(SVidHandle); } SVidFrameEnd += SVidFrameLength; Smacker_GetNextFrame(SVidHandle); Smacker_GetFrame(SVidHandle, SVidFrameBuffer.get()); return true; } void UpdatePalette() { constexpr size_t NumColors = 256; uint8_t paletteData[NumColors * 3]; Smacker_GetPalette(SVidHandle, paletteData); SDL_Color *colors = SVidPalette->colors; for (unsigned i = 0; i < NumColors; ++i) { colors[i].r = paletteData[i * 3]; colors[i].g = paletteData[i * 3 + 1]; colors[i].b = paletteData[i * 3 + 2]; #ifndef USE_SDL1 colors[i].a = SDL_ALPHA_OPAQUE; #endif } #ifdef USE_SDL1 #if SDL1_VIDEO_MODE_BPP == 8 // When the video surface is 8bit, we need to set the output palette. SDL_SetColors(SDL_GetVideoSurface(), colors, 0, NumColors); #endif if (SDL_SetPalette(SVidSurface.get(), SDL_LOGPAL, colors, 0, NumColors) <= 0) { ErrSdl(); } #else if (!SDLC_SetSurfacePalette(SVidSurface.get(), SVidPalette.get())) { ErrSdl(); } const SDL_Surface *surface = GetOutputSurface(); if (SDLC_SURFACE_BITSPERPIXEL(surface) == 8) { if (!SDLC_SetSurfacePalette(GetOutputSurface(), SVidPalette.get())) { ErrSdl(); } } #endif } bool BlitFrame() { #ifndef USE_SDL1 if (renderer != nullptr) { if ( #ifdef USE_SDL3 !SDL_BlitSurface(SVidSurface.get(), nullptr, GetOutputSurface(), nullptr) #else SDL_BlitSurface(SVidSurface.get(), nullptr, GetOutputSurface(), nullptr) <= -1 #endif ) { Log("{}", SDL_GetError()); return false; } } else #endif { SDL_Surface *outputSurface = GetOutputSurface(); #ifdef USE_SDL1 const bool isIndexedOutputFormat = SDLBackport_IsPixelFormatIndexed(outputSurface->format); #else #ifdef USE_SDL3 const SDL_PixelFormat wndFormat = SDL_GetWindowPixelFormat(ghMainWnd); #else const Uint32 wndFormat = SDL_GetWindowPixelFormat(ghMainWnd); #endif const bool isIndexedOutputFormat = SDL_ISPIXELFORMAT_INDEXED(wndFormat); #endif SDL_Rect outputRect; if (isIndexedOutputFormat) { // Cannot scale if the output format is indexed (8-bit palette). outputRect.w = static_cast(SVidWidth); outputRect.h = static_cast(SVidHeight); } else if (IsLandscapeFit(SVidWidth, SVidHeight, outputSurface->w, outputSurface->h)) { outputRect.w = outputSurface->w; outputRect.h = SVidHeight * outputSurface->w / SVidWidth; } else { outputRect.w = SVidWidth * outputSurface->h / SVidHeight; outputRect.h = outputSurface->h; } outputRect.x = (outputSurface->w - outputRect.w) / 2; outputRect.y = (outputSurface->h - outputRect.h) / 2; if (isIndexedOutputFormat || outputSurface->w == static_cast(SVidWidth) || outputSurface->h == static_cast(SVidHeight)) { if ( #ifdef USE_SDL3 SDL_BlitSurface(SVidSurface.get(), nullptr, outputSurface, &outputRect) #else SDL_BlitSurface(SVidSurface.get(), nullptr, outputSurface, &outputRect) <= -1 #endif ) { ErrSdl(); } } else { // The source surface is always 8-bit, and the output surface is never 8-bit in this branch. // We must convert to the output format before calling SDL_BlitScaled. #ifdef USE_SDL1 SDLSurfaceUniquePtr converted = SDLWrap::ConvertSurface(SVidSurface.get(), ghMainWnd->format, 0); #else SDLSurfaceUniquePtr converted = SDLWrap::ConvertSurfaceFormat(SVidSurface.get(), wndFormat, 0); #endif if ( #ifdef USE_SDL3 SDL_BlitSurfaceScaled(converted.get(), nullptr, outputSurface, &outputRect, SDL_SCALEMODE_LINEAR) #else SDL_BlitScaled(converted.get(), nullptr, outputSurface, &outputRect) <= -1 #endif ) { Log("{}", SDL_GetError()); return false; } } } RenderPresent(); return true; } #if defined(USE_SDL3) && !defined(NOSOUND) void SVidInitAudioStream(const SmackerAudioInfo &audioInfo) { SVidAutoStreamEnabled = diablo_is_focused(); SDL_AudioSpec srcSpec = {}; srcSpec.channels = static_cast(audioInfo.nChannels); srcSpec.freq = static_cast(audioInfo.sampleRate); srcSpec.format = audioInfo.bitsPerSample == 8 ? SDL_AUDIO_U8 : SDL_AUDIO_S16LE; SVidAudioStream = SDL_CreateAudioStream(&srcSpec, /*dstSpec=*/&srcSpec); if (SVidAudioStream == nullptr) { LogError(LogCategory::Audio, "SDL_CreateAudioStream (from SVidPlayBegin): {}", SDL_GetError()); SDL_ClearError(); SVidAudioStream = nullptr; return; } if (!SDL_BindAudioStream(CurrentAudioDeviceId, SVidAudioStream)) { LogError(LogCategory::Audio, "SDL_BindAudioStream (from SVidPlayBegin): {}", SDL_GetError()); SDL_ClearError(); SDL_DestroyAudioStream(SVidAudioStream); SVidAudioStream = nullptr; return; } if (!SDL_SetAudioStreamGain(SVidAudioStream, GetVolume())) { LogWarn(LogCategory::Audio, "SDL_SetAudioStreamGain (from SVidPlayBegin): {}", SDL_GetError()); SDL_ClearError(); } } #endif } // namespace bool SVidPlayBegin(const char *filename, int flags) { if ((flags & 0x10000) != 0 || (flags & 0x20000000) != 0) { return false; } SVidLoop = false; if ((flags & 0x40000) != 0) SVidLoop = true; // 0x8 // Non-interlaced // 0x200, 0x800 // Upscale video // 0x80000 // Center horizontally // 0x100000 // Disable video // 0x800000 // Edge detection // 0x200800 // Clear FB auto *videoStream = OpenAssetAsSdlRwOps(filename); SVidHandle = Smacker_Open(videoStream); if (!SVidHandle.isValid) { return false; } #ifndef NOSOUND const bool enableAudio = (flags & 0x1000000) == 0; auto audioInfo = Smacker_GetAudioTrackDetails(SVidHandle, 0); LogVerbose(LogCategory::Audio, "SVid audio depth={} channels={} rate={}", audioInfo.bitsPerSample, audioInfo.nChannels, audioInfo.sampleRate); if (enableAudio && audioInfo.bitsPerSample != 0) { sound_stop(); // Stop in-progress music and sound effects SVidAudioDepth = audioInfo.bitsPerSample; SVidAudioBuffer = std::unique_ptr { new int16_t[audioInfo.idealBufferSize / 2] }; #ifndef USE_SDL3 auto decoder = std::make_unique(audioInfo.nChannels, audioInfo.sampleRate); SVidAudioDecoder = decoder.get(); SVidAudioStream.emplace(/*rwops=*/nullptr, std::move(decoder), CreateAulibResampler(audioInfo.sampleRate), /*closeRw=*/false); SVidAudioStream->setVolume(GetVolume()); #endif #ifdef USE_SDL3 SVidInitAudioStream(audioInfo); #else if (!diablo_is_focused()) SVidMute(); if (!SVidAudioStream->open()) { LogError(LogCategory::Audio, "Aulib::Stream::open (from SVidPlayBegin): {}", SDL_GetError()); SVidAudioStream = std::nullopt; SVidAudioDecoder = nullptr; } if (!SVidAudioStream->play()) { LogError(LogCategory::Audio, "Aulib::Stream::play (from SVidPlayBegin): {}", SDL_GetError()); SVidAudioStream = std::nullopt; SVidAudioDecoder = nullptr; } #endif } #endif // SMK format internally defines the frame rate as the frame duration // in either milliseconds or SMK time units (0.01ms). The library converts it // to FPS, which is always an integer, and here we convert it back to SMK time units. SVidFrameLength = 100000 / static_cast(Smacker_GetFrameRate(SVidHandle)); Smacker_GetFrameSize(SVidHandle, SVidWidth, SVidHeight); #ifndef USE_SDL1 if (renderer != nullptr) { const int renderWidth = static_cast(SVidWidth); const int renderHeight = static_cast(SVidHeight); texture = SDLWrap::CreateTexture(renderer, DEVILUTIONX_DISPLAY_TEXTURE_FORMAT, SDL_TEXTUREACCESS_STREAMING, renderWidth, renderHeight); if ( #ifdef USE_SDL3 !SDL_SetRenderLogicalPresentation(renderer, renderWidth, renderHeight, *GetOptions().Graphics.integerScaling ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_STRETCH) #else SDL_RenderSetLogicalSize(renderer, renderWidth, renderHeight) <= -1 #endif ) { ErrSdl(); } } #if defined(DEVILUTIONX_DISPLAY_PIXELFORMAT) && DEVILUTIONX_DISPLAY_PIXELFORMAT == SDL_PIXELFORMAT_INDEX8 else { const Size windowSize = { static_cast(SVidWidth), static_cast(SVidHeight) }; SDL_DisplayMode nearestDisplayMode = GetNearestDisplayMode(windowSize, DEVILUTIONX_DISPLAY_PIXELFORMAT); if (SDL_SetWindowDisplayMode(ghMainWnd, &nearestDisplayMode) != 0) { ErrSdl(); } } #endif #else TrySetVideoModeToSVidForSDL1(); #endif // Set the background to black. SDL_FillSurfaceRect(GetOutputSurface(), nullptr, 0x000000); // The buffer for the frame. It is not the same as the SDL surface because the SDL surface also has pitch padding. SVidFrameBuffer = std::unique_ptr { new uint8_t[static_cast(SVidWidth * SVidHeight)] }; // Decode first frame. Smacker_GetNextFrame(SVidHandle); Smacker_GetFrame(SVidHandle, SVidFrameBuffer.get()); // Create the surface from the frame buffer data. // It will be rendered in `SVidPlayContinue`, called immediately after this function. // Subsequents frames will also be copied to this surface. SVidSurface = SDLWrap::CreateRGBSurfaceWithFormatFrom( reinterpret_cast(SVidFrameBuffer.get()), static_cast(SVidWidth), static_cast(SVidHeight), 8, static_cast(SVidWidth), SDL_PIXELFORMAT_INDEX8); SVidPalette = SDLWrap::AllocPalette(); UpdatePalette(); SVidFrameEnd = GetTicksSmk() + SVidFrameLength; return true; } bool SVidPlayContinue() { if (Smacker_DidPaletteChange(SVidHandle)) { UpdatePalette(); } if (GetTicksSmk() >= SVidFrameEnd) { #if defined(USE_SDL3) && !defined(NOSOUND) if (ShouldPushAudioData()) SDL_ClearAudioStream(SVidAudioStream); #endif return SVidLoadNextFrame(); // Skip video and audio if the system is to slow } #ifndef NOSOUND if (ShouldPushAudioData()) { std::int16_t *buf = SVidAudioBuffer.get(); const auto len = Smacker_GetAudioData(SVidHandle, 0, buf); #ifdef USE_SDL3 if (!SDL_PutAudioStreamData(SVidAudioStream, buf, static_cast(len))) { LogError(LogCategory::Audio, "SDL_PutAudioStreamData (from SVidPlayContinue): {}", SDL_GetError()); SDL_ClearError(); SDL_DestroyAudioStream(SVidAudioStream); SVidAudioStream = nullptr; } #else if (SVidAudioDepth == 16) { SVidAudioDecoder->PushSamples(buf, len / 2); } else { SVidAudioDecoder->PushSamples(reinterpret_cast(buf), len); } #endif } #endif if (GetTicksSmk() >= SVidFrameEnd) { return SVidLoadNextFrame(); // Skip video if the system is to slow } if (!BlitFrame()) return false; const uint64_t now = GetTicksSmk(); if (now < SVidFrameEnd) { SDL_Delay(static_cast(TimeSmkToMs(SVidFrameEnd - now))); // wait with next frame if the system is too fast } return SVidLoadNextFrame(); } void SVidPlayEnd() { #ifndef NOSOUND if (SVidAudioStream) { #ifdef USE_SDL3 SDL_DestroyAudioStream(SVidAudioStream); SVidAudioStream = nullptr; #else SVidAudioStream = std::nullopt; SVidAudioDecoder = nullptr; #endif SVidAudioBuffer = nullptr; } #endif if (SVidHandle.isValid) Smacker_Close(SVidHandle); SVidPalette = nullptr; SVidSurface = nullptr; SVidFrameBuffer = nullptr; #ifndef USE_SDL1 if (renderer != nullptr) { texture = SDLWrap::CreateTexture(renderer, DEVILUTIONX_DISPLAY_TEXTURE_FORMAT, SDL_TEXTUREACCESS_STREAMING, gnScreenWidth, gnScreenHeight); if ( #ifdef USE_SDL3 !SDL_SetRenderLogicalPresentation(renderer, gnScreenWidth, gnScreenHeight, *GetOptions().Graphics.integerScaling ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_STRETCH) #else SDL_RenderSetLogicalSize(renderer, gnScreenWidth, gnScreenHeight) <= -1 #endif ) { ErrSdl(); } } #if defined(DEVILUTIONX_DISPLAY_PIXELFORMAT) && DEVILUTIONX_DISPLAY_PIXELFORMAT == SDL_PIXELFORMAT_INDEX8 else { const Size windowSize = { static_cast(gnScreenWidth), static_cast(gnScreenHeight) }; SDL_DisplayMode nearestDisplayMode = GetNearestDisplayMode(windowSize, DEVILUTIONX_DISPLAY_PIXELFORMAT); if (SDL_SetWindowDisplayMode(ghMainWnd, &nearestDisplayMode) != 0) { ErrSdl(); } } #endif #else if (IsSVidVideoMode) { SetVideoModeToPrimary(IsFullScreen(), gnScreenWidth, gnScreenHeight); IsSVidVideoMode = false; } #endif } void SVidMute() { #ifndef NOSOUND #ifdef USE_SDL3 SVidAutoStreamEnabled = false; #else if (SVidAudioStream) SVidAudioStream->mute(); #endif #endif } void SVidUnmute() { #ifndef NOSOUND #ifdef USE_SDL3 if (SVidAudioStream != nullptr) SVidAutoStreamEnabled = true; #else if (SVidAudioStream) SVidAudioStream->unmute(); #endif #endif } } // namespace devilution ================================================ FILE: Source/storm/storm_svid.h ================================================ #pragma once namespace devilution { bool SVidPlayBegin(const char *filename, int flags); bool SVidPlayContinue(); void SVidPlayEnd(); void SVidMute(); void SVidUnmute(); } // namespace devilution ================================================ FILE: Source/sync.cpp ================================================ /** * @file sync.cpp * * Implementation of functionality for syncing game state with other players. */ #include #include #include "levels/gendung.h" #include "lighting.h" #include "monster.h" #include "monsters/validation.hpp" #include "player.h" #include "utils/endian_swap.hpp" #include "utils/is_of.hpp" namespace devilution { namespace { uint16_t sgnMonsterPriority[MaxMonsters]; size_t sgnMonsters; uint16_t sgwLRU[MaxMonsters]; int sgnSyncItem; int sgnSyncPInv; void SyncOneMonster() { for (size_t i = 0; i < ActiveMonsterCount; i++) { const unsigned m = ActiveMonsters[i]; const Monster &monster = Monsters[m]; sgnMonsterPriority[m] = MyPlayer->position.tile.ManhattanDistance(monster.position.tile); if (monster.activeForTicks == 0) { sgnMonsterPriority[m] += 0x1000; } else if (sgwLRU[m] != 0) { sgwLRU[m]--; } } } void SyncMonsterPos(TSyncMonster &monsterSync, int ndx) { Monster &monster = Monsters[ndx]; monsterSync._mndx = ndx; monsterSync._mx = monster.position.tile.x; monsterSync._my = monster.position.tile.y; monsterSync._menemy = encode_enemy(monster); monsterSync._mdelta = sgnMonsterPriority[ndx] > 255 ? 255 : sgnMonsterPriority[ndx]; monsterSync.mWhoHit = monster.whoHit; monsterSync._mhitpoints = Swap32LE(monster.hitPoints); sgnMonsterPriority[ndx] = 0xFFFF; sgwLRU[ndx] = monster.activeForTicks == 0 ? 0xFFFF : 0xFFFE; } bool SyncMonsterActive(TSyncMonster &monsterSync) { unsigned ndx = std::numeric_limits::max(); uint32_t lru = 0xFFFFFFFF; for (size_t i = 0; i < ActiveMonsterCount; i++) { const unsigned m = ActiveMonsters[i]; if (sgnMonsterPriority[m] < lru && sgwLRU[m] < 0xFFFE) { lru = sgnMonsterPriority[m]; ndx = ActiveMonsters[i]; } } if (ndx == std::numeric_limits::max()) { return false; } SyncMonsterPos(monsterSync, static_cast(ndx)); return true; } bool SyncMonsterActive2(TSyncMonster &monsterSync) { unsigned ndx = std::numeric_limits::max(); uint32_t lru = 0xFFFE; for (size_t i = 0; i < ActiveMonsterCount; i++) { if (sgnMonsters >= ActiveMonsterCount) { sgnMonsters = 0; } const unsigned m = ActiveMonsters[sgnMonsters]; if (sgwLRU[m] < lru) { lru = sgwLRU[m]; ndx = ActiveMonsters[sgnMonsters]; } sgnMonsters++; } if (ndx == std::numeric_limits::max()) { return false; } SyncMonsterPos(monsterSync, static_cast(ndx)); return true; } void SyncPlrInv(TSyncHeader *pHdr) { pHdr->bItemI = -1; if (ActiveItemCount > 0) { if (sgnSyncItem >= ActiveItemCount) { sgnSyncItem = 0; } pHdr->bItemI = ActiveItems[sgnSyncItem]; sgnSyncItem++; auto &item = Items[pHdr->bItemI]; pHdr->bItemX = item.position.x; pHdr->bItemY = item.position.y; pHdr->wItemIndx = Swap16LE(item.IDidx); if (item.IDidx == IDI_EAR) { pHdr->wItemCI = Swap16LE((item._iIName[0] << 8) | item._iIName[1]); pHdr->dwItemSeed = Swap32LE((item._iIName[2] << 24) | (item._iIName[3] << 16) | (item._iIName[4] << 8) | item._iIName[5]); pHdr->bItemId = item._iIName[6]; pHdr->bItemDur = item._iIName[7]; pHdr->bItemMDur = item._iIName[8]; pHdr->bItemCh = item._iIName[9]; pHdr->bItemMCh = item._iIName[10]; pHdr->wItemVal = Swap16LE((item._iIName[11] << 8) | ((item._iCurs - ICURS_EAR_SORCERER) << 6) | item._ivalue); pHdr->dwItemBuff = Swap32LE((item._iIName[12] << 24) | (item._iIName[13] << 16) | (item._iIName[14] << 8) | item._iIName[15]); } else { pHdr->wItemCI = Swap16LE(item._iCreateInfo); pHdr->dwItemSeed = Swap32LE(item._iSeed); pHdr->bItemId = item._iIdentified ? 1 : 0; pHdr->bItemDur = item._iDurability; pHdr->bItemMDur = item._iMaxDur; pHdr->bItemCh = item._iCharges; pHdr->bItemMCh = item._iMaxCharges; if (item.IDidx == IDI_GOLD) { pHdr->wItemVal = Swap16LE(item._ivalue); } } } pHdr->bPInvLoc = -1; assert(sgnSyncPInv > -1 && sgnSyncPInv < NUM_INVLOC); const auto &item = MyPlayer->InvBody[sgnSyncPInv]; if (!item.isEmpty()) { pHdr->bPInvLoc = sgnSyncPInv; pHdr->wPInvIndx = Swap16LE(item.IDidx); pHdr->wPInvCI = Swap16LE(item._iCreateInfo); pHdr->dwPInvSeed = Swap32LE(item._iSeed); pHdr->bPInvId = item._iIdentified ? 1 : 0; } sgnSyncPInv++; if (sgnSyncPInv >= NUM_INVLOC) { sgnSyncPInv = 0; } } void SyncMonster(bool isOwner, const TSyncMonster &monsterSync) { Monster &monster = Monsters[monsterSync._mndx]; if (monster.hitPoints <= 0 || monster.mode == MonsterMode::Death) { return; } const Point position { monsterSync._mx, monsterSync._my }; const uint8_t enemyId = monsterSync._menemy; if (monster.activeForTicks != 0) { uint32_t delta = MyPlayer->position.tile.ManhattanDistance(monster.position.tile); if (delta > 255) { delta = 255; } if (delta < monsterSync._mdelta || (delta == monsterSync._mdelta && isOwner)) { return; } if (monster.position.future == position) { return; } } if (IsAnyOf(monster.mode, MonsterMode::Charge, MonsterMode::Petrified)) { return; } if (monster.position.tile.WalkingDistance(position) <= 2) { if (!monster.isWalking()) { const Direction md = GetDirection(monster.position.tile, position); if (DirOK(monster, md)) { M_ClearSquares(monster); monster.occupyTile(monster.position.tile, false); Walk(monster, md); monster.activeForTicks = std::numeric_limits::max(); } } } else if (dMonster[position.x][position.y] == 0) { M_ClearSquares(monster); monster.occupyTile(position, false); monster.position.tile = position; if (monster.lightId != NO_LIGHT) ChangeLightXY(monster.lightId, position); decode_enemy(monster, enemyId); const Direction md = GetDirection(position, monster.enemyPosition); M_StartStand(monster, md); monster.activeForTicks = std::numeric_limits::max(); } decode_enemy(monster, enemyId); monster.whoHit = static_cast(monster.whoHit | monsterSync.mWhoHit); } bool IsTSyncMonsterValid(const TSyncMonster &monsterSync) { const size_t monsterId = monsterSync._mndx; if (monsterId >= MaxMonsters) return false; if (!InDungeonBounds({ monsterSync._mx, monsterSync._my })) return false; if (!IsEnemyIdValid(monsterSync._menemy)) return false; return true; } bool IsTSyncEnemyValid(const TSyncMonster &monsterSync) { return IsEnemyValid(monsterSync._mndx, monsterSync._menemy); } } // namespace size_t sync_all_monsters(std::byte *pbBuf, size_t dwMaxLen) { if (ActiveMonsterCount < 1) { return dwMaxLen; } if (dwMaxLen < sizeof(TSyncHeader) + sizeof(TSyncMonster)) { return dwMaxLen; } if (MyPlayer->_pLvlChanging) { return dwMaxLen; } auto *pHdr = reinterpret_cast(pbBuf); pbBuf += sizeof(TSyncHeader); dwMaxLen -= sizeof(TSyncHeader); pHdr->bCmd = CMD_SYNCDATA; pHdr->bLevel = GetLevelForMultiplayer(*MyPlayer); pHdr->wLen = 0; SyncPlrInv(pHdr); assert(dwMaxLen <= 0xffff); SyncOneMonster(); for (size_t i = 0; i < ActiveMonsterCount && dwMaxLen >= sizeof(TSyncMonster); i++) { auto &monsterSync = *reinterpret_cast(pbBuf); bool sync = false; if (i < 2) { sync = SyncMonsterActive2(monsterSync); } if (!sync) { sync = SyncMonsterActive(monsterSync); } if (!sync) { break; } pbBuf += sizeof(TSyncMonster); pHdr->wLen += sizeof(TSyncMonster); dwMaxLen -= sizeof(TSyncMonster); } pHdr->wLen = Swap16LE(pHdr->wLen); return dwMaxLen; } size_t OnSyncData(const TSyncHeader &header, size_t maxCmdSize, const Player &player) { const uint16_t wLen = Swap16LE(header.wLen); if (!ValidateCmdSize(wLen + sizeof(header), maxCmdSize, player.getId())) return maxCmdSize; assert(gbBufferMsgs != 2); if (gbBufferMsgs == 1) { return wLen + sizeof(header); } if (&player == MyPlayer) { return wLen + sizeof(header); } assert(header.wLen % sizeof(TSyncMonster) == 0); const int monsterCount = static_cast(wLen / sizeof(TSyncMonster)); const uint8_t level = header.bLevel; const bool syncLocalLevel = !MyPlayer->_pLvlChanging && GetLevelForMultiplayer(*MyPlayer) == level; if (IsValidLevelForMultiplayer(level)) { const auto *monsterSyncs = reinterpret_cast(&header + 1); const bool isOwner = player.getId() > MyPlayerId; for (int i = 0; i < monsterCount; i++) { if (!IsTSyncMonsterValid(monsterSyncs[i])) continue; if (syncLocalLevel) { if (!IsTSyncEnemyValid(monsterSyncs[i])) continue; SyncMonster(isOwner, monsterSyncs[i]); } delta_sync_monster(monsterSyncs[i], level); } } return wLen + sizeof(header); } void sync_init() { sgnMonsters = static_cast(16 * MyPlayerId); memset(sgwLRU, 255, sizeof(sgwLRU)); } } // namespace devilution ================================================ FILE: Source/sync.h ================================================ /** * @file sync.h * * Interface of functionality for syncing game state with other players. */ #pragma once #include #include #include "msg.h" #include "player.h" namespace devilution { size_t sync_all_monsters(std::byte *pbBuf, size_t dwMaxLen); size_t OnSyncData(const TSyncHeader &header, size_t maxCmdSize, const Player &player); void sync_init(); } // namespace devilution ================================================ FILE: Source/tables/itemdat.cpp ================================================ /** * @file itemdat.cpp * * Implementation of all item data. */ #include "tables/itemdat.h" #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" #include "lua/lua_event.hpp" #include "tables/spelldat.h" #include "utils/str_cat.hpp" namespace devilution { /** Contains the data related to each item ID. */ std::vector AllItemsList; /** Contains item mapping IDs, with item indices assigned to them. This is used for loading saved games. */ ankerl::unordered_dense::map ItemMappingIdsToIndices; /** Contains the mapping between unique base item ID strings and indices, used for parsing additional item data. */ ankerl::unordered_dense::map AdditionalUniqueBaseItemStringsToIndices; /** Contains the data related to each unique item ID. */ std::vector UniqueItems; /** Contains unique item mapping IDs, with unique item indices assigned to them. This is used for loading saved games. */ ankerl::unordered_dense::map UniqueItemMappingIdsToIndices; /** Contains the data related to each item prefix. */ std::vector ItemPrefixes; /** Contains the data related to each item suffix. */ std::vector ItemSuffixes; tl::expected<_item_indexes, std::string> ParseItemId(std::string_view value) { const std::optional<_item_indexes> enumValueOpt = magic_enum::enum_cast<_item_indexes>(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } namespace { tl::expected ParseItemClass(std::string_view value) { if (value == "None") return ICLASS_NONE; if (value == "Weapon") return ICLASS_WEAPON; if (value == "Armor") return ICLASS_ARMOR; if (value == "Misc") return ICLASS_MISC; if (value == "Gold") return ICLASS_GOLD; if (value == "Quest") return ICLASS_QUEST; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseItemEquipType(std::string_view value) { if (value == "None") return ILOC_NONE; if (value == "One-handed") return ILOC_ONEHAND; if (value == "Two-handed") return ILOC_TWOHAND; if (value == "Armor") return ILOC_ARMOR; if (value == "Helm") return ILOC_HELM; if (value == "Ring") return ILOC_RING; if (value == "Amulet") return ILOC_AMULET; if (value == "Unequippable") return ILOC_UNEQUIPABLE; if (value == "Belt") return ILOC_BELT; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseItemCursorGraphic(std::string_view value) { if (value == "POTION_OF_FULL_MANA") return ICURS_POTION_OF_FULL_MANA; if (value == "SCROLL_OF") return ICURS_SCROLL_OF; if (value == "GOLD_SMALL") return ICURS_GOLD_SMALL; if (value == "GOLD_MEDIUM") return ICURS_GOLD_MEDIUM; if (value == "GOLD_LARGE") return ICURS_GOLD_LARGE; if (value == "THE_BLEEDER") return ICURS_THE_BLEEDER; if (value == "BRAMBLE") return ICURS_BRAMBLE; if (value == "RING_OF_TRUTH") return ICURS_RING_OF_TRUTH; if (value == "RING_OF_REGHA") return ICURS_RING_OF_REGHA; if (value == "RING") return ICURS_RING; if (value == "RING_OF_ENGAGEMENT") return ICURS_RING_OF_ENGAGEMENT; if (value == "CONSTRICTING_RING") return ICURS_CONSTRICTING_RING; if (value == "SPECTRAL_ELIXIR") return ICURS_SPECTRAL_ELIXIR; if (value == "ARENA_POTION") return ICURS_ARENA_POTION; if (value == "GOLDEN_ELIXIR") return ICURS_GOLDEN_ELIXIR; if (value == "EMPYREAN_BAND") return ICURS_EMPYREAN_BAND; if (value == "EAR_SORCERER") return ICURS_EAR_SORCERER; if (value == "EAR_WARRIOR") return ICURS_EAR_WARRIOR; if (value == "EAR_ROGUE") return ICURS_EAR_ROGUE; if (value == "BLOOD_STONE") return ICURS_BLOOD_STONE; if (value == "OIL") return ICURS_OIL; if (value == "ELIXIR_OF_VITALITY") return ICURS_ELIXIR_OF_VITALITY; if (value == "POTION_OF_HEALING") return ICURS_POTION_OF_HEALING; if (value == "POTION_OF_FULL_REJUVENATION") return ICURS_POTION_OF_FULL_REJUVENATION; if (value == "ELIXIR_OF_MAGIC") return ICURS_ELIXIR_OF_MAGIC; if (value == "POTION_OF_FULL_HEALING") return ICURS_POTION_OF_FULL_HEALING; if (value == "ELIXIR_OF_DEXTERITY") return ICURS_ELIXIR_OF_DEXTERITY; if (value == "POTION_OF_REJUVENATION") return ICURS_POTION_OF_REJUVENATION; if (value == "ELIXIR_OF_STRENGTH") return ICURS_ELIXIR_OF_STRENGTH; if (value == "POTION_OF_MANA") return ICURS_POTION_OF_MANA; if (value == "BRAIN") return ICURS_BRAIN; if (value == "OPTIC_AMULET") return ICURS_OPTIC_AMULET; if (value == "AMULET") return ICURS_AMULET; if (value == "WIZARDSPIKE") return ICURS_WIZARDSPIKE; if (value == "DAGGER") return ICURS_DAGGER; if (value == "BLACK_RAZOR") return ICURS_BLACK_RAZOR; if (value == "GONNAGALS_DIRK") return ICURS_GONNAGALS_DIRK; if (value == "BLADE") return ICURS_BLADE; if (value == "BASTARD_SWORD") return ICURS_BASTARD_SWORD; if (value == "THE_EXECUTIONERS_BLADE") return ICURS_THE_EXECUTIONERS_BLADE; if (value == "MACE") return ICURS_MACE; if (value == "LONG_SWORD") return ICURS_LONG_SWORD; if (value == "BROAD_SWORD") return ICURS_BROAD_SWORD; if (value == "FALCHION") return ICURS_FALCHION; if (value == "MORNING_STAR") return ICURS_MORNING_STAR; if (value == "SHORT_SWORD") return ICURS_SHORT_SWORD; if (value == "CLAYMORE") return ICURS_CLAYMORE; if (value == "CLUB") return ICURS_CLUB; if (value == "SABRE") return ICURS_SABRE; if (value == "GRYPHONS_CLAW") return ICURS_GRYPHONS_CLAW; if (value == "SPIKED_CLUB") return ICURS_SPIKED_CLUB; if (value == "SCIMITAR") return ICURS_SCIMITAR; if (value == "FULL_HELM") return ICURS_FULL_HELM; if (value == "MAGIC_ROCK") return ICURS_MAGIC_ROCK; if (value == "HELM_OF_SPIRITS") return ICURS_HELM_OF_SPIRITS; if (value == "THE_UNDEAD_CROWN") return ICURS_THE_UNDEAD_CROWN; if (value == "ROYAL_CIRCLET") return ICURS_ROYAL_CIRCLET; if (value == "FOOLS_CREST") return ICURS_FOOLS_CREST; if (value == "HARLEQUIN_CREST") return ICURS_HARLEQUIN_CREST; if (value == "HELM") return ICURS_HELM; if (value == "BUCKLER") return ICURS_BUCKLER; if (value == "VEIL_OF_STEEL") return ICURS_VEIL_OF_STEEL; if (value == "BOOK_GREY") return ICURS_BOOK_GREY; if (value == "BOOK_RED") return ICURS_BOOK_RED; if (value == "BOOK_BLUE") return ICURS_BOOK_BLUE; if (value == "BLACK_MUSHROOM") return ICURS_BLACK_MUSHROOM; if (value == "SKULL_CAP") return ICURS_SKULL_CAP; if (value == "CAP") return ICURS_CAP; if (value == "TORN_FLESH_OF_SOULS") return ICURS_TORN_FLESH_OF_SOULS; if (value == "THINKING_CAP") return ICURS_THINKING_CAP; if (value == "CROWN") return ICURS_CROWN; if (value == "MAP_OF_THE_STARS") return ICURS_MAP_OF_THE_STARS; if (value == "FUNGAL_TOME") return ICURS_FUNGAL_TOME; if (value == "GREAT_HELM") return ICURS_GREAT_HELM; if (value == "OVERLORDS_HELM") return ICURS_OVERLORDS_HELM; if (value == "BATTLE_AXE") return ICURS_BATTLE_AXE; if (value == "HUNTERS_BOW") return ICURS_HUNTERS_BOW; if (value == "FIELD_PLATE") return ICURS_FIELD_PLATE; if (value == "STONECLEAVER") return ICURS_STONECLEAVER; if (value == "SMALL_SHIELD") return ICURS_SMALL_SHIELD; if (value == "CLEAVER") return ICURS_CLEAVER; if (value == "STUDDED_LEATHER_ARMOR") return ICURS_STUDDED_LEATHER_ARMOR; if (value == "DEADLY_HUNTER") return ICURS_DEADLY_HUNTER; if (value == "SHORT_STAFF") return ICURS_SHORT_STAFF; if (value == "TWO_HANDED_SWORD") return ICURS_TWO_HANDED_SWORD; if (value == "CHAIN_MAIL") return ICURS_CHAIN_MAIL; if (value == "SMALL_AXE") return ICURS_SMALL_AXE; if (value == "KITE_SHIELD") return ICURS_KITE_SHIELD; if (value == "SCALE_MAIL") return ICURS_SCALE_MAIL; if (value == "SPLIT_SKULL_SHIELD") return ICURS_SPLIT_SKULL_SHIELD; if (value == "DRAGONS_BREACH") return ICURS_DRAGONS_BREACH; if (value == "SHORT_BOW") return ICURS_SHORT_BOW; if (value == "LONG_BATTLE_BOW") return ICURS_LONG_BATTLE_BOW; if (value == "LONG_WAR_BOW") return ICURS_LONG_WAR_BOW; if (value == "WAR_HAMMER") return ICURS_WAR_HAMMER; if (value == "MAUL") return ICURS_MAUL; if (value == "LONG_STAFF") return ICURS_LONG_STAFF; if (value == "WAR_STAFF") return ICURS_WAR_STAFF; if (value == "TAVERN_SIGN") return ICURS_TAVERN_SIGN; if (value == "HARD_LEATHER_ARMOR") return ICURS_HARD_LEATHER_ARMOR; if (value == "RAGS") return ICURS_RAGS; if (value == "QUILTED_ARMOR") return ICURS_QUILTED_ARMOR; if (value == "FLAIL") return ICURS_FLAIL; if (value == "TOWER_SHIELD") return ICURS_TOWER_SHIELD; if (value == "COMPOSITE_BOW") return ICURS_COMPOSITE_BOW; if (value == "GREAT_SWORD") return ICURS_GREAT_SWORD; if (value == "LEATHER_ARMOR") return ICURS_LEATHER_ARMOR; if (value == "SPLINT_MAIL") return ICURS_SPLINT_MAIL; if (value == "ROBE") return ICURS_ROBE; if (value == "THE_RAINBOW_CLOAK") return ICURS_THE_RAINBOW_CLOAK; if (value == "ANVIL_OF_FURY") return ICURS_ANVIL_OF_FURY; if (value == "BROAD_AXE") return ICURS_BROAD_AXE; if (value == "LARGE_AXE") return ICURS_LARGE_AXE; if (value == "GREAT_AXE") return ICURS_GREAT_AXE; if (value == "AXE") return ICURS_AXE; if (value == "BLACKOAK_SHIELD") return ICURS_BLACKOAK_SHIELD; if (value == "LARGE_SHIELD") return ICURS_LARGE_SHIELD; if (value == "GOTHIC_SHIELD") return ICURS_GOTHIC_SHIELD; if (value == "CLOAK") return ICURS_CLOAK; if (value == "CAPE") return ICURS_CAPE; if (value == "FULL_PLATE_MAIL") return ICURS_FULL_PLATE_MAIL; if (value == "GOTHIC_PLATE") return ICURS_GOTHIC_PLATE; if (value == "BREAST_PLATE") return ICURS_BREAST_PLATE; if (value == "RING_MAIL") return ICURS_RING_MAIL; if (value == "STAFF_OF_LAZARUS") return ICURS_STAFF_OF_LAZARUS; if (value == "ARKAINES_VALOR") return ICURS_ARKAINES_VALOR; if (value == "THE_NEEDLER") return ICURS_THE_NEEDLER; if (value == "NAJS_LIGHT_PLATE") return ICURS_NAJS_LIGHT_PLATE; if (value == "THE_GRIZZLY") return ICURS_THE_GRIZZLY; if (value == "THE_GRANDFATHER") return ICURS_THE_GRANDFATHER; if (value == "THE_PROTECTOR") return ICURS_THE_PROTECTOR; if (value == "MESSERSCHMIDTS_REAVER") return ICURS_MESSERSCHMIDTS_REAVER; if (value == "WINDFORCE") return ICURS_WINDFORCE; if (value == "SHORT_WAR_BOW") return ICURS_SHORT_WAR_BOW; if (value == "COMPOSITE_STAFF") return ICURS_COMPOSITE_STAFF; if (value == "SHORT_BATTLE_BOW") return ICURS_SHORT_BATTLE_BOW; if (value == "XORINES_RING") return ICURS_XORINES_RING; if (value == "AMULET_OF_WARDING") return ICURS_AMULET_OF_WARDING; if (value == "KARIKS_RING") return ICURS_KARIKS_RING; if (value == "MERCURIAL_RING") return ICURS_MERCURIAL_RING; if (value == "RING_OF_THUNDER") return ICURS_RING_OF_THUNDER; if (value == "GIANTS_KNUCKLE") return ICURS_GIANTS_KNUCKLE; if (value == "AURIC_AMULET") return ICURS_AURIC_AMULET; if (value == "RING_OF_THE_MYSTICS") return ICURS_RING_OF_THE_MYSTICS; if (value == "ACOLYTES_AMULET") return ICURS_ACOLYTES_AMULET; if (value == "RING_OF_MAGMA") return ICURS_RING_OF_MAGMA; if (value == "GLADIATORS_RING") return ICURS_GLADIATORS_RING; if (value == "RUNE_BOMB") return ICURS_RUNE_BOMB; if (value == "THEODORE") return ICURS_THEODORE; if (value == "TORN_NOTE_1") return ICURS_TORN_NOTE_1; if (value == "TORN_NOTE_2") return ICURS_TORN_NOTE_2; if (value == "TORN_NOTE_3") return ICURS_TORN_NOTE_3; if (value == "RECONSTRUCTED_NOTE") return ICURS_RECONSTRUCTED_NOTE; if (value == "RUNE_OF_FIRE") return ICURS_RUNE_OF_FIRE; if (value == "GREATER_RUNE_OF_FIRE") return ICURS_GREATER_RUNE_OF_FIRE; if (value == "RUNE_OF_LIGHTNING") return ICURS_RUNE_OF_LIGHTNING; if (value == "GREATER_RUNE_OF_LIGHTNING") return ICURS_GREATER_RUNE_OF_LIGHTNING; if (value == "RUNE_OF_STONE") return ICURS_RUNE_OF_STONE; if (value == "GREY_SUIT") return ICURS_GREY_SUIT; if (value == "BROWN_SUIT") return ICURS_BROWN_SUIT; if (value == "EATER_OF_SOULS") return ICURS_EATER_OF_SOULS; if (value == "ARMOR_OF_GLOOM") return ICURS_ARMOR_OF_GLOOM; if (value == "BONE_CHAIN_ARMOR") return ICURS_BONE_CHAIN_ARMOR; if (value == "THUNDERCLAP") return ICURS_THUNDERCLAP; if (value == "DIAMONDEDGE") return ICURS_DIAMONDEDGE; if (value == "FLAMBEAU") return ICURS_FLAMBEAU; if (value == "GNAT_STING") return ICURS_GNAT_STING; if (value == "BLITZEN") return ICURS_BLITZEN; if (value == "DEMON_PLATE_ARMOR") return ICURS_DEMON_PLATE_ARMOR; if (value == "BOVINE") return ICURS_BOVINE; if (value == "") return ICURS_DEFAULT; // also support providing the item cursor icon frame number directly return ParseInt(value) .map([](auto numericalValue) { return static_cast(numericalValue); }) .map_error([](auto) { return std::string("Unknown enum value"); }); } tl::expected ParseItemType(std::string_view value) { if (value == "Misc") return ItemType::Misc; if (value == "Sword") return ItemType::Sword; if (value == "Axe") return ItemType::Axe; if (value == "Bow") return ItemType::Bow; if (value == "Mace") return ItemType::Mace; if (value == "Shield") return ItemType::Shield; if (value == "LightArmor") return ItemType::LightArmor; if (value == "Helm") return ItemType::Helm; if (value == "MediumArmor") return ItemType::MediumArmor; if (value == "HeavyArmor") return ItemType::HeavyArmor; if (value == "Staff") return ItemType::Staff; if (value == "Gold") return ItemType::Gold; if (value == "Ring") return ItemType::Ring; if (value == "Amulet") return ItemType::Amulet; if (value == "None") return ItemType::None; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseUniqueBaseItem(std::string_view value) { if (value == "NONE") return UITYPE_NONE; if (value == "SHORTBOW") return UITYPE_SHORTBOW; if (value == "LONGBOW") return UITYPE_LONGBOW; if (value == "HUNTBOW") return UITYPE_HUNTBOW; if (value == "COMPBOW") return UITYPE_COMPBOW; if (value == "WARBOW") return UITYPE_WARBOW; if (value == "BATTLEBOW") return UITYPE_BATTLEBOW; if (value == "DAGGER") return UITYPE_DAGGER; if (value == "FALCHION") return UITYPE_FALCHION; if (value == "CLAYMORE") return UITYPE_CLAYMORE; if (value == "BROADSWR") return UITYPE_BROADSWR; if (value == "SABRE") return UITYPE_SABRE; if (value == "SCIMITAR") return UITYPE_SCIMITAR; if (value == "LONGSWR") return UITYPE_LONGSWR; if (value == "BASTARDSWR") return UITYPE_BASTARDSWR; if (value == "TWOHANDSWR") return UITYPE_TWOHANDSWR; if (value == "GREATSWR") return UITYPE_GREATSWR; if (value == "CLEAVER") return UITYPE_CLEAVER; if (value == "LARGEAXE") return UITYPE_LARGEAXE; if (value == "BROADAXE") return UITYPE_BROADAXE; if (value == "SMALLAXE") return UITYPE_SMALLAXE; if (value == "BATTLEAXE") return UITYPE_BATTLEAXE; if (value == "GREATAXE") return UITYPE_GREATAXE; if (value == "MACE") return UITYPE_MACE; if (value == "MORNSTAR") return UITYPE_MORNSTAR; if (value == "SPIKCLUB") return UITYPE_SPIKCLUB; if (value == "MAUL") return UITYPE_MAUL; if (value == "WARHAMMER") return UITYPE_WARHAMMER; if (value == "FLAIL") return UITYPE_FLAIL; if (value == "LONGSTAFF") return UITYPE_LONGSTAFF; if (value == "SHORTSTAFF") return UITYPE_SHORTSTAFF; if (value == "COMPSTAFF") return UITYPE_COMPSTAFF; if (value == "QUARSTAFF") return UITYPE_QUARSTAFF; if (value == "WARSTAFF") return UITYPE_WARSTAFF; if (value == "SKULLCAP") return UITYPE_SKULLCAP; if (value == "HELM") return UITYPE_HELM; if (value == "GREATHELM") return UITYPE_GREATHELM; if (value == "CROWN") return UITYPE_CROWN; if (value == "RAGS") return UITYPE_RAGS; if (value == "STUDARMOR") return UITYPE_STUDARMOR; if (value == "CLOAK") return UITYPE_CLOAK; if (value == "ROBE") return UITYPE_ROBE; if (value == "CHAINMAIL") return UITYPE_CHAINMAIL; if (value == "LEATHARMOR") return UITYPE_LEATHARMOR; if (value == "BREASTPLATE") return UITYPE_BREASTPLATE; if (value == "CAPE") return UITYPE_CAPE; if (value == "PLATEMAIL") return UITYPE_PLATEMAIL; if (value == "FULLPLATE") return UITYPE_FULLPLATE; if (value == "BUCKLER") return UITYPE_BUCKLER; if (value == "SMALLSHIELD") return UITYPE_SMALLSHIELD; if (value == "LARGESHIELD") return UITYPE_LARGESHIELD; if (value == "KITESHIELD") return UITYPE_KITESHIELD; if (value == "GOTHSHIELD") return UITYPE_GOTHSHIELD; if (value == "RING") return UITYPE_RING; if (value == "AMULET") return UITYPE_AMULET; if (value == "SKCROWN") return UITYPE_SKCROWN; if (value == "INFRARING") return UITYPE_INFRARING; if (value == "OPTAMULET") return UITYPE_OPTAMULET; if (value == "TRING") return UITYPE_TRING; if (value == "HARCREST") return UITYPE_HARCREST; if (value == "MAPOFDOOM") return UITYPE_MAPOFDOOM; if (value == "ELIXIR") return UITYPE_ELIXIR; if (value == "ARMOFVAL") return UITYPE_ARMOFVAL; if (value == "STEELVEIL") return UITYPE_STEELVEIL; if (value == "GRISWOLD") return UITYPE_GRISWOLD; if (value == "LGTFORGE") return UITYPE_LGTFORGE; if (value == "LAZSTAFF") return UITYPE_LAZSTAFF; if (value == "BOVINE") return UITYPE_BOVINE; if (value == "INVALID") return UITYPE_INVALID; const auto findIt = AdditionalUniqueBaseItemStringsToIndices.find(std::string(value)); if (findIt != AdditionalUniqueBaseItemStringsToIndices.end()) { return static_cast(findIt->second); } return tl::make_unexpected("Unknown enum value"); } tl::expected ParseOrAddUniqueBaseItem(std::string_view value) { const auto parseResult = ParseUniqueBaseItem(value); if (parseResult.has_value()) { return parseResult.value(); } const size_t newUniqueBaseItemIndex = static_cast(NUM_DEFAULT_UITYPES) + AdditionalUniqueBaseItemStringsToIndices.size(); if (newUniqueBaseItemIndex >= static_cast(NUM_MAX_UITYPES)) { return tl::make_unexpected(fmt::format("Could not define new unique base item \"{}\", since the maximum number of {} has already been reached.", value, static_cast(NUM_MAX_UITYPES))); } const unique_base_item newUniqueBaseItem = static_cast(newUniqueBaseItemIndex); AdditionalUniqueBaseItemStringsToIndices[std::string(value)] = newUniqueBaseItem; return newUniqueBaseItem; } tl::expected ParseItemSpecialEffect(std::string_view value) { if (value == "RandomStealLife") return ItemSpecialEffect::RandomStealLife; if (value == "RandomArrowVelocity") return ItemSpecialEffect::RandomArrowVelocity; if (value == "FireArrows") return ItemSpecialEffect::FireArrows; if (value == "FireDamage") return ItemSpecialEffect::FireDamage; if (value == "LightningDamage") return ItemSpecialEffect::LightningDamage; if (value == "DrainLife") return ItemSpecialEffect::DrainLife; if (value == "MultipleArrows") return ItemSpecialEffect::MultipleArrows; if (value == "Knockback") return ItemSpecialEffect::Knockback; if (value == "StealMana3") return ItemSpecialEffect::StealMana3; if (value == "StealMana5") return ItemSpecialEffect::StealMana5; if (value == "StealLife3") return ItemSpecialEffect::StealLife3; if (value == "StealLife5") return ItemSpecialEffect::StealLife5; if (value == "QuickAttack") return ItemSpecialEffect::QuickAttack; if (value == "FastAttack") return ItemSpecialEffect::FastAttack; if (value == "FasterAttack") return ItemSpecialEffect::FasterAttack; if (value == "FastestAttack") return ItemSpecialEffect::FastestAttack; if (value == "FastHitRecovery") return ItemSpecialEffect::FastHitRecovery; if (value == "FasterHitRecovery") return ItemSpecialEffect::FasterHitRecovery; if (value == "FastestHitRecovery") return ItemSpecialEffect::FastestHitRecovery; if (value == "FastBlock") return ItemSpecialEffect::FastBlock; if (value == "LightningArrows") return ItemSpecialEffect::LightningArrows; if (value == "Thorns") return ItemSpecialEffect::Thorns; if (value == "NoMana") return ItemSpecialEffect::NoMana; if (value == "HalfTrapDamage") return ItemSpecialEffect::HalfTrapDamage; if (value == "TripleDemonDamage") return ItemSpecialEffect::TripleDemonDamage; if (value == "ZeroResistance") return ItemSpecialEffect::ZeroResistance; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseItemMiscId(std::string_view value) { if (value == "NONE") return IMISC_NONE; if (value == "USEFIRST") return IMISC_USEFIRST; if (value == "FULLHEAL") return IMISC_FULLHEAL; if (value == "HEAL") return IMISC_HEAL; if (value == "MANA") return IMISC_MANA; if (value == "FULLMANA") return IMISC_FULLMANA; if (value == "ELIXSTR") return IMISC_ELIXSTR; if (value == "ELIXMAG") return IMISC_ELIXMAG; if (value == "ELIXDEX") return IMISC_ELIXDEX; if (value == "ELIXVIT") return IMISC_ELIXVIT; if (value == "REJUV") return IMISC_REJUV; if (value == "FULLREJUV") return IMISC_FULLREJUV; if (value == "USELAST") return IMISC_USELAST; if (value == "SCROLL") return IMISC_SCROLL; if (value == "SCROLLT") return IMISC_SCROLLT; if (value == "STAFF") return IMISC_STAFF; if (value == "BOOK") return IMISC_BOOK; if (value == "RING") return IMISC_RING; if (value == "AMULET") return IMISC_AMULET; if (value == "UNIQUE") return IMISC_UNIQUE; if (value == "OILFIRST") return IMISC_OILFIRST; if (value == "OILOF") return IMISC_OILOF; if (value == "OILACC") return IMISC_OILACC; if (value == "OILMAST") return IMISC_OILMAST; if (value == "OILSHARP") return IMISC_OILSHARP; if (value == "OILDEATH") return IMISC_OILDEATH; if (value == "OILSKILL") return IMISC_OILSKILL; if (value == "OILBSMTH") return IMISC_OILBSMTH; if (value == "OILFORT") return IMISC_OILFORT; if (value == "OILPERM") return IMISC_OILPERM; if (value == "OILHARD") return IMISC_OILHARD; if (value == "OILIMP") return IMISC_OILIMP; if (value == "OILLAST") return IMISC_OILLAST; if (value == "MAPOFDOOM") return IMISC_MAPOFDOOM; if (value == "EAR") return IMISC_EAR; if (value == "SPECELIX") return IMISC_SPECELIX; if (value == "RUNEFIRST") return IMISC_RUNEFIRST; if (value == "RUNEF") return IMISC_RUNEF; if (value == "RUNEL") return IMISC_RUNEL; if (value == "GR_RUNEL") return IMISC_GR_RUNEL; if (value == "GR_RUNEF") return IMISC_GR_RUNEF; if (value == "RUNES") return IMISC_RUNES; if (value == "RUNELAST") return IMISC_RUNELAST; if (value == "AURIC") return IMISC_AURIC; if (value == "NOTE") return IMISC_NOTE; if (value == "ARENAPOT") return IMISC_ARENAPOT; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseItemEffectType(std::string_view value) { if (value == "TOHIT") return IPL_TOHIT; if (value == "TOHIT_CURSE") return IPL_TOHIT_CURSE; if (value == "DAMP") return IPL_DAMP; if (value == "DAMP_CURSE") return IPL_DAMP_CURSE; if (value == "TOHIT_DAMP") return IPL_TOHIT_DAMP; if (value == "TOHIT_DAMP_CURSE") return IPL_TOHIT_DAMP_CURSE; if (value == "ACP") return IPL_ACP; if (value == "ACP_CURSE") return IPL_ACP_CURSE; if (value == "FIRERES") return IPL_FIRERES; if (value == "LIGHTRES") return IPL_LIGHTRES; if (value == "MAGICRES") return IPL_MAGICRES; if (value == "ALLRES") return IPL_ALLRES; if (value == "SPLLVLADD") return IPL_SPLLVLADD; if (value == "CHARGES") return IPL_CHARGES; if (value == "FIREDAM") return IPL_FIREDAM; if (value == "LIGHTDAM") return IPL_LIGHTDAM; if (value == "STR") return IPL_STR; if (value == "STR_CURSE") return IPL_STR_CURSE; if (value == "MAG") return IPL_MAG; if (value == "MAG_CURSE") return IPL_MAG_CURSE; if (value == "DEX") return IPL_DEX; if (value == "DEX_CURSE") return IPL_DEX_CURSE; if (value == "VIT") return IPL_VIT; if (value == "VIT_CURSE") return IPL_VIT_CURSE; if (value == "ATTRIBS") return IPL_ATTRIBS; if (value == "ATTRIBS_CURSE") return IPL_ATTRIBS_CURSE; if (value == "GETHIT_CURSE") return IPL_GETHIT_CURSE; if (value == "GETHIT") return IPL_GETHIT; if (value == "LIFE") return IPL_LIFE; if (value == "LIFE_CURSE") return IPL_LIFE_CURSE; if (value == "MANA") return IPL_MANA; if (value == "MANA_CURSE") return IPL_MANA_CURSE; if (value == "DUR") return IPL_DUR; if (value == "DUR_CURSE") return IPL_DUR_CURSE; if (value == "INDESTRUCTIBLE") return IPL_INDESTRUCTIBLE; if (value == "LIGHT") return IPL_LIGHT; if (value == "LIGHT_CURSE") return IPL_LIGHT_CURSE; if (value == "MULT_ARROWS") return IPL_MULT_ARROWS; if (value == "FIRE_ARROWS") return IPL_FIRE_ARROWS; if (value == "LIGHT_ARROWS") return IPL_LIGHT_ARROWS; if (value == "THORNS") return IPL_THORNS; if (value == "NOMANA") return IPL_NOMANA; if (value == "FIREBALL") return IPL_FIREBALL; if (value == "ABSHALFTRAP") return IPL_ABSHALFTRAP; if (value == "KNOCKBACK") return IPL_KNOCKBACK; if (value == "STEALMANA") return IPL_STEALMANA; if (value == "STEALLIFE") return IPL_STEALLIFE; if (value == "TARGAC") return IPL_TARGAC; if (value == "FASTATTACK") return IPL_FASTATTACK; if (value == "FASTRECOVER") return IPL_FASTRECOVER; if (value == "FASTBLOCK") return IPL_FASTBLOCK; if (value == "DAMMOD") return IPL_DAMMOD; if (value == "RNDARROWVEL") return IPL_RNDARROWVEL; if (value == "SETDAM") return IPL_SETDAM; if (value == "SETDUR") return IPL_SETDUR; if (value == "NOMINSTR") return IPL_NOMINSTR; if (value == "SPELL") return IPL_SPELL; if (value == "ONEHAND") return IPL_ONEHAND; if (value == "3XDAMVDEM") return IPL_3XDAMVDEM; if (value == "ALLRESZERO") return IPL_ALLRESZERO; if (value == "DRAINLIFE") return IPL_DRAINLIFE; if (value == "RNDSTEALLIFE") return IPL_RNDSTEALLIFE; if (value == "SETAC") return IPL_SETAC; if (value == "ADDACLIFE") return IPL_ADDACLIFE; if (value == "ADDMANAAC") return IPL_ADDMANAAC; if (value == "AC_CURSE") return IPL_AC_CURSE; if (value == "LASTDIABLO") return IPL_LASTDIABLO; if (value == "FIRERES_CURSE") return IPL_FIRERES_CURSE; if (value == "LIGHTRES_CURSE") return IPL_LIGHTRES_CURSE; if (value == "MAGICRES_CURSE") return IPL_MAGICRES_CURSE; if (value == "DEVASTATION") return IPL_DEVASTATION; if (value == "DECAY") return IPL_DECAY; if (value == "PERIL") return IPL_PERIL; if (value == "JESTERS") return IPL_JESTERS; if (value == "CRYSTALLINE") return IPL_CRYSTALLINE; if (value == "DOPPELGANGER") return IPL_DOPPELGANGER; if (value == "ACDEMON") return IPL_ACDEMON; if (value == "ACUNDEAD") return IPL_ACUNDEAD; if (value == "MANATOLIFE") return IPL_MANATOLIFE; if (value == "LIFETOMANA") return IPL_LIFETOMANA; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseAffixItemType(std::string_view value) { if (value == "Misc") return AffixItemType::Misc; if (value == "Bow") return AffixItemType::Bow; if (value == "Staff") return AffixItemType::Staff; if (value == "Weapon") return AffixItemType::Weapon; if (value == "Shield") return AffixItemType::Shield; if (value == "Armor") return AffixItemType::Armor; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseAffixAlignment(std::string_view value) { if (value == "Any") return GOE_ANY; if (value == "Evil") return GOE_EVIL; if (value == "Good") return GOE_GOOD; return tl::make_unexpected("Unknown enum value"); } } // namespace void LoadItemDatFromFile(DataFile &dataFile, std::string_view filename, int32_t baseMappingId) { dataFile.skipHeaderOrDie(filename); int32_t currentMappingId = baseMappingId; AllItemsList.reserve(AllItemsList.size() + dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; ItemData &item = AllItemsList.emplace_back(); reader.advance(); // Skip the first column (item ID). reader.readInt("dropRate", item.dropRate); reader.read("class", item.iClass, ParseItemClass); reader.read("equipType", item.iLoc, ParseItemEquipType); reader.read("cursorGraphic", item.iCurs, ParseItemCursorGraphic); reader.read("itemType", item.itype, ParseItemType); reader.read("uniqueBaseItem", item.iItemId, ParseOrAddUniqueBaseItem); reader.readString("name", item.iName); reader.readString("shortName", item.iSName); reader.readInt("minMonsterLevel", item.iMinMLvl); reader.readInt("durability", item.iDurability); reader.readInt("minDamage", item.iMinDam); reader.readInt("maxDamage", item.iMaxDam); reader.readInt("minArmor", item.iMinAC); reader.readInt("maxArmor", item.iMaxAC); reader.readInt("minStrength", item.iMinStr); reader.readInt("minMagic", item.iMinMag); reader.readInt("minDexterity", item.iMinDex); reader.readEnumList("specialEffects", item.iFlags, ParseItemSpecialEffect); reader.read("miscId", item.iMiscId, ParseItemMiscId); reader.read("spell", item.iSpell, ParseSpellId); reader.readBool("usable", item.iUsable); reader.readInt("value", item.iValue); item.iMappingId = currentMappingId; const auto [it, inserted] = ItemMappingIdsToIndices.emplace(item.iMappingId, static_cast(AllItemsList.size()) - 1); if (!inserted) { DisplayFatalErrorAndExit("Adding Item Failed", fmt::format("An item already exists for mapping ID {}.", item.iMappingId)); } ++currentMappingId; } AllItemsList.shrink_to_fit(); } namespace { void LoadItemDat() { const std::string_view filename = "txtdata\\items\\itemdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); AllItemsList.clear(); AdditionalUniqueBaseItemStringsToIndices.clear(); ItemMappingIdsToIndices.clear(); LoadItemDatFromFile(dataFile, filename, 0); lua::ItemDataLoaded(); } void ReadItemPower(RecordReader &reader, std::string_view fieldName, ItemPower &power) { reader.read(fieldName, power.type, ParseItemEffectType); reader.readOptionalInt(StrCat(fieldName, ".value1"), power.param1); reader.readOptionalInt(StrCat(fieldName, ".value2"), power.param2); } } // namespace void LoadUniqueItemDatFromFile(DataFile &dataFile, std::string_view filename, int32_t baseMappingId) { dataFile.skipHeaderOrDie(filename); int32_t currentMappingId = baseMappingId; UniqueItems.reserve(UniqueItems.size() + dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; UniqueItem &item = UniqueItems.emplace_back(); reader.readString("name", item.UIName); reader.read("cursorGraphic", item.UICurs, ParseItemCursorGraphic); reader.read("uniqueBaseItem", item.UIItemId, ParseUniqueBaseItem); reader.readInt("minLevel", item.UIMinLvl); reader.readInt("value", item.UIValue); // powers (up to 6) item.UINumPL = 0; for (size_t i = 0; i < 6; ++i) { if (reader.value().empty()) break; ReadItemPower(reader, StrCat("power", i), item.powers[item.UINumPL++]); } item.mappingId = currentMappingId; const auto [it, inserted] = UniqueItemMappingIdsToIndices.emplace(item.mappingId, static_cast(UniqueItems.size()) - 1); if (!inserted) { DisplayFatalErrorAndExit("Adding Unique Item Failed", fmt::format("A unique item already exists for mapping ID {}.", item.mappingId)); } ++currentMappingId; } UniqueItems.shrink_to_fit(); } namespace { void LoadUniqueItemDat() { const std::string_view filename = "txtdata\\items\\unique_itemdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); UniqueItems.clear(); UniqueItemMappingIdsToIndices.clear(); LoadUniqueItemDatFromFile(dataFile, filename, 0); lua::UniqueItemDataLoaded(); } void LoadItemAffixesDat(std::string_view filename, std::vector &out) { DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); out.clear(); out.reserve(dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; PLStruct &item = out.emplace_back(); reader.readString("name", item.PLName); ReadItemPower(reader, "power", item.power); reader.readInt("minLevel", item.PLMinLvl); reader.readEnumList("itemTypes", item.PLIType, ParseAffixItemType); reader.read("alignment", item.PLGOE, ParseAffixAlignment); reader.readInt("chance", item.PLChance); reader.readBool("useful", item.PLOk); reader.readInt("minVal", item.minVal); reader.readInt("maxVal", item.maxVal); reader.readInt("multVal", item.multVal); } out.shrink_to_fit(); } } // namespace void LoadItemData() { LoadItemDat(); LoadUniqueItemDat(); LoadItemAffixesDat("txtdata\\items\\item_prefixes.tsv", ItemPrefixes); LoadItemAffixesDat("txtdata\\items\\item_suffixes.tsv", ItemSuffixes); } std::string_view ItemTypeToString(ItemType itemType) { switch (itemType) { case ItemType::Misc: return "Misc"; case ItemType::Sword: return "Sword"; case ItemType::Axe: return "Axe"; case ItemType::Bow: return "Bow"; case ItemType::Mace: return "Mace"; case ItemType::Shield: return "Shield"; case ItemType::LightArmor: return "LightArmor"; case ItemType::Helm: return "Helm"; case ItemType::MediumArmor: return "MediumArmor"; case ItemType::HeavyArmor: return "HeavyArmor"; case ItemType::Staff: return "Staff"; case ItemType::Gold: return "Gold"; case ItemType::Ring: return "Ring"; case ItemType::Amulet: return "Amulet"; case ItemType::None: return "None"; } return ""; } } // namespace devilution ================================================ FILE: Source/tables/itemdat.h ================================================ /** * @file itemdat.h * * Interface of all item data. */ #pragma once #include #include #include #include #include #include #include "tables/objdat.h" #include "tables/spelldat.h" namespace devilution { class DataFile; /** @todo add missing values and apply */ enum _item_indexes : int16_t { // TODO defines all indexes in AllItemsList IDI_GOLD, IDI_WARRIOR, IDI_WARRSHLD, IDI_WARRCLUB, IDI_ROGUE, IDI_SORCERER, IDI_CLEAVER, IDI_FIRSTQUEST = IDI_CLEAVER, IDI_SKCROWN, IDI_INFRARING, IDI_ROCK, IDI_OPTAMULET, IDI_TRING, IDI_BANNER, IDI_HARCREST, IDI_STEELVEIL, IDI_GLDNELIX, IDI_ANVIL, IDI_MUSHROOM, IDI_BRAIN, IDI_FUNGALTM, IDI_SPECELIX, IDI_BLDSTONE, IDI_MAPOFDOOM, IDI_LASTQUEST = IDI_MAPOFDOOM, IDI_EAR, IDI_HEAL, IDI_MANA, IDI_IDENTIFY, IDI_PORTAL, IDI_ARMOFVAL, IDI_FULLHEAL, IDI_FULLMANA, IDI_GRISWOLD, IDI_LGTFORGE, IDI_LAZSTAFF, IDI_RESURRECT, IDI_OIL, IDI_SHORTSTAFF, IDI_BARDSWORD, IDI_BARDDAGGER, IDI_RUNEBOMB, IDI_THEODORE, IDI_AURIC, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3, IDI_FULLNOTE, IDI_BROWNSUIT, IDI_GREYSUIT, IDI_BOOK1 = 114, IDI_BOOK2, IDI_BOOK3, IDI_BOOK4, IDI_BARBARIAN = 139, IDI_SHORT_BATTLE_BOW = 148, IDI_RUNEOFSTONE = 165, IDI_SORCERER_DIABLO, IDI_ARENAPOT, IDI_NUM_DEFAULT_ITEMS, IDI_NONE = -1, }; enum item_class : uint8_t { ICLASS_NONE, ICLASS_WEAPON, ICLASS_ARMOR, ICLASS_MISC, ICLASS_GOLD, ICLASS_QUEST, }; enum item_equip_type : int8_t { ILOC_NONE, ILOC_ONEHAND, ILOC_TWOHAND, ILOC_ARMOR, ILOC_HELM, ILOC_RING, ILOC_AMULET, ILOC_UNEQUIPABLE, ILOC_BELT, ILOC_INVALID = -1, }; /// Item graphic IDs; frame_num-11 of objcurs.cel. enum item_cursor_graphic : uint8_t { // clang-format off ICURS_POTION_OF_FULL_MANA = 0, ICURS_SCROLL_OF = 1, ICURS_GOLD_SMALL = 4, ICURS_GOLD_MEDIUM = 5, ICURS_GOLD_LARGE = 6, ICURS_THE_BLEEDER = 8, ICURS_BRAMBLE = 9, ICURS_RING_OF_TRUTH = 10, ICURS_RING_OF_REGHA = 11, ICURS_RING = 12, ICURS_RING_OF_ENGAGEMENT = 13, ICURS_CONSTRICTING_RING = 14, ICURS_SPECTRAL_ELIXIR = 15, ICURS_ARENA_POTION = 16, ICURS_GOLDEN_ELIXIR = 17, ICURS_EMPYREAN_BAND = 18, ICURS_EAR_SORCERER = 19, ICURS_EAR_WARRIOR = 20, ICURS_EAR_ROGUE = 21, ICURS_BLOOD_STONE = 25, ICURS_OIL = 30, ICURS_ELIXIR_OF_VITALITY = 31, ICURS_POTION_OF_HEALING = 32, ICURS_POTION_OF_FULL_REJUVENATION = 33, ICURS_ELIXIR_OF_MAGIC = 34, ICURS_POTION_OF_FULL_HEALING = 35, ICURS_ELIXIR_OF_DEXTERITY = 36, ICURS_POTION_OF_REJUVENATION = 37, ICURS_ELIXIR_OF_STRENGTH = 38, ICURS_POTION_OF_MANA = 39, ICURS_BRAIN = 40, ICURS_OPTIC_AMULET = 44, ICURS_AMULET = 45, ICURS_WIZARDSPIKE = 50, ICURS_DAGGER = 51, ICURS_BLACK_RAZOR = 53, ICURS_GONNAGALS_DIRK = 54, ICURS_BLADE = 56, ICURS_BASTARD_SWORD = 57, ICURS_THE_EXECUTIONERS_BLADE = 58, ICURS_MACE = 59, ICURS_LONG_SWORD = 60, ICURS_BROAD_SWORD = 61, ICURS_FALCHION = 62, ICURS_MORNING_STAR = 63, ICURS_SHORT_SWORD = 64, ICURS_CLAYMORE = 65, ICURS_CLUB = 66, ICURS_SABRE = 67, ICURS_GRYPHONS_CLAW = 68, ICURS_SPIKED_CLUB = 70, ICURS_SCIMITAR = 72, ICURS_FULL_HELM = 75, ICURS_MAGIC_ROCK = 76, ICURS_HELM_OF_SPIRITS = 77, ICURS_THE_UNDEAD_CROWN = 78, ICURS_ROYAL_CIRCLET = 79, ICURS_FOOLS_CREST = 80, ICURS_HARLEQUIN_CREST = 81, ICURS_HELM = 82, ICURS_BUCKLER = 83, ICURS_VEIL_OF_STEEL = 85, ICURS_BOOK_GREY = 86, ICURS_BOOK_RED = 87, ICURS_BOOK_BLUE = 88, ICURS_BLACK_MUSHROOM = 89, ICURS_SKULL_CAP = 90, ICURS_CAP = 91, ICURS_TORN_FLESH_OF_SOULS = 92, ICURS_THINKING_CAP = 93, ICURS_CROWN = 95, ICURS_MAP_OF_THE_STARS = 96, ICURS_FUNGAL_TOME = 97, ICURS_GREAT_HELM = 98, ICURS_OVERLORDS_HELM = 99, ICURS_BATTLE_AXE = 101, ICURS_HUNTERS_BOW = 102, ICURS_FIELD_PLATE = 103, ICURS_STONECLEAVER = 104, ICURS_SMALL_SHIELD = 105, ICURS_CLEAVER = 106, ICURS_STUDDED_LEATHER_ARMOR = 107, ICURS_DEADLY_HUNTER = 108, ICURS_SHORT_STAFF = 109, ICURS_TWO_HANDED_SWORD = 110, ICURS_CHAIN_MAIL = 111, ICURS_SMALL_AXE = 112, ICURS_KITE_SHIELD = 113, ICURS_SCALE_MAIL = 114, ICURS_SPLIT_SKULL_SHIELD = 116, ICURS_DRAGONS_BREACH = 117, ICURS_SHORT_BOW = 118, ICURS_LONG_BATTLE_BOW = 119, ICURS_LONG_WAR_BOW = 120, ICURS_WAR_HAMMER = 121, ICURS_MAUL = 122, ICURS_LONG_STAFF = 123, ICURS_WAR_STAFF = 124, ICURS_TAVERN_SIGN = 126, ICURS_HARD_LEATHER_ARMOR = 127, ICURS_RAGS = 128, ICURS_QUILTED_ARMOR = 129, ICURS_FLAIL = 131, ICURS_TOWER_SHIELD = 132, ICURS_COMPOSITE_BOW = 133, ICURS_GREAT_SWORD = 134, ICURS_LEATHER_ARMOR = 135, ICURS_SPLINT_MAIL = 136, ICURS_ROBE = 137, ICURS_THE_RAINBOW_CLOAK = 138, ICURS_ANVIL_OF_FURY = 140, ICURS_BROAD_AXE = 141, ICURS_LARGE_AXE = 142, ICURS_GREAT_AXE = 143, ICURS_AXE = 144, ICURS_BLACKOAK_SHIELD = 146, ICURS_LARGE_SHIELD = 147, ICURS_GOTHIC_SHIELD = 148, ICURS_CLOAK = 149, ICURS_CAPE = 150, ICURS_FULL_PLATE_MAIL = 151, ICURS_GOTHIC_PLATE = 152, ICURS_BREAST_PLATE = 153, ICURS_RING_MAIL = 154, ICURS_STAFF_OF_LAZARUS = 155, ICURS_ARKAINES_VALOR = 157, ICURS_THE_NEEDLER = 158, ICURS_NAJS_LIGHT_PLATE = 159, ICURS_THE_GRIZZLY = 160, ICURS_THE_GRANDFATHER = 161, ICURS_THE_PROTECTOR = 162, ICURS_MESSERSCHMIDTS_REAVER = 163, ICURS_WINDFORCE = 164, ICURS_SHORT_WAR_BOW = 165, ICURS_COMPOSITE_STAFF = 166, ICURS_SHORT_BATTLE_BOW = 167, // Hellfire items: ICURS_XORINES_RING = 168, ICURS_AMULET_OF_WARDING = 170, ICURS_KARIKS_RING = 173, ICURS_MERCURIAL_RING = 176, ICURS_RING_OF_THUNDER = 177, ICURS_GIANTS_KNUCKLE = 179, ICURS_AURIC_AMULET = 180, ICURS_RING_OF_THE_MYSTICS = 181, ICURS_ACOLYTES_AMULET = 183, ICURS_RING_OF_MAGMA = 184, ICURS_GLADIATORS_RING = 186, ICURS_RUNE_BOMB = 187, ICURS_THEODORE = 188, ICURS_TORN_NOTE_1 = 189, ICURS_TORN_NOTE_2 = 190, ICURS_TORN_NOTE_3 = 191, ICURS_RECONSTRUCTED_NOTE = 192, ICURS_RUNE_OF_FIRE = 193, ICURS_GREATER_RUNE_OF_FIRE = 194, ICURS_RUNE_OF_LIGHTNING = 195, ICURS_GREATER_RUNE_OF_LIGHTNING = 196, ICURS_RUNE_OF_STONE = 197, ICURS_GREY_SUIT = 198, ICURS_BROWN_SUIT = 199, ICURS_EATER_OF_SOULS = 200, ICURS_ARMOR_OF_GLOOM = 203, ICURS_BONE_CHAIN_ARMOR = 204, ICURS_THUNDERCLAP = 205, ICURS_DIAMONDEDGE = 206, ICURS_FLAMBEAU = 209, ICURS_GNAT_STING = 210, ICURS_BLITZEN = 219, ICURS_DEMON_PLATE_ARMOR = 225, ICURS_BOVINE = 226, ICURS_DEFAULT = static_cast(-1), // clang-format on }; enum class ItemType : int8_t { Misc, Sword, Axe, Bow, Mace, Shield, LightArmor, Helm, MediumArmor, HeavyArmor, Staff, Gold, Ring, Amulet, None = -1, }; std::string_view ItemTypeToString(ItemType itemType); enum unique_base_item : int8_t { UITYPE_NONE, UITYPE_SHORTBOW, UITYPE_LONGBOW, UITYPE_HUNTBOW, UITYPE_COMPBOW, UITYPE_WARBOW, UITYPE_BATTLEBOW, UITYPE_DAGGER, UITYPE_FALCHION, UITYPE_CLAYMORE, UITYPE_BROADSWR, UITYPE_SABRE, UITYPE_SCIMITAR, UITYPE_LONGSWR, UITYPE_BASTARDSWR, UITYPE_TWOHANDSWR, UITYPE_GREATSWR, UITYPE_CLEAVER, UITYPE_LARGEAXE, UITYPE_BROADAXE, UITYPE_SMALLAXE, UITYPE_BATTLEAXE, UITYPE_GREATAXE, UITYPE_MACE, UITYPE_MORNSTAR, UITYPE_SPIKCLUB, UITYPE_MAUL, UITYPE_WARHAMMER, UITYPE_FLAIL, UITYPE_LONGSTAFF, UITYPE_SHORTSTAFF, UITYPE_COMPSTAFF, UITYPE_QUARSTAFF, UITYPE_WARSTAFF, UITYPE_SKULLCAP, UITYPE_HELM, UITYPE_GREATHELM, UITYPE_CROWN, UITYPE_38, UITYPE_RAGS, UITYPE_STUDARMOR, UITYPE_CLOAK, UITYPE_ROBE, UITYPE_CHAINMAIL, UITYPE_LEATHARMOR, UITYPE_BREASTPLATE, UITYPE_CAPE, UITYPE_PLATEMAIL, UITYPE_FULLPLATE, UITYPE_BUCKLER, UITYPE_SMALLSHIELD, UITYPE_LARGESHIELD, UITYPE_KITESHIELD, UITYPE_GOTHSHIELD, UITYPE_RING, UITYPE_55, UITYPE_AMULET, UITYPE_SKCROWN, UITYPE_INFRARING, UITYPE_OPTAMULET, UITYPE_TRING, UITYPE_HARCREST, UITYPE_MAPOFDOOM, UITYPE_ELIXIR, UITYPE_ARMOFVAL, UITYPE_STEELVEIL, UITYPE_GRISWOLD, UITYPE_LGTFORGE, UITYPE_LAZSTAFF, UITYPE_BOVINE, NUM_DEFAULT_UITYPES, NUM_MAX_UITYPES = std::numeric_limits::max(), UITYPE_INVALID = -1, }; enum class ItemSpecialEffect : uint32_t { // clang-format off None = 0, RandomStealLife = 1 << 1, RandomArrowVelocity = 1 << 2, FireArrows = 1 << 3, FireDamage = 1 << 4, LightningDamage = 1 << 5, DrainLife = 1 << 6, MultipleArrows = 1 << 9, Knockback = 1 << 11, StealMana3 = 1 << 13, StealMana5 = 1 << 14, StealLife3 = 1 << 15, StealLife5 = 1 << 16, QuickAttack = 1 << 17, FastAttack = 1 << 18, FasterAttack = 1 << 19, FastestAttack = 1 << 20, FastHitRecovery = 1 << 21, FasterHitRecovery = 1 << 22, FastestHitRecovery = 1 << 23, FastBlock = 1 << 24, LightningArrows = 1 << 25, Thorns = 1 << 26, NoMana = 1 << 27, HalfTrapDamage = 1 << 28, TripleDemonDamage = 1 << 30, ZeroResistance = 1U << 31, // clang-format on }; use_enum_as_flags(ItemSpecialEffect); enum class ItemSpecialEffectHf : uint8_t { // clang-format off None = 0, Devastation = 1 << 0, Decay = 1 << 1, Peril = 1 << 2, Jesters = 1 << 3, Doppelganger = 1 << 4, ACAgainstDemons = 1 << 5, ACAgainstUndead = 1 << 6, // clang-format on }; use_enum_as_flags(ItemSpecialEffectHf); enum item_misc_id : int8_t { IMISC_NONE, IMISC_USEFIRST, IMISC_FULLHEAL, IMISC_HEAL, IMISC_0x4, // Unused IMISC_0x5, // Unused IMISC_MANA, IMISC_FULLMANA, IMISC_0x8, // Unused IMISC_0x9, // Unused IMISC_ELIXSTR, IMISC_ELIXMAG, IMISC_ELIXDEX, IMISC_ELIXVIT, IMISC_0xE, // Unused IMISC_0xF, // Unused IMISC_0x10, // Unused IMISC_0x11, // Unused IMISC_REJUV, IMISC_FULLREJUV, IMISC_USELAST, IMISC_SCROLL, IMISC_SCROLLT, IMISC_STAFF, IMISC_BOOK, IMISC_RING, IMISC_AMULET, IMISC_UNIQUE, IMISC_0x1C, // Unused IMISC_OILFIRST, IMISC_OILOF, /* oils are beta or hellfire only */ IMISC_OILACC, IMISC_OILMAST, IMISC_OILSHARP, IMISC_OILDEATH, IMISC_OILSKILL, IMISC_OILBSMTH, IMISC_OILFORT, IMISC_OILPERM, IMISC_OILHARD, IMISC_OILIMP, IMISC_OILLAST, IMISC_MAPOFDOOM, IMISC_EAR, IMISC_SPECELIX, IMISC_0x2D, // Unused IMISC_RUNEFIRST, IMISC_RUNEF, IMISC_RUNEL, IMISC_GR_RUNEL, IMISC_GR_RUNEF, IMISC_RUNES, IMISC_RUNELAST, IMISC_AURIC, IMISC_NOTE, IMISC_ARENAPOT, IMISC_INVALID = -1, }; struct ItemData { uint8_t dropRate; enum item_class iClass; enum item_equip_type iLoc; enum item_cursor_graphic iCurs; enum ItemType itype; enum unique_base_item iItemId; std::string iName; std::string iSName; uint8_t iMinMLvl; uint8_t iDurability; uint8_t iMinDam; uint8_t iMaxDam; uint8_t iMinAC; uint8_t iMaxAC; uint8_t iMinStr; uint8_t iMinMag; uint8_t iMinDex; ItemSpecialEffect iFlags; // ItemSpecialEffect as bit flags enum item_misc_id iMiscId; SpellID iSpell; bool iUsable; uint16_t iValue; int32_t iMappingId; }; enum item_effect_type : int8_t { IPL_TOHIT, IPL_TOHIT_CURSE, IPL_DAMP, IPL_DAMP_CURSE, IPL_TOHIT_DAMP, IPL_TOHIT_DAMP_CURSE, IPL_ACP, IPL_ACP_CURSE, IPL_FIRERES, IPL_LIGHTRES, IPL_MAGICRES, IPL_ALLRES, IPL_SPLLVLADD = 14, IPL_CHARGES, IPL_FIREDAM, IPL_LIGHTDAM, IPL_STR = 19, IPL_STR_CURSE, IPL_MAG, IPL_MAG_CURSE, IPL_DEX, IPL_DEX_CURSE, IPL_VIT, IPL_VIT_CURSE, IPL_ATTRIBS, IPL_ATTRIBS_CURSE, IPL_GETHIT_CURSE, IPL_GETHIT, IPL_LIFE, IPL_LIFE_CURSE, IPL_MANA, IPL_MANA_CURSE, IPL_DUR, IPL_DUR_CURSE, IPL_INDESTRUCTIBLE, IPL_LIGHT, IPL_LIGHT_CURSE, IPL_MULT_ARROWS = 41, /* only used in hellfire */ IPL_FIRE_ARROWS, IPL_LIGHT_ARROWS, IPL_THORNS = 45, IPL_NOMANA, IPL_FIREBALL = 50, /* only used in hellfire */ IPL_ABSHALFTRAP = 52, IPL_KNOCKBACK, IPL_STEALMANA = 55, IPL_STEALLIFE, IPL_TARGAC, IPL_FASTATTACK, IPL_FASTRECOVER, IPL_FASTBLOCK, IPL_DAMMOD, IPL_RNDARROWVEL, IPL_SETDAM, IPL_SETDUR, IPL_NOMINSTR, IPL_SPELL, IPL_ONEHAND = 68, IPL_3XDAMVDEM, IPL_ALLRESZERO, IPL_DRAINLIFE = 72, IPL_RNDSTEALLIFE, IPL_SETAC = 75, IPL_ADDACLIFE, IPL_ADDMANAAC, IPL_AC_CURSE = 79, IPL_LASTDIABLO = IPL_AC_CURSE, IPL_FIRERES_CURSE, IPL_LIGHTRES_CURSE, IPL_MAGICRES_CURSE, IPL_DEVASTATION = 84, IPL_DECAY, IPL_PERIL, IPL_JESTERS, IPL_CRYSTALLINE, IPL_DOPPELGANGER, IPL_ACDEMON, IPL_ACUNDEAD, IPL_MANATOLIFE, IPL_LIFETOMANA, IPL_INVALID = -1, }; enum goodorevil : uint8_t { GOE_ANY, GOE_EVIL, GOE_GOOD, }; enum class AffixItemType : uint8_t { // clang-format off None = 0, Misc = 1 << 0, Bow = 1 << 1, Staff = 1 << 2, Weapon = 1 << 3, Shield = 1 << 4, Armor = 1 << 5, // clang-format on }; use_enum_as_flags(AffixItemType); struct ItemPower { item_effect_type type = IPL_INVALID; int param1 = 0; int param2 = 0; }; struct PLStruct { std::string PLName; ItemPower power; int8_t PLMinLvl; AffixItemType PLIType; // AffixItemType as bit flags enum goodorevil PLGOE; uint8_t PLChance; bool PLOk; int minVal; int maxVal; int multVal; }; struct UniqueItem { std::string UIName; enum item_cursor_graphic UICurs; enum unique_base_item UIItemId; int8_t UIMinLvl; uint8_t UINumPL; int UIValue; ItemPower powers[6]; int32_t mappingId; }; extern DVL_API_FOR_TEST std::vector AllItemsList; extern ankerl::unordered_dense::map ItemMappingIdsToIndices; extern std::vector ItemPrefixes; extern std::vector ItemSuffixes; extern DVL_API_FOR_TEST std::vector UniqueItems; extern ankerl::unordered_dense::map UniqueItemMappingIdsToIndices; tl::expected<_item_indexes, std::string> ParseItemId(std::string_view value); void LoadItemDatFromFile(DataFile &dataFile, std::string_view filename, int32_t baseMappingId); void LoadUniqueItemDatFromFile(DataFile &dataFile, std::string_view filename, int32_t baseMappingId); void LoadItemData(); } // namespace devilution template <> struct magic_enum::customize::enum_range { static constexpr int min = devilution::_item_indexes::IDI_NONE; static constexpr int max = devilution::_item_indexes::IDI_NUM_DEFAULT_ITEMS; }; ================================================ FILE: Source/tables/misdat.cpp ================================================ /** * @file misdat.cpp * * Implementation of data related to missiles. */ #include "tables/misdat.h" #include #include #include #include #include #include #include #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" #include "engine/clx_sprite.hpp" #include "headless_mode.hpp" #include "missiles.h" #include "mpq/mpq_common.hpp" #include "sound_effect_enums.h" #include "tables/spelldat.h" #include "utils/file_name_generator.hpp" #include "utils/status_macros.hpp" #include "utils/str_cat.hpp" #ifdef UNPACKED_MPQS #include "engine/load_clx.hpp" #else #include "engine/load_cl2.hpp" #endif namespace devilution { namespace { /** Data related to each missile graphic ID. */ std::vector MissileSpriteData; std::vector> MissileAnimDelays; std::vector> MissileAnimLengths; /** Data related to each missile ID. */ std::vector MissilesData; size_t ToIndex(std::vector> &all, const std::array &value) { for (size_t i = 0; i < all.size(); ++i) { if (all[i] == value) return i; } all.push_back(value); return all.size() - 1; } tl::expected ParseMissileGraphicsFlag(std::string_view value) { if (value.empty()) return MissileGraphicsFlags::None; if (value == "MonsterOwned") return MissileGraphicsFlags::MonsterOwned; if (value == "NotAnimated") return MissileGraphicsFlags::NotAnimated; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMissileGraphicID(std::string_view value) { if (value.empty()) return MissileGraphicID::None; if (value == "Arrow") return MissileGraphicID::Arrow; if (value == "Fireball") return MissileGraphicID::Fireball; if (value == "Guardian") return MissileGraphicID::Guardian; if (value == "Lightning") return MissileGraphicID::Lightning; if (value == "FireWall") return MissileGraphicID::FireWall; if (value == "MagmaBallExplosion") return MissileGraphicID::MagmaBallExplosion; if (value == "TownPortal") return MissileGraphicID::TownPortal; if (value == "FlashBottom") return MissileGraphicID::FlashBottom; if (value == "FlashTop") return MissileGraphicID::FlashTop; if (value == "ManaShield") return MissileGraphicID::ManaShield; if (value == "BloodHit") return MissileGraphicID::BloodHit; if (value == "BoneHit") return MissileGraphicID::BoneHit; if (value == "MetalHit") return MissileGraphicID::MetalHit; if (value == "FireArrow") return MissileGraphicID::FireArrow; if (value == "DoomSerpents") return MissileGraphicID::DoomSerpents; if (value == "Golem") return MissileGraphicID::Golem; if (value == "Spurt") return MissileGraphicID::Spurt; if (value == "ApocalypseBoom") return MissileGraphicID::ApocalypseBoom; if (value == "StoneCurseShatter") return MissileGraphicID::StoneCurseShatter; if (value == "BigExplosion") return MissileGraphicID::BigExplosion; if (value == "Inferno") return MissileGraphicID::Inferno; if (value == "ThinLightning") return MissileGraphicID::ThinLightning; if (value == "BloodStar") return MissileGraphicID::BloodStar; if (value == "BloodStarExplosion") return MissileGraphicID::BloodStarExplosion; if (value == "MagmaBall") return MissileGraphicID::MagmaBall; if (value == "Krull") return MissileGraphicID::Krull; if (value == "ChargedBolt") return MissileGraphicID::ChargedBolt; if (value == "HolyBolt") return MissileGraphicID::HolyBolt; if (value == "HolyBoltExplosion") return MissileGraphicID::HolyBoltExplosion; if (value == "LightningArrow") return MissileGraphicID::LightningArrow; if (value == "FireArrowExplosion") return MissileGraphicID::FireArrowExplosion; if (value == "Acid") return MissileGraphicID::Acid; if (value == "AcidSplat") return MissileGraphicID::AcidSplat; if (value == "AcidPuddle") return MissileGraphicID::AcidPuddle; if (value == "Etherealize") return MissileGraphicID::Etherealize; if (value == "Elemental") return MissileGraphicID::Elemental; if (value == "Resurrect") return MissileGraphicID::Resurrect; if (value == "BoneSpirit") return MissileGraphicID::BoneSpirit; if (value == "RedPortal") return MissileGraphicID::RedPortal; if (value == "DiabloApocalypseBoom") return MissileGraphicID::DiabloApocalypseBoom; if (value == "BloodStarBlue") return MissileGraphicID::BloodStarBlue; if (value == "BloodStarBlueExplosion") return MissileGraphicID::BloodStarBlueExplosion; if (value == "BloodStarYellow") return MissileGraphicID::BloodStarYellow; if (value == "BloodStarYellowExplosion") return MissileGraphicID::BloodStarYellowExplosion; if (value == "BloodStarRed") return MissileGraphicID::BloodStarRed; if (value == "BloodStarRedExplosion") return MissileGraphicID::BloodStarRedExplosion; if (value == "HorkSpawn") return MissileGraphicID::HorkSpawn; if (value == "Reflect") return MissileGraphicID::Reflect; if (value == "OrangeFlare") return MissileGraphicID::OrangeFlare; if (value == "BlueFlare") return MissileGraphicID::BlueFlare; if (value == "RedFlare") return MissileGraphicID::RedFlare; if (value == "YellowFlare") return MissileGraphicID::YellowFlare; if (value == "Rune") return MissileGraphicID::Rune; if (value == "YellowFlareExplosion") return MissileGraphicID::YellowFlareExplosion; if (value == "BlueFlareExplosion") return MissileGraphicID::BlueFlareExplosion; if (value == "RedFlareExplosion") return MissileGraphicID::RedFlareExplosion; if (value == "BlueFlare2") return MissileGraphicID::BlueFlare2; if (value == "OrangeFlareExplosion") return MissileGraphicID::OrangeFlareExplosion; if (value == "BlueFlareExplosion2") return MissileGraphicID::BlueFlareExplosion2; return tl::make_unexpected("Unknown enum value"); } void LoadMissileSpriteData() { const std::string_view filename = "txtdata\\missiles\\missile_sprites.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); MissileAnimDelays.clear(); MissileAnimLengths.clear(); MissileSpriteData.clear(); MissileSpriteData.reserve(dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; MissileFileData &item = MissileSpriteData.emplace_back(); MissileGraphicID id; reader.read("id", id, ParseMissileGraphicID); assert(static_cast(id) + 1 == MissileSpriteData.size()); reader.readInt("width", item.animWidth); reader.readInt("width2", item.animWidth2); reader.readString("name", item.name); reader.readInt("numFrames", item.animFAmt); reader.read("flags", item.flags, ParseMissileGraphicsFlag); std::array arr; reader.readIntArray("frameDelay", arr); item.animDelayIdx = static_cast(ToIndex(MissileAnimDelays, arr)); reader.readIntArray("frameLength", arr); item.animLenIdx = static_cast(ToIndex(MissileAnimLengths, arr)); } MissileSpriteData.shrink_to_fit(); MissileAnimDelays.shrink_to_fit(); MissileAnimLengths.shrink_to_fit(); } tl::expected ParseMissileDataFlag(std::string_view value) { if (value == "Physical") return MissileDataFlags::Physical; if (value == "Fire") return MissileDataFlags::Fire; if (value == "Lightning") return MissileDataFlags::Lightning; if (value == "Magic") return MissileDataFlags::Magic; if (value == "Acid") return MissileDataFlags::Acid; if (value == "Arrow") return MissileDataFlags::Arrow; if (value == "Invisible") return MissileDataFlags::Invisible; return tl::make_unexpected("Unknown enum value"); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) tl::expected ParseMissileAddFn(std::string_view value) { if (value.empty()) return nullptr; if (value == "AddOpenNest") return AddOpenNest; if (value == "AddRuneOfFire") return AddRuneOfFire; if (value == "AddRuneOfLight") return AddRuneOfLight; if (value == "AddRuneOfNova") return AddRuneOfNova; if (value == "AddRuneOfImmolation") return AddRuneOfImmolation; if (value == "AddRuneOfStone") return AddRuneOfStone; if (value == "AddReflect") return AddReflect; if (value == "AddBerserk") return AddBerserk; if (value == "AddHorkSpawn") return AddHorkSpawn; if (value == "AddJester") return AddJester; if (value == "AddStealPotions") return AddStealPotions; if (value == "AddStealMana") return AddStealMana; if (value == "AddSpectralArrow") return AddSpectralArrow; if (value == "AddWarp") return AddWarp; if (value == "AddLightningWall") return AddLightningWall; if (value == "AddBigExplosion") return AddBigExplosion; if (value == "AddImmolation") return AddImmolation; if (value == "AddLightningBow") return AddLightningBow; if (value == "AddMana") return AddMana; if (value == "AddMagi") return AddMagi; if (value == "AddRingOfFire") return AddRingOfFire; if (value == "AddSearch") return AddSearch; if (value == "AddChargedBoltBow") return AddChargedBoltBow; if (value == "AddElementalArrow") return AddElementalArrow; if (value == "AddArrow") return AddArrow; if (value == "AddPhasing") return AddPhasing; if (value == "AddFirebolt") return AddFirebolt; if (value == "AddMagmaBall") return AddMagmaBall; if (value == "AddTeleport") return AddTeleport; if (value == "AddNovaBall") return AddNovaBall; if (value == "AddFireWall") return AddFireWall; if (value == "AddFireball") return AddFireball; if (value == "AddLightningControl") return AddLightningControl; if (value == "AddLightning") return AddLightning; if (value == "AddMissileExplosion") return AddMissileExplosion; if (value == "AddWeaponExplosion") return AddWeaponExplosion; if (value == "AddTownPortal") return AddTownPortal; if (value == "AddFlashBottom") return AddFlashBottom; if (value == "AddFlashTop") return AddFlashTop; if (value == "AddManaShield") return AddManaShield; if (value == "AddFlameWave") return AddFlameWave; if (value == "AddGuardian") return AddGuardian; if (value == "AddChainLightning") return AddChainLightning; if (value == "AddRhino") return AddRhino; if (value == "AddGenericMagicMissile") return AddGenericMagicMissile; if (value == "AddAcid") return AddAcid; if (value == "AddAcidPuddle") return AddAcidPuddle; if (value == "AddStoneCurse") return AddStoneCurse; if (value == "AddGolem") return AddGolem; if (value == "AddApocalypseBoom") return AddApocalypseBoom; if (value == "AddHealing") return AddHealing; if (value == "AddHealOther") return AddHealOther; if (value == "AddElemental") return AddElemental; if (value == "AddIdentify") return AddIdentify; if (value == "AddWallControl") return AddWallControl; if (value == "AddInfravision") return AddInfravision; if (value == "AddFlameWaveControl") return AddFlameWaveControl; if (value == "AddNova") return AddNova; if (value == "AddRage") return AddRage; if (value == "AddItemRepair") return AddItemRepair; if (value == "AddStaffRecharge") return AddStaffRecharge; if (value == "AddTrapDisarm") return AddTrapDisarm; if (value == "AddApocalypse") return AddApocalypse; if (value == "AddInferno") return AddInferno; if (value == "AddInfernoControl") return AddInfernoControl; if (value == "AddChargedBolt") return AddChargedBolt; if (value == "AddHolyBolt") return AddHolyBolt; if (value == "AddResurrect") return AddResurrect; if (value == "AddResurrectBeam") return AddResurrectBeam; if (value == "AddTelekinesis") return AddTelekinesis; if (value == "AddBoneSpirit") return AddBoneSpirit; if (value == "AddRedPortal") return AddRedPortal; if (value == "AddDiabloApocalypse") return AddDiabloApocalypse; return tl::make_unexpected("Unknown MissileData::AddFn name"); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) tl::expected ParseMissileProcessFn(std::string_view value) { if (value.empty()) return nullptr; if (value == "ProcessElementalArrow") return ProcessElementalArrow; if (value == "ProcessArrow") return ProcessArrow; if (value == "ProcessGenericProjectile") return ProcessGenericProjectile; if (value == "ProcessNovaBall") return ProcessNovaBall; if (value == "ProcessAcidPuddle") return ProcessAcidPuddle; if (value == "ProcessFireWall") return ProcessFireWall; if (value == "ProcessFireball") return ProcessFireball; if (value == "ProcessHorkSpawn") return ProcessHorkSpawn; if (value == "ProcessRune") return ProcessRune; if (value == "ProcessLightningWall") return ProcessLightningWall; if (value == "ProcessBigExplosion") return ProcessBigExplosion; if (value == "ProcessLightningBow") return ProcessLightningBow; if (value == "ProcessRingOfFire") return ProcessRingOfFire; if (value == "ProcessSearch") return ProcessSearch; if (value == "ProcessImmolation") return ProcessImmolation; if (value == "ProcessSpectralArrow") return ProcessSpectralArrow; if (value == "ProcessLightningControl") return ProcessLightningControl; if (value == "ProcessLightning") return ProcessLightning; if (value == "ProcessTownPortal") return ProcessTownPortal; if (value == "ProcessFlashBottom") return ProcessFlashBottom; if (value == "ProcessFlashTop") return ProcessFlashTop; if (value == "ProcessFlameWave") return ProcessFlameWave; if (value == "ProcessGuardian") return ProcessGuardian; if (value == "ProcessChainLightning") return ProcessChainLightning; if (value == "ProcessWeaponExplosion") return ProcessWeaponExplosion; if (value == "ProcessMissileExplosion") return ProcessMissileExplosion; if (value == "ProcessAcidSplate") return ProcessAcidSplate; if (value == "ProcessTeleport") return ProcessTeleport; if (value == "ProcessStoneCurse") return ProcessStoneCurse; if (value == "ProcessApocalypseBoom") return ProcessApocalypseBoom; if (value == "ProcessRhino") return ProcessRhino; if (value == "ProcessWallControl") return ProcessWallControl; if (value == "ProcessInfravision") return ProcessInfravision; if (value == "ProcessApocalypse") return ProcessApocalypse; if (value == "ProcessFlameWaveControl") return ProcessFlameWaveControl; if (value == "ProcessNova") return ProcessNova; if (value == "ProcessRage") return ProcessRage; if (value == "ProcessInferno") return ProcessInferno; if (value == "ProcessInfernoControl") return ProcessInfernoControl; if (value == "ProcessChargedBolt") return ProcessChargedBolt; if (value == "ProcessHolyBolt") return ProcessHolyBolt; if (value == "ProcessElemental") return ProcessElemental; if (value == "ProcessBoneSpirit") return ProcessBoneSpirit; if (value == "ProcessResurrectBeam") return ProcessResurrectBeam; if (value == "ProcessRedPortal") return ProcessRedPortal; return tl::make_unexpected("Unknown MissileData::ProcessFn name"); } // A temporary solution for parsing SfxID until we have a more general one. // NOLINTNEXTLINE(readability-function-cognitive-complexity) tl::expected ParseCastSound(std::string_view value) { if (value.empty()) return SfxID::None; if (value == "BigExplosion") return SfxID::BigExplosion; if (value == "ShootFireballBow") return SfxID::ShootFireballBow; if (value == "SpellAcid") return SfxID::SpellAcid; if (value == "SpellApocalypse") return SfxID::SpellApocalypse; if (value == "SpellBloodStar") return SfxID::SpellBloodStar; if (value == "SpellBoneSpirit") return SfxID::SpellBoneSpirit; if (value == "SpellChargedBolt") return SfxID::SpellChargedBolt; if (value == "SpellDoomSerpents") return SfxID::SpellDoomSerpents; if (value == "SpellElemental") return SfxID::SpellElemental; if (value == "SpellEnd") return SfxID::SpellEnd; if (value == "SpellEtherealize") return SfxID::SpellEtherealize; if (value == "SpellFireHit") return SfxID::SpellFireHit; if (value == "SpellFireWall") return SfxID::SpellFireWall; if (value == "SpellFirebolt") return SfxID::SpellFirebolt; if (value == "SpellFlameWave") return SfxID::SpellFlameWave; if (value == "SpellGolem") return SfxID::SpellGolem; if (value == "SpellGuardian") return SfxID::SpellGuardian; if (value == "SpellHolyBolt") return SfxID::SpellHolyBolt; if (value == "SpellInferno") return SfxID::SpellInferno; if (value == "SpellInfravision") return SfxID::SpellInfravision; if (value == "SpellInvisibility") return SfxID::SpellInvisibility; if (value == "SpellLightning") return SfxID::SpellLightning; if (value == "SpellLightningWall") return SfxID::SpellLightningWall; if (value == "SpellManaShield") return SfxID::SpellManaShield; if (value == "SpellNova") return SfxID::SpellNova; if (value == "SpellPortal") return SfxID::SpellPortal; if (value == "SpellPuddle") return SfxID::SpellPuddle; if (value == "SpellStoneCurse") return SfxID::SpellStoneCurse; if (value == "SpellTeleport") return SfxID::SpellTeleport; if (value == "SpellTrapDisarm") return SfxID::SpellTrapDisarm; return tl::make_unexpected("Unknown enum value (only a few are supported for now)"); } // A temporary solution for parsing SfxID until we have a more general one. tl::expected ParseHitSound(std::string_view value) { if (value.empty()) return SfxID::None; if (value == "BigExplosion") return SfxID::BigExplosion; if (value == "SpellBloodStarHit") return SfxID::SpellBloodStarHit; if (value == "SpellBoneSpiritHit") return SfxID::SpellBoneSpiritHit; if (value == "SpellFireHit") return SfxID::SpellFireHit; if (value == "SpellLightningHit") return SfxID::SpellLightningHit; if (value == "SpellResurrect") return SfxID::SpellResurrect; return tl::make_unexpected("Unknown enum value (only a few are supported for now)"); } tl::expected ParseMissileMovementDistribution(std::string_view value) { if (value.empty()) return MissileMovementDistribution::Disabled; if (value == "Blockable") return MissileMovementDistribution::Blockable; if (value == "Unblockable") return MissileMovementDistribution::Unblockable; return tl::make_unexpected("Unknown enum value"); } void LoadMisdat() { const std::string_view filename = "txtdata\\missiles\\misdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); MissilesData.clear(); MissilesData.reserve(dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; MissileData &item = MissilesData.emplace_back(); reader.advance(); // skip id reader.read("addFn", item.addFn, ParseMissileAddFn); reader.read("processFn", item.processFn, ParseMissileProcessFn); reader.read("castSound", item.castSound, ParseCastSound); reader.read("hitSound", item.hitSound, ParseHitSound); reader.read("graphic", item.graphic, ParseMissileGraphicID); reader.readEnumList("flags", item.flags, ParseMissileDataFlag); reader.read("movementDistribution", item.movementDistribution, ParseMissileMovementDistribution); } // Sanity check because we do not actually parse the IDs yet. assert(static_cast(MissileID::LastDiablo) + 1 == MissilesData.size() || static_cast(MissileID::LAST) + 1 == MissilesData.size()); MissilesData.shrink_to_fit(); } } // namespace uint8_t MissileFileData::animDelay(uint8_t dir) const { return MissileAnimDelays[animDelayIdx][dir]; } uint8_t MissileFileData::animLen(uint8_t dir) const { return MissileAnimLengths[animLenIdx][dir]; } tl::expected MissileFileData::LoadGFX() { if (sprites) return {}; if (name[0] == '\0') return {}; #ifdef UNPACKED_MPQS char path[MaxMpqPathSize]; *BufCopy(path, "missiles\\", name, ".clx") = '\0'; ASSIGN_OR_RETURN(sprites, LoadClxListOrSheetWithStatus(path)); #else if (animFAmt == 1) { char path[MaxMpqPathSize]; *BufCopy(path, "missiles\\", name) = '\0'; ASSIGN_OR_RETURN(OwnedClxSpriteList spriteList, LoadCl2WithStatus(path, animWidth)); sprites.emplace(OwnedClxSpriteListOrSheet { std::move(spriteList) }); } else { FileNameGenerator pathGenerator({ "missiles\\", name }, DEVILUTIONX_CL2_EXT); ASSIGN_OR_RETURN(OwnedClxSpriteSheet spriteSheet, LoadMultipleCl2Sheet<16>(pathGenerator, animFAmt, animWidth)); sprites.emplace(OwnedClxSpriteListOrSheet { std::move(spriteSheet) }); } #endif return {}; } MissileFileData &GetMissileSpriteData(MissileGraphicID graphicId) { return MissileSpriteData[static_cast>(graphicId)]; } void LoadMissileData() { LoadMissileSpriteData(); LoadMisdat(); } const MissileData &GetMissileData(MissileID missileId) { return MissilesData[static_cast>(missileId)]; } tl::expected InitMissileGFX() { if (HeadlessMode) return {}; for (MissileFileData &missileSprite : MissileSpriteData) { if (missileSprite.flags == MissileGraphicsFlags::MonsterOwned) continue; RETURN_IF_ERROR(missileSprite.LoadGFX()); } return {}; } void FreeMissileGFX() { for (MissileFileData &missileData : MissileSpriteData) { missileData.FreeGFX(); } } } // namespace devilution ================================================ FILE: Source/tables/misdat.h ================================================ /** * @file misdat.h * * Interface of data related to missiles. */ #pragma once #include #include #include #include #include #include "effects.h" #include "engine/clx_sprite.hpp" #include "tables/spelldat.h" #include "utils/enum_traits.h" namespace devilution { enum mienemy_type : uint8_t { TARGET_MONSTERS, TARGET_PLAYERS, TARGET_BOTH, }; enum class DamageType : uint8_t { Physical, Fire, Lightning, Magic, Acid, }; enum class MissileGraphicID : uint8_t { Arrow, Fireball, Guardian, Lightning, FireWall, MagmaBallExplosion, TownPortal, FlashBottom, FlashTop, ManaShield, BloodHit, BoneHit, MetalHit, FireArrow, DoomSerpents, Golem, Spurt, ApocalypseBoom, StoneCurseShatter, BigExplosion, Inferno, ThinLightning, BloodStar, BloodStarExplosion, MagmaBall, Krull, ChargedBolt, HolyBolt, HolyBoltExplosion, LightningArrow, FireArrowExplosion, Acid, AcidSplat, AcidPuddle, Etherealize, Elemental, Resurrect, BoneSpirit, RedPortal, DiabloApocalypseBoom, BloodStarBlue, BloodStarBlueExplosion, BloodStarYellow, BloodStarYellowExplosion, BloodStarRed, BloodStarRedExplosion, HorkSpawn, Reflect, OrangeFlare, BlueFlare, RedFlare, YellowFlare, Rune, YellowFlareExplosion, BlueFlareExplosion, RedFlareExplosion, BlueFlare2, OrangeFlareExplosion, BlueFlareExplosion2, None, }; /** * @brief Specifies what if and how movement distribution is applied */ enum class MissileMovementDistribution : uint8_t { /** * @brief No movement distribution is calculated. Normally this means the missile doesn't move at all. */ Disabled, /** * @brief The missile moves and if it hits an enemy it stops (for example firebolt) */ Blockable, /** * @brief The missile moves and even it hits an enemy it keeps moving (for example flame wave) */ Unblockable, }; struct Missile; struct AddMissileParameter; enum class MissileDataFlags : uint8_t { // The lower 3 bytes are used to store DamageType. Physical = static_cast(DamageType::Physical), Fire = static_cast(DamageType::Fire), Lightning = static_cast(DamageType::Lightning), Magic = static_cast(DamageType::Magic), Acid = static_cast(DamageType::Acid), Arrow = 1 << 4, Invisible = 1 << 5, }; use_enum_as_flags(MissileDataFlags); /** * Represent a more fine-grained direction than the 8 value Direction enum. * * This is used when rendering projectiles like arrows which have additional sprites for "half-winds" on a 16-point compass. * The sprite sheets are typically 0-indexed and use the following layout (relative to the screen projection) * * W WSW SW SSW S * ^ * WNW | SSE * | * NW -------+------> SE * | * NNW | ESE * | * N NNE NE ENE E */ enum class Direction16 : uint8_t { South, South_SouthWest, SouthWest, West_SouthWest, West, West_NorthWest, NorthWest, North_NorthWest, North, North_NorthEast, NorthEast, East_NorthEast, East, East_SouthEast, SouthEast, South_SouthEast, }; struct MissileData { using AddFn = void (*)(Missile &, AddMissileParameter &); using ProcessFn = void (*)(Missile &); AddFn addFn; ProcessFn processFn; /** * @brief Sound emitted when cast. */ SfxID castSound; /** * @brief Sound emitted on impact. */ SfxID hitSound; MissileGraphicID graphic; MissileDataFlags flags; MissileMovementDistribution movementDistribution; [[nodiscard]] bool isDrawn() const { return !HasAnyOf(flags, MissileDataFlags::Invisible); } [[nodiscard]] bool isArrow() const { return HasAnyOf(flags, MissileDataFlags::Arrow); } [[nodiscard]] DamageType damageType() const { return static_cast(static_cast::type>(flags) & 0b111U); } }; enum class MissileGraphicsFlags : uint8_t { // clang-format off None = 0, MonsterOwned = 1 << 0, NotAnimated = 1 << 1, // clang-format on }; struct MissileFileData { OptionalOwnedClxSpriteListOrSheet sprites; uint16_t animWidth; int8_t animWidth2; std::string name; uint8_t animFAmt; MissileGraphicsFlags flags; uint8_t animDelayIdx; uint8_t animLenIdx; [[nodiscard]] uint8_t animDelay(uint8_t dir) const; [[nodiscard]] uint8_t animLen(uint8_t dir) const; tl::expected LoadGFX(); void FreeGFX() { sprites = std::nullopt; } /** * @brief Returns the sprite list for a given direction. * * @param direction One of the 16 directions. Valid range: [0, 15]. * @return OptionalClxSpriteList */ [[nodiscard]] OptionalClxSpriteList spritesForDirection(Direction16 direction) const { if (!sprites) return std::nullopt; return sprites->isSheet() ? sprites->sheet()[static_cast(direction)] : sprites->list(); } }; const MissileData &GetMissileData(MissileID missileId); MissileFileData &GetMissileSpriteData(MissileGraphicID graphicId); void LoadMissileData(); tl::expected InitMissileGFX(); void FreeMissileGFX(); } // namespace devilution ================================================ FILE: Source/tables/monstdat.cpp ================================================ /** * @file monstdat.cpp * * Implementation of all monster data. */ #include "tables/monstdat.h" #include #include #include #include #include #include #include #include #include #include #include #include "appfat.h" #include "cursor.h" #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" #include "items.h" #include "lua/lua_event.hpp" #include "monster.h" #include "tables/textdat.h" #include "utils/language.h" namespace devilution { namespace { // Returns a `treasure` value for the given item. constexpr uint16_t Uniq(_unique_items item) { return static_cast(T_UNIQ) + item; } std::vector MonsterSpritePaths; } // namespace const char *MonsterData::spritePath() const { return MonsterSpritePaths[static_cast(spriteId)].c_str(); } /** Contains the data related to each monster ID. */ std::vector MonstersData; /** Contains the data related to each unique monster ID. */ std::vector UniqueMonstersData; /** * Map between .DUN file value and monster type enum */ const _monster_id MonstConvTbl[] = { MT_NZOMBIE, MT_BZOMBIE, MT_GZOMBIE, MT_YZOMBIE, MT_RFALLSP, MT_DFALLSP, MT_YFALLSP, MT_BFALLSP, MT_WSKELAX, MT_TSKELAX, MT_RSKELAX, MT_XSKELAX, MT_RFALLSD, MT_DFALLSD, MT_YFALLSD, MT_BFALLSD, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_WSKELBW, MT_TSKELBW, MT_RSKELBW, MT_XSKELBW, MT_WSKELSD, MT_TSKELSD, MT_RSKELSD, MT_XSKELSD, MT_SNEAK, MT_STALKER, MT_UNSEEN, MT_ILLWEAV, MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC, MT_FIEND, MT_GLOOM, MT_BLINK, MT_FAMILIAR, MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SKING, MT_FAT, MT_MUDMAN, MT_TOAD, MT_FLAYED, MT_WYRM, MT_CAVSLUG, MT_DEVOUR, MT_DVLWYRM, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA, MT_HORNED, MT_MUDRUN, MT_FROSTC, MT_OBLORD, MT_BONEDMN, MT_REDDTH, MT_LTCHDMN, MT_UDEDBLRG, MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, MT_INCIN, MT_FLAMLRD, MT_DOOMFIRE, MT_HELLBURN, MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, MT_RSTORM, MT_STORM, MT_STORML, MT_MAEL, MT_WINGED, MT_GARGOYLE, MT_BLOODCLW, MT_DEATHW, MT_MEGA, MT_GUARD, MT_VTEXLRD, MT_BALROG, MT_NSNAKE, MT_RSNAKE, MT_GSNAKE, MT_BSNAKE, MT_NBLACK, MT_RTBLACK, MT_BTBLACK, MT_RBLACK, MT_UNRAV, MT_HOLOWONE, MT_PAINMSTR, MT_REALWEAV, MT_SUCCUBUS, MT_SNOWWICH, MT_HLSPWN, MT_SOLBRNR, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE, MT_INVALID, MT_DIABLO, MT_INVALID, MT_GOLEM, MT_INVALID, MT_INVALID, MT_INVALID, // Monster from blood1.dun and blood2.dun MT_INVALID, MT_INVALID, MT_INVALID, MT_INVALID, // Snotspill from banner2.dun MT_INVALID, MT_INVALID, MT_BIGFALL, MT_DARKMAGE, MT_HELLBOAR, MT_STINGER, MT_PSYCHORB, MT_ARACHNON, MT_FELLTWIN, MT_HORKSPWN, MT_VENMTAIL, MT_NECRMORB, MT_SPIDLORD, MT_LASHWORM, MT_TORCHANT, MT_HORKDMN, MT_DEFILER, MT_GRAVEDIG, MT_TOMBRAT, MT_FIREBAT, MT_SKLWING, MT_LICH, MT_CRYPTDMN, MT_HELLBAT, MT_BONEDEMN, MT_LICH, MT_BICLOPS, MT_FLESTHNG, MT_REAPER, MT_NAKRUL, MT_CLEAVER, MT_INVILORD, MT_LRDSAYTR, }; namespace { /** Contains the mapping between monster ID strings and indices, used for parsing additional monster data. */ ankerl::unordered_dense::map AdditionalMonsterIdStringsToIndices; tl::expected<_monster_id, std::string> ParseMonsterId(std::string_view value) { const std::optional<_monster_id> enumValueOpt = magic_enum::enum_cast<_monster_id>(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } const auto findIt = AdditionalMonsterIdStringsToIndices.find(std::string(value)); if (findIt != AdditionalMonsterIdStringsToIndices.end()) { return static_cast<_monster_id>(findIt->second); } return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterAvailability(std::string_view value) { if (value == "Always") return MonsterAvailability::Always; if (value == "Never") return MonsterAvailability::Never; if (value == "Retail") return MonsterAvailability::Retail; return tl::make_unexpected("Expected one of: Always, Never, or Retail"); } tl::expected ParseAiId(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterFlag(std::string_view value) { if (value == "HIDDEN") return MFLAG_HIDDEN; if (value == "LOCK_ANIMATION") return MFLAG_LOCK_ANIMATION; if (value == "ALLOW_SPECIAL") return MFLAG_ALLOW_SPECIAL; if (value == "TARGETS_MONSTER") return MFLAG_TARGETS_MONSTER; if (value == "GOLEM") return MFLAG_GOLEM; if (value == "QUEST_COMPLETE") return MFLAG_QUEST_COMPLETE; if (value == "KNOCKBACK") return MFLAG_KNOCKBACK; if (value == "SEARCH") return MFLAG_SEARCH; if (value == "CAN_OPEN_DOOR") return MFLAG_CAN_OPEN_DOOR; if (value == "NO_ENEMY") return MFLAG_NO_ENEMY; if (value == "BERSERK") return MFLAG_BERSERK; if (value == "NOLIFESTEAL") return MFLAG_NOLIFESTEAL; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterClass(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterResistance(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } tl::expected ParseSelectionRegion(std::string_view value) { if (value.empty()) return SelectionRegion::None; if (value == "Bottom") return SelectionRegion::Bottom; if (value == "Middle") return SelectionRegion::Middle; if (value == "Top") return SelectionRegion::Top; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMonsterTreasure(std::string_view value) { // TODO: Replace this hack with proper parsing. if (value.empty()) return 0; if (value == "None") return T_NODROP; if (value == "Uniq(SKCROWN)") return Uniq(UITEM_SKCROWN); if (value == "Uniq(CLEAVER)") return Uniq(UITEM_CLEAVER); return tl::make_unexpected("Invalid value. NOTE: Parser is incomplete"); } tl::expected ParseUniqueMonsterPack(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } } // namespace void LoadMonstDatFromFile(DataFile &dataFile, const std::string_view filename, bool grow) { dataFile.skipHeaderOrDie(filename); if (grow) { MonstersData.reserve(MonstersData.size() + dataFile.numRecords()); } for (DataFileRecord record : dataFile) { if (MonstersData.size() >= static_cast(NUM_MAX_MTYPES)) { DisplayFatalErrorAndExit(_("Loading Monster Data Failed"), fmt::format(fmt::runtime(_("Could not add a monster, since the maximum monster type number of {} has already been reached.")), static_cast(NUM_MAX_MTYPES))); } RecordReader reader { record, filename }; std::string monsterId; reader.readString("_monster_id", monsterId); const std::optional<_monster_id> monsterIdEnumValueOpt = magic_enum::enum_cast<_monster_id>(monsterId); if (!monsterIdEnumValueOpt.has_value()) { const size_t monsterIndex = MonstersData.size(); const auto [it, inserted] = AdditionalMonsterIdStringsToIndices.emplace(monsterId, static_cast(monsterIndex)); if (!inserted) { DisplayFatalErrorAndExit(_("Loading Monster Data Failed"), fmt::format(fmt::runtime(_("A monster type already exists for ID \"{}\".")), monsterId)); } } // for hardcoded monsters, use their predetermined slot; for non-hardcoded ones, use the slots after that MonsterData &monster = monsterIdEnumValueOpt.has_value() ? MonstersData[monsterIdEnumValueOpt.value()] : MonstersData.emplace_back(); reader.readString("name", monster.name); { std::string assetsSuffix; reader.readString("assetsSuffix", assetsSuffix); const auto findIt = std::find(MonsterSpritePaths.begin(), MonsterSpritePaths.end(), assetsSuffix); if (findIt != MonsterSpritePaths.end()) { monster.spriteId = static_cast(findIt - MonsterSpritePaths.begin()); } else { monster.spriteId = static_cast(MonsterSpritePaths.size()); MonsterSpritePaths.push_back(std::string(assetsSuffix)); } } reader.readString("soundSuffix", monster.soundSuffix); reader.readString("trnFile", monster.trnFile); reader.read("availability", monster.availability, ParseMonsterAvailability); reader.readInt("width", monster.width); reader.readInt("image", monster.image); reader.readBool("hasSpecial", monster.hasSpecial); reader.readBool("hasSpecialSound", monster.hasSpecialSound); reader.readIntArray("frames", monster.frames); reader.readIntArray("rate", monster.rate); reader.readInt("minDunLvl", monster.minDunLvl); reader.readInt("maxDunLvl", monster.maxDunLvl); reader.readInt("level", monster.level); reader.readInt("hitPointsMinimum", monster.hitPointsMinimum); reader.readInt("hitPointsMaximum", monster.hitPointsMaximum); reader.read("ai", monster.ai, ParseAiId); reader.readEnumList("abilityFlags", monster.abilityFlags, ParseMonsterFlag); reader.readInt("intelligence", monster.intelligence); reader.readInt("toHit", monster.toHit); reader.readInt("animFrameNum", monster.animFrameNum); reader.readInt("minDamage", monster.minDamage); reader.readInt("maxDamage", monster.maxDamage); reader.readInt("toHitSpecial", monster.toHitSpecial); reader.readInt("animFrameNumSpecial", monster.animFrameNumSpecial); reader.readInt("minDamageSpecial", monster.minDamageSpecial); reader.readInt("maxDamageSpecial", monster.maxDamageSpecial); reader.readInt("reducePlayerStrength", monster.reducePlayerStrength); reader.readInt("reducePlayerMagic", monster.reducePlayerMagic); reader.readInt("reducePlayerDexterity", monster.reducePlayerDexterity); reader.readInt("reducePlayerVitality", monster.reducePlayerVitality); reader.readInt("reducePlayerMaxHP", monster.reducePlayerMaxHP); reader.readInt("reducePlayerMaxMana", monster.reducePlayerMaxMana); reader.readInt("armorClass", monster.armorClass); reader.read("monsterClass", monster.monsterClass, ParseMonsterClass); reader.readEnumList("resistance", monster.resistance, ParseMonsterResistance); reader.readEnumList("resistanceHell", monster.resistanceHell, ParseMonsterResistance); reader.readEnumList("selectionRegion", monster.selectionRegion, ParseSelectionRegion); reader.read("treasure", monster.treasure, ParseMonsterTreasure); reader.readInt("exp", monster.exp); } } namespace { void LoadMonstDat() { const std::string_view filename = "txtdata\\monsters\\monstdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); MonstersData.clear(); AdditionalMonsterIdStringsToIndices.clear(); MonstersData.resize(NUM_DEFAULT_MTYPES); // ensure the hardcoded monster type slots are filled LoadMonstDatFromFile(dataFile, filename, false); lua::MonsterDataLoaded(); MonstersData.shrink_to_fit(); } } // namespace void LoadUniqueMonstDatFromFile(DataFile &dataFile, std::string_view filename) { dataFile.skipHeaderOrDie(filename); UniqueMonstersData.reserve(UniqueMonstersData.size() + dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; UniqueMonsterData &monster = UniqueMonstersData.emplace_back(); reader.read("type", monster.mtype, ParseMonsterId); reader.readString("name", monster.mName); reader.readString("trn", monster.mTrnName); reader.readInt("level", monster.mlevel); reader.readInt("maxHp", monster.mmaxhp); reader.read("ai", monster.mAi, ParseAiId); reader.readInt("intelligence", monster.mint); reader.readInt("minDamage", monster.mMinDamage); reader.readInt("maxDamage", monster.mMaxDamage); reader.readInt("reducePlayerStrength", monster.reducePlayerStrength); reader.readInt("reducePlayerMagic", monster.reducePlayerMagic); reader.readInt("reducePlayerDexterity", monster.reducePlayerDexterity); reader.readInt("reducePlayerVitality", monster.reducePlayerVitality); reader.readInt("reducePlayerMaxHP", monster.reducePlayerMaxHP); reader.readInt("reducePlayerMaxMana", monster.reducePlayerMaxMana); reader.readEnumList("resistance", monster.mMagicRes, ParseMonsterResistance); reader.read("monsterPack", monster.monsterPack, ParseUniqueMonsterPack); reader.readInt("customToHit", monster.customToHit); reader.readInt("customArmorClass", monster.customArmorClass); reader.read("talkMessage", monster.mtalkmsg, ParseSpeechId); } } namespace { void LoadUniqueMonstDat() { const std::string_view filename = "txtdata\\monsters\\unique_monstdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); UniqueMonstersData.clear(); LoadUniqueMonstDatFromFile(dataFile, filename); lua::UniqueMonsterDataLoaded(); UniqueMonstersData.shrink_to_fit(); } } // namespace void LoadMonsterData() { LoadMonstDat(); LoadUniqueMonstDat(); } size_t GetNumMonsterSprites() { return MonsterSpritePaths.size(); } } // namespace devilution ================================================ FILE: Source/tables/monstdat.h ================================================ /** * @file monstdat.h * * Interface of all monster data. */ #pragma once #include #include #include #include #include #include "cursor.h" #include "tables/textdat.h" namespace devilution { class DataFile; enum class MonsterAIID : int8_t { Zombie, Fat, SkeletonMelee, SkeletonRanged, Scavenger, Rhino, GoatMelee, GoatRanged, Fallen, Magma, SkeletonKing, Bat, Gargoyle, Butcher, Succubus, Sneak, Storm, FireMan, Gharbad, Acid, AcidUnique, Golem, Zhar, Snotspill, Snake, Counselor, Mega, Diablo, Lazarus, LazarusSuccubus, Lachdanan, Warlord, FireBat, Torchant, HorkDemon, Lich, ArchLich, Psychorb, Necromorb, BoneDemon, Invalid = -1, }; enum class MonsterClass : uint8_t { Undead, Demon, Animal, }; enum monster_resistance : uint8_t { // clang-format off RESIST_MAGIC = 1 << 0, RESIST_FIRE = 1 << 1, RESIST_LIGHTNING = 1 << 2, IMMUNE_MAGIC = 1 << 3, IMMUNE_FIRE = 1 << 4, IMMUNE_LIGHTNING = 1 << 5, IMMUNE_ACID = 1 << 7, // clang-format on }; enum monster_treasure : uint16_t { // clang-format off T_MASK = 0xFFF, T_NODROP = 0x4000, // monster doesn't drop any loot T_UNIQ = 0x8000, // use combined with unique item's ID - for example butcher's cleaver = T_UNIQ+UITEM_CLEAVE // clang-format on }; enum class MonsterAvailability : uint8_t { Never, Always, Retail, }; struct MonsterData { std::string name; std::string soundSuffix; std::string trnFile; uint16_t spriteId = 0; MonsterAvailability availability = MonsterAvailability::Never; uint16_t width = 0; uint16_t image = 0; bool hasSpecial = false; bool hasSpecialSound = false; int8_t frames[6] {}; int8_t rate[6] {}; int8_t minDunLvl = 0; int8_t maxDunLvl = 0; int8_t level = 0; uint16_t hitPointsMinimum = 0; uint16_t hitPointsMaximum = 0; MonsterAIID ai = MonsterAIID::Invalid; /** * @brief Denotes monster's abilities defined in @p monster_flag as bitflags * For usage, see @p MonstersData in monstdat.cpp */ uint16_t abilityFlags = 0; uint8_t intelligence = 0; uint8_t toHit = 0; int8_t animFrameNum = 0; uint8_t minDamage = 0; uint8_t maxDamage = 0; uint8_t toHitSpecial = 0; int8_t animFrameNumSpecial = 0; uint8_t minDamageSpecial = 0; uint8_t maxDamageSpecial = 0; uint8_t reducePlayerStrength = 0; uint8_t reducePlayerMagic = 0; uint8_t reducePlayerDexterity = 0; uint8_t reducePlayerVitality = 0; uint8_t reducePlayerMaxHP = 0; uint8_t reducePlayerMaxMana = 0; uint8_t armorClass = 0; MonsterClass monsterClass {}; /** Using monster_resistance as bitflags */ uint8_t resistance = 0; /** Using monster_resistance as bitflags */ uint8_t resistanceHell = 0; SelectionRegion selectionRegion = SelectionRegion::None; /** Using monster_treasure */ uint16_t treasure = 0; uint16_t exp = 0; [[nodiscard]] const char *spritePath() const; [[nodiscard]] const char *soundPath() const { return !soundSuffix.empty() ? soundSuffix.c_str() : spritePath(); } [[nodiscard]] bool hasAnim(size_t index) const { return frames[index] != 0; } }; enum _monster_id : int16_t { MT_NZOMBIE, MT_BZOMBIE, MT_GZOMBIE, MT_YZOMBIE, MT_RFALLSP, MT_DFALLSP, MT_YFALLSP, MT_BFALLSP, MT_WSKELAX, MT_TSKELAX, MT_RSKELAX, MT_XSKELAX, MT_RFALLSD, MT_DFALLSD, MT_YFALLSD, MT_BFALLSD, MT_NSCAV, MT_BSCAV, MT_WSCAV, MT_YSCAV, MT_WSKELBW, MT_TSKELBW, MT_RSKELBW, MT_XSKELBW, MT_WSKELSD, MT_TSKELSD, MT_RSKELSD, MT_XSKELSD, MT_INVILORD, MT_SNEAK, MT_STALKER, MT_UNSEEN, MT_ILLWEAV, MT_LRDSAYTR, MT_NGOATMC, MT_BGOATMC, MT_RGOATMC, MT_GGOATMC, MT_FIEND, MT_BLINK, MT_GLOOM, MT_FAMILIAR, MT_NGOATBW, MT_BGOATBW, MT_RGOATBW, MT_GGOATBW, MT_NACID, MT_RACID, MT_BACID, MT_XACID, MT_SKING, MT_CLEAVER, MT_FAT, MT_MUDMAN, MT_TOAD, MT_FLAYED, MT_WYRM, MT_CAVSLUG, MT_DVLWYRM, MT_DEVOUR, MT_NMAGMA, MT_YMAGMA, MT_BMAGMA, MT_WMAGMA, MT_HORNED, MT_MUDRUN, MT_FROSTC, MT_OBLORD, MT_BONEDMN, MT_REDDTH, MT_LTCHDMN, MT_UDEDBLRG, MT_INCIN, MT_FLAMLRD, MT_DOOMFIRE, MT_HELLBURN, MT_STORM, MT_RSTORM, MT_STORML, MT_MAEL, MT_BIGFALL, MT_WINGED, MT_GARGOYLE, MT_BLOODCLW, MT_DEATHW, MT_MEGA, MT_GUARD, MT_VTEXLRD, MT_BALROG, MT_NSNAKE, MT_RSNAKE, MT_BSNAKE, MT_GSNAKE, MT_NBLACK, MT_RTBLACK, MT_BTBLACK, MT_RBLACK, MT_UNRAV, MT_HOLOWONE, MT_PAINMSTR, MT_REALWEAV, MT_SUCCUBUS, MT_SNOWWICH, MT_HLSPWN, MT_SOLBRNR, MT_COUNSLR, MT_MAGISTR, MT_CABALIST, MT_ADVOCATE, MT_GOLEM, MT_DIABLO, MT_DARKMAGE, MT_HELLBOAR, MT_STINGER, MT_PSYCHORB, MT_ARACHNON, MT_FELLTWIN, MT_HORKSPWN, MT_VENMTAIL, MT_NECRMORB, MT_SPIDLORD, MT_LASHWORM, MT_TORCHANT, MT_HORKDMN, MT_DEFILER, MT_GRAVEDIG, MT_TOMBRAT, MT_FIREBAT, MT_SKLWING, MT_LICH, MT_CRYPTDMN, MT_HELLBAT, MT_BONEDEMN, MT_ARCHLICH, MT_BICLOPS, MT_FLESTHNG, MT_REAPER, MT_NAKRUL, NUM_DEFAULT_MTYPES, NUM_MAX_MTYPES = 200, // same as MaxMonsters, for the sake of save game compability MT_INVALID = -1, }; /** * @brief Defines if and how a group of monsters should be spawned with the unique monster */ enum class UniqueMonsterPack : uint8_t { /** * @brief Don't spawn a group of monsters with the unique monster */ None, /** * @brief Spawn a group of monsters that are independent from the unique monster */ Independent, /** * @brief Spawn a group of monsters that are leashed to the unique monster */ Leashed, }; struct UniqueMonsterData { _monster_id mtype; std::string mName; std::string mTrnName; uint8_t mlevel; uint16_t mmaxhp; MonsterAIID mAi; uint8_t mint; uint8_t mMinDamage; uint8_t mMaxDamage; /** Using monster_resistance as bitflags */ uint16_t mMagicRes; /** * @brief Defines if and how a group of monsters should be spawned with the unique monster */ uint8_t reducePlayerStrength; uint8_t reducePlayerMagic; uint8_t reducePlayerDexterity; uint8_t reducePlayerVitality; uint8_t reducePlayerMaxHP; uint8_t reducePlayerMaxMana; UniqueMonsterPack monsterPack; uint8_t customToHit; uint8_t customArmorClass; _speech_id mtalkmsg; }; extern std::vector MonstersData; extern const _monster_id MonstConvTbl[]; extern std::vector UniqueMonstersData; void LoadMonstDatFromFile(DataFile &dataFile, std::string_view filename, bool grow); void LoadUniqueMonstDatFromFile(DataFile &dataFile, std::string_view filename); void LoadMonsterData(); /** * @brief Returns the number of the monster sprite files. * * Different monsters can use the same sprite with different TRNs, these count as 1. */ size_t GetNumMonsterSprites(); } // namespace devilution template <> struct magic_enum::customize::enum_range { static constexpr int min = devilution::MT_INVALID; static constexpr int max = devilution::NUM_DEFAULT_MTYPES; }; template <> struct magic_enum::customize::enum_range { static constexpr int min = 0; static constexpr int max = 128; }; ================================================ FILE: Source/tables/objdat.cpp ================================================ /** * @file objdat.cpp * * Implementation of all object data. */ #include "tables/objdat.h" #include #include #include #include #include #include "cursor.h" #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" namespace devilution { /** Maps from dun_object_id to object_id. */ const _object_id ObjTypeConv[] = { OBJ_NULL, OBJ_LEVER, OBJ_CRUX1, OBJ_CRUX2, OBJ_CRUX3, OBJ_ANGEL, OBJ_BANNERL, OBJ_BANNERM, OBJ_BANNERR, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_BOOK2L, OBJ_BOOK2R, OBJ_BCROSS, OBJ_NULL, OBJ_CANDLE1, OBJ_CANDLE2, OBJ_CANDLEO, OBJ_CAULDRON, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_FLAMEHOLE, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_MCIRCLE1, OBJ_MCIRCLE2, OBJ_SKFIRE, OBJ_SKPILE, OBJ_SKSTICK1, OBJ_SKSTICK2, OBJ_SKSTICK3, OBJ_SKSTICK4, OBJ_SKSTICK5, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_SWITCHSKL, OBJ_NULL, OBJ_TRAPL, OBJ_TRAPR, OBJ_TORTURE1, OBJ_TORTURE2, OBJ_TORTURE3, OBJ_TORTURE4, OBJ_TORTURE5, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NUDEW2R, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_TNUDEM1, OBJ_TNUDEM2, OBJ_TNUDEM3, OBJ_TNUDEM4, OBJ_TNUDEW1, OBJ_TNUDEW2, OBJ_TNUDEW3, OBJ_CHEST1, OBJ_CHEST1, OBJ_CHEST1, OBJ_CHEST2, OBJ_CHEST2, OBJ_CHEST2, OBJ_CHEST3, OBJ_CHEST3, OBJ_CHEST3, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_PEDESTAL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_NULL, OBJ_ALTBOY, OBJ_NULL, OBJ_NULL, OBJ_WARARMOR, OBJ_WARWEAP, OBJ_TORCHR2, OBJ_TORCHL2, OBJ_MUSHPATCH, OBJ_STAND, OBJ_TORCHL, OBJ_TORCHR, OBJ_FLAMELVR, OBJ_SARC, OBJ_BARREL, OBJ_BARRELEX, OBJ_BOOKSHELF, OBJ_BOOKCASEL, OBJ_BOOKCASER, OBJ_ARMORSTANDN, OBJ_WEAPONRACKN, OBJ_BLOODFTN, OBJ_PURIFYINGFTN, OBJ_SHRINEL, OBJ_SHRINER, OBJ_GOATSHRINE, OBJ_MURKYFTN, OBJ_TEARFTN, OBJ_DECAP, OBJ_TCHEST1, OBJ_TCHEST2, OBJ_TCHEST3, OBJ_LAZSTAND, OBJ_BOOKSTAND, OBJ_BOOKSHELFR, OBJ_POD, OBJ_PODEX, OBJ_URN, OBJ_URNEX, OBJ_L5BOOKS, OBJ_L5CANDLE, OBJ_L5LEVER, OBJ_L5SARC, }; /** Contains the data related to each object ID. */ std::vector AllObjects; /** Maps from object_graphic_id to object CEL name. */ std::vector ObjMasterLoadList; namespace { tl::expected ParseTheme(std::string_view value) { if (value.empty()) return THEME_NONE; if (value == "THEME_BARREL") return THEME_BARREL; if (value == "THEME_SHRINE") return THEME_SHRINE; if (value == "THEME_MONSTPIT") return THEME_MONSTPIT; if (value == "THEME_SKELROOM") return THEME_SKELROOM; if (value == "THEME_TREASURE") return THEME_TREASURE; if (value == "THEME_LIBRARY") return THEME_LIBRARY; if (value == "THEME_TORTURE") return THEME_TORTURE; if (value == "THEME_BLOODFOUNTAIN") return THEME_BLOODFOUNTAIN; if (value == "THEME_DECAPITATED") return THEME_DECAPITATED; if (value == "THEME_PURIFYINGFOUNTAIN") return THEME_PURIFYINGFOUNTAIN; if (value == "THEME_ARMORSTAND") return THEME_ARMORSTAND; if (value == "THEME_GOATSHRINE") return THEME_GOATSHRINE; if (value == "THEME_CAULDRON") return THEME_CAULDRON; if (value == "THEME_MURKYFOUNTAIN") return THEME_MURKYFOUNTAIN; if (value == "THEME_TEARFOUNTAIN") return THEME_TEARFOUNTAIN; if (value == "THEME_BRNCROSS") return THEME_BRNCROSS; if (value == "THEME_WEAPONRACK") return THEME_WEAPONRACK; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseQuest(std::string_view value) { if (value.empty()) return Q_INVALID; if (value == "Q_ROCK") return Q_ROCK; if (value == "Q_MUSHROOM") return Q_MUSHROOM; if (value == "Q_GARBUD") return Q_GARBUD; if (value == "Q_ZHAR") return Q_ZHAR; if (value == "Q_VEIL") return Q_VEIL; if (value == "Q_DIABLO") return Q_DIABLO; if (value == "Q_BUTCHER") return Q_BUTCHER; if (value == "Q_LTBANNER") return Q_LTBANNER; if (value == "Q_BLIND") return Q_BLIND; if (value == "Q_BLOOD") return Q_BLOOD; if (value == "Q_ANVIL") return Q_ANVIL; if (value == "Q_WARLORD") return Q_WARLORD; if (value == "Q_SKELKING") return Q_SKELKING; if (value == "Q_PWATER") return Q_PWATER; if (value == "Q_SCHAMB") return Q_SCHAMB; if (value == "Q_BETRAYER") return Q_BETRAYER; if (value == "Q_GRAVE") return Q_GRAVE; if (value == "Q_FARMER") return Q_FARMER; if (value == "Q_GIRL") return Q_GIRL; if (value == "Q_TRADER") return Q_TRADER; if (value == "Q_DEFILER") return Q_DEFILER; if (value == "Q_NAKRUL") return Q_NAKRUL; if (value == "Q_CORNSTN") return Q_CORNSTN; if (value == "Q_JERSEY") return Q_JERSEY; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseObjectDataFlags(std::string_view value) { if (value.empty()) return ObjectDataFlags::None; if (value == "Animated") return ObjectDataFlags::Animated; if (value == "Solid") return ObjectDataFlags::Solid; if (value == "MissilesPassThrough") return ObjectDataFlags::MissilesPassThrough; if (value == "Light") return ObjectDataFlags::Light; if (value == "Trap") return ObjectDataFlags::Trap; if (value == "Breakable") return ObjectDataFlags::Breakable; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseSelectionRegion(std::string_view value) { if (value.empty()) return SelectionRegion::None; if (value == "Bottom") return SelectionRegion::Bottom; if (value == "Middle") return SelectionRegion::Middle; if (value == "Top") return SelectionRegion::Top; return tl::make_unexpected("Unknown enum value"); } } // namespace void LoadObjectData() { const std::string_view filename = "txtdata\\objects\\objdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); AllObjects.clear(); ObjMasterLoadList.clear(); ankerl::unordered_dense::map filenameToId; for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; ObjectData &item = AllObjects.emplace_back(); reader.advance(); // skip id std::string objFilename; reader.readString("file", objFilename); if (const auto it = filenameToId.find(objFilename); it != filenameToId.end()) { item.ofindex = it->second; } else { const auto id = static_cast(ObjMasterLoadList.size()); ObjMasterLoadList.push_back(objFilename); filenameToId.emplace(std::move(objFilename), id); item.ofindex = id; } reader.readInt("minLevel", item.minlvl); reader.readInt("maxLevel", item.maxlvl); reader.read("levelType", item.olvltype, ParseDungeonType); reader.read("theme", item.otheme, ParseTheme); reader.read("quest", item.oquest, ParseQuest); reader.readEnumList("flags", item.flags, ParseObjectDataFlags); reader.readInt("animDelay", item.animDelay); reader.readInt("animLen", item.animLen); reader.readInt("animWidth", item.animWidth); reader.readEnumList("selectionRegion", item.selectionRegion, ParseSelectionRegion); } // Sanity check because we do not actually parse the IDs yet. assert(static_cast(OBJ_LAST) + 1 == AllObjects.size()); AllObjects.shrink_to_fit(); ObjMasterLoadList.shrink_to_fit(); } } // namespace devilution ================================================ FILE: Source/tables/objdat.h ================================================ /** * @file objdat.h * * Interface of all object data. */ #pragma once #include #include #include "cursor.h" #include "levels/gendung.h" #include "utils/enum_traits.h" namespace devilution { enum theme_id : int8_t { THEME_BARREL, THEME_SHRINE, THEME_MONSTPIT, THEME_SKELROOM, THEME_TREASURE, THEME_LIBRARY, THEME_TORTURE, THEME_BLOODFOUNTAIN, THEME_DECAPITATED, THEME_PURIFYINGFOUNTAIN, THEME_ARMORSTAND, THEME_GOATSHRINE, THEME_CAULDRON, THEME_MURKYFOUNTAIN, THEME_TEARFOUNTAIN, THEME_BRNCROSS, THEME_WEAPONRACK, THEME_NONE = -1, }; // Index into ObjMasterLoadList. using object_graphic_id = uint8_t; enum _object_id : int8_t { OBJ_L1LIGHT, OBJ_L1LDOOR, OBJ_L1RDOOR, OBJ_SKFIRE, OBJ_LEVER, OBJ_CHEST1, OBJ_CHEST2, OBJ_CHEST3, OBJ_CANDLE1, OBJ_CANDLE2, OBJ_CANDLEO, OBJ_BANNERL, OBJ_BANNERM, OBJ_BANNERR, OBJ_SKPILE, OBJ_SKSTICK1, OBJ_SKSTICK2, OBJ_SKSTICK3, OBJ_SKSTICK4, OBJ_SKSTICK5, OBJ_CRUX1, OBJ_CRUX2, OBJ_CRUX3, OBJ_STAND, OBJ_ANGEL, OBJ_BOOK2L, OBJ_BCROSS, OBJ_NUDEW2R, OBJ_SWITCHSKL, OBJ_TNUDEM1, OBJ_TNUDEM2, OBJ_TNUDEM3, OBJ_TNUDEM4, OBJ_TNUDEW1, OBJ_TNUDEW2, OBJ_TNUDEW3, OBJ_TORTURE1, OBJ_TORTURE2, OBJ_TORTURE3, OBJ_TORTURE4, OBJ_TORTURE5, OBJ_BOOK2R, OBJ_L2LDOOR, OBJ_L2RDOOR, OBJ_TORCHL, OBJ_TORCHR, OBJ_TORCHL2, OBJ_TORCHR2, OBJ_SARC, OBJ_FLAMEHOLE, OBJ_FLAMELVR, OBJ_WATER, OBJ_BOOKLVR, OBJ_TRAPL, OBJ_TRAPR, OBJ_BOOKSHELF, OBJ_WEAPRACK, OBJ_BARREL, OBJ_BARRELEX, OBJ_SHRINEL, OBJ_SHRINER, OBJ_SKELBOOK, OBJ_BOOKCASEL, OBJ_BOOKCASER, OBJ_BOOKSTAND, OBJ_BOOKCANDLE, OBJ_BLOODFTN, OBJ_DECAP, OBJ_TCHEST1, OBJ_TCHEST2, OBJ_TCHEST3, OBJ_BLINDBOOK, OBJ_BLOODBOOK, OBJ_PEDESTAL, OBJ_L3LDOOR, OBJ_L3RDOOR, OBJ_PURIFYINGFTN, OBJ_ARMORSTAND, OBJ_ARMORSTANDN, OBJ_GOATSHRINE, OBJ_CAULDRON, OBJ_MURKYFTN, OBJ_TEARFTN, OBJ_ALTBOY, OBJ_MCIRCLE1, OBJ_MCIRCLE2, OBJ_STORYBOOK, OBJ_STORYCANDLE, OBJ_STEELTOME, OBJ_WARARMOR, OBJ_WARWEAP, OBJ_TBCROSS, OBJ_WEAPONRACK, OBJ_WEAPONRACKN, OBJ_MUSHPATCH, OBJ_LAZSTAND, OBJ_SLAINHERO, OBJ_SIGNCHEST, OBJ_BOOKSHELFR, OBJ_POD, OBJ_PODEX, OBJ_URN, OBJ_URNEX, OBJ_L5BOOKS, OBJ_L5CANDLE, OBJ_L5LDOOR, OBJ_L5RDOOR, OBJ_L5LEVER, OBJ_L5SARC, OBJ_LAST = OBJ_L5SARC, OBJ_NULL = -1, }; enum quest_id : int8_t { Q_ROCK, Q_MUSHROOM, Q_GARBUD, Q_ZHAR, Q_VEIL, Q_DIABLO, Q_BUTCHER, Q_LTBANNER, Q_BLIND, Q_BLOOD, Q_ANVIL, Q_WARLORD, Q_SKELKING, Q_PWATER, Q_SCHAMB, Q_BETRAYER, Q_GRAVE, Q_FARMER, Q_GIRL, Q_TRADER, Q_DEFILER, Q_NAKRUL, Q_CORNSTN, Q_JERSEY, Q_INVALID = -1, }; enum class ObjectDataFlags : uint8_t { None = 0, Animated = 1U, Solid = 1U << 1, MissilesPassThrough = 1U << 2, Light = 1U << 3, Trap = 1U << 4, Breakable = 1U << 5, }; use_enum_as_flags(ObjectDataFlags); struct ObjectData { object_graphic_id ofindex; int8_t minlvl; int8_t maxlvl; dungeon_type olvltype; theme_id otheme; quest_id oquest; ObjectDataFlags flags; uint8_t animDelay; // Tick length of each frame in the current animation uint8_t animLen; // Number of frames in current animation uint8_t animWidth; SelectionRegion selectionRegion; [[nodiscard]] bool isAnimated() const { return HasAnyOf(flags, ObjectDataFlags::Animated); } [[nodiscard]] bool isSolid() const { return HasAnyOf(flags, ObjectDataFlags::Solid); } [[nodiscard]] bool missilesPassThrough() const { return HasAnyOf(flags, ObjectDataFlags::MissilesPassThrough); } [[nodiscard]] bool applyLighting() const { return HasAnyOf(flags, ObjectDataFlags::Light); } [[nodiscard]] bool isTrap() const { return HasAnyOf(flags, ObjectDataFlags::Trap); } [[nodiscard]] bool isBreakable() const { return HasAnyOf(flags, ObjectDataFlags::Breakable); } }; extern const _object_id ObjTypeConv[]; extern std::vector AllObjects; extern std::vector ObjMasterLoadList; void LoadObjectData(); } // namespace devilution ================================================ FILE: Source/tables/playerdat.cpp ================================================ /** * @file playerdat.cpp * * Implementation of all player data. */ #include "tables/playerdat.hpp" #include #include #include #include #include #include #include #include #include #include "data/file.hpp" #include "data/record_reader.hpp" #include "data/value_reader.hpp" #include "items.h" #include "player.h" #include "tables/textdat.h" #include "utils/language.h" #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { class ExperienceData { /** Specifies the experience point limit of each level. */ std::vector levelThresholds; public: uint8_t getMaxLevel() const { return static_cast(std::min(levelThresholds.size(), std::numeric_limits::max())); } DVL_REINITIALIZES void clear() { levelThresholds.clear(); } [[nodiscard]] uint32_t getThresholdForLevel(unsigned level) const { if (level > 0) return levelThresholds[std::min(level - 1, getMaxLevel())]; return 0; } void setThresholdForLevel(unsigned level, uint32_t experience) { if (level > 0) { if (level > levelThresholds.size()) { // To avoid ValidatePlayer() resetting players to 0 experience we need to use the maximum possible value here // As long as the file has no gaps it'll get initialised properly. levelThresholds.resize(level, std::numeric_limits::max()); } levelThresholds[static_cast(level - 1)] = experience; } } } ExperienceData; enum class ExperienceColumn { Level, Experience, LAST = Experience }; tl::expected mapExperienceColumnFromName(std::string_view name) { if (name == "Level") { return ExperienceColumn::Level; } if (name == "Experience") { return ExperienceColumn::Experience; } return tl::unexpected { ColumnDefinition::Error::UnknownColumn }; } void ReloadExperienceData() { constexpr std::string_view filename = "txtdata\\Experience.tsv"; auto dataFileResult = DataFile::load(filename); if (!dataFileResult.has_value()) { DataFile::reportFatalError(dataFileResult.error(), filename); } DataFile &dataFile = dataFileResult.value(); constexpr unsigned ExpectedColumnCount = enum_size::value; std::array columns; auto parseHeaderResult = dataFile.parseHeader(columns.data(), columns.data() + columns.size(), mapExperienceColumnFromName); if (!parseHeaderResult.has_value()) { DataFile::reportFatalError(parseHeaderResult.error(), filename); } ExperienceData.clear(); for (DataFileRecord record : dataFile) { uint8_t level = 0; uint32_t experience = 0; bool skipRecord = false; FieldIterator fieldIt = record.begin(); const FieldIterator endField = record.end(); for (auto &column : columns) { fieldIt += column.skipLength; if (fieldIt == endField) { DataFile::reportFatalError(DataFile::Error::NotEnoughColumns, filename); } DataFileField field = *fieldIt; switch (static_cast(column)) { case ExperienceColumn::Level: { auto parseIntResult = field.parseInt(level); if (!parseIntResult.has_value()) { if (*field == "MaxLevel") { skipRecord = true; } else { DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Level", field); } } } break; case ExperienceColumn::Experience: { auto parseIntResult = field.parseInt(experience); if (!parseIntResult.has_value()) { DataFile::reportFatalFieldError(parseIntResult.error(), filename, "Experience", field); } } break; default: break; } if (skipRecord) break; ++fieldIt; } if (!skipRecord) ExperienceData.setThresholdForLevel(level, experience); } } tl::expected ParsePlayerClassFlag(std::string_view value) { const std::optional enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } void LoadClassData(std::string_view classPath, ClassAttributes &attributes, PlayerCombatData &combat) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\attributes.tsv"); tl::expected dataFileResult = DataFile::loadOrDie(filename); DataFile &dataFile = dataFileResult.value(); dataFile.skipHeaderOrDie(filename); ValueReader reader { dataFile, filename }; reader.readEnumList("classFlags", attributes.classFlags, ParsePlayerClassFlag); reader.readInt("baseStr", attributes.baseStr); reader.readInt("baseMag", attributes.baseMag); reader.readInt("baseDex", attributes.baseDex); reader.readInt("baseVit", attributes.baseVit); reader.readInt("maxStr", attributes.maxStr); reader.readInt("maxMag", attributes.maxMag); reader.readInt("maxDex", attributes.maxDex); reader.readInt("maxVit", attributes.maxVit); reader.readInt("blockBonus", combat.baseToBlock); reader.readDecimal("adjLife", attributes.adjLife); reader.readDecimal("adjMana", attributes.adjMana); reader.readDecimal("lvlLife", attributes.lvlLife); reader.readDecimal("lvlMana", attributes.lvlMana); reader.readDecimal("chrLife", attributes.chrLife); reader.readDecimal("chrMana", attributes.chrMana); reader.readDecimal("itmLife", attributes.itmLife); reader.readDecimal("itmMana", attributes.itmMana); reader.readInt("baseMagicToHit", combat.baseMagicToHit); reader.readInt("baseMeleeToHit", combat.baseMeleeToHit); reader.readInt("baseRangedToHit", combat.baseRangedToHit); } void LoadClassStartingLoadoutData(std::string_view classPath, PlayerStartingLoadoutData &startingLoadoutData) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\starting_loadout.tsv"); tl::expected dataFileResult = DataFile::loadOrDie(filename); DataFile &dataFile = dataFileResult.value(); dataFile.skipHeaderOrDie(filename); ValueReader reader { dataFile, filename }; reader.read("skill", startingLoadoutData.skill, ParseSpellId); reader.read("spell", startingLoadoutData.spell, ParseSpellId); reader.readInt("spellLevel", startingLoadoutData.spellLevel); for (size_t i = 0; i < startingLoadoutData.items.size(); ++i) { reader.read(StrCat("item", i), startingLoadoutData.items[i], ParseItemId); } reader.readInt("gold", startingLoadoutData.gold); } void LoadClassSpriteData(std::string_view classPath, PlayerSpriteData &spriteData) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\sprites.tsv"); tl::expected dataFileResult = DataFile::loadOrDie(filename); DataFile &dataFile = dataFileResult.value(); dataFile.skipHeaderOrDie(filename); ValueReader reader { dataFile, filename }; reader.readString("classPath", spriteData.classPath); reader.readChar("classChar", spriteData.classChar); reader.readString("trn", spriteData.trn); reader.readInt("stand", spriteData.stand); reader.readInt("walk", spriteData.walk); reader.readInt("attack", spriteData.attack); reader.readInt("bow", spriteData.bow); reader.readInt("swHit", spriteData.swHit); reader.readInt("block", spriteData.block); reader.readInt("lightning", spriteData.lightning); reader.readInt("fire", spriteData.fire); reader.readInt("magic", spriteData.magic); reader.readInt("death", spriteData.death); } void LoadClassAnimData(std::string_view classPath, PlayerAnimData &animData) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\animations.tsv"); tl::expected dataFileResult = DataFile::loadOrDie(filename); DataFile &dataFile = dataFileResult.value(); dataFile.skipHeaderOrDie(filename); ValueReader reader { dataFile, filename }; reader.readInt("unarmedFrames", animData.unarmedFrames); reader.readInt("unarmedActionFrame", animData.unarmedActionFrame); reader.readInt("unarmedShieldFrames", animData.unarmedShieldFrames); reader.readInt("unarmedShieldActionFrame", animData.unarmedShieldActionFrame); reader.readInt("swordFrames", animData.swordFrames); reader.readInt("swordActionFrame", animData.swordActionFrame); reader.readInt("swordShieldFrames", animData.swordShieldFrames); reader.readInt("swordShieldActionFrame", animData.swordShieldActionFrame); reader.readInt("bowFrames", animData.bowFrames); reader.readInt("bowActionFrame", animData.bowActionFrame); reader.readInt("axeFrames", animData.axeFrames); reader.readInt("axeActionFrame", animData.axeActionFrame); reader.readInt("maceFrames", animData.maceFrames); reader.readInt("maceActionFrame", animData.maceActionFrame); reader.readInt("maceShieldFrames", animData.maceShieldFrames); reader.readInt("maceShieldActionFrame", animData.maceShieldActionFrame); reader.readInt("staffFrames", animData.staffFrames); reader.readInt("staffActionFrame", animData.staffActionFrame); reader.readInt("idleFrames", animData.idleFrames); reader.readInt("walkingFrames", animData.walkingFrames); reader.readInt("blockingFrames", animData.blockingFrames); reader.readInt("deathFrames", animData.deathFrames); reader.readInt("castingFrames", animData.castingFrames); reader.readInt("recoveryFrames", animData.recoveryFrames); reader.readInt("townIdleFrames", animData.townIdleFrames); reader.readInt("townWalkingFrames", animData.townWalkingFrames); reader.readInt("castingActionFrame", animData.castingActionFrame); } void LoadClassSounds(std::string_view classPath, ankerl::unordered_dense::map &sounds) { const std::string filename = StrCat("txtdata\\classes\\", classPath, "\\sounds.tsv"); tl::expected dataFileResult = DataFile::loadOrDie(filename); DataFile &dataFile = dataFileResult.value(); dataFile.skipHeaderOrDie(filename); ValueReader reader { dataFile, filename }; magic_enum::enum_for_each([&](const HeroSpeech speech) { reader.read(magic_enum::enum_name(speech), sounds[speech], ParseSfxId); }); } /** Contains the data related to each player class. */ std::vector PlayersData; std::vector ClassAttributesPerClass; std::vector PlayersCombatData; std::vector PlayersStartingLoadoutData; /** Contains the data related to each player class. */ std::vector PlayersSpriteData; std::vector PlayersAnimData; std::vector> herosounds; } // namespace void LoadClassDatFromFile(DataFile &dataFile, const std::string_view filename) { dataFile.skipHeaderOrDie(filename); PlayersData.reserve(PlayersData.size() + dataFile.numRecords()); for (DataFileRecord record : dataFile) { if (PlayersData.size() >= static_cast(HeroClass::NUM_MAX_CLASSES)) { DisplayFatalErrorAndExit(_("Loading Class Data Failed"), fmt::format(fmt::runtime(_("Could not add a class, since the maximum class number of {} has already been reached.")), static_cast(HeroClass::NUM_MAX_CLASSES))); } RecordReader reader { record, filename }; PlayerData &playerData = PlayersData.emplace_back(); reader.readString("className", playerData.className); reader.readString("folderName", playerData.folderName); reader.readInt("portrait", playerData.portrait); reader.readString("inv", playerData.inv); } } namespace { void LoadClassDat() { const std::string_view filename = "txtdata\\classes\\classdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); PlayersData.clear(); LoadClassDatFromFile(dataFile, filename); PlayersData.shrink_to_fit(); } void LoadClassesAttributes() { ClassAttributesPerClass.clear(); ClassAttributesPerClass.reserve(PlayersData.size()); PlayersCombatData.clear(); PlayersCombatData.reserve(PlayersData.size()); PlayersStartingLoadoutData.clear(); PlayersStartingLoadoutData.reserve(PlayersData.size()); PlayersSpriteData.clear(); PlayersSpriteData.reserve(PlayersData.size()); PlayersAnimData.clear(); PlayersAnimData.reserve(PlayersData.size()); herosounds.clear(); herosounds.reserve(PlayersData.size()); for (const PlayerData &playerData : PlayersData) { LoadClassData(playerData.folderName, ClassAttributesPerClass.emplace_back(), PlayersCombatData.emplace_back()); LoadClassStartingLoadoutData(playerData.folderName, PlayersStartingLoadoutData.emplace_back()); LoadClassSpriteData(playerData.folderName, PlayersSpriteData.emplace_back()); LoadClassAnimData(playerData.folderName, PlayersAnimData.emplace_back()); LoadClassSounds(playerData.folderName, herosounds.emplace_back()); } } } // namespace const ClassAttributes &GetClassAttributes(HeroClass playerClass) { return ClassAttributesPerClass[static_cast(playerClass)]; } void LoadPlayerDataFiles() { ReloadExperienceData(); LoadClassDat(); LoadClassesAttributes(); } SfxID GetHeroSound(HeroClass clazz, HeroSpeech speech) { const size_t playerClassIndex = static_cast(clazz); assert(playerClassIndex < herosounds.size()); const auto findIt = herosounds[playerClassIndex].find(speech); if (findIt != herosounds[playerClassIndex].end()) { return findIt->second; } return SfxID::None; } uint32_t GetNextExperienceThresholdForLevel(unsigned level) { return ExperienceData.getThresholdForLevel(level); } uint8_t GetMaximumCharacterLevel() { return ExperienceData.getMaxLevel(); } size_t GetNumPlayerClasses() { return PlayersData.size(); } const PlayerData &GetPlayerDataForClass(HeroClass playerClass) { const size_t playerClassIndex = static_cast(playerClass); assert(playerClassIndex < PlayersData.size()); return PlayersData[playerClassIndex]; } const PlayerCombatData &GetPlayerCombatDataForClass(HeroClass pClass) { const size_t playerClassIndex = static_cast(pClass); assert(playerClassIndex < PlayersCombatData.size()); return PlayersCombatData[playerClassIndex]; } const PlayerStartingLoadoutData &GetPlayerStartingLoadoutForClass(HeroClass pClass) { const size_t playerClassIndex = static_cast(pClass); assert(playerClassIndex < PlayersStartingLoadoutData.size()); return PlayersStartingLoadoutData[playerClassIndex]; } const PlayerSpriteData &GetPlayerSpriteDataForClass(HeroClass pClass) { const size_t playerClassIndex = static_cast(pClass); assert(playerClassIndex < PlayersSpriteData.size()); return PlayersSpriteData[playerClassIndex]; } const PlayerAnimData &GetPlayerAnimDataForClass(HeroClass pClass) { const size_t playerClassIndex = static_cast(pClass); assert(playerClassIndex < PlayersAnimData.size()); return PlayersAnimData[playerClassIndex]; } } // namespace devilution ================================================ FILE: Source/tables/playerdat.hpp ================================================ /** * @file playerdat.hpp * * Interface of all player data. */ #pragma once #include #include #include "effects.h" #include "tables/itemdat.h" #include "tables/spelldat.h" namespace devilution { enum class HeroClass : uint8_t { Warrior, Rogue, Sorcerer, Monk, Bard, Barbarian, NUM_MAX_CLASSES = std::numeric_limits::max(), LAST = Barbarian, }; enum class PlayerClassFlag : uint8_t { // clang-format off None = 0, CriticalStrike = 1 << 0, DualWield = 1 << 1, IronSkin = 1 << 2, NaturalResistance = 1 << 3, TrapSense = 1 << 4, Last = TrapSense // clang-format on }; use_enum_as_flags(PlayerClassFlag); struct PlayerData { /* Class Name */ std::string className; /* Class Folder Name */ std::string folderName; /* Class Portrait Index */ uint8_t portrait; /* Class Inventory UI File */ std::string inv; }; struct ClassAttributes { /* Class Flags */ PlayerClassFlag classFlags; /* Class Starting Strength Stat */ uint8_t baseStr; /* Class Starting Magic Stat */ uint8_t baseMag; /* Class Starting Dexterity Stat */ uint8_t baseDex; /* Class Starting Vitality Stat */ uint8_t baseVit; /* Class Maximum Strength Stat */ uint8_t maxStr; /* Class Maximum Magic Stat */ uint8_t maxMag; /* Class Maximum Dexterity Stat */ uint8_t maxDex; /* Class Maximum Vitality Stat */ uint8_t maxVit; /* Class Life Adjustment */ int16_t adjLife; /* Class Mana Adjustment */ int16_t adjMana; /* Life gained on level up */ int16_t lvlLife; /* Mana gained on level up */ int16_t lvlMana; /* Life from base Vitality */ int16_t chrLife; /* Mana from base Magic */ int16_t chrMana; /* Life from item bonus Vitality */ int16_t itmLife; /* Mana from item bonus Magic */ int16_t itmMana; }; const ClassAttributes &GetClassAttributes(HeroClass playerClass); struct PlayerCombatData { /* Class starting chance to Block (used as a %) */ uint8_t baseToBlock; /* Class starting chance to hit when using melee attacks (used as a %) */ uint8_t baseMeleeToHit; /* Class starting chance to hit when using ranged weapons (used as a %) */ uint8_t baseRangedToHit; /* Class starting chance to hit when using spells (used as a %) */ uint8_t baseMagicToHit; }; /** * @brief Data used to set known skills and provide initial equipment when starting a new game * * Items will be created in order starting with item 1, 2, etc. If the item can be equipped it * will be placed in the first available slot, otherwise if it fits on the belt it will be * placed in the first free space, finally being placed in the first free inventory position. * * The active game mode at the time we're creating a new character controls the choice of item * type. ItemType.hellfire is used if we're in Hellfire mode, ItemType.diablo otherwise. */ struct PlayerStartingLoadoutData { /* Class Skill */ SpellID skill; /* Starting Spell (if any) */ SpellID spell; /* Initial level of the starting spell */ uint8_t spellLevel; std::array<_item_indexes, 5> items; /* Initial gold amount, up to a single stack (5000 gold) */ uint16_t gold; }; struct PlayerSpriteData { /* Class Directory Path */ std::string classPath; /* Class letter used in graphic files */ char classChar; /* Class TRN file */ std::string trn; /* Sprite width: Stand */ uint8_t stand; /* Sprite width: Walk */ uint8_t walk; /* Sprite width: Attack */ uint8_t attack; /* Sprite width: Attack (Bow) */ uint8_t bow; /* Sprite width: Hit Recovery */ uint8_t swHit; /* Sprite width: Block */ uint8_t block; /* Sprite width: Cast Lightning Spell */ uint8_t lightning; /* Sprite width: Cast Fire Spell */ uint8_t fire; /* Sprite width: Cast Magic Spell */ uint8_t magic; /* Sprite width: Death */ uint8_t death; }; struct PlayerAnimData { /* Unarmed frame count */ int8_t unarmedFrames; /* Unarmed action frame number */ int8_t unarmedActionFrame; /* UnarmedShield frame count */ int8_t unarmedShieldFrames; /* UnarmedShield action frame number */ int8_t unarmedShieldActionFrame; /* Sword frame count */ int8_t swordFrames; /* Sword action frame number */ int8_t swordActionFrame; /* SwordShield frame count */ int8_t swordShieldFrames; /* SwordShield action frame number */ int8_t swordShieldActionFrame; /* Bow frame count */ int8_t bowFrames; /* Bow action frame number */ int8_t bowActionFrame; /* Axe frame count */ int8_t axeFrames; /* Axe action frame number */ int8_t axeActionFrame; /* Mace frame count */ int8_t maceFrames; /* Mace action frame */ int8_t maceActionFrame; /* MaceShield frame count */ int8_t maceShieldFrames; /* MaceShield action frame number */ int8_t maceShieldActionFrame; /* Staff frame count */ int8_t staffFrames; /* Staff action frame number */ int8_t staffActionFrame; /* Nothing (Idle) frame count */ int8_t idleFrames; /* Walking frame count */ int8_t walkingFrames; /* Blocking frame count */ int8_t blockingFrames; /* Death frame count */ int8_t deathFrames; /* Spellcasting frame count */ int8_t castingFrames; /* Hit Recovery frame count */ int8_t recoveryFrames; /* Town Nothing (Idle) frame count */ int8_t townIdleFrames; /* Town Walking frame count */ int8_t townWalkingFrames; /* Spellcasting action frame number */ int8_t castingActionFrame; }; /** * @brief Attempts to load data values from external files. */ void LoadClassDatFromFile(DataFile &dataFile, const std::string_view filename); void LoadPlayerDataFiles(); SfxID GetHeroSound(HeroClass clazz, HeroSpeech speech); uint32_t GetNextExperienceThresholdForLevel(unsigned level); uint8_t GetMaximumCharacterLevel(); size_t GetNumPlayerClasses(); const PlayerData &GetPlayerDataForClass(HeroClass clazz); const PlayerCombatData &GetPlayerCombatDataForClass(HeroClass clazz); const PlayerStartingLoadoutData &GetPlayerStartingLoadoutForClass(HeroClass clazz); const PlayerSpriteData &GetPlayerSpriteDataForClass(HeroClass clazz); const PlayerAnimData &GetPlayerAnimDataForClass(HeroClass clazz); } // namespace devilution template <> struct magic_enum::customize::enum_range { static constexpr uint8_t min = static_cast(devilution::PlayerClassFlag::None); static constexpr uint8_t max = static_cast(devilution::PlayerClassFlag::Last); }; ================================================ FILE: Source/tables/spelldat.cpp ================================================ /** * @file spelldat.cpp * * Implementation of all spell data. */ #include "tables/spelldat.h" #include #include #include #include "data/file.hpp" #include "data/iterators.hpp" #include "data/record_reader.hpp" namespace devilution { namespace { void AddNullSpell() { SpellData &null = SpellsData.emplace_back(); null.sSFX = SfxID::None; null.bookCost10 = null.staffCost10 = null.sManaCost = 0; null.flags = SpellDataFlags::Fire; null.sBookLvl = null.sStaffLvl = 0; null.minInt = 0; null.sMissiles[0] = null.sMissiles[1] = MissileID::Null; null.sManaAdj = null.sMinMana = 0; null.sStaffMin = 40; null.sStaffMax = 80; } // A temporary solution for parsing soundID until we have a more general one. tl::expected ParseSpellSoundId(std::string_view value) { if (value == "CastFire") return SfxID::CastFire; if (value == "CastHealing") return SfxID::CastHealing; if (value == "CastLightning") return SfxID::CastLightning; if (value == "CastSkill") return SfxID::CastSkill; return tl::make_unexpected("Unknown enum value (only a few are supported for now)"); } tl::expected ParseSpellDataFlag(std::string_view value) { if (value == "Fire") return SpellDataFlags::Fire; if (value == "Lightning") return SpellDataFlags::Lightning; if (value == "Magic") return SpellDataFlags::Magic; if (value == "Targeted") return SpellDataFlags::Targeted; if (value == "AllowedInTown") return SpellDataFlags::AllowedInTown; return tl::make_unexpected("Unknown enum value"); } tl::expected ParseMissileId(std::string_view value) { if (value == "Arrow") return MissileID::Arrow; if (value == "Firebolt") return MissileID::Firebolt; if (value == "Guardian") return MissileID::Guardian; if (value == "Phasing") return MissileID::Phasing; if (value == "NovaBall") return MissileID::NovaBall; if (value == "FireWall") return MissileID::FireWall; if (value == "Fireball") return MissileID::Fireball; if (value == "LightningControl") return MissileID::LightningControl; if (value == "Lightning") return MissileID::Lightning; if (value == "MagmaBallExplosion") return MissileID::MagmaBallExplosion; if (value == "TownPortal") return MissileID::TownPortal; if (value == "FlashBottom") return MissileID::FlashBottom; if (value == "FlashTop") return MissileID::FlashTop; if (value == "ManaShield") return MissileID::ManaShield; if (value == "FlameWave") return MissileID::FlameWave; if (value == "ChainLightning") return MissileID::ChainLightning; if (value == "ChainBall") return MissileID::ChainBall; if (value == "BloodHit") return MissileID::BloodHit; if (value == "BoneHit") return MissileID::BoneHit; if (value == "MetalHit") return MissileID::MetalHit; if (value == "Rhino") return MissileID::Rhino; if (value == "MagmaBall") return MissileID::MagmaBall; if (value == "ThinLightningControl") return MissileID::ThinLightningControl; if (value == "ThinLightning") return MissileID::ThinLightning; if (value == "BloodStar") return MissileID::BloodStar; if (value == "BloodStarExplosion") return MissileID::BloodStarExplosion; if (value == "Teleport") return MissileID::Teleport; if (value == "FireArrow") return MissileID::FireArrow; if (value == "DoomSerpents") return MissileID::DoomSerpents; if (value == "FireOnly") return MissileID::FireOnly; if (value == "StoneCurse") return MissileID::StoneCurse; if (value == "BloodRitual") return MissileID::BloodRitual; if (value == "Invisibility") return MissileID::Invisibility; if (value == "Golem") return MissileID::Golem; if (value == "Etherealize") return MissileID::Etherealize; if (value == "Spurt") return MissileID::Spurt; if (value == "ApocalypseBoom") return MissileID::ApocalypseBoom; if (value == "Healing") return MissileID::Healing; if (value == "FireWallControl") return MissileID::FireWallControl; if (value == "Infravision") return MissileID::Infravision; if (value == "Identify") return MissileID::Identify; if (value == "FlameWaveControl") return MissileID::FlameWaveControl; if (value == "Nova") return MissileID::Nova; if (value == "Rage") return MissileID::Rage; if (value == "Apocalypse") return MissileID::Apocalypse; if (value == "ItemRepair") return MissileID::ItemRepair; if (value == "StaffRecharge") return MissileID::StaffRecharge; if (value == "TrapDisarm") return MissileID::TrapDisarm; if (value == "Inferno") return MissileID::Inferno; if (value == "InfernoControl") return MissileID::InfernoControl; if (value == "FireMan") return MissileID::FireMan; if (value == "Krull") return MissileID::Krull; if (value == "ChargedBolt") return MissileID::ChargedBolt; if (value == "HolyBolt") return MissileID::HolyBolt; if (value == "Resurrect") return MissileID::Resurrect; if (value == "Telekinesis") return MissileID::Telekinesis; if (value == "LightningArrow") return MissileID::LightningArrow; if (value == "Acid") return MissileID::Acid; if (value == "AcidSplat") return MissileID::AcidSplat; if (value == "AcidPuddle") return MissileID::AcidPuddle; if (value == "HealOther") return MissileID::HealOther; if (value == "Elemental") return MissileID::Elemental; if (value == "ResurrectBeam") return MissileID::ResurrectBeam; if (value == "BoneSpirit") return MissileID::BoneSpirit; if (value == "WeaponExplosion") return MissileID::WeaponExplosion; if (value == "RedPortal") return MissileID::RedPortal; if (value == "DiabloApocalypseBoom") return MissileID::DiabloApocalypseBoom; if (value == "DiabloApocalypse") return MissileID::DiabloApocalypse; if (value == "Mana") return MissileID::Mana; if (value == "Magi") return MissileID::Magi; if (value == "LightningWall") return MissileID::LightningWall; if (value == "LightningWallControl") return MissileID::LightningWallControl; if (value == "Immolation") return MissileID::Immolation; if (value == "SpectralArrow") return MissileID::SpectralArrow; if (value == "FireballBow") return MissileID::FireballBow; if (value == "LightningBow") return MissileID::LightningBow; if (value == "ChargedBoltBow") return MissileID::ChargedBoltBow; if (value == "HolyBoltBow") return MissileID::HolyBoltBow; if (value == "Warp") return MissileID::Warp; if (value == "Reflect") return MissileID::Reflect; if (value == "Berserk") return MissileID::Berserk; if (value == "RingOfFire") return MissileID::RingOfFire; if (value == "StealPotions") return MissileID::StealPotions; if (value == "StealMana") return MissileID::StealMana; if (value == "RingOfLightning") return MissileID::RingOfLightning; if (value == "Search") return MissileID::Search; if (value == "Aura") return MissileID::Aura; if (value == "Aura2") return MissileID::Aura2; if (value == "SpiralFireball") return MissileID::SpiralFireball; if (value == "RuneOfFire") return MissileID::RuneOfFire; if (value == "RuneOfLight") return MissileID::RuneOfLight; if (value == "RuneOfNova") return MissileID::RuneOfNova; if (value == "RuneOfImmolation") return MissileID::RuneOfImmolation; if (value == "RuneOfStone") return MissileID::RuneOfStone; if (value == "BigExplosion") return MissileID::BigExplosion; if (value == "HorkSpawn") return MissileID::HorkSpawn; if (value == "Jester") return MissileID::Jester; if (value == "OpenNest") return MissileID::OpenNest; if (value == "OrangeFlare") return MissileID::OrangeFlare; if (value == "BlueFlare") return MissileID::BlueFlare; if (value == "RedFlare") return MissileID::RedFlare; if (value == "YellowFlare") return MissileID::YellowFlare; if (value == "BlueFlare2") return MissileID::BlueFlare2; if (value == "YellowExplosion") return MissileID::YellowExplosion; if (value == "RedExplosion") return MissileID::RedExplosion; if (value == "BlueExplosion") return MissileID::BlueExplosion; if (value == "BlueExplosion2") return MissileID::BlueExplosion2; if (value == "OrangeExplosion") return MissileID::OrangeExplosion; return tl::make_unexpected("Unknown enum value"); } } // namespace /** Data related to each spell ID. */ std::vector SpellsData; tl::expected ParseSpellId(std::string_view value) { if (value == "Null") return SpellID::Null; if (value == "Firebolt") return SpellID::Firebolt; if (value == "Healing") return SpellID::Healing; if (value == "Lightning") return SpellID::Lightning; if (value == "Flash") return SpellID::Flash; if (value == "Identify") return SpellID::Identify; if (value == "FireWall") return SpellID::FireWall; if (value == "TownPortal") return SpellID::TownPortal; if (value == "StoneCurse") return SpellID::StoneCurse; if (value == "Infravision") return SpellID::Infravision; if (value == "Phasing") return SpellID::Phasing; if (value == "ManaShield") return SpellID::ManaShield; if (value == "Fireball") return SpellID::Fireball; if (value == "Guardian") return SpellID::Guardian; if (value == "ChainLightning") return SpellID::ChainLightning; if (value == "FlameWave") return SpellID::FlameWave; if (value == "DoomSerpents") return SpellID::DoomSerpents; if (value == "BloodRitual") return SpellID::BloodRitual; if (value == "Nova") return SpellID::Nova; if (value == "Invisibility") return SpellID::Invisibility; if (value == "Inferno") return SpellID::Inferno; if (value == "Golem") return SpellID::Golem; if (value == "Rage") return SpellID::Rage; if (value == "Teleport") return SpellID::Teleport; if (value == "Apocalypse") return SpellID::Apocalypse; if (value == "Etherealize") return SpellID::Etherealize; if (value == "ItemRepair") return SpellID::ItemRepair; if (value == "StaffRecharge") return SpellID::StaffRecharge; if (value == "TrapDisarm") return SpellID::TrapDisarm; if (value == "Elemental") return SpellID::Elemental; if (value == "ChargedBolt") return SpellID::ChargedBolt; if (value == "HolyBolt") return SpellID::HolyBolt; if (value == "Resurrect") return SpellID::Resurrect; if (value == "Telekinesis") return SpellID::Telekinesis; if (value == "HealOther") return SpellID::HealOther; if (value == "BloodStar") return SpellID::BloodStar; if (value == "BoneSpirit") return SpellID::BoneSpirit; if (value == "Mana") return SpellID::Mana; if (value == "Magi") return SpellID::Magi; if (value == "Jester") return SpellID::Jester; if (value == "LightningWall") return SpellID::LightningWall; if (value == "Immolation") return SpellID::Immolation; if (value == "Warp") return SpellID::Warp; if (value == "Reflect") return SpellID::Reflect; if (value == "Berserk") return SpellID::Berserk; if (value == "RingOfFire") return SpellID::RingOfFire; if (value == "Search") return SpellID::Search; if (value == "RuneOfFire") return SpellID::RuneOfFire; if (value == "RuneOfLight") return SpellID::RuneOfLight; if (value == "RuneOfNova") return SpellID::RuneOfNova; if (value == "RuneOfImmolation") return SpellID::RuneOfImmolation; if (value == "RuneOfStone") return SpellID::RuneOfStone; return tl::make_unexpected("Unknown enum value"); } void LoadSpellData() { SpellsData.clear(); const std::string_view filename = "txtdata\\spells\\spelldat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); SpellsData.reserve(dataFile.numRecords() + 1); AddNullSpell(); dataFile.skipHeaderOrDie(filename); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; SpellData &item = SpellsData.emplace_back(); reader.advance(); // skip id reader.readString("name", item.sNameText); reader.read("soundId", item.sSFX, ParseSpellSoundId); reader.readInt("bookCost10", item.bookCost10); reader.readInt("staffCost10", item.staffCost10); reader.readInt("manaCost", item.sManaCost); reader.readEnumList("flags", item.flags, ParseSpellDataFlag); reader.readInt("bookLevel", item.sBookLvl); reader.readInt("staffLevel", item.sStaffLvl); reader.readInt("minIntelligence", item.minInt); reader.readEnumArray("missiles", /*fillMissing=*/std::make_optional(MissileID::Null), item.sMissiles, ParseMissileId); reader.readInt("manaMultiplier", item.sManaAdj); reader.readInt("minMana", item.sMinMana); reader.readInt("staffMin", item.sStaffMin); reader.readInt("staffMax", item.sStaffMax); } SpellsData.shrink_to_fit(); } } // namespace devilution ================================================ FILE: Source/tables/spelldat.h ================================================ /** * @file spelldat.h * * Interface of all spell data. */ #pragma once #include #include #include #include #include #include #include "sound_effect_enums.h" #include "utils/enum_traits.h" namespace devilution { enum class SpellType : uint8_t { Skill, FIRST = Skill, Spell, Scroll, Charges, LAST = Charges, Invalid, }; enum class SpellID : int8_t { Null, FIRST = Null, Firebolt, Healing, Lightning, Flash, Identify, FireWall, TownPortal, StoneCurse, Infravision, Phasing, ManaShield, Fireball, Guardian, ChainLightning, FlameWave, DoomSerpents, BloodRitual, Nova, Invisibility, Inferno, Golem, Rage, Teleport, Apocalypse, Etherealize, ItemRepair, StaffRecharge, TrapDisarm, Elemental, ChargedBolt, HolyBolt, Resurrect, Telekinesis, HealOther, BloodStar, BoneSpirit, LastDiablo = BoneSpirit, Mana, Magi, Jester, LightningWall, Immolation, Warp, Reflect, Berserk, RingOfFire, Search, RuneOfFire, RuneOfLight, RuneOfNova, RuneOfImmolation, RuneOfStone, LAST = RuneOfStone, Invalid = -1, }; tl::expected ParseSpellId(std::string_view value); enum class MagicType : uint8_t { Fire, Lightning, Magic, }; enum class MissileID : int8_t { // clang-format off Arrow, Firebolt, Guardian, Phasing, NovaBall, FireWall, Fireball, LightningControl, Lightning, MagmaBallExplosion, TownPortal, FlashBottom, FlashTop, ManaShield, FlameWave, ChainLightning, ChainBall, // unused BloodHit, // unused BoneHit, // unused MetalHit, // unused Rhino, MagmaBall, ThinLightningControl, ThinLightning, BloodStar, BloodStarExplosion, Teleport, FireArrow, DoomSerpents, // unused FireOnly, // unused StoneCurse, BloodRitual, // unused Invisibility, // unused Golem, Etherealize, Spurt, // unused ApocalypseBoom, Healing, FireWallControl, Infravision, Identify, FlameWaveControl, Nova, Rage, // BloodBoil in Diablo Apocalypse, ItemRepair, StaffRecharge, TrapDisarm, Inferno, InfernoControl, FireMan, // unused Krull, // unused ChargedBolt, HolyBolt, Resurrect, Telekinesis, LightningArrow, Acid, AcidSplat, AcidPuddle, HealOther, Elemental, ResurrectBeam, BoneSpirit, WeaponExplosion, RedPortal, DiabloApocalypseBoom, DiabloApocalypse, LastDiablo = DiabloApocalypse, Mana, Magi, LightningWall, LightningWallControl, Immolation, SpectralArrow, FireballBow, LightningBow, ChargedBoltBow, HolyBoltBow, Warp, Reflect, Berserk, RingOfFire, StealPotions, StealMana, RingOfLightning, // unused Search, Aura, // unused Aura2, // unused SpiralFireball, // unused RuneOfFire, RuneOfLight, RuneOfNova, RuneOfImmolation, RuneOfStone, BigExplosion, HorkSpawn, Jester, OpenNest, OrangeFlare, BlueFlare, RedFlare, YellowFlare, BlueFlare2, YellowExplosion, RedExplosion, BlueExplosion, BlueExplosion2, OrangeExplosion, LAST = OrangeExplosion, Null = -1, // clang-format on }; enum class SpellDataFlags : uint8_t { // The lower 2 bytes are used to store MagicType. Fire = static_cast(MagicType::Fire), Lightning = static_cast(MagicType::Lightning), Magic = static_cast(MagicType::Magic), Targeted = 1U << 2, AllowedInTown = 1U << 3, }; use_enum_as_flags(SpellDataFlags); struct SpellData { std::string sNameText; SfxID sSFX; uint16_t bookCost10; uint8_t staffCost10; uint8_t sManaCost; SpellDataFlags flags; int8_t sBookLvl; int8_t sStaffLvl; uint8_t minInt; MissileID sMissiles[2]; uint8_t sManaAdj; uint8_t sMinMana; uint8_t sStaffMin; uint8_t sStaffMax; [[nodiscard]] MagicType type() const { return static_cast(static_cast::type>(flags) & 0b11U); } [[nodiscard]] uint32_t bookCost() const { return bookCost10 * 10; } [[nodiscard]] uint16_t staffCost() const { return staffCost10 * 10; } [[nodiscard]] bool isTargeted() const { return HasAnyOf(flags, SpellDataFlags::Targeted); } [[nodiscard]] bool isAllowedInTown() const { return HasAnyOf(flags, SpellDataFlags::AllowedInTown); } }; extern std::vector SpellsData; inline const SpellData &GetSpellData(SpellID spellId) { return SpellsData[static_cast::type>(spellId)]; } void LoadSpellData(); } // namespace devilution ================================================ FILE: Source/tables/textdat.cpp ================================================ /** * @file textdat.cpp * * Implementation of all dialog texts. */ #include "tables/textdat.h" #include #include #include #include "data/file.hpp" #include "data/record_reader.hpp" #include "effects.h" #include "utils/language.h" namespace devilution { /* todo: move text out of struct */ /** Contains the data related to each speech ID. */ std::vector Speeches; /** Contains the mapping between text ID strings and indices, used for parsing additional text data. */ ankerl::unordered_dense::map AdditionalTextIdStringsToIndices; tl::expected<_speech_id, std::string> ParseSpeechId(std::string_view value) { if (value.empty()) { return TEXT_NONE; } const std::optional<_speech_id> enumValueOpt = magic_enum::enum_cast<_speech_id>(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } const auto findIt = AdditionalTextIdStringsToIndices.find(std::string(value)); if (findIt != AdditionalTextIdStringsToIndices.end()) { return static_cast<_speech_id>(findIt->second); } return tl::make_unexpected("Invalid value."); } namespace { void LoadTextDatFromFile(DataFile &dataFile, std::string_view filename, bool grow) { dataFile.skipHeaderOrDie(filename); if (grow) { Speeches.reserve(Speeches.size() + dataFile.numRecords()); } for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; std::string txtstrid; reader.readString("txtstrid", txtstrid); if (txtstrid.empty()) { continue; } const std::optional<_speech_id> speechIdEnumValueOpt = magic_enum::enum_cast<_speech_id>(txtstrid); if (!speechIdEnumValueOpt.has_value()) { const size_t textEntryIndex = Speeches.size(); const auto [it, inserted] = AdditionalTextIdStringsToIndices.emplace(txtstrid, static_cast(textEntryIndex)); if (!inserted) { DisplayFatalErrorAndExit(_("Loading Text Data Failed"), fmt::format(fmt::runtime(_("A text data entry already exists for ID \"{}\".")), txtstrid)); } } // for hardcoded speeches, use their predetermined slot; for non-hardcoded ones, use the slots after that Speech &speech = speechIdEnumValueOpt.has_value() ? Speeches[speechIdEnumValueOpt.value()] : Speeches.emplace_back(); reader.readString("txtstr", speech.txtstr); { std::string processed; processed.reserve(speech.txtstr.size()); for (size_t i = 0; i < speech.txtstr.size();) { if (i + 1 < speech.txtstr.size() && speech.txtstr[i] == '\\' && speech.txtstr[i + 1] == 'n') { processed.push_back('\n'); i += 2; } else { processed.push_back(speech.txtstr[i]); ++i; } } speech.txtstr = std::move(processed); } reader.readBool("scrlltxt", speech.scrlltxt); reader.read("sfxnr", speech.sfxnr, ParseSfxId); } } } // namespace void LoadTextData() { const std::string_view filename = "txtdata\\text\\textdat.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); Speeches.clear(); AdditionalTextIdStringsToIndices.clear(); Speeches.resize(NUM_DEFAULT_TEXT_IDS); // ensure the hardcoded text entry slots are filled LoadTextDatFromFile(dataFile, filename, false); Speeches.shrink_to_fit(); } } // namespace devilution ================================================ FILE: Source/tables/textdat.h ================================================ /** * @file textdat.h * * Interface of all dialog texts. */ #pragma once #include #include #include #include #include "sound_effect_enums.h" namespace devilution { enum _speech_id : int16_t { TEXT_KING1, TEXT_KING2, TEXT_KING3, TEXT_KING4, TEXT_KING5, TEXT_KING6, TEXT_KING7, TEXT_KING8, TEXT_KING9, TEXT_KING10, TEXT_KING11, TEXT_BANNER1, TEXT_BANNER2, TEXT_BANNER3, TEXT_BANNER4, TEXT_BANNER5, TEXT_BANNER6, TEXT_BANNER7, TEXT_BANNER8, TEXT_BANNER9, TEXT_BANNER10, TEXT_BANNER11, TEXT_BANNER12, TEXT_VILE1, TEXT_VILE2, TEXT_VILE3, TEXT_VILE4, TEXT_VILE5, TEXT_VILE6, TEXT_VILE7, TEXT_VILE8, TEXT_VILE9, TEXT_VILE10, TEXT_VILE11, TEXT_VILE12, TEXT_VILE13, TEXT_VILE14, TEXT_POISON1, TEXT_POISON2, TEXT_POISON3, TEXT_POISON4, TEXT_POISON5, TEXT_POISON6, TEXT_POISON7, TEXT_POISON8, TEXT_POISON9, TEXT_POISON10, TEXT_BONE1, TEXT_BONE2, TEXT_BONE3, TEXT_BONE4, TEXT_BONE5, TEXT_BONE6, TEXT_BONE7, TEXT_BONE8, TEXT_BUTCH1, TEXT_BUTCH2, TEXT_BUTCH3, TEXT_BUTCH4, TEXT_BUTCH5, TEXT_BUTCH6, TEXT_BUTCH7, TEXT_BUTCH8, TEXT_BUTCH9, TEXT_BUTCH10, TEXT_BLIND1, TEXT_BLIND2, TEXT_BLIND3, TEXT_BLIND4, TEXT_BLIND5, TEXT_BLIND6, TEXT_BLIND7, TEXT_BLIND8, TEXT_VEIL1, TEXT_VEIL2, TEXT_VEIL3, TEXT_VEIL4, TEXT_VEIL5, TEXT_VEIL6, TEXT_VEIL7, TEXT_VEIL8, TEXT_VEIL9, TEXT_VEIL10, TEXT_VEIL11, TEXT_ANVIL1, TEXT_ANVIL2, TEXT_ANVIL3, TEXT_ANVIL4, TEXT_ANVIL5, TEXT_ANVIL6, TEXT_ANVIL7, TEXT_ANVIL8, TEXT_ANVIL9, TEXT_ANVIL10, TEXT_BLOOD1, TEXT_BLOOD2, TEXT_BLOOD3, TEXT_BLOOD4, TEXT_BLOOD5, TEXT_BLOOD6, TEXT_BLOOD7, TEXT_BLOOD8, TEXT_WARLRD1, TEXT_WARLRD2, TEXT_WARLRD3, TEXT_WARLRD4, TEXT_WARLRD5, TEXT_WARLRD6, TEXT_WARLRD7, TEXT_WARLRD8, TEXT_WARLRD9, TEXT_INFRA1, TEXT_INFRA2, TEXT_INFRA3, TEXT_INFRA4, TEXT_INFRA5, TEXT_INFRA6, TEXT_INFRA7, TEXT_INFRA8, TEXT_INFRA9, TEXT_INFRA10, TEXT_MUSH1, TEXT_MUSH2, TEXT_MUSH3, TEXT_MUSH4, TEXT_MUSH5, TEXT_MUSH6, TEXT_MUSH7, TEXT_MUSH8, TEXT_MUSH9, TEXT_MUSH10, TEXT_MUSH11, TEXT_MUSH12, TEXT_MUSH13, TEXT_DOOM1, TEXT_DOOM2, TEXT_DOOM3, TEXT_DOOM4, TEXT_DOOM5, TEXT_DOOM6, TEXT_DOOM7, TEXT_DOOM8, TEXT_DOOM9, TEXT_DOOM10, TEXT_GARBUD1, TEXT_GARBUD2, TEXT_GARBUD3, TEXT_GARBUD4, TEXT_ZHAR1, TEXT_ZHAR2, TEXT_STORY1, TEXT_STORY2, TEXT_STORY3, TEXT_STORY4, TEXT_STORY5, TEXT_STORY6, TEXT_STORY7, TEXT_STORY9, TEXT_STORY10, TEXT_STORY11, TEXT_OGDEN1, TEXT_OGDEN2, TEXT_OGDEN3, TEXT_OGDEN4, TEXT_OGDEN5, TEXT_OGDEN6, TEXT_OGDEN8, TEXT_OGDEN9, TEXT_OGDEN10, TEXT_PEPIN1, TEXT_PEPIN2, TEXT_PEPIN3, TEXT_PEPIN4, TEXT_PEPIN5, TEXT_PEPIN6, TEXT_PEPIN7, TEXT_PEPIN9, TEXT_PEPIN10, TEXT_PEPIN11, TEXT_GILLIAN1, TEXT_GILLIAN2, TEXT_GILLIAN3, TEXT_GILLIAN4, TEXT_GILLIAN5, TEXT_GILLIAN6, TEXT_GILLIAN7, TEXT_GILLIAN9, TEXT_GILLIAN10, TEXT_GRISWOLD1, TEXT_GRISWOLD2, TEXT_GRISWOLD3, TEXT_GRISWOLD4, TEXT_GRISWOLD5, TEXT_GRISWOLD6, TEXT_GRISWOLD7, TEXT_GRISWOLD8, TEXT_GRISWOLD9, TEXT_GRISWOLD10, TEXT_GRISWOLD12, TEXT_GRISWOLD13, TEXT_FARNHAM1, TEXT_FARNHAM2, TEXT_FARNHAM3, TEXT_FARNHAM4, TEXT_FARNHAM5, TEXT_FARNHAM6, TEXT_FARNHAM8, TEXT_FARNHAM9, TEXT_FARNHAM10, TEXT_FARNHAM11, TEXT_FARNHAM12, TEXT_FARNHAM13, TEXT_ADRIA1, TEXT_ADRIA2, TEXT_ADRIA3, TEXT_ADRIA4, TEXT_ADRIA5, TEXT_ADRIA6, TEXT_ADRIA7, TEXT_ADRIA8, TEXT_ADRIA9, TEXT_ADRIA10, TEXT_ADRIA12, TEXT_ADRIA13, TEXT_WIRT1, TEXT_WIRT2, TEXT_WIRT3, TEXT_WIRT4, TEXT_WIRT5, TEXT_WIRT6, TEXT_WIRT7, TEXT_WIRT8, TEXT_WIRT9, TEXT_WIRT11, TEXT_WIRT12, TEXT_BONER, TEXT_BLOODY, TEXT_BLINDING, TEXT_BLOODWAR, TEXT_MBONER, TEXT_MBLOODY, TEXT_MBLINDING, TEXT_MBLOODWAR, TEXT_RBONER, TEXT_RBLOODY, TEXT_RBLINDING, TEXT_RBLOODWAR, TEXT_COW1, TEXT_COW2, TEXT_BOOK11, TEXT_BOOK12, TEXT_BOOK13, TEXT_BOOK21, TEXT_BOOK22, TEXT_BOOK23, TEXT_BOOK31, TEXT_BOOK32, TEXT_BOOK33, TEXT_INTRO, TEXT_HBONER, TEXT_HBLOODY, TEXT_HBLINDING, TEXT_HBLOODWAR, TEXT_BBONER, TEXT_BBLOODY, TEXT_BBLINDING, TEXT_BBLOODWAR, TEXT_GRAVE1, TEXT_GRAVE2, TEXT_GRAVE3, TEXT_GRAVE4, TEXT_GRAVE5, TEXT_GRAVE6, TEXT_GRAVE7, TEXT_GRAVE8, TEXT_GRAVE9, TEXT_GRAVE10, TEXT_FARMER1, TEXT_FARMER2, TEXT_FARMER3, TEXT_FARMER4, TEXT_FARMER5, TEXT_GIRL1, TEXT_GIRL2, TEXT_GIRL3, TEXT_GIRL4, TEXT_DEFILER1, TEXT_DEFILER2, TEXT_DEFILER3, TEXT_DEFILER4, TEXT_DEFILER5, TEXT_NAKRUL1, TEXT_NAKRUL2, TEXT_NAKRUL3, TEXT_NAKRUL4, TEXT_NAKRUL5, TEXT_CORNSTN, TEXT_JERSEY1, TEXT_JERSEY2, TEXT_JERSEY3, TEXT_JERSEY4, TEXT_JERSEY5, TEXT_JERSEY6, TEXT_JERSEY7, TEXT_JERSEY8, TEXT_JERSEY9, TEXT_TRADER, TEXT_FARMER6, TEXT_FARMER7, TEXT_FARMER8, TEXT_FARMER9, TEXT_FARMER10, TEXT_JERSEY10, TEXT_JERSEY11, TEXT_JERSEY12, TEXT_JERSEY13, TEXT_SKLJRN, TEXT_BOOK4, TEXT_BOOK5, TEXT_BOOK6, TEXT_BOOK7, TEXT_BOOK8, TEXT_BOOK9, TEXT_BOOKA, TEXT_BOOKB, TEXT_BOOKC, TEXT_OBOOKA, TEXT_OBOOKB, TEXT_OBOOKC, TEXT_MBOOKA, TEXT_MBOOKB, TEXT_MBOOKC, TEXT_RBOOKA, TEXT_RBOOKB, TEXT_RBOOKC, TEXT_BBOOKA, TEXT_BBOOKB, TEXT_BBOOKC, TEXT_DEADGUY, TEXT_FARNHAM14, TEXT_FARNHAM15, TEXT_FARNHAM16, TEXT_FARNHAM17, TEXT_FARNHAM18, TEXT_FARNHAM19, TEXT_FARNHAM20, TEXT_FARNHAM21, TEXT_FARNHAM22, TEXT_GILLIAN11, TEXT_GILLIAN12, TEXT_GILLIAN13, TEXT_GILLIAN14, TEXT_GILLIAN15, TEXT_GILLIAN16, TEXT_GILLIAN17, TEXT_GILLIAN18, TEXT_GILLIAN19, TEXT_GILLIAN20, TEXT_GILLIAN21, TEXT_GILLIAN22, TEXT_GILLIAN23, TEXT_GILLIAN24, TEXT_GILLIAN25, TEXT_GILLIAN26, TEXT_PEPIN12, TEXT_PEPIN13, TEXT_PEPIN14, TEXT_PEPIN15, TEXT_PEPIN16, TEXT_PEPIN17, TEXT_PEPIN18, TEXT_PEPIN19, TEXT_PEPIN20, TEXT_PEPIN21, TEXT_PEPIN22, TEXT_PEPIN23, TEXT_PEPIN24, TEXT_PEPIN25, TEXT_PEPIN26, TEXT_PEPIN27, TEXT_PEPIN28, TEXT_PEPIN29, TEXT_PEPIN30, TEXT_GRISWOLD14, TEXT_GRISWOLD15, TEXT_GRISWOLD16, TEXT_GRISWOLD17, TEXT_GRISWOLD18, TEXT_GRISWOLD19, TEXT_GRISWOLD20, TEXT_GRISWOLD21, TEXT_GRISWOLD22, TEXT_GRISWOLD23, TEXT_GRISWOLD24, TEXT_GRISWOLD25, TEXT_GRISWOLD26, TEXT_GRISWOLD27, TEXT_GRISWOLD28, TEXT_GRISWOLD29, TEXT_GRISWOLD30, TEXT_GRISWOLD31, TEXT_GRISWOLD32, TEXT_GRISWOLD33, TEXT_GRISWOLD34, TEXT_GRISWOLD35, TEXT_GRISWOLD36, TEXT_GRISWOLD37, NUM_DEFAULT_TEXT_IDS, TEXT_NONE = -1, }; struct Speech { std::string txtstr; bool scrlltxt; SfxID sfxnr; }; extern std::vector Speeches; tl::expected<_speech_id, std::string> ParseSpeechId(std::string_view value); void LoadTextData(); } // namespace devilution template <> struct magic_enum::customize::enum_range { static constexpr int min = devilution::TEXT_NONE; static constexpr int max = devilution::NUM_DEFAULT_TEXT_IDS; }; ================================================ FILE: Source/tables/townerdat.cpp ================================================ /** * @file townerdat.cpp * * Implementation of towner data loading from TSV files. */ #include "tables/townerdat.hpp" #include #include #include #include #include #include #include #include "data/file.hpp" #include "data/record_reader.hpp" namespace devilution { std::vector TownersDataEntries; std::unordered_map<_talker_id, std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable; namespace { /** * @brief Generic enum parser using magic_enum. * @tparam EnumT The enum type to parse * @param value The string representation of the enum value * @return The parsed enum value, or an error message */ template tl::expected ParseEnum(std::string_view value) { const auto enumValueOpt = magic_enum::enum_cast(value); if (enumValueOpt.has_value()) { return enumValueOpt.value(); } return tl::make_unexpected("Unknown enum value"); } /** * @brief Parses a comma-separated list of values. * @tparam T The output type * @tparam Parser A callable that converts string_view to optional * @param value The comma-separated string * @param out Vector to store parsed values (cleared first) * @param parser Function to parse individual tokens */ template void ParseCommaSeparatedList(std::string_view value, std::vector &out, Parser parser) { out.clear(); if (value.empty()) return; size_t start = 0; while (start < value.size()) { size_t end = value.find(',', start); if (end == std::string_view::npos) end = value.size(); std::string_view token = value.substr(start, end - start); if (auto result = parser(token)) { out.push_back(*result); } start = end + 1; } } /** * @brief Parses a comma-separated list of speech IDs. */ void ParseGossipTexts(std::string_view value, std::vector<_speech_id> &out) { ParseCommaSeparatedList(value, out, [](std::string_view token) -> std::optional<_speech_id> { if (auto result = ParseSpeechId(token); result.has_value()) { return result.value(); } return std::nullopt; }); } /** * @brief Parses a comma-separated list of integers for animation frame order. */ void ParseAnimOrder(std::string_view value, std::vector &out) { ParseCommaSeparatedList(value, out, [](std::string_view token) -> std::optional { int val = 0; if (auto [ptr, ec] = std::from_chars(token.data(), token.data() + token.size(), val); ec == std::errc()) { return static_cast(val); } return std::nullopt; }); } void LoadTownersFromFile() { const std::string_view filename = "txtdata\\towners\\towners.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); dataFile.skipHeaderOrDie(filename); TownersDataEntries.clear(); TownersDataEntries.reserve(dataFile.numRecords()); for (DataFileRecord record : dataFile) { RecordReader reader { record, filename }; TownerDataEntry &entry = TownersDataEntries.emplace_back(); reader.read("type", entry.type, ParseEnum<_talker_id>); reader.readString("name", entry.name); reader.readInt("position_x", entry.position.x); reader.readInt("position_y", entry.position.y); reader.read("direction", entry.direction, ParseEnum); reader.readInt("animWidth", entry.animWidth); reader.readString("animPath", entry.animPath); reader.readOptionalInt("animFrames", entry.animFrames); reader.readOptionalInt("animDelay", entry.animDelay); std::string gossipStr; reader.readString("gossipTexts", gossipStr); ParseGossipTexts(gossipStr, entry.gossipTexts); std::string animOrderStr; reader.readString("animOrder", animOrderStr); ParseAnimOrder(animOrderStr, entry.animOrder); } TownersDataEntries.shrink_to_fit(); } void LoadQuestDialogFromFile() { const std::string_view filename = "txtdata\\towners\\quest_dialog.tsv"; DataFile dataFile = DataFile::loadOrDie(filename); // Initialize table (will be populated as we read rows) TownerQuestDialogTable.clear(); // Parse header to find which quest columns exist // Store the iterator to avoid temporary lifetime issues auto headerIt = dataFile.begin(); DataFileRecord headerRecord = *headerIt; std::unordered_map columnMap; unsigned columnIndex = 0; for (DataFileField field : headerRecord) { columnMap[std::string(field.value())] = columnIndex++; } // Reset header position and skip for data reading dataFile.resetHeader(); dataFile.skipHeaderOrDie(filename); // Find the towner_type column index if (!columnMap.contains("towner_type")) { return; // Invalid file format } unsigned townerTypeColIndex = columnMap["towner_type"]; // Build quest column index map std::unordered_map questColumnMap; for (quest_id quest : magic_enum::enum_values()) { if (quest == Q_INVALID || quest >= MAXQUESTS) continue; auto questName = std::string(magic_enum::enum_name(quest)); if (columnMap.contains(questName)) { questColumnMap[quest] = columnMap[questName]; } } // Read data rows for (DataFileRecord record : dataFile) { // Read all fields into a map keyed by column index for indexed access std::unordered_map fields; for (DataFileField field : record) { fields[field.column()] = field.value(); } // Read towner_type if (!fields.contains(townerTypeColIndex)) { continue; // Invalid row } auto townerTypeResult = ParseEnum<_talker_id>(fields[townerTypeColIndex]); if (!townerTypeResult.has_value()) { continue; // Invalid towner type } _talker_id townerType = townerTypeResult.value(); // Initialize row if it doesn't exist, then get reference auto [it, inserted] = TownerQuestDialogTable.try_emplace(townerType); if (inserted) { it->second.fill(TEXT_NONE); } auto &dialogRow = it->second; // Read quest columns that exist in this file for (const auto &[quest, colIndex] : questColumnMap) { if (!fields.contains(colIndex)) { continue; // Column missing in this row } auto speechResult = ParseSpeechId(fields[colIndex]); if (speechResult.has_value()) { dialogRow[quest] = speechResult.value(); } } } } } // namespace void LoadTownerData() { LoadTownersFromFile(); LoadQuestDialogFromFile(); } _speech_id GetTownerQuestDialog(_talker_id type, quest_id quest) { if (quest < 0 || quest >= MAXQUESTS) { return TEXT_NONE; } auto it = TownerQuestDialogTable.find(type); if (it == TownerQuestDialogTable.end()) { return TEXT_NONE; } return it->second[quest]; } void SetTownerQuestDialog(_talker_id type, quest_id quest, _speech_id speech) { if (quest < 0 || quest >= MAXQUESTS) { return; } // Initialize row if it doesn't exist auto [it, inserted] = TownerQuestDialogTable.try_emplace(type); if (inserted) { it->second.fill(TEXT_NONE); } it->second[quest] = speech; } } // namespace devilution ================================================ FILE: Source/tables/townerdat.hpp ================================================ /** * @file townerdat.hpp * * Interface for loading towner data from TSV files. */ #pragma once #include #include #include #include #include "engine/direction.hpp" #include "levels/gendung.h" #include "tables/objdat.h" #include "tables/textdat.h" #include "towners.h" #include "utils/attributes.h" namespace devilution { /** * @brief Data for a single towner entry loaded from TSV. */ struct TownerDataEntry { _talker_id type; // Parsed from TSV using magic_enum std::string name; Point position; Direction direction; uint16_t animWidth; std::string animPath; uint8_t animFrames; int16_t animDelay; std::vector<_speech_id> gossipTexts; std::vector animOrder; }; /** Contains the data for all towners loaded from TSV. */ extern DVL_API_FOR_TEST std::vector TownersDataEntries; /** Contains the quest dialog table loaded from TSV. Indexed by [towner_type][quest_id]. */ extern std::unordered_map<_talker_id, std::array<_speech_id, MAXQUESTS>> TownerQuestDialogTable; /** * @brief Loads towner data from TSV files. * * This function loads data from: * - txtdata/towners/towners.tsv - Main towner definitions * - txtdata/towners/quest_dialog.tsv - Quest dialog mappings */ void LoadTownerData(); /** * @brief Gets the quest dialog speech ID for a towner and quest combination. * @param type The towner type * @param quest The quest ID * @return The speech ID for the dialog, or TEXT_NONE if not available */ _speech_id GetTownerQuestDialog(_talker_id type, quest_id quest); /** * @brief Sets the quest dialog speech ID for a towner and quest combination. * @param type The towner type * @param quest The quest ID * @param speech The speech ID to set */ void SetTownerQuestDialog(_talker_id type, quest_id quest, _speech_id speech); } // namespace devilution ================================================ FILE: Source/tmsg.cpp ================================================ /** * @file tmsg.cpp * * Implementation of functionality transmitting chat messages. */ #include #include #ifdef USE_SDL3 #include #else #include #endif #include "diablo.h" #include "tmsg.h" namespace devilution { namespace { struct TMsg { uint32_t time; std::unique_ptr body; uint8_t len; TMsg(uint32_t time, const std::byte *data, uint8_t len) : time(time) , body(new std::byte[len]) , len(len) { memcpy(body.get(), data, len); } }; std::list TimedMsgList; } // namespace uint8_t tmsg_get(std::unique_ptr *msg) { if (TimedMsgList.empty()) return 0; TMsg &head = TimedMsgList.front(); if ((int)(head.time - SDL_GetTicks()) >= 0) return 0; const uint8_t len = head.len; *msg = std::move(head.body); TimedMsgList.pop_front(); return len; } void tmsg_add(const std::byte *msg, uint8_t len) { const uint32_t time = SDL_GetTicks() + gnTickDelay * 10; TimedMsgList.emplace_back(time, msg, len); } void tmsg_start() { assert(TimedMsgList.empty()); } void tmsg_cleanup() { TimedMsgList.clear(); } } // namespace devilution ================================================ FILE: Source/tmsg.h ================================================ /** * @file tmsg.h * * Interface of functionality transmitting chat messages. */ #pragma once #include #include #include namespace devilution { uint8_t tmsg_get(std::unique_ptr *msg); void tmsg_add(const std::byte *msg, uint8_t bLen); void tmsg_start(); void tmsg_cleanup(); } // namespace devilution ================================================ FILE: Source/towners.cpp ================================================ #include "towners.h" #include #include #include #include "cursor.h" #include "engine/clx_sprite.hpp" #include "engine/load_cel.hpp" #include "engine/load_file.hpp" #include "engine/random.hpp" #include "game_mode.hpp" #include "inv.h" #include "minitext.h" #include "stores.h" #include "tables/textdat.h" #include "tables/townerdat.hpp" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/str_case.hpp" namespace devilution { namespace { OptionalOwnedClxSpriteSheet CowSprites; int CowMsg; int CowClicks; /** Specifies the active sound effect ID for interacting with cows. */ SfxID CowPlaying = SfxID::None; /** Storage for animation order data loaded from TSV (needs stable addresses for span). */ std::vector> TownerAnimOrderStorage; /** * @brief Defines the behavior (init and talk functions) for each towner type. * * The actual data (position, animation, gossip) comes from TSV files. * This struct only holds the code that can't be data-driven. */ struct TownerData { _talker_id type; /** Custom initialization function, or nullptr to use the default InitTownerFromData. */ void (*init)(Towner &towner, const TownerDataEntry &entry); /** Function called when the player talks to this towner. */ void (*talk)(Player &player, Towner &towner); }; /** * @brief Lookup table from towner type to its behavior data. * * Populated during InitTowners() from the TownersData array. */ std::unordered_map<_talker_id, const TownerData *> TownerBehaviors; /** * @brief Default towner initialization using TSV data. * * Sets up animation, gossip texts, and other properties from the TSV entry. * Used for most towners; special cases (cows, cow farmer) have custom init functions. */ void InitTownerFromData(Towner &towner, const TownerDataEntry &entry); #ifdef _DEBUG /** * @brief Finds the towner data entry from TSV for a given type. */ const TownerDataEntry *FindTownerDataEntry(_talker_id type, Point position = {}) { for (const auto &entry : TownersDataEntries) { if (entry.type == type) { // For types with multiple instances (like cows), match by position if (position != Point {} && entry.position != position) continue; return &entry; } } return nullptr; } #endif void NewTownerAnim(Towner &towner, ClxSpriteList sprites, uint8_t numFrames, int delay) { towner.anim.emplace(sprites); towner._tAnimLen = numFrames; towner._tAnimFrame = 0; towner._tAnimCnt = 0; towner._tAnimDelay = delay; } void InitTownerInfo(Towner &towner, const TownerData &townerData, const TownerDataEntry &entry) { towner._ttype = townerData.type; auto nameIt = TownerLongNames.find(townerData.type); towner.name = nameIt != TownerLongNames.end() ? _(nameIt->second.c_str()) : std::string_view(entry.name); towner.position = entry.position; towner.talk = townerData.talk; if (townerData.init != nullptr) { townerData.init(towner, entry); } else { InitTownerFromData(towner, entry); } } void LoadTownerAnimations(Towner &towner, const char *path, int frames, int delay) { towner.ownedAnim = std::nullopt; towner.ownedAnim = LoadCel(path, towner._tAnimWidth); NewTownerAnim(towner, *towner.ownedAnim, frames, delay); } /** * @brief Default towner initialization using TSV data. */ void InitTownerFromData(Towner &towner, const TownerDataEntry &entry) { towner._tAnimWidth = entry.animWidth; // Store animation order and set the span if (!entry.animOrder.empty()) { TownerAnimOrderStorage.push_back(entry.animOrder); towner.animOrder = { TownerAnimOrderStorage.back() }; } else { towner.animOrder = {}; } if (!entry.animPath.empty()) { LoadTownerAnimations(towner, entry.animPath.c_str(), entry.animFrames, entry.animDelay); } // Set gossip from TSV data if (!entry.gossipTexts.empty()) { const auto index = std::max(GenerateRnd(static_cast(entry.gossipTexts.size())), 0); towner.gossip = entry.gossipTexts[index]; } } /** * @brief Special initialization for cows. * * Cows differ from other towners: * - They share a sprite sheet (CowSprites) instead of loading individual animations * - They occupy multiple tiles (4 tiles for collision purposes) * - Animation frame is randomized on spawn */ void InitCows(Towner &towner, const TownerDataEntry &entry) { // Cows use a shared sprite sheet and need special handling towner._tAnimWidth = entry.animWidth; towner.animOrder = {}; NewTownerAnim(towner, (*CowSprites)[static_cast(entry.direction)], 12, 3); towner._tAnimFrame = GenerateRnd(11); const Point position = entry.position; const int16_t cowId = dMonster[position.x][position.y]; // Cows are large sprites so take up multiple tiles. Vanilla Diablo/Hellfire allowed the player to stand adjacent // to a cow facing an ordinal direction (the two top-right cows) which leads to visual clipping. It's easier to // treat all cows as 4 tile sprites since this works for all facings. // The active tile is always the south tile as this is closest to the camera, we mark the other 3 tiles as occupied // using -id to match the convention used for moving/large monsters and players. Point offset = position + Direction::NorthWest; dMonster[offset.x][offset.y] = -cowId; offset = position + Direction::NorthEast; dMonster[offset.x][offset.y] = -cowId; offset = position + Direction::North; dMonster[offset.x][offset.y] = -cowId; } /** * @brief Special initialization for the cow farmer (Complete Nut). * * Uses different sprites depending on whether the Jersey quest is complete. */ void InitCowFarmer(Towner &towner, const TownerDataEntry &entry) { towner._tAnimWidth = entry.animWidth; towner.animOrder = {}; // CowFarmer has special logic for quest state const char *celPath = "towners\\farmer\\cfrmrn2"; if (Quests[Q_JERSEY]._qactive == QUEST_DONE) { celPath = "towners\\farmer\\mfrmrn2"; } LoadTownerAnimations(towner, celPath, 15, 3); } void TownDead(Towner &towner) { if (qtextflag) { if (Quests[Q_BUTCHER]._qvar1 == 1) towner._tAnimCnt = 0; // Freeze while speaking return; } if ((Quests[Q_BUTCHER]._qactive == QUEST_DONE || Quests[Q_BUTCHER]._qvar1 == 1) && towner._tAnimLen != 1) { towner._tAnimLen = 1; towner.name = _("Slain Townsman"); } } void TownerTalk(_speech_id message) { CowClicks = 0; CowMsg = 0; InitQTextMsg(message); } void TalkToBarOwner(Player &player, Towner &barOwner) { if (!player._pLvlVisited[0]) { InitQTextMsg(TEXT_INTRO); return; } auto &kingQuest = Quests[Q_SKELKING]; if (kingQuest._qactive != QUEST_NOTAVAIL) { if (player._pLvlVisited[2] || player._pLvlVisited[4]) { if (kingQuest._qvar2 == 0) { kingQuest._qvar2 = 1; kingQuest._qlog = true; if (kingQuest._qactive == QUEST_INIT) { kingQuest._qactive = QUEST_ACTIVE; kingQuest._qvar1 = 1; } InitQTextMsg(TEXT_KING2); NetSendCmdQuest(true, kingQuest); return; } if (kingQuest._qactive == QUEST_DONE && kingQuest._qvar2 == 1) { kingQuest._qvar2 = 2; kingQuest._qvar1 = 2; InitQTextMsg(TEXT_KING4); NetSendCmdQuest(true, kingQuest); return; } } } auto &bannerQuest = Quests[Q_LTBANNER]; if (bannerQuest._qactive != QUEST_NOTAVAIL) { if ((player._pLvlVisited[3] || player._pLvlVisited[4]) && bannerQuest._qactive != QUEST_DONE) { if (bannerQuest._qvar2 == 0) { bannerQuest._qvar2 = 1; if (bannerQuest._qactive == QUEST_INIT) { bannerQuest._qvar1 = 1; bannerQuest._qactive = QUEST_ACTIVE; } bannerQuest._qlog = true; NetSendCmdQuest(true, bannerQuest); InitQTextMsg(TEXT_BANNER2); return; } if (bannerQuest._qvar2 == 1 && RemoveInventoryItemById(player, IDI_BANNER)) { bannerQuest._qactive = QUEST_DONE; bannerQuest._qvar1 = 3; NetSendCmdQuest(true, bannerQuest); SpawnUnique(UITEM_HARCREST, barOwner.position + Direction::SouthWest, bannerQuest._qlevel); InitQTextMsg(TEXT_BANNER3); return; } } } TownerTalk(TEXT_OGDEN1); StartStore(TalkID::Tavern); } void TalkToDeadguy(Player &player, Towner & /*deadguy*/) { auto &quest = Quests[Q_BUTCHER]; if (quest._qactive == QUEST_DONE) return; if (quest._qvar1 == 1) { player.SaySpecific(HeroSpeech::YourDeathWillBeAvenged); return; } quest._qactive = QUEST_ACTIVE; quest._qlog = true; quest._qmsg = TEXT_BUTCH9; quest._qvar1 = 1; InitQTextMsg(TEXT_BUTCH9); NetSendCmdQuest(true, quest); } void TalkToBlackSmith(Player &player, Towner &blackSmith) { if (Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { if ((player._pLvlVisited[4] || player._pLvlVisited[5]) && Quests[Q_ROCK]._qactive != QUEST_DONE) { if (Quests[Q_ROCK]._qvar2 == 0) { Quests[Q_ROCK]._qvar2 = 1; Quests[Q_ROCK]._qlog = true; if (Quests[Q_ROCK]._qactive == QUEST_INIT) { Quests[Q_ROCK]._qactive = QUEST_ACTIVE; } NetSendCmdQuest(true, Quests[Q_ROCK]); InitQTextMsg(TEXT_INFRA5); return; } if (Quests[Q_ROCK]._qvar2 == 1 && RemoveInventoryItemById(player, IDI_ROCK)) { Quests[Q_ROCK]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_ROCK]); SpawnUnique(UITEM_INFRARING, blackSmith.position + Direction::SouthWest, Quests[Q_ROCK]._qlevel); InitQTextMsg(TEXT_INFRA7); return; } } } if (IsNoneOf(Quests[Q_ANVIL]._qactive, QUEST_NOTAVAIL, QUEST_DONE)) { if ((player._pLvlVisited[9] || player._pLvlVisited[10]) && Quests[Q_ANVIL]._qvar2 == 0) { Quests[Q_ANVIL]._qvar2 = 1; Quests[Q_ANVIL]._qlog = true; if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; } NetSendCmdQuest(true, Quests[Q_ANVIL]); InitQTextMsg(TEXT_ANVIL5); return; } if (Quests[Q_ANVIL]._qvar2 == 1 && RemoveInventoryItemById(player, IDI_ANVIL)) { Quests[Q_ANVIL]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_ANVIL]); SpawnUnique(UITEM_GRISWOLD, blackSmith.position + Direction::SouthWest, Quests[Q_ANVIL]._qlevel); InitQTextMsg(TEXT_ANVIL7); return; } } TownerTalk(TEXT_GRISWOLD1); StartStore(TalkID::Smith); } void TalkToWitch(Player &player, Towner & /*witch*/) { if (Quests[Q_MUSHROOM]._qactive != QUEST_NOTAVAIL) { if (Quests[Q_MUSHROOM]._qactive == QUEST_INIT && RemoveInventoryItemById(player, IDI_FUNGALTM)) { Quests[Q_MUSHROOM]._qactive = QUEST_ACTIVE; Quests[Q_MUSHROOM]._qlog = true; Quests[Q_MUSHROOM]._qvar1 = QS_TOMEGIVEN; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); InitQTextMsg(TEXT_MUSH8); return; } if (Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE) { if (Quests[Q_MUSHROOM]._qvar1 >= QS_TOMEGIVEN && Quests[Q_MUSHROOM]._qvar1 < QS_MUSHGIVEN) { if (RemoveInventoryItemById(player, IDI_MUSHROOM)) { Quests[Q_MUSHROOM]._qvar1 = QS_MUSHGIVEN; SetTownerQuestDialog(TOWN_HEALER, Q_MUSHROOM, TEXT_MUSH3); SetTownerQuestDialog(TOWN_WITCH, Q_MUSHROOM, TEXT_NONE); Quests[Q_MUSHROOM]._qmsg = TEXT_MUSH10; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); InitQTextMsg(TEXT_MUSH10); return; } if (Quests[Q_MUSHROOM]._qmsg != TEXT_MUSH9) { Quests[Q_MUSHROOM]._qmsg = TEXT_MUSH9; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); InitQTextMsg(TEXT_MUSH9); return; } } if (Quests[Q_MUSHROOM]._qvar1 >= QS_MUSHGIVEN) { if (HasInventoryItemWithId(player, IDI_BRAIN)) { Quests[Q_MUSHROOM]._qmsg = TEXT_MUSH11; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); InitQTextMsg(TEXT_MUSH11); return; } if (HasInventoryOrBeltItemWithId(player, IDI_SPECELIX)) { Quests[Q_MUSHROOM]._qactive = QUEST_DONE; NetSendCmdQuest(true, Quests[Q_MUSHROOM]); InitQTextMsg(TEXT_MUSH12); return; } } } } TownerTalk(TEXT_ADRIA1); StartStore(TalkID::Witch); } void TalkToBarmaid(Player &player, Towner & /*barmaid*/) { if (!player._pLvlVisited[21] && HasInventoryItemWithId(player, IDI_MAPOFDOOM) && Quests[Q_GRAVE]._qmsg != TEXT_GRAVE8) { Quests[Q_GRAVE]._qactive = QUEST_ACTIVE; Quests[Q_GRAVE]._qlog = true; Quests[Q_GRAVE]._qmsg = TEXT_GRAVE8; NetSendCmdQuest(true, Quests[Q_GRAVE]); InitQTextMsg(TEXT_GRAVE8); return; } TownerTalk(TEXT_GILLIAN1); StartStore(TalkID::Barmaid); } void TalkToDrunk(Player & /*player*/, Towner & /*drunk*/) { TownerTalk(TEXT_FARNHAM1); StartStore(TalkID::Drunk); } void TalkToHealer(Player &player, Towner &healer) { Quest &poisonWater = Quests[Q_PWATER]; if (poisonWater._qactive != QUEST_NOTAVAIL) { if ((poisonWater._qactive == QUEST_INIT && (player._pLvlVisited[1] || player._pLvlVisited[5])) || (poisonWater._qactive == QUEST_ACTIVE && !poisonWater._qlog)) { // Play the dialog and make the quest visible in the log if the player has not started the quest but has // visited the dungeon at least once, or if they've found the poison water cave before speaking to Pepin poisonWater._qactive = QUEST_ACTIVE; poisonWater._qlog = true; poisonWater._qmsg = TEXT_POISON3; InitQTextMsg(TEXT_POISON3); NetSendCmdQuest(true, poisonWater); return; } if (poisonWater._qactive == QUEST_DONE && poisonWater._qvar1 != 2) { poisonWater._qvar1 = 2; InitQTextMsg(TEXT_POISON5); SpawnUnique(UITEM_TRING, healer.position + Direction::SouthWest, poisonWater._qlevel); NetSendCmdQuest(true, poisonWater); return; } } Quest &blackMushroom = Quests[Q_MUSHROOM]; if (blackMushroom._qactive == QUEST_ACTIVE) { if (blackMushroom._qvar1 >= QS_MUSHGIVEN && blackMushroom._qvar1 < QS_BRAINGIVEN && RemoveInventoryItemById(player, IDI_BRAIN)) { SpawnQuestItem(IDI_SPECELIX, healer.position + Displacement { 0, 1 }, 0, SelectionRegion::None, true); InitQTextMsg(TEXT_MUSH4); blackMushroom._qvar1 = QS_BRAINGIVEN; SetTownerQuestDialog(TOWN_HEALER, Q_MUSHROOM, TEXT_NONE); NetSendCmdQuest(true, blackMushroom); return; } } TownerTalk(TEXT_PEPIN1); StartStore(TalkID::Healer); } void TalkToBoy(Player & /*player*/, Towner & /*boy*/) { TownerTalk(TEXT_WIRT1); StartStore(TalkID::Boy); } void TalkToStoryteller(Player &player, Towner & /*storyteller*/) { auto &betrayerQuest = Quests[Q_BETRAYER]; if (!UseMultiplayerQuests()) { if (betrayerQuest._qactive == QUEST_INIT && RemoveInventoryItemById(player, IDI_LAZSTAFF)) { InitQTextMsg(TEXT_VILE1); betrayerQuest._qlog = true; betrayerQuest._qactive = QUEST_ACTIVE; betrayerQuest._qvar1 = 2; NetSendCmdQuest(true, betrayerQuest); return; } } else { if (betrayerQuest._qactive == QUEST_ACTIVE && !betrayerQuest._qlog) { InitQTextMsg(TEXT_VILE1); betrayerQuest._qlog = true; NetSendCmdQuest(true, betrayerQuest); return; } } if (betrayerQuest._qactive == QUEST_DONE && betrayerQuest._qvar1 == 7) { betrayerQuest._qvar1 = 8; InitQTextMsg(TEXT_VILE3); auto &diabloQuest = Quests[Q_DIABLO]; diabloQuest._qlog = true; if (gbIsMultiplayer) { NetSendCmdQuest(true, betrayerQuest); NetSendCmdQuest(true, diabloQuest); } return; } TownerTalk(TEXT_STORY1); StartStore(TalkID::Storyteller); } void TalkToCow(Player &player, Towner &cow) { if (CowPlaying != SfxID::None && effect_is_playing(CowPlaying)) return; CowClicks++; CowPlaying = SfxID::Cow1; if (CowClicks == 4) { if (gbIsSpawn) CowClicks = 0; CowPlaying = SfxID::Cow2; } else if (CowClicks >= 8 && !gbIsSpawn) { CowClicks = 4; static const HeroSpeech SnSfx[3] = { HeroSpeech::YepThatsACowAlright, HeroSpeech::ImNotThirsty, HeroSpeech::ImNoMilkmaid, }; player.SaySpecific(SnSfx[CowMsg]); CowMsg++; if (CowMsg >= 3) CowMsg = 0; } PlaySfxLoc(CowPlaying, cow.position); } void TalkToFarmer(Player &player, Towner &farmer) { auto &quest = Quests[Q_FARMER]; switch (quest._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: if (HasInventoryItemWithId(player, IDI_RUNEBOMB)) { InitQTextMsg(TEXT_FARMER2); quest._qactive = QUEST_ACTIVE; quest._qvar1 = 1; quest._qmsg = TEXT_FARMER1; quest._qlog = true; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; } if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) { _speech_id qt = TEXT_FARMER8; if (player._pLvlVisited[2]) qt = TEXT_FARMER5; if (player._pLvlVisited[5]) qt = TEXT_FARMER7; if (player._pLvlVisited[7]) qt = TEXT_FARMER9; InitQTextMsg(qt); break; } InitQTextMsg(TEXT_FARMER1); quest._qactive = QUEST_ACTIVE; quest._qvar1 = 1; quest._qlog = true; quest._qmsg = TEXT_FARMER1; SpawnRuneBomb(farmer.position + Displacement { 1, 0 }, true); if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; case QUEST_ACTIVE: InitQTextMsg(HasInventoryItemWithId(player, IDI_RUNEBOMB) ? TEXT_FARMER2 : TEXT_FARMER3); break; case QUEST_DONE: InitQTextMsg(TEXT_FARMER4); SpawnRewardItem(IDI_AURIC, farmer.position + Displacement { 1, 0 }, true); quest._qactive = QUEST_HIVE_DONE; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; case QUEST_HIVE_DONE: break; default: InitQTextMsg(TEXT_FARMER4); break; } } void TalkToCowFarmer(Player &player, Towner &cowFarmer) { if (RemoveInventoryItemById(player, IDI_GREYSUIT)) { InitQTextMsg(TEXT_JERSEY7); return; } auto &quest = Quests[Q_JERSEY]; if (RemoveInventoryItemById(player, IDI_BROWNSUIT)) { SpawnUnique(UITEM_BOVINE, cowFarmer.position + Direction::SouthEast, quest._qlevel); InitQTextMsg(TEXT_JERSEY8); quest._qactive = QUEST_DONE; UpdateCowFarmerAnimAfterQuestComplete(); NetSendCmdQuest(true, quest); return; } if (HasInventoryItemWithId(player, IDI_RUNEBOMB)) { InitQTextMsg(TEXT_JERSEY5); quest._qactive = QUEST_ACTIVE; quest._qvar1 = 1; quest._qmsg = TEXT_JERSEY4; quest._qlog = true; NetSendCmdQuest(true, quest); return; } switch (quest._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: InitQTextMsg(TEXT_JERSEY1); quest._qactive = QUEST_HIVE_TEASE1; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; case QUEST_DONE: InitQTextMsg(TEXT_JERSEY1); break; case QUEST_HIVE_TEASE1: InitQTextMsg(TEXT_JERSEY2); quest._qactive = QUEST_HIVE_TEASE2; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; case QUEST_HIVE_TEASE2: InitQTextMsg(TEXT_JERSEY3); quest._qactive = QUEST_HIVE_ACTIVE; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; case QUEST_HIVE_ACTIVE: if (!player._pLvlVisited[9] && player.getCharacterLevel() < 15) { InitQTextMsg(PickRandomlyAmong({ TEXT_JERSEY9, TEXT_JERSEY10, TEXT_JERSEY11, TEXT_JERSEY12 })); break; } InitQTextMsg(TEXT_JERSEY4); quest._qactive = QUEST_ACTIVE; quest._qvar1 = 1; quest._qmsg = TEXT_JERSEY4; quest._qlog = true; SpawnRuneBomb(cowFarmer.position + Displacement { 1, 0 }, true); if (gbIsMultiplayer) NetSendCmdQuest(true, quest); break; default: InitQTextMsg(TEXT_JERSEY5); break; } } void TalkToGirl(Player &player, Towner &girl) { auto &quest = Quests[Q_GIRL]; if (quest._qactive != QUEST_DONE && RemoveInventoryItemById(player, IDI_THEODORE)) { InitQTextMsg(TEXT_GIRL4); CreateAmulet(girl.position, 13, false, false, true); quest._qactive = QUEST_DONE; UpdateGirlAnimAfterQuestComplete(); if (gbIsMultiplayer) NetSendCmdQuest(true, quest); return; } switch (quest._qactive) { case QUEST_NOTAVAIL: case QUEST_INIT: InitQTextMsg(TEXT_GIRL2); quest._qactive = QUEST_ACTIVE; quest._qvar1 = 1; quest._qlog = true; quest._qmsg = TEXT_GIRL2; if (gbIsMultiplayer) NetSendCmdQuest(true, quest); return; case QUEST_ACTIVE: InitQTextMsg(TEXT_GIRL3); return; default: return; } } const TownerData TownersData[] = { // clang-format off // type init (nullptr = default) talk { TOWN_SMITH, nullptr, TalkToBlackSmith }, { TOWN_HEALER, nullptr, TalkToHealer }, { TOWN_DEADGUY, nullptr, TalkToDeadguy }, { TOWN_TAVERN, nullptr, TalkToBarOwner }, { TOWN_STORY, nullptr, TalkToStoryteller }, { TOWN_DRUNK, nullptr, TalkToDrunk }, { TOWN_WITCH, nullptr, TalkToWitch }, { TOWN_BMAID, nullptr, TalkToBarmaid }, { TOWN_PEGBOY, nullptr, TalkToBoy }, { TOWN_COW, InitCows, TalkToCow }, { TOWN_COWFARM, InitCowFarmer, TalkToCowFarmer }, { TOWN_FARMER, nullptr, TalkToFarmer }, { TOWN_GIRL, nullptr, TalkToGirl }, // clang-format on }; } // namespace std::vector Towners; std::unordered_map<_talker_id, std::string> TownerLongNames; size_t GetNumTownerTypes() { return TownerLongNames.size(); } size_t GetNumTowners() { return Towners.size(); } bool IsTownerPresent(_talker_id npc) { switch (npc) { case TOWN_DEADGUY: return Quests[Q_BUTCHER]._qactive != QUEST_NOTAVAIL && Quests[Q_BUTCHER]._qactive != QUEST_DONE; case TOWN_FARMER: return gbIsHellfire && sgGameInitInfo.bCowQuest == 0 && Quests[Q_FARMER]._qactive != QUEST_HIVE_DONE; case TOWN_COWFARM: return gbIsHellfire && sgGameInitInfo.bCowQuest != 0; case TOWN_GIRL: return gbIsHellfire && sgGameInitInfo.bTheoQuest != 0 && MyPlayer->_pLvlVisited[17] && Quests[Q_GIRL]._qactive != QUEST_DONE; default: return true; } } Towner *GetTowner(_talker_id type) { for (Towner &towner : Towners) { if (towner._ttype == type) return &towner; } return nullptr; } void InitTowners() { assert(!CowSprites); // Load towner data from TSV files LoadTownerData(); TownerAnimOrderStorage.clear(); // Build lookup table for towner behaviors TownerBehaviors.clear(); for (const auto &behavior : TownersData) { TownerBehaviors[behavior.type] = &behavior; } // Build TownerLongNames from TSV data (first occurrence of each type wins) TownerLongNames.clear(); for (const auto &entry : TownersDataEntries) { TownerLongNames.try_emplace(entry.type, entry.name); } CowSprites.emplace(LoadCelSheet("towners\\animals\\cow", 128)); Towners.clear(); Towners.reserve(TownersDataEntries.size()); int16_t i = 0; for (const auto &entry : TownersDataEntries) { if (!IsTownerPresent(entry.type)) continue; auto behaviorIt = TownerBehaviors.find(entry.type); if (behaviorIt == TownerBehaviors.end() || behaviorIt->second == nullptr) continue; // It's necessary to assign this before invoking townerData.init() // specifically for the cows that need to read this value to fill adjacent tiles dMonster[entry.position.x][entry.position.y] = i + 1; Towners.emplace_back(); InitTownerInfo(Towners.back(), *behaviorIt->second, entry); i++; } } void FreeTownerGFX() { for (Towner &towner : Towners) { towner.ownedAnim = std::nullopt; } CowSprites = std::nullopt; } void ProcessTowners() { // BUGFIX: should be `i < numtowners`, was `i < NUM_TOWNERS` for (auto &towner : Towners) { if (towner._ttype == TOWN_DEADGUY) { TownDead(towner); } towner._tAnimCnt++; if (towner._tAnimCnt < towner._tAnimDelay) { continue; } towner._tAnimCnt = 0; if (!towner.animOrder.empty()) { towner._tAnimFrameCnt++; if (towner._tAnimFrameCnt > towner.animOrder.size() - 1) towner._tAnimFrameCnt = 0; towner._tAnimFrame = towner.animOrder[towner._tAnimFrameCnt]; continue; } towner._tAnimFrame++; if (towner._tAnimFrame >= towner._tAnimLen) towner._tAnimFrame = 0; } } void TalkToTowner(Player &player, int t) { auto &towner = Towners[t]; if (player.position.tile.WalkingDistance(towner.position) >= 2) return; if (!player.HoldItem.isEmpty()) { return; } towner.talk(player, towner); } void UpdateGirlAnimAfterQuestComplete() { Towner *girl = GetTowner(TOWN_GIRL); if (girl == nullptr || !girl->ownedAnim) return; // Girl is not spawned in town yet auto curFrame = girl->_tAnimFrame; LoadTownerAnimations(*girl, "towners\\girl\\girls1", 20, 6); girl->_tAnimFrame = std::min(curFrame, girl->_tAnimLen - 1); } void UpdateCowFarmerAnimAfterQuestComplete() { Towner *cowFarmer = GetTowner(TOWN_COWFARM); auto curFrame = cowFarmer->_tAnimFrame; LoadTownerAnimations(*cowFarmer, "towners\\farmer\\mfrmrn2", 15, 3); cowFarmer->_tAnimFrame = std::min(curFrame, cowFarmer->_tAnimLen - 1); } #ifdef _DEBUG bool DebugTalkToTowner(_talker_id type) { if (!IsTownerPresent(type)) return false; // cows have an init function that differs from the rest and isn't compatible with this code, skip them :( if (type == TOWN_COW) return false; const TownerData *behavior = TownerBehaviors[type]; if (behavior == nullptr) return false; const TownerDataEntry *entry = FindTownerDataEntry(type); if (entry == nullptr) return false; SetupTownStores(); Player &myPlayer = *MyPlayer; Towner fakeTowner; InitTownerInfo(fakeTowner, *behavior, *entry); fakeTowner.position = myPlayer.position.tile; behavior->talk(myPlayer, fakeTowner); return true; } #endif } // namespace devilution ================================================ FILE: Source/towners.h ================================================ /** * @file towners.h * * Interface of functionality for loading and spawning towners. */ #pragma once #include #include #include #include #include #include #include #include #include "engine/clx_sprite.hpp" #include "items.h" #include "levels/dun_tile.hpp" #include "player.h" #include "quests.h" #include "utils/attributes.h" namespace devilution { enum _talker_id : uint8_t { TOWN_SMITH, TOWN_HEALER, TOWN_DEADGUY, TOWN_TAVERN, TOWN_STORY, TOWN_DRUNK, TOWN_WITCH, TOWN_BMAID, TOWN_PEGBOY, TOWN_COW, TOWN_FARMER, TOWN_GIRL, TOWN_COWFARM, // Note: Enum values are parsed from TSV using magic_enum // The actual count is determined dynamically from TSV data }; // Runtime mappings built from TSV data extern DVL_API_FOR_TEST std::unordered_map<_talker_id, std::string> TownerLongNames; // Maps towner type enum to display name struct Towner { OptionalOwnedClxSpriteList ownedAnim; OptionalClxSpriteList anim; /** Specifies the animation frame sequence. */ std::span animOrder; void (*talk)(Player &player, Towner &towner); std::string_view name; /** Tile position of NPC */ Point position; /** Randomly chosen topic for discussion (picked when loading into town) */ _speech_id gossip; uint16_t _tAnimWidth; /** Tick length of each frame in the current animation */ int16_t _tAnimDelay; /** Increases by one each game tick, counting how close we are to _pAnimDelay */ int16_t _tAnimCnt; /** Number of frames in current animation */ uint8_t _tAnimLen; /** Current frame of animation. */ uint8_t _tAnimFrame; uint8_t _tAnimFrameCnt; _talker_id _ttype; [[nodiscard]] ClxSprite currentSprite() const { return (*anim)[_tAnimFrame]; } [[nodiscard]] Displacement getRenderingOffset() const { return { -CalculateSpriteTileCenterX(_tAnimWidth), 0 }; } }; extern std::vector Towners; /** * @brief Returns the number of unique towner types found in TSV data. * This is dynamically determined from the loaded towner data. */ size_t GetNumTownerTypes(); /** * @brief Returns the number of towner instances (actual spawned towners). * This is dynamically determined from the loaded towner data. */ size_t GetNumTowners(); bool IsTownerPresent(_talker_id npc); /** * @brief Maps from a _talker_id value to a pointer to the Towner object, if they have been initialised * @param type enum constant identifying the towner * @return Pointer to the Towner or nullptr if they are not available */ Towner *GetTowner(_talker_id type); void InitTowners(); void FreeTownerGFX(); void ProcessTowners(); void TalkToTowner(Player &player, int t); void UpdateGirlAnimAfterQuestComplete(); void UpdateCowFarmerAnimAfterQuestComplete(); #ifdef _DEBUG bool DebugTalkToTowner(_talker_id type); #endif } // namespace devilution ================================================ FILE: Source/track.cpp ================================================ /** * @file track.cpp * * Implementation of functionality tracking what the mouse cursor is pointing at. */ #include "track.h" #include "controls/control_mode.hpp" #include "controls/game_controls.h" #include "controls/plrctrls.h" #include "cursor.h" #include "engine/point.hpp" #include "player.h" #include "stores.h" namespace devilution { namespace { void RepeatWalk(Player &player) { if (!InDungeonBounds(cursPosition)) return; if (player._pmode != PM_STAND && (!player.isWalking() || player.AnimInfo.getFrameToUseForRendering() <= 6)) return; const Point target = player.GetTargetPosition(); if (cursPosition == target) return; NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, cursPosition); } } // namespace void InvalidateTargets() { if (pcursmonst != -1) { const Monster &monster = Monsters[pcursmonst]; if (monster.isInvalid || monster.hasNoLife() || (monster.flags & MFLAG_HIDDEN) != 0 || !IsTileLit(monster.position.tile)) { pcursmonst = -1; } } if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->canInteractWith()) ObjectUnderCursor = nullptr; if (PlayerUnderCursor != nullptr) { const Player &targetPlayer = *PlayerUnderCursor; if (targetPlayer._pmode == PM_DEATH || targetPlayer._pmode == PM_QUIT || !targetPlayer.plractive || !targetPlayer.isOnActiveLevel() || targetPlayer.hasNoLife() || !IsTileLit(targetPlayer.position.tile)) PlayerUnderCursor = nullptr; } } void RepeatPlayerAction() { if (pcurs != CURSOR_HAND) return; if (sgbMouseDown == CLICK_NONE && ControllerActionHeld == GameActionType_NONE) return; if (IsPlayerInStore()) return; if (LastPlayerAction == PlayerActionType::None) return; Player &myPlayer = *MyPlayer; if (myPlayer.destAction != ACTION_NONE) return; if (myPlayer._pInvincible) return; if (!myPlayer.CanChangeAction()) return; const bool rangedAttack = myPlayer.UsesRangedWeapon(); switch (LastPlayerAction) { case PlayerActionType::Attack: if (InDungeonBounds(cursPosition)) NetSendCmdLoc(MyPlayerId, true, rangedAttack ? CMD_RATTACKXY : CMD_SATTACKXY, cursPosition); break; case PlayerActionType::AttackMonsterTarget: if (pcursmonst != -1) NetSendCmdParam1(true, rangedAttack ? CMD_RATTACKID : CMD_ATTACKID, pcursmonst); break; case PlayerActionType::AttackPlayerTarget: if (PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) NetSendCmdParam1(true, rangedAttack ? CMD_RATTACKPID : CMD_ATTACKPID, PlayerUnderCursor->getId()); break; case PlayerActionType::Spell: if (ControlMode != ControlTypes::KeyboardAndMouse) { UpdateSpellTarget(MyPlayer->_pRSpell); } CheckPlrSpell(ControlMode == ControlTypes::KeyboardAndMouse); break; case PlayerActionType::SpellMonsterTarget: if (pcursmonst != -1) CheckPlrSpell(false); break; case PlayerActionType::SpellPlayerTarget: if (PlayerUnderCursor != nullptr && !myPlayer.friendlyMode) CheckPlrSpell(false); break; case PlayerActionType::OperateObject: if (ObjectUnderCursor != nullptr && !ObjectUnderCursor->isDoor()) { NetSendCmdLoc(MyPlayerId, true, CMD_OPOBJXY, cursPosition); } break; case PlayerActionType::Walk: RepeatWalk(myPlayer); break; case PlayerActionType::None: break; } } bool track_isscrolling() { return LastPlayerAction == PlayerActionType::Walk; } } // namespace devilution ================================================ FILE: Source/track.h ================================================ /** * @file track.h * * Interface of functionality tracking what the mouse cursor is pointing at. */ #pragma once namespace devilution { void InvalidateTargets(); void RepeatPlayerAction(); bool track_isscrolling(); } // namespace devilution ================================================ FILE: Source/translation_dummy.cpp ================================================ /** * @file translation_dummy.cpp * * Do not edit this file manually, it is automatically generated * and updated by the extract_translation_data.py script. */ #include "utils/language.h" namespace { const char *CLASS_WARRIOR_NAME = N_("Warrior"); const char *CLASS_ROGUE_NAME = N_("Rogue"); const char *CLASS_SORCERER_NAME = N_("Sorcerer"); const char *CLASS_MONK_NAME = N_("Monk"); const char *CLASS_BARD_NAME = N_("Bard"); const char *CLASS_BARBARIAN_NAME = N_("Barbarian"); const char *MT_NZOMBIE_NAME = P_("monster", "Zombie"); const char *MT_BZOMBIE_NAME = P_("monster", "Ghoul"); const char *MT_GZOMBIE_NAME = P_("monster", "Rotting Carcass"); const char *MT_YZOMBIE_NAME = P_("monster", "Black Death"); const char *MT_RFALLSP_NAME = P_("monster", "Fallen One"); const char *MT_DFALLSP_NAME = P_("monster", "Carver"); const char *MT_YFALLSP_NAME = P_("monster", "Devil Kin"); const char *MT_BFALLSP_NAME = P_("monster", "Dark One"); const char *MT_WSKELAX_NAME = P_("monster", "Skeleton"); const char *MT_TSKELAX_NAME = P_("monster", "Corpse Axe"); const char *MT_RSKELAX_NAME = P_("monster", "Burning Dead"); const char *MT_XSKELAX_NAME = P_("monster", "Horror"); const char *MT_NSCAV_NAME = P_("monster", "Scavenger"); const char *MT_BSCAV_NAME = P_("monster", "Plague Eater"); const char *MT_WSCAV_NAME = P_("monster", "Shadow Beast"); const char *MT_YSCAV_NAME = P_("monster", "Bone Gasher"); const char *MT_TSKELBW_NAME = P_("monster", "Corpse Bow"); const char *MT_WSKELSD_NAME = P_("monster", "Skeleton Captain"); const char *MT_TSKELSD_NAME = P_("monster", "Corpse Captain"); const char *MT_RSKELSD_NAME = P_("monster", "Burning Dead Captain"); const char *MT_XSKELSD_NAME = P_("monster", "Horror Captain"); const char *MT_INVILORD_NAME = P_("monster", "Invisible Lord"); const char *MT_SNEAK_NAME = P_("monster", "Hidden"); const char *MT_STALKER_NAME = P_("monster", "Stalker"); const char *MT_UNSEEN_NAME = P_("monster", "Unseen"); const char *MT_ILLWEAV_NAME = P_("monster", "Illusion Weaver"); const char *MT_LRDSAYTR_NAME = P_("monster", "Satyr Lord"); const char *MT_NGOATMC_NAME = P_("monster", "Flesh Clan"); const char *MT_BGOATMC_NAME = P_("monster", "Stone Clan"); const char *MT_RGOATMC_NAME = P_("monster", "Fire Clan"); const char *MT_GGOATMC_NAME = P_("monster", "Night Clan"); const char *MT_FIEND_NAME = P_("monster", "Fiend"); const char *MT_BLINK_NAME = P_("monster", "Blink"); const char *MT_GLOOM_NAME = P_("monster", "Gloom"); const char *MT_FAMILIAR_NAME = P_("monster", "Familiar"); const char *MT_NACID_NAME = P_("monster", "Acid Beast"); const char *MT_RACID_NAME = P_("monster", "Poison Spitter"); const char *MT_BACID_NAME = P_("monster", "Pit Beast"); const char *MT_XACID_NAME = P_("monster", "Lava Maw"); const char *MT_SKING_NAME = P_("monster", "Skeleton King"); const char *MT_CLEAVER_NAME = P_("monster", "The Butcher"); const char *MT_FAT_NAME = P_("monster", "Overlord"); const char *MT_MUDMAN_NAME = P_("monster", "Mud Man"); const char *MT_TOAD_NAME = P_("monster", "Toad Demon"); const char *MT_FLAYED_NAME = P_("monster", "Flayed One"); const char *MT_WYRM_NAME = P_("monster", "Wyrm"); const char *MT_CAVSLUG_NAME = P_("monster", "Cave Slug"); const char *MT_DVLWYRM_NAME = P_("monster", "Devil Wyrm"); const char *MT_DEVOUR_NAME = P_("monster", "Devourer"); const char *MT_NMAGMA_NAME = P_("monster", "Magma Demon"); const char *MT_YMAGMA_NAME = P_("monster", "Blood Stone"); const char *MT_BMAGMA_NAME = P_("monster", "Hell Stone"); const char *MT_WMAGMA_NAME = P_("monster", "Lava Lord"); const char *MT_HORNED_NAME = P_("monster", "Horned Demon"); const char *MT_MUDRUN_NAME = P_("monster", "Mud Runner"); const char *MT_FROSTC_NAME = P_("monster", "Frost Charger"); const char *MT_OBLORD_NAME = P_("monster", "Obsidian Lord"); const char *MT_BONEDMN_NAME = P_("monster", "oldboned"); const char *MT_REDDTH_NAME = P_("monster", "Red Death"); const char *MT_LTCHDMN_NAME = P_("monster", "Litch Demon"); const char *MT_UDEDBLRG_NAME = P_("monster", "Undead Balrog"); const char *MT_INCIN_NAME = P_("monster", "Incinerator"); const char *MT_FLAMLRD_NAME = P_("monster", "Flame Lord"); const char *MT_DOOMFIRE_NAME = P_("monster", "Doom Fire"); const char *MT_HELLBURN_NAME = P_("monster", "Hell Burner"); const char *MT_STORM_NAME = P_("monster", "Red Storm"); const char *MT_RSTORM_NAME = P_("monster", "Storm Rider"); const char *MT_STORML_NAME = P_("monster", "Storm Lord"); const char *MT_MAEL_NAME = P_("monster", "Maelstrom"); const char *MT_BIGFALL_NAME = P_("monster", "Devil Kin Brute"); const char *MT_WINGED_NAME = P_("monster", "Winged-Demon"); const char *MT_GARGOYLE_NAME = P_("monster", "Gargoyle"); const char *MT_BLOODCLW_NAME = P_("monster", "Blood Claw"); const char *MT_DEATHW_NAME = P_("monster", "Death Wing"); const char *MT_MEGA_NAME = P_("monster", "Slayer"); const char *MT_GUARD_NAME = P_("monster", "Guardian"); const char *MT_VTEXLRD_NAME = P_("monster", "Vortex Lord"); const char *MT_BALROG_NAME = P_("monster", "Balrog"); const char *MT_NSNAKE_NAME = P_("monster", "Cave Viper"); const char *MT_RSNAKE_NAME = P_("monster", "Fire Drake"); const char *MT_BSNAKE_NAME = P_("monster", "Gold Viper"); const char *MT_GSNAKE_NAME = P_("monster", "Azure Drake"); const char *MT_NBLACK_NAME = P_("monster", "Black Knight"); const char *MT_RTBLACK_NAME = P_("monster", "Doom Guard"); const char *MT_BTBLACK_NAME = P_("monster", "Steel Lord"); const char *MT_RBLACK_NAME = P_("monster", "Blood Knight"); const char *MT_UNRAV_NAME = P_("monster", "The Shredded"); const char *MT_HOLOWONE_NAME = P_("monster", "Hollow One"); const char *MT_PAINMSTR_NAME = P_("monster", "Pain Master"); const char *MT_REALWEAV_NAME = P_("monster", "Reality Weaver"); const char *MT_SUCCUBUS_NAME = P_("monster", "Succubus"); const char *MT_SNOWWICH_NAME = P_("monster", "Snow Witch"); const char *MT_HLSPWN_NAME = P_("monster", "Hell Spawn"); const char *MT_SOLBRNR_NAME = P_("monster", "Soul Burner"); const char *MT_COUNSLR_NAME = P_("monster", "Counselor"); const char *MT_MAGISTR_NAME = P_("monster", "Magistrate"); const char *MT_CABALIST_NAME = P_("monster", "Cabalist"); const char *MT_ADVOCATE_NAME = P_("monster", "Advocate"); const char *MT_GOLEM_NAME = P_("monster", "Golem"); const char *MT_DIABLO_NAME = P_("monster", "The Dark Lord"); const char *MT_DARKMAGE_NAME = P_("monster", "The Arch-Litch Malignus"); const char *GHARBAD_THE_WEAK_NAME = P_("monster", "Gharbad the Weak"); const char *ZHAR_THE_MAD_NAME = P_("monster", "Zhar the Mad"); const char *SNOTSPILL_NAME = P_("monster", "Snotspill"); const char *ARCH_BISHOP_LAZARUS_NAME = P_("monster", "Arch-Bishop Lazarus"); const char *RED_VEX_NAME = P_("monster", "Red Vex"); const char *BLACK_JADE_NAME = P_("monster", "Black Jade"); const char *LACHDANAN_NAME = P_("monster", "Lachdanan"); const char *WARLORD_OF_BLOOD_NAME = P_("monster", "Warlord of Blood"); const char *HORK_DEMON_NAME = P_("monster", "Hork Demon"); const char *THE_DEFILER_NAME = P_("monster", "The Defiler"); const char *NA_KRUL_NAME = P_("monster", "Na-Krul"); const char *BONEHEAD_KEENAXE_NAME = P_("monster", "Bonehead Keenaxe"); const char *BLADESKIN_THE_SLASHER_NAME = P_("monster", "Bladeskin the Slasher"); const char *SOULPUS_NAME = P_("monster", "Soulpus"); const char *PUKERAT_THE_UNCLEAN_NAME = P_("monster", "Pukerat the Unclean"); const char *BONERIPPER_NAME = P_("monster", "Boneripper"); const char *ROTFEAST_THE_HUNGRY_NAME = P_("monster", "Rotfeast the Hungry"); const char *GUTSHANK_THE_QUICK_NAME = P_("monster", "Gutshank the Quick"); const char *BROKENHEAD_BANGSHIELD_NAME = P_("monster", "Brokenhead Bangshield"); const char *BONGO_NAME = P_("monster", "Bongo"); const char *ROTCARNAGE_NAME = P_("monster", "Rotcarnage"); const char *SHADOWBITE_NAME = P_("monster", "Shadowbite"); const char *DEADEYE_NAME = P_("monster", "Deadeye"); const char *MADEYE_THE_DEAD_NAME = P_("monster", "Madeye the Dead"); const char *EL_CHUPACABRAS_NAME = P_("monster", "El Chupacabras"); const char *SKULLFIRE_NAME = P_("monster", "Skullfire"); const char *WARPSKULL_NAME = P_("monster", "Warpskull"); const char *GORETONGUE_NAME = P_("monster", "Goretongue"); const char *PULSECRAWLER_NAME = P_("monster", "Pulsecrawler"); const char *MOONBENDER_NAME = P_("monster", "Moonbender"); const char *WRATHRAVEN_NAME = P_("monster", "Wrathraven"); const char *SPINEEATER_NAME = P_("monster", "Spineeater"); const char *BLACKASH_THE_BURNING_NAME = P_("monster", "Blackash the Burning"); const char *SHADOWCROW_NAME = P_("monster", "Shadowcrow"); const char *BLIGHTSTONE_THE_WEAK_NAME = P_("monster", "Blightstone the Weak"); const char *BILEFROTH_THE_PIT_MASTER_NAME = P_("monster", "Bilefroth the Pit Master"); const char *BLOODSKIN_DARKBOW_NAME = P_("monster", "Bloodskin Darkbow"); const char *FOULWING_NAME = P_("monster", "Foulwing"); const char *SHADOWDRINKER_NAME = P_("monster", "Shadowdrinker"); const char *HAZESHIFTER_NAME = P_("monster", "Hazeshifter"); const char *DEATHSPIT_NAME = P_("monster", "Deathspit"); const char *BLOODGUTTER_NAME = P_("monster", "Bloodgutter"); const char *DEATHSHADE_FLESHMAUL_NAME = P_("monster", "Deathshade Fleshmaul"); const char *WARMAGGOT_THE_MAD_NAME = P_("monster", "Warmaggot the Mad"); const char *GLASSKULL_THE_JAGGED_NAME = P_("monster", "Glasskull the Jagged"); const char *BLIGHTFIRE_NAME = P_("monster", "Blightfire"); const char *NIGHTWING_THE_COLD_NAME = P_("monster", "Nightwing the Cold"); const char *GORESTONE_NAME = P_("monster", "Gorestone"); const char *BRONZEFIST_FIRESTONE_NAME = P_("monster", "Bronzefist Firestone"); const char *WRATHFIRE_THE_DOOMED_NAME = P_("monster", "Wrathfire the Doomed"); const char *FIREWOUND_THE_GRIM_NAME = P_("monster", "Firewound the Grim"); const char *BARON_SLUDGE_NAME = P_("monster", "Baron Sludge"); const char *BLIGHTHORN_STEELMACE_NAME = P_("monster", "Blighthorn Steelmace"); const char *CHAOSHOWLER_NAME = P_("monster", "Chaoshowler"); const char *DOOMGRIN_THE_ROTTING_NAME = P_("monster", "Doomgrin the Rotting"); const char *MADBURNER_NAME = P_("monster", "Madburner"); const char *BONESAW_THE_LITCH_NAME = P_("monster", "Bonesaw the Litch"); const char *BREAKSPINE_NAME = P_("monster", "Breakspine"); const char *DEVILSKULL_SHARPBONE_NAME = P_("monster", "Devilskull Sharpbone"); const char *BROKENSTORM_NAME = P_("monster", "Brokenstorm"); const char *STORMBANE_NAME = P_("monster", "Stormbane"); const char *OOZEDROOL_NAME = P_("monster", "Oozedrool"); const char *GOLDBLIGHT_OF_THE_FLAME_NAME = P_("monster", "Goldblight of the Flame"); const char *BLACKSTORM_NAME = P_("monster", "Blackstorm"); const char *PLAGUEWRATH_NAME = P_("monster", "Plaguewrath"); const char *THE_FLAYER_NAME = P_("monster", "The Flayer"); const char *BLUEHORN_NAME = P_("monster", "Bluehorn"); const char *WARPFIRE_HELLSPAWN_NAME = P_("monster", "Warpfire Hellspawn"); const char *FANGSPEIR_NAME = P_("monster", "Fangspeir"); const char *FESTERSKULL_NAME = P_("monster", "Festerskull"); const char *LIONSKULL_THE_BENT_NAME = P_("monster", "Lionskull the Bent"); const char *BLACKTONGUE_NAME = P_("monster", "Blacktongue"); const char *VILETOUCH_NAME = P_("monster", "Viletouch"); const char *VIPERFLAME_NAME = P_("monster", "Viperflame"); const char *FANGSKIN_NAME = P_("monster", "Fangskin"); const char *WITCHFIRE_THE_UNHOLY_NAME = P_("monster", "Witchfire the Unholy"); const char *BLACKSKULL_NAME = P_("monster", "Blackskull"); const char *SOULSLASH_NAME = P_("monster", "Soulslash"); const char *WINDSPAWN_NAME = P_("monster", "Windspawn"); const char *LORD_OF_THE_PIT_NAME = P_("monster", "Lord of the Pit"); const char *RUSTWEAVER_NAME = P_("monster", "Rustweaver"); const char *HOWLINGIRE_THE_SHADE_NAME = P_("monster", "Howlingire the Shade"); const char *DOOMCLOUD_NAME = P_("monster", "Doomcloud"); const char *BLOODMOON_SOULFIRE_NAME = P_("monster", "Bloodmoon Soulfire"); const char *WITCHMOON_NAME = P_("monster", "Witchmoon"); const char *GOREFEAST_NAME = P_("monster", "Gorefeast"); const char *GRAYWAR_THE_SLAYER_NAME = P_("monster", "Graywar the Slayer"); const char *DREADJUDGE_NAME = P_("monster", "Dreadjudge"); const char *STAREYE_THE_WITCH_NAME = P_("monster", "Stareye the Witch"); const char *STEELSKULL_THE_HUNTER_NAME = P_("monster", "Steelskull the Hunter"); const char *SIR_GORASH_NAME = P_("monster", "Sir Gorash"); const char *THE_VIZIER_NAME = P_("monster", "The Vizier"); const char *ZAMPHIR_NAME = P_("monster", "Zamphir"); const char *BLOODLUST_NAME = P_("monster", "Bloodlust"); const char *WEBWIDOW_NAME = P_("monster", "Webwidow"); const char *FLESHDANCER_NAME = P_("monster", "Fleshdancer"); const char *GRIMSPIKE_NAME = P_("monster", "Grimspike"); const char *DOOMLOCK_NAME = P_("monster", "Doomlock"); const char *IDI_GOLD_NAME = N_("Gold"); const char *IDI_WARRIOR_NAME = N_("Short Sword"); const char *IDI_WARRSHLD_NAME = N_("Buckler"); const char *IDI_WARRCLUB_NAME = N_("Club"); const char *IDI_ROGUE_NAME = N_("Short Bow"); const char *IDI_SORCERER_NAME = N_("Short Staff of Mana"); const char *IDI_CLEAVER_NAME = N_("Cleaver"); const char *IDI_SKCROWN_NAME = N_("The Undead Crown"); const char *IDI_INFRARING_NAME = N_("Empyrean Band"); const char *IDI_ROCK_NAME = N_("Magic Rock"); const char *IDI_OPTAMULET_NAME = N_("Optic Amulet"); const char *IDI_TRING_NAME = N_("Ring of Truth"); const char *IDI_BANNER_NAME = N_("Tavern Sign"); const char *IDI_HARCREST_NAME = N_("Harlequin Crest"); const char *IDI_STEELVEIL_NAME = N_("Veil of Steel"); const char *IDI_GLDNELIX_NAME = N_("Golden Elixir"); const char *IDI_ANVIL_NAME = N_("Anvil of Fury"); const char *IDI_MUSHROOM_NAME = N_("Black Mushroom"); const char *IDI_BRAIN_NAME = N_("Brain"); const char *IDI_FUNGALTM_NAME = N_("Fungal Tome"); const char *IDI_SPECELIX_NAME = N_("Spectral Elixir"); const char *IDI_BLDSTONE_NAME = N_("Blood Stone"); const char *IDI_MAPOFDOOM_NAME = N_("Cathedral Map"); const char *IDI_EAR_NAME = N_("Ear"); const char *IDI_HEAL_NAME = N_("Potion of Healing"); const char *IDI_MANA_NAME = N_("Potion of Mana"); const char *IDI_IDENTIFY_NAME = N_("Scroll of Identify"); const char *IDI_PORTAL_NAME = N_("Scroll of Town Portal"); const char *IDI_ARMOFVAL_NAME = N_("Arkaine's Valor"); const char *IDI_FULLHEAL_NAME = N_("Potion of Full Healing"); const char *IDI_FULLMANA_NAME = N_("Potion of Full Mana"); const char *IDI_GRISWOLD_NAME = N_("Griswold's Edge"); const char *IDI_LGTFORGE_NAME = N_("Bovine Plate"); const char *IDI_LAZSTAFF_NAME = N_("Staff of Lazarus"); const char *IDI_RESURRECT_NAME = N_("Scroll of Resurrect"); const char *IDI_OIL_NAME = N_("Blacksmith Oil"); const char *IDI_SHORTSTAFF_NAME = N_("Short Staff"); const char *IDI_BARDSWORD_NAME = N_("Sword"); const char *IDI_BARDDAGGER_NAME = N_("Dagger"); const char *IDI_RUNEBOMB_NAME = N_("Rune Bomb"); const char *IDI_THEODORE_NAME = N_("Theodore"); const char *IDI_AURIC_NAME = N_("Auric Amulet"); const char *IDI_NOTE1_NAME = N_("Torn Note 1"); const char *IDI_NOTE2_NAME = N_("Torn Note 2"); const char *IDI_NOTE3_NAME = N_("Torn Note 3"); const char *IDI_FULLNOTE_NAME = N_("Reconstructed Note"); const char *IDI_BROWNSUIT_NAME = N_("Brown Suit"); const char *IDI_GREYSUIT_NAME = N_("Grey Suit"); const char *ITEM_48_NAME = N_("Cap"); const char *ITEM_49_NAME = N_("Skull Cap"); const char *ITEM_50_NAME = N_("Helm"); const char *ITEM_51_NAME = N_("Full Helm"); const char *ITEM_52_NAME = N_("Crown"); const char *ITEM_53_NAME = N_("Great Helm"); const char *ITEM_54_NAME = N_("Cape"); const char *ITEM_55_NAME = N_("Rags"); const char *ITEM_56_NAME = N_("Cloak"); const char *ITEM_57_NAME = N_("Robe"); const char *ITEM_58_NAME = N_("Quilted Armor"); const char *ITEM_58_SHORT_NAME = N_("Armor"); const char *ITEM_59_NAME = N_("Leather Armor"); const char *ITEM_60_NAME = N_("Hard Leather Armor"); const char *ITEM_61_NAME = N_("Studded Leather Armor"); const char *ITEM_62_NAME = N_("Ring Mail"); const char *ITEM_62_SHORT_NAME = N_("Mail"); const char *ITEM_63_NAME = N_("Chain Mail"); const char *ITEM_64_NAME = N_("Scale Mail"); const char *ITEM_65_NAME = N_("Breast Plate"); const char *ITEM_65_SHORT_NAME = N_("Plate"); const char *ITEM_66_NAME = N_("Splint Mail"); const char *ITEM_67_NAME = N_("Plate Mail"); const char *ITEM_68_NAME = N_("Field Plate"); const char *ITEM_69_NAME = N_("Gothic Plate"); const char *ITEM_70_NAME = N_("Full Plate Mail"); const char *ITEM_71_SHORT_NAME = N_("Shield"); const char *ITEM_72_NAME = N_("Small Shield"); const char *ITEM_73_NAME = N_("Large Shield"); const char *ITEM_74_NAME = N_("Kite Shield"); const char *ITEM_75_NAME = N_("Tower Shield"); const char *ITEM_76_NAME = N_("Gothic Shield"); const char *ITEM_81_NAME = N_("Potion of Rejuvenation"); const char *ITEM_82_NAME = N_("Potion of Full Rejuvenation"); const char *ITEM_84_NAME = N_("Oil of Accuracy"); const char *ITEM_85_NAME = N_("Oil of Sharpness"); const char *ITEM_86_NAME = N_("Oil"); const char *ITEM_87_NAME = N_("Elixir of Strength"); const char *ITEM_88_NAME = N_("Elixir of Magic"); const char *ITEM_89_NAME = N_("Elixir of Dexterity"); const char *ITEM_90_NAME = N_("Elixir of Vitality"); const char *ITEM_91_NAME = N_("Scroll of Healing"); const char *ITEM_92_NAME = N_("Scroll of Search"); const char *ITEM_93_NAME = N_("Scroll of Lightning"); const char *ITEM_96_NAME = N_("Scroll of Fire Wall"); const char *ITEM_97_NAME = N_("Scroll of Inferno"); const char *ITEM_99_NAME = N_("Scroll of Flash"); const char *ITEM_100_NAME = N_("Scroll of Infravision"); const char *ITEM_101_NAME = N_("Scroll of Phasing"); const char *ITEM_102_NAME = N_("Scroll of Mana Shield"); const char *ITEM_103_NAME = N_("Scroll of Flame Wave"); const char *ITEM_104_NAME = N_("Scroll of Fireball"); const char *ITEM_105_NAME = N_("Scroll of Stone Curse"); const char *ITEM_106_NAME = N_("Scroll of Chain Lightning"); const char *ITEM_107_NAME = N_("Scroll of Guardian"); const char *ITEM_109_NAME = N_("Scroll of Nova"); const char *ITEM_110_NAME = N_("Scroll of Golem"); const char *ITEM_112_NAME = N_("Scroll of Teleport"); const char *ITEM_113_NAME = N_("Scroll of Apocalypse"); const char *ITEM_120_NAME = N_("Falchion"); const char *ITEM_121_NAME = N_("Scimitar"); const char *ITEM_122_NAME = N_("Claymore"); const char *ITEM_123_NAME = N_("Blade"); const char *ITEM_124_NAME = N_("Sabre"); const char *ITEM_125_NAME = N_("Long Sword"); const char *ITEM_126_NAME = N_("Broad Sword"); const char *ITEM_127_NAME = N_("Bastard Sword"); const char *ITEM_128_NAME = N_("Two-Handed Sword"); const char *ITEM_129_NAME = N_("Great Sword"); const char *ITEM_130_NAME = N_("Small Axe"); const char *ITEM_130_SHORT_NAME = N_("Axe"); const char *ITEM_132_NAME = N_("Large Axe"); const char *ITEM_133_NAME = N_("Broad Axe"); const char *ITEM_134_NAME = N_("Battle Axe"); const char *ITEM_135_NAME = N_("Great Axe"); const char *ITEM_136_NAME = N_("Mace"); const char *ITEM_137_NAME = N_("Morning Star"); const char *ITEM_138_NAME = N_("War Hammer"); const char *ITEM_138_SHORT_NAME = N_("Hammer"); const char *ITEM_139_NAME = N_("Spiked Club"); const char *ITEM_141_NAME = N_("Flail"); const char *ITEM_142_NAME = N_("Maul"); const char *ITEM_143_SHORT_NAME = N_("Bow"); const char *ITEM_144_NAME = N_("Hunter's Bow"); const char *ITEM_145_NAME = N_("Long Bow"); const char *ITEM_146_NAME = N_("Composite Bow"); const char *IDI_SHORT_BATTLE_BOW_NAME = N_("Short Battle Bow"); const char *ITEM_148_NAME = N_("Long Battle Bow"); const char *ITEM_149_NAME = N_("Short War Bow"); const char *ITEM_150_NAME = N_("Long War Bow"); const char *ITEM_151_SHORT_NAME = N_("Staff"); const char *ITEM_152_NAME = N_("Long Staff"); const char *ITEM_153_NAME = N_("Composite Staff"); const char *ITEM_154_NAME = N_("Quarter Staff"); const char *ITEM_155_NAME = N_("War Staff"); const char *ITEM_156_NAME = N_("Ring"); const char *ITEM_159_NAME = N_("Amulet"); const char *ITEM_161_NAME = N_("Rune of Fire"); const char *ITEM_161_SHORT_NAME = N_("Rune"); const char *ITEM_162_NAME = N_("Rune of Lightning"); const char *ITEM_163_NAME = N_("Greater Rune of Fire"); const char *ITEM_164_NAME = N_("Greater Rune of Lightning"); const char *ITEM_165_NAME = N_("Rune of Stone"); const char *ITEM_166_NAME = N_("Short Staff of Charged Bolt"); const char *IDI_ARENAPOT_NAME = N_("Arena Potion"); const char *UNIQUE_ITEM_0_NAME = N_("The Butcher's Cleaver"); const char *UNIQUE_ITEM_9_NAME = N_("Lightforge"); const char *UNIQUE_ITEM_10_NAME = N_("The Rift Bow"); const char *UNIQUE_ITEM_11_NAME = N_("The Needler"); const char *UNIQUE_ITEM_12_NAME = N_("The Celestial Bow"); const char *UNIQUE_ITEM_13_NAME = N_("Deadly Hunter"); const char *UNIQUE_ITEM_14_NAME = N_("Bow of the Dead"); const char *UNIQUE_ITEM_15_NAME = N_("The Blackoak Bow"); const char *UNIQUE_ITEM_16_NAME = N_("Flamedart"); const char *UNIQUE_ITEM_17_NAME = N_("Fleshstinger"); const char *UNIQUE_ITEM_18_NAME = N_("Windforce"); const char *UNIQUE_ITEM_19_NAME = N_("Eaglehorn"); const char *UNIQUE_ITEM_20_NAME = N_("Gonnagal's Dirk"); const char *UNIQUE_ITEM_21_NAME = N_("The Defender"); const char *UNIQUE_ITEM_22_NAME = N_("Gryphon's Claw"); const char *UNIQUE_ITEM_23_NAME = N_("Black Razor"); const char *UNIQUE_ITEM_24_NAME = N_("Gibbous Moon"); const char *UNIQUE_ITEM_25_NAME = N_("Ice Shank"); const char *UNIQUE_ITEM_26_NAME = N_("The Executioner's Blade"); const char *UNIQUE_ITEM_27_NAME = N_("The Bonesaw"); const char *UNIQUE_ITEM_28_NAME = N_("Shadowhawk"); const char *UNIQUE_ITEM_29_NAME = N_("Wizardspike"); const char *UNIQUE_ITEM_30_NAME = N_("Lightsabre"); const char *UNIQUE_ITEM_31_NAME = N_("The Falcon's Talon"); const char *UNIQUE_ITEM_32_NAME = N_("Inferno"); const char *UNIQUE_ITEM_33_NAME = N_("Doombringer"); const char *UNIQUE_ITEM_34_NAME = N_("The Grizzly"); const char *UNIQUE_ITEM_35_NAME = N_("The Grandfather"); const char *UNIQUE_ITEM_36_NAME = N_("The Mangler"); const char *UNIQUE_ITEM_37_NAME = N_("Sharp Beak"); const char *UNIQUE_ITEM_38_NAME = N_("BloodSlayer"); const char *UNIQUE_ITEM_39_NAME = N_("The Celestial Axe"); const char *UNIQUE_ITEM_40_NAME = N_("Wicked Axe"); const char *UNIQUE_ITEM_41_NAME = N_("Stonecleaver"); const char *UNIQUE_ITEM_42_NAME = N_("Aguinara's Hatchet"); const char *UNIQUE_ITEM_43_NAME = N_("Hellslayer"); const char *UNIQUE_ITEM_44_NAME = N_("Messerschmidt's Reaver"); const char *UNIQUE_ITEM_45_NAME = N_("Crackrust"); const char *UNIQUE_ITEM_46_NAME = N_("Hammer of Jholm"); const char *UNIQUE_ITEM_47_NAME = N_("Civerb's Cudgel"); const char *UNIQUE_ITEM_48_NAME = N_("The Celestial Star"); const char *UNIQUE_ITEM_49_NAME = N_("Baranar's Star"); const char *UNIQUE_ITEM_50_NAME = N_("Gnarled Root"); const char *UNIQUE_ITEM_51_NAME = N_("The Cranium Basher"); const char *UNIQUE_ITEM_52_NAME = N_("Schaefer's Hammer"); const char *UNIQUE_ITEM_53_NAME = N_("Dreamflange"); const char *UNIQUE_ITEM_54_NAME = N_("Staff of Shadows"); const char *UNIQUE_ITEM_55_NAME = N_("Immolator"); const char *UNIQUE_ITEM_56_NAME = N_("Storm Spire"); const char *UNIQUE_ITEM_57_NAME = N_("Gleamsong"); const char *UNIQUE_ITEM_58_NAME = N_("Thundercall"); const char *UNIQUE_ITEM_59_NAME = N_("The Protector"); const char *UNIQUE_ITEM_60_NAME = N_("Naj's Puzzler"); const char *UNIQUE_ITEM_61_NAME = N_("Mindcry"); const char *UNIQUE_ITEM_62_NAME = N_("Rod of Onan"); const char *UNIQUE_ITEM_63_NAME = N_("Helm of Spirits"); const char *UNIQUE_ITEM_64_NAME = N_("Thinking Cap"); const char *UNIQUE_ITEM_65_NAME = N_("OverLord's Helm"); const char *UNIQUE_ITEM_66_NAME = N_("Fool's Crest"); const char *UNIQUE_ITEM_67_NAME = N_("Gotterdamerung"); const char *UNIQUE_ITEM_68_NAME = N_("Royal Circlet"); const char *UNIQUE_ITEM_69_NAME = N_("Torn Flesh of Souls"); const char *UNIQUE_ITEM_70_NAME = N_("The Gladiator's Bane"); const char *UNIQUE_ITEM_71_NAME = N_("The Rainbow Cloak"); const char *UNIQUE_ITEM_72_NAME = N_("Leather of Aut"); const char *UNIQUE_ITEM_73_NAME = N_("Wisdom's Wrap"); const char *UNIQUE_ITEM_74_NAME = N_("Sparking Mail"); const char *UNIQUE_ITEM_75_NAME = N_("Scavenger Carapace"); const char *UNIQUE_ITEM_76_NAME = N_("Nightscape"); const char *UNIQUE_ITEM_77_NAME = N_("Naj's Light Plate"); const char *UNIQUE_ITEM_78_NAME = N_("Demonspike Coat"); const char *UNIQUE_ITEM_79_NAME = N_("The Deflector"); const char *UNIQUE_ITEM_80_NAME = N_("Split Skull Shield"); const char *UNIQUE_ITEM_81_NAME = N_("Dragon's Breach"); const char *UNIQUE_ITEM_82_NAME = N_("Blackoak Shield"); const char *UNIQUE_ITEM_83_NAME = N_("Holy Defender"); const char *UNIQUE_ITEM_84_NAME = N_("Stormshield"); const char *UNIQUE_ITEM_85_NAME = N_("Bramble"); const char *UNIQUE_ITEM_86_NAME = N_("Ring of Regha"); const char *UNIQUE_ITEM_87_NAME = N_("The Bleeder"); const char *UNIQUE_ITEM_88_NAME = N_("Constricting Ring"); const char *UNIQUE_ITEM_89_NAME = N_("Ring of Engagement"); const char *ITEM_PREFIX_0_NAME = N_("Tin"); const char *ITEM_PREFIX_1_NAME = N_("Brass"); const char *ITEM_PREFIX_2_NAME = N_("Bronze"); const char *ITEM_PREFIX_3_NAME = N_("Iron"); const char *ITEM_PREFIX_4_NAME = N_("Steel"); const char *ITEM_PREFIX_5_NAME = N_("Silver"); const char *ITEM_PREFIX_7_NAME = N_("Platinum"); const char *ITEM_PREFIX_8_NAME = N_("Mithril"); const char *ITEM_PREFIX_9_NAME = N_("Meteoric"); const char *ITEM_PREFIX_10_NAME = N_("Weird"); const char *ITEM_PREFIX_11_NAME = N_("Strange"); const char *ITEM_PREFIX_12_NAME = N_("Useless"); const char *ITEM_PREFIX_13_NAME = N_("Bent"); const char *ITEM_PREFIX_14_NAME = N_("Weak"); const char *ITEM_PREFIX_15_NAME = N_("Jagged"); const char *ITEM_PREFIX_16_NAME = N_("Deadly"); const char *ITEM_PREFIX_17_NAME = N_("Heavy"); const char *ITEM_PREFIX_18_NAME = N_("Vicious"); const char *ITEM_PREFIX_19_NAME = N_("Brutal"); const char *ITEM_PREFIX_20_NAME = N_("Massive"); const char *ITEM_PREFIX_21_NAME = N_("Savage"); const char *ITEM_PREFIX_22_NAME = N_("Ruthless"); const char *ITEM_PREFIX_23_NAME = N_("Merciless"); const char *ITEM_PREFIX_24_NAME = N_("Clumsy"); const char *ITEM_PREFIX_25_NAME = N_("Dull"); const char *ITEM_PREFIX_26_NAME = N_("Sharp"); const char *ITEM_PREFIX_27_NAME = N_("Fine"); const char *ITEM_PREFIX_28_NAME = N_("Warrior's"); const char *ITEM_PREFIX_29_NAME = N_("Soldier's"); const char *ITEM_PREFIX_30_NAME = N_("Lord's"); const char *ITEM_PREFIX_31_NAME = N_("Knight's"); const char *ITEM_PREFIX_32_NAME = N_("Master's"); const char *ITEM_PREFIX_33_NAME = N_("Champion's"); const char *ITEM_PREFIX_34_NAME = N_("King's"); const char *ITEM_PREFIX_35_NAME = N_("Vulnerable"); const char *ITEM_PREFIX_36_NAME = N_("Rusted"); const char *ITEM_PREFIX_38_NAME = N_("Strong"); const char *ITEM_PREFIX_39_NAME = N_("Grand"); const char *ITEM_PREFIX_40_NAME = N_("Valiant"); const char *ITEM_PREFIX_41_NAME = N_("Glorious"); const char *ITEM_PREFIX_42_NAME = N_("Blessed"); const char *ITEM_PREFIX_43_NAME = N_("Saintly"); const char *ITEM_PREFIX_44_NAME = N_("Awesome"); const char *ITEM_PREFIX_45_NAME = N_("Holy"); const char *ITEM_PREFIX_46_NAME = N_("Godly"); const char *ITEM_PREFIX_47_NAME = N_("Red"); const char *ITEM_PREFIX_48_NAME = N_("Crimson"); const char *ITEM_PREFIX_50_NAME = N_("Garnet"); const char *ITEM_PREFIX_51_NAME = N_("Ruby"); const char *ITEM_PREFIX_52_NAME = N_("Blue"); const char *ITEM_PREFIX_53_NAME = N_("Azure"); const char *ITEM_PREFIX_54_NAME = N_("Lapis"); const char *ITEM_PREFIX_55_NAME = N_("Cobalt"); const char *ITEM_PREFIX_56_NAME = N_("Sapphire"); const char *ITEM_PREFIX_57_NAME = N_("White"); const char *ITEM_PREFIX_58_NAME = N_("Pearl"); const char *ITEM_PREFIX_59_NAME = N_("Ivory"); const char *ITEM_PREFIX_60_NAME = N_("Crystal"); const char *ITEM_PREFIX_61_NAME = N_("Diamond"); const char *ITEM_PREFIX_62_NAME = N_("Topaz"); const char *ITEM_PREFIX_63_NAME = N_("Amber"); const char *ITEM_PREFIX_64_NAME = N_("Jade"); const char *ITEM_PREFIX_65_NAME = N_("Obsidian"); const char *ITEM_PREFIX_66_NAME = N_("Emerald"); const char *ITEM_PREFIX_67_NAME = N_("Hyena's"); const char *ITEM_PREFIX_68_NAME = N_("Frog's"); const char *ITEM_PREFIX_69_NAME = N_("Spider's"); const char *ITEM_PREFIX_70_NAME = N_("Raven's"); const char *ITEM_PREFIX_71_NAME = N_("Snake's"); const char *ITEM_PREFIX_72_NAME = N_("Serpent's"); const char *ITEM_PREFIX_73_NAME = N_("Drake's"); const char *ITEM_PREFIX_74_NAME = N_("Dragon's"); const char *ITEM_PREFIX_75_NAME = N_("Wyrm's"); const char *ITEM_PREFIX_76_NAME = N_("Hydra's"); const char *ITEM_PREFIX_77_NAME = N_("Angel's"); const char *ITEM_PREFIX_78_NAME = N_("Arch-Angel's"); const char *ITEM_PREFIX_79_NAME = N_("Plentiful"); const char *ITEM_PREFIX_80_NAME = N_("Bountiful"); const char *ITEM_PREFIX_81_NAME = N_("Flaming"); const char *ITEM_PREFIX_82_NAME = N_("Lightning"); const char *ITEM_SUFFIX_0_NAME = N_("quality"); const char *ITEM_SUFFIX_1_NAME = N_("maiming"); const char *ITEM_SUFFIX_2_NAME = N_("slaying"); const char *ITEM_SUFFIX_3_NAME = N_("gore"); const char *ITEM_SUFFIX_4_NAME = N_("carnage"); const char *ITEM_SUFFIX_5_NAME = N_("slaughter"); const char *ITEM_SUFFIX_6_NAME = N_("pain"); const char *ITEM_SUFFIX_7_NAME = N_("tears"); const char *ITEM_SUFFIX_8_NAME = N_("health"); const char *ITEM_SUFFIX_9_NAME = N_("protection"); const char *ITEM_SUFFIX_10_NAME = N_("absorption"); const char *ITEM_SUFFIX_11_NAME = N_("deflection"); const char *ITEM_SUFFIX_12_NAME = N_("osmosis"); const char *ITEM_SUFFIX_13_NAME = N_("frailty"); const char *ITEM_SUFFIX_14_NAME = N_("weakness"); const char *ITEM_SUFFIX_15_NAME = N_("strength"); const char *ITEM_SUFFIX_16_NAME = N_("might"); const char *ITEM_SUFFIX_17_NAME = N_("power"); const char *ITEM_SUFFIX_18_NAME = N_("giants"); const char *ITEM_SUFFIX_19_NAME = N_("titans"); const char *ITEM_SUFFIX_20_NAME = N_("paralysis"); const char *ITEM_SUFFIX_21_NAME = N_("atrophy"); const char *ITEM_SUFFIX_22_NAME = N_("dexterity"); const char *ITEM_SUFFIX_23_NAME = N_("skill"); const char *ITEM_SUFFIX_24_NAME = N_("accuracy"); const char *ITEM_SUFFIX_25_NAME = N_("precision"); const char *ITEM_SUFFIX_26_NAME = N_("perfection"); const char *ITEM_SUFFIX_27_NAME = N_("the fool"); const char *ITEM_SUFFIX_28_NAME = N_("dyslexia"); const char *ITEM_SUFFIX_29_NAME = N_("magic"); const char *ITEM_SUFFIX_30_NAME = N_("the mind"); const char *ITEM_SUFFIX_31_NAME = N_("brilliance"); const char *ITEM_SUFFIX_32_NAME = N_("sorcery"); const char *ITEM_SUFFIX_33_NAME = N_("wizardry"); const char *ITEM_SUFFIX_34_NAME = N_("illness"); const char *ITEM_SUFFIX_35_NAME = N_("disease"); const char *ITEM_SUFFIX_36_NAME = N_("vitality"); const char *ITEM_SUFFIX_37_NAME = N_("zest"); const char *ITEM_SUFFIX_38_NAME = N_("vim"); const char *ITEM_SUFFIX_39_NAME = N_("vigor"); const char *ITEM_SUFFIX_40_NAME = N_("life"); const char *ITEM_SUFFIX_41_NAME = N_("trouble"); const char *ITEM_SUFFIX_42_NAME = N_("the pit"); const char *ITEM_SUFFIX_43_NAME = N_("the sky"); const char *ITEM_SUFFIX_44_NAME = N_("the moon"); const char *ITEM_SUFFIX_45_NAME = N_("the stars"); const char *ITEM_SUFFIX_46_NAME = N_("the heavens"); const char *ITEM_SUFFIX_47_NAME = N_("the zodiac"); const char *ITEM_SUFFIX_48_NAME = N_("the vulture"); const char *ITEM_SUFFIX_49_NAME = N_("the jackal"); const char *ITEM_SUFFIX_50_NAME = N_("the fox"); const char *ITEM_SUFFIX_51_NAME = N_("the jaguar"); const char *ITEM_SUFFIX_52_NAME = N_("the eagle"); const char *ITEM_SUFFIX_53_NAME = N_("the wolf"); const char *ITEM_SUFFIX_54_NAME = N_("the tiger"); const char *ITEM_SUFFIX_55_NAME = N_("the lion"); const char *ITEM_SUFFIX_56_NAME = N_("the mammoth"); const char *ITEM_SUFFIX_57_NAME = N_("the whale"); const char *ITEM_SUFFIX_58_NAME = N_("fragility"); const char *ITEM_SUFFIX_59_NAME = N_("brittleness"); const char *ITEM_SUFFIX_60_NAME = N_("sturdiness"); const char *ITEM_SUFFIX_61_NAME = N_("craftsmanship"); const char *ITEM_SUFFIX_62_NAME = N_("structure"); const char *ITEM_SUFFIX_63_NAME = N_("the ages"); const char *ITEM_SUFFIX_64_NAME = N_("the dark"); const char *ITEM_SUFFIX_65_NAME = N_("the night"); const char *ITEM_SUFFIX_66_NAME = N_("light"); const char *ITEM_SUFFIX_67_NAME = N_("radiance"); const char *ITEM_SUFFIX_68_NAME = N_("flame"); const char *ITEM_SUFFIX_69_NAME = N_("fire"); const char *ITEM_SUFFIX_70_NAME = N_("burning"); const char *ITEM_SUFFIX_71_NAME = N_("shock"); const char *ITEM_SUFFIX_72_NAME = N_("lightning"); const char *ITEM_SUFFIX_73_NAME = N_("thunder"); const char *ITEM_SUFFIX_74_NAME = N_("many"); const char *ITEM_SUFFIX_75_NAME = N_("plenty"); const char *ITEM_SUFFIX_76_NAME = N_("thorns"); const char *ITEM_SUFFIX_77_NAME = N_("corruption"); const char *ITEM_SUFFIX_78_NAME = N_("thieves"); const char *ITEM_SUFFIX_79_NAME = N_("the bear"); const char *ITEM_SUFFIX_80_NAME = N_("the bat"); const char *ITEM_SUFFIX_81_NAME = N_("vampires"); const char *ITEM_SUFFIX_82_NAME = N_("the leech"); const char *ITEM_SUFFIX_83_NAME = N_("blood"); const char *ITEM_SUFFIX_84_NAME = N_("piercing"); const char *ITEM_SUFFIX_85_NAME = N_("puncturing"); const char *ITEM_SUFFIX_86_NAME = N_("bashing"); const char *ITEM_SUFFIX_87_NAME = N_("readiness"); const char *ITEM_SUFFIX_88_NAME = N_("swiftness"); const char *ITEM_SUFFIX_89_NAME = N_("speed"); const char *ITEM_SUFFIX_90_NAME = N_("haste"); const char *ITEM_SUFFIX_91_NAME = N_("balance"); const char *ITEM_SUFFIX_92_NAME = N_("stability"); const char *ITEM_SUFFIX_93_NAME = N_("harmony"); const char *ITEM_SUFFIX_94_NAME = N_("blocking"); const char *QUEST_THE_MAGIC_ROCK_NAME = N_("The Magic Rock"); const char *QUEST_GHARBAD_THE_WEAK_NAME = N_("Gharbad The Weak"); const char *QUEST_ZHAR_THE_MAD_NAME = N_("Zhar the Mad"); const char *QUEST_LACHDANAN_NAME = N_("Lachdanan"); const char *QUEST_DIABLO_NAME = N_("Diablo"); const char *QUEST_THE_BUTCHER_NAME = N_("The Butcher"); const char *QUEST_OGDENS_SIGN_NAME = N_("Ogden's Sign"); const char *QUEST_HALLS_OF_THE_BLIND_NAME = N_("Halls of the Blind"); const char *QUEST_VALOR_NAME = N_("Valor"); const char *QUEST_WARLORD_OF_BLOOD_NAME = N_("Warlord of Blood"); const char *QUEST_THE_CURSE_OF_KING_LEORIC_NAME = N_("The Curse of King Leoric"); const char *QUEST_POISONED_WATER_SUPPLY_NAME = N_("Poisoned Water Supply"); const char *QUEST_THE_CHAMBER_OF_BONE_NAME = N_("The Chamber of Bone"); const char *QUEST_ARCHBISHOP_LAZARUS_NAME = N_("Archbishop Lazarus"); const char *QUEST_GRAVE_MATTERS_NAME = N_("Grave Matters"); const char *QUEST_FARMERS_ORCHARD_NAME = N_("Farmer's Orchard"); const char *QUEST_LITTLE_GIRL_NAME = N_("Little Girl"); const char *QUEST_WANDERING_TRADER_NAME = N_("Wandering Trader"); const char *QUEST_THE_DEFILER_NAME = N_("The Defiler"); const char *QUEST_NA_KRUL_NAME = N_("Na-Krul"); const char *QUEST_CORNERSTONE_OF_THE_WORLD_NAME = N_("Cornerstone of the World"); const char *QUEST_THE_JERSEYS_JERSEY_NAME = N_("The Jersey's Jersey"); const char *SPELL_FIREBOLT_NAME = P_("spell", "Firebolt"); const char *SPELL_HEALING_NAME = P_("spell", "Healing"); const char *SPELL_LIGHTNING_NAME = P_("spell", "Lightning"); const char *SPELL_FLASH_NAME = P_("spell", "Flash"); const char *SPELL_IDENTIFY_NAME = P_("spell", "Identify"); const char *SPELL_FIRE_WALL_NAME = P_("spell", "Fire Wall"); const char *SPELL_TOWN_PORTAL_NAME = P_("spell", "Town Portal"); const char *SPELL_STONE_CURSE_NAME = P_("spell", "Stone Curse"); const char *SPELL_INFRAVISION_NAME = P_("spell", "Infravision"); const char *SPELL_PHASING_NAME = P_("spell", "Phasing"); const char *SPELL_MANA_SHIELD_NAME = P_("spell", "Mana Shield"); const char *SPELL_FIREBALL_NAME = P_("spell", "Fireball"); const char *SPELL_GUARDIAN_NAME = P_("spell", "Guardian"); const char *SPELL_CHAIN_LIGHTNING_NAME = P_("spell", "Chain Lightning"); const char *SPELL_FLAME_WAVE_NAME = P_("spell", "Flame Wave"); const char *SPELL_DOOM_SERPENTS_NAME = P_("spell", "Doom Serpents"); const char *SPELL_BLOOD_RITUAL_NAME = P_("spell", "Blood Ritual"); const char *SPELL_NOVA_NAME = P_("spell", "Nova"); const char *SPELL_INVISIBILITY_NAME = P_("spell", "Invisibility"); const char *SPELL_INFERNO_NAME = P_("spell", "Inferno"); const char *SPELL_GOLEM_NAME = P_("spell", "Golem"); const char *SPELL_RAGE_NAME = P_("spell", "Rage"); const char *SPELL_TELEPORT_NAME = P_("spell", "Teleport"); const char *SPELL_APOCALYPSE_NAME = P_("spell", "Apocalypse"); const char *SPELL_ETHEREALIZE_NAME = P_("spell", "Etherealize"); const char *SPELL_ITEM_REPAIR_NAME = P_("spell", "Item Repair"); const char *SPELL_STAFF_RECHARGE_NAME = P_("spell", "Staff Recharge"); const char *SPELL_TRAP_DISARM_NAME = P_("spell", "Trap Disarm"); const char *SPELL_ELEMENTAL_NAME = P_("spell", "Elemental"); const char *SPELL_CHARGED_BOLT_NAME = P_("spell", "Charged Bolt"); const char *SPELL_HOLY_BOLT_NAME = P_("spell", "Holy Bolt"); const char *SPELL_RESURRECT_NAME = P_("spell", "Resurrect"); const char *SPELL_TELEKINESIS_NAME = P_("spell", "Telekinesis"); const char *SPELL_HEAL_OTHER_NAME = P_("spell", "Heal Other"); const char *SPELL_BLOOD_STAR_NAME = P_("spell", "Blood Star"); const char *SPELL_BONE_SPIRIT_NAME = P_("spell", "Bone Spirit"); const char *TEXT_0 = N_(" Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh blow to this land. The people always loved the King, and now they live in mortal fear of him. The question that I keep asking myself is how he could have fallen so far from the Light, as Leoric had always been the holiest of men. Only the vilest powers of Hell could so utterly destroy a man from within..."); const char *TEXT_1 = N_("The village needs your help, good master! Some months ago King Leoric's son, Prince Albrecht, was kidnapped. The King went into a rage and scoured the village for his missing child. With each passing day, Leoric seemed to slip deeper into madness. He sought to blame innocent townsfolk for the boy's disappearance and had them brutally executed. Less than half of us survived his insanity...\n \nThe King's Knights and Priests tried to placate him, but he turned against them and sadly, they were forced to kill him. With his dying breath the King called down a terrible curse upon his former followers. He vowed that they would serve him in darkness forever...\n \nThis is where things take an even darker twist than I thought possible! Our former King has risen from his eternal sleep and now commands a legion of undead minions within the Labyrinth. His body was buried in a tomb three levels beneath the Cathedral. Please, good master, put his soul at ease by destroying his now cursed form..."); const char *TEXT_2 = N_("As I told you, good master, the King was entombed three levels below. He's down there, waiting in the putrid darkness for his chance to destroy this land..."); const char *TEXT_3 = N_("The curse of our King has passed, but I fear that it was only part of a greater evil at work. However, we may yet be saved from the darkness that consumes our land, for your victory is a good omen. May Light guide you on your way, good master."); const char *TEXT_4 = N_("The loss of his son was too much for King Leoric. I did what I could to ease his madness, but in the end it overcame him. A black curse has hung over this kingdom from that day forward, but perhaps if you were to free his spirit from his earthly prison, the curse would be lifted..."); const char *TEXT_5 = N_("I don't like to think about how the King died. I like to remember him for the kind and just ruler that he was. His death was so sad and seemed very wrong, somehow."); const char *TEXT_6 = N_("I made many of the weapons and most of the armor that King Leoric used to outfit his knights. I even crafted a huge two-handed sword of the finest mithril for him, as well as a field crown to match. I still cannot believe how he died, but it must have been some sinister force that drove him insane!"); const char *TEXT_7 = N_("I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is King. King, so you hear me? HAIL TO THE KING!"); const char *TEXT_8 = N_("The dead who walk among the living follow the cursed King. He holds the power to raise yet more warriors for an ever growing army of the undead. If you do not stop his reign, he will surely march across this land and slay all who still live here."); const char *TEXT_9 = N_("Look, I'm running a business here. I don't sell information, and I don't care about some King that's been dead longer than I've been alive. If you need something to use against this King of the undead, then I can help you out..."); const char *TEXT_10 = N_("The warmth of life has entered my tomb. Prepare yourself, mortal, to serve my Master for eternity!"); const char *TEXT_11 = N_("I see that this strange behavior puzzles you as well. I would surmise that since many demons fear the light of the sun and believe that it holds great power, it may be that the rising sun depicted on the sign you speak of has led them to believe that it too holds some arcane powers. Hmm, perhaps they are not all as smart as we had feared..."); const char *TEXT_12 = N_("Master, I have a strange experience to relate. I know that you have a great knowledge of those monstrosities that inhabit the labyrinth, and this is something that I cannot understand for the very life of me... I was awakened during the night by a scraping sound just outside of my tavern. When I looked out from my bedroom, I saw the shapes of small demon-like creatures in the inn yard. After a short time, they ran off, but not before stealing the sign to my inn. I don't know why the demons would steal my sign but leave my family in peace... 'tis strange, no?"); const char *TEXT_13 = N_("Oh, you didn't have to bring back my sign, but I suppose that it does save me the expense of having another one made. Well, let me see, what could I give you as a fee for finding it? Hmmm, what have we here... ah, yes! This cap was left in one of the rooms by a magician who stayed here some time ago. Perhaps it may be of some value to you."); const char *TEXT_14 = N_("My goodness, demons running about the village at night, pillaging our homes - is nothing sacred? I hope that Ogden and Garda are all right. I suppose that they would come to see me if they were hurt..."); const char *TEXT_15 = N_("Oh my! Is that where the sign went? My Grandmother and I must have slept right through the whole thing. Thank the Light that those monsters didn't attack the inn."); const char *TEXT_16 = N_("Demons stole Ogden's sign, you say? That doesn't sound much like the atrocities I've heard of - or seen. \n \nDemons are concerned with ripping out your heart, not your signpost."); const char *TEXT_17 = N_("You know what I think? Somebody took that sign, and they gonna want lots of money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece of cheese..."); const char *TEXT_18 = N_("No mortal can truly understand the mind of the demon. \n \nNever let their erratic actions confuse you, as that too may be their plan."); const char *TEXT_19 = N_("What - is he saying I took that? I suppose that Griswold is on his side, too. \n \nLook, I got over simple sign stealing months ago. You can't turn a profit on a piece of wood."); const char *TEXT_20 = N_("Hey - You that one that kill all! You get me Magic Banner or we attack! You no leave with life! You kill big uglies and give back Magic. Go past corner and door, find uglies. You give, you go!"); const char *TEXT_21 = N_("You kill uglies, get banner. You bring to me, or else..."); const char *TEXT_22 = N_("You give! Yes, good! Go now, we strong. We kill all with big Magic!"); const char *TEXT_23 = N_("This does not bode well, for it confirms my darkest fears. While I did not allow myself to believe the ancient legends, I cannot deny them now. Perhaps the time has come to reveal who I am.\n \nMy true name is Deckard Cain the Elder, and I am the last descendant of an ancient Brotherhood that was dedicated to safeguarding the secrets of a timeless evil. An evil that quite obviously has now been released.\n \nThe Archbishop Lazarus, once King Leoric's most trusted advisor, led a party of simple townsfolk into the Labyrinth to find the King's missing son, Albrecht. Quite some time passed before they returned, and only a few of them escaped with their lives.\n \nCurse me for a fool! I should have suspected his veiled treachery then. It must have been Lazarus himself who kidnapped Albrecht and has since hidden him within the Labyrinth. I do not understand why the Archbishop turned to the darkness, or what his interest is in the child, unless he means to sacrifice him to his dark masters!\n \nThat must be what he has planned! The survivors of his 'rescue party' say that Lazarus was last seen running into the deepest bowels of the labyrinth. You must hurry and save the prince from the sacrificial blade of this demented fiend!"); const char *TEXT_24 = N_("You must hurry and rescue Albrecht from the hands of Lazarus. The prince and the people of this kingdom are counting on you!"); const char *TEXT_25 = N_("Your story is quite grim, my friend. Lazarus will surely burn in Hell for his horrific deed. The boy that you describe is not our prince, but I believe that Albrecht may yet be in danger. The symbol of power that you speak of must be a portal in the very heart of the labyrinth.\n \nKnow this, my friend - The evil that you move against is the dark Lord of Terror. He is known to mortal men as Diablo. It was he who was imprisoned within the Labyrinth many centuries ago and I fear that he seeks to once again sow chaos in the realm of mankind. You must venture through the portal and destroy Diablo before it is too late!"); const char *TEXT_26 = N_("Lazarus was the Archbishop who led many of the townspeople into the labyrinth. I lost many good friends that day, and Lazarus never returned. I suppose he was killed along with most of the others. If you would do me a favor, good master - please do not talk to Farnham about that day."); const char *TEXT_29 = N_("I was shocked when I heard of what the townspeople were planning to do that night. I thought that of all people, Lazarus would have had more sense than that. He was an Archbishop, and always seemed to care so much for the townsfolk of Tristram. So many were injured, I could not save them all..."); const char *TEXT_30 = N_("I remember Lazarus as being a very kind and giving man. He spoke at my mother's funeral, and was supportive of my grandmother and myself in a very troubled time. I pray every night that somehow, he is still alive and safe."); const char *TEXT_31 = N_("I was there when Lazarus led us into the labyrinth. He spoke of holy retribution, but when we started fighting those hellspawn, he did not so much as lift his mace against them. He just ran deeper into the dim, endless chambers that were filled with the servants of darkness!"); const char *TEXT_32 = N_("They stab, then bite, then they're all around you. Liar! LIAR! They're all dead! Dead! Do you hear me? They just keep falling and falling... their blood spilling out all over the floor... all his fault..."); const char *TEXT_33 = N_("I did not know this Lazarus of whom you speak, but I do sense a great conflict within his being. He poses a great danger, and will stop at nothing to serve the powers of darkness which have claimed him as theirs."); const char *TEXT_34 = N_("Yes, the righteous Lazarus, who was sooo effective against those monsters down there. Didn't help save my leg, did it? Look, I'll give you a free piece of advice. Ask Farnham, he was there."); const char *TEXT_35 = N_("Abandon your foolish quest. All that awaits you is the wrath of my Master! You are too late to save the child. Now you will join him in Hell!"); const char *TEXT_37 = N_("Hmm, I don't know what I can really tell you about this that will be of any help. The water that fills our wells comes from an underground spring. I have heard of a tunnel that leads to a great lake - perhaps they are one and the same. Unfortunately, I do not know what would cause our water supply to be tainted."); const char *TEXT_38 = N_("I have always tried to keep a large supply of foodstuffs and drink in our storage cellar, but with the entire town having no source of fresh water, even our stores will soon run dry. \n \nPlease, do what you can or I don't know what we will do."); const char *TEXT_39 = N_("I'm glad I caught up to you in time! Our wells have become brackish and stagnant and some of the townspeople have become ill drinking from them. Our reserves of fresh water are quickly running dry. I believe that there is a passage that leads to the springs that serve our town. Please find what has caused this calamity, or we all will surely perish."); const char *TEXT_40 = N_("Please, you must hurry. Every hour that passes brings us closer to having no water to drink. \n \nWe cannot survive for long without your help."); const char *TEXT_41 = N_("What's that you say - the mere presence of the demons had caused the water to become tainted? Oh, truly a great evil lurks beneath our town, but your perseverance and courage gives us hope. Please take this ring - perhaps it will aid you in the destruction of such vile creatures."); const char *TEXT_42 = N_("My grandmother is very weak, and Garda says that we cannot drink the water from the wells. Please, can you do something to help us?"); const char *TEXT_43 = N_("Pepin has told you the truth. We will need fresh water badly, and soon. I have tried to clear one of the smaller wells, but it reeks of stagnant filth. It must be getting clogged at the source."); const char *TEXT_44 = N_("You drink water?"); const char *TEXT_45 = N_("The people of Tristram will die if you cannot restore fresh water to their wells. \n \nKnow this - demons are at the heart of this matter, but they remain ignorant of what they have spawned."); const char *TEXT_46 = N_("For once, I'm with you. My business runs dry - so to speak - if I have no market to sell to. You better find out what is going on, and soon!"); const char *TEXT_47 = N_("A book that speaks of a chamber of human bones? Well, a Chamber of Bone is mentioned in certain archaic writings that I studied in the libraries of the East. These tomes inferred that when the Lords of the underworld desired to protect great treasures, they would create domains where those who died in the attempt to steal that treasure would be forever bound to defend it. A twisted, but strangely fitting, end?"); const char *TEXT_48 = N_("I am afraid that I don't know anything about that, good master. Cain has many books that may be of some help."); const char *TEXT_49 = N_("This sounds like a very dangerous place. If you venture there, please take great care."); const char *TEXT_50 = N_("I am afraid that I haven't heard anything about that. Perhaps Cain the Storyteller could be of some help."); const char *TEXT_51 = N_("I know nothing of this place, but you may try asking Cain. He talks about many things, and it would not surprise me if he had some answers to your question."); const char *TEXT_52 = N_("Okay, so listen. There's this chamber of wood, see. And his wife, you know - her - tells the tree... cause you gotta wait. Then I says, that might work against him, but if you think I'm gonna PAY for this... you... uh... yeah."); const char *TEXT_53 = N_("You will become an eternal servant of the dark lords should you perish within this cursed domain. \n \nEnter the Chamber of Bone at your own peril."); const char *TEXT_54 = N_("A vast and mysterious treasure, you say? Maybe I could be interested in picking up a few things from you... or better yet, don't you need some rare and expensive supplies to get you through this ordeal?"); const char *TEXT_55 = N_("It seems that the Archbishop Lazarus goaded many of the townsmen into venturing into the Labyrinth to find the King's missing son. He played upon their fears and whipped them into a frenzied mob. None of them were prepared for what lay within the cold earth... Lazarus abandoned them down there - left in the clutches of unspeakable horrors - to die."); const char *TEXT_56 = N_("Yes, Farnham has mumbled something about a hulking brute who wielded a fierce weapon. I believe he called him a butcher."); const char *TEXT_57 = N_("By the Light, I know of this vile demon. There were many that bore the scars of his wrath upon their bodies when the few survivors of the charge led by Lazarus crawled from the Cathedral. I don't know what he used to slice open his victims, but it could not have been of this world. It left wounds festering with disease and even I found them almost impossible to treat. Beware if you plan to battle this fiend..."); const char *TEXT_58 = N_("When Farnham said something about a butcher killing people, I immediately discounted it. But since you brought it up, maybe it is true."); const char *TEXT_59 = N_("I saw what Farnham calls the Butcher as it swathed a path through the bodies of my friends. He swung a cleaver as large as an axe, hewing limbs and cutting down brave men where they stood. I was separated from the fray by a host of small screeching demons and somehow found the stairway leading out. I never saw that hideous beast again, but his blood-stained visage haunts me to this day."); const char *TEXT_60 = N_("Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, couldn't save them. Trapped in a room with so many bodies... so many friends... NOOOOOOOOOO!"); const char *TEXT_61 = N_("The Butcher is a sadistic creature that delights in the torture and pain of others. You have seen his handiwork in the drunkard Farnham. His destruction will do much to ensure the safety of this village."); const char *TEXT_62 = N_("I know more than you'd think about that grisly fiend. His little friends got a hold of me and managed to get my leg before Griswold pulled me out of that hole. \n \nI'll put it bluntly - kill him before he kills you and adds your corpse to his collection."); const char *TEXT_63 = N_("Please, listen to me. The Archbishop Lazarus, he led us down here to find the lost prince. The bastard led us into a trap! Now everyone is dead... killed by a demon he called the Butcher. Avenge us! Find this Butcher and slay him so that our souls may finally rest..."); const char *TEXT_65 = N_("You recite an interesting rhyme written in a style that reminds me of other works. Let me think now - what was it?\n \n...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of razor claws briefly scraping to torment those poor souls who have been made sightless for all eternity. The prison for those so damned is named the Halls of the Blind..."); const char *TEXT_66 = N_("I never much cared for poetry. Occasionally, I had cause to hire minstrels when the inn was doing well, but that seems like such a long time ago now. \n \nWhat? Oh, yes... uh, well, I suppose you could see what someone else knows."); const char *TEXT_67 = N_("This does seem familiar, somehow. I seem to recall reading something very much like that poem while researching the history of demonic afflictions. It spoke of a place of great evil that... wait - you're not going there are you?"); const char *TEXT_68 = N_("If you have questions about blindness, you should talk to Pepin. I know that he gave my grandmother a potion that helped clear her vision, so maybe he can help you, too."); const char *TEXT_69 = N_("I am afraid that I have neither heard nor seen a place that matches your vivid description, my friend. Perhaps Cain the Storyteller could be of some help."); const char *TEXT_70 = N_("Look here... that's pretty funny, huh? Get it? Blind - look here?"); const char *TEXT_71 = N_("This is a place of great anguish and terror, and so serves its master well. \n \nTread carefully or you may yourself be staying much longer than you had anticipated."); const char *TEXT_72 = N_("Lets see, am I selling you something? No. Are you giving me money to tell you about this? No. Are you now leaving and going to talk to the storyteller who lives for this kind of thing? Yes."); const char *TEXT_73 = N_("You claim to have spoken with Lachdanan? He was a great hero during his life. Lachdanan was an honorable and just man who served his King faithfully for years. But of course, you already know that.\n \nOf those who were caught within the grasp of the King's Curse, Lachdanan would be the least likely to submit to the darkness without a fight, so I suppose that your story could be true. If I were in your place, my friend, I would find a way to release him from his torture."); const char *TEXT_74 = N_("You speak of a brave warrior long dead! I'll have no such talk of speaking with departed souls in my inn yard, thank you very much."); const char *TEXT_75 = N_("A golden elixir, you say. I have never concocted a potion of that color before, so I can't tell you how it would effect you if you were to try to drink it. As your healer, I strongly advise that should you find such an elixir, do as Lachdanan asks and DO NOT try to use it."); const char *TEXT_76 = N_("I've never heard of a Lachdanan before. I'm sorry, but I don't think that I can be of much help to you."); const char *TEXT_77 = N_("If it is actually Lachdanan that you have met, then I would advise that you aid him. I dealt with him on several occasions and found him to be honest and loyal in nature. The curse that fell upon the followers of King Leoric would fall especially hard upon him."); const char *TEXT_78 = N_(" Lachdanan is dead. Everybody knows that, and you can't fool me into thinking any other way. You can't talk to the dead. I know!"); const char *TEXT_79 = N_("You may meet people who are trapped within the Labyrinth, such as Lachdanan. \n \nI sense in him honor and great guilt. Aid him, and you aid all of Tristram."); const char *TEXT_80 = N_("Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened beneath him. He was incinerated in a ball of hellfire, and can't answer your questions anymore. Oh, that isn't what happened? Then I guess you'll be buying something or you'll be on your way."); const char *TEXT_81 = N_("Please, don't kill me, just hear me out. I was once Captain of King Leoric's Knights, upholding the laws of this land with justice and honor. Then his dark Curse fell upon us for the role we played in his tragic death. As my fellow Knights succumbed to their twisted fate, I fled from the King's burial chamber, searching for some way to free myself from the Curse. I failed...\n \nI have heard of a Golden Elixir that could lift the Curse and allow my soul to rest, but I have been unable to find it. My strength now wanes, and with it the last of my humanity as well. Please aid me and find the Elixir. I will repay your efforts - I swear upon my honor."); const char *TEXT_82 = N_("You have not found the Golden Elixir. I fear that I am doomed for eternity. Please, keep trying..."); const char *TEXT_83 = N_("You have saved my soul from damnation, and for that I am in your debt. If there is ever a way that I can repay you from beyond the grave I will find it, but for now - take my helm. On the journey I am about to take I will have little use for it. May it protect you against the dark powers below. Go with the Light, my friend..."); const char *TEXT_84 = N_("Griswold speaks of The Anvil of Fury - a legendary artifact long searched for, but never found. Crafted from the metallic bones of the Razor Pit demons, the Anvil of Fury was smelt around the skulls of the five most powerful magi of the underworld. Carved with runes of power and chaos, any weapon or armor forged upon this Anvil will be immersed into the realm of Chaos, imbedding it with magical properties. It is said that the unpredictable nature of Chaos makes it difficult to know what the outcome of this smithing will be..."); const char *TEXT_85 = N_("Don't you think that Griswold would be a better person to ask about this? He's quite handy, you know."); const char *TEXT_86 = N_("If you had been looking for information on the Pestle of Curing or the Silver Chalice of Purification, I could have assisted you, my friend. However, in this matter, you would be better served to speak to either Griswold or Cain."); const char *TEXT_87 = N_("Griswold's father used to tell some of us when we were growing up about a giant anvil that was used to make mighty weapons. He said that when a hammer was struck upon this anvil, the ground would shake with a great fury. Whenever the earth moves, I always remember that story."); const char *TEXT_88 = N_("Greetings! It's always a pleasure to see one of my best customers! I know that you have been venturing deeper into the Labyrinth, and there is a story I was told that you may find worth the time to listen to...\n \nOne of the men who returned from the Labyrinth told me about a mystic anvil that he came across during his escape. His description reminded me of legends I had heard in my youth about the burning Hellforge where powerful weapons of magic are crafted. The legend had it that deep within the Hellforge rested the Anvil of Fury! This Anvil contained within it the very essence of the demonic underworld...\n \nIt is said that any weapon crafted upon the burning Anvil is imbued with great power. If this anvil is indeed the Anvil of Fury, I may be able to make you a weapon capable of defeating even the darkest lord of Hell! \n \nFind the Anvil for me, and I'll get to work!"); const char *TEXT_89 = N_("Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could be your best hope, and I am sure that I can make you one of legendary proportions."); const char *TEXT_90 = N_("I can hardly believe it! This is the Anvil of Fury - good work, my friend. Now we'll show those bastards that there are no weapons in Hell more deadly than those made by men! Take this and may Light protect you."); const char *TEXT_91 = N_("Griswold can't sell his anvil. What will he do then? And I'd be angry too if someone took my anvil!"); const char *TEXT_92 = N_("There are many artifacts within the Labyrinth that hold powers beyond the comprehension of mortals. Some of these hold fantastic power that can be used by either the Light or the Darkness. Securing the Anvil from below could shift the course of the Sin War towards the Light."); const char *TEXT_93 = N_("If you were to find this artifact for Griswold, it could put a serious damper on my business here. Awwww, you'll never find it."); const char *TEXT_94 = N_("The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. Wherever this book you read from resides it is surely a place of great power.\n \nLegends speak of a pedestal that is carved from obsidian stone and has a pool of boiling blood atop its bone encrusted surface. There are also allusions to Stones of Blood that will open a door that guards an ancient treasure...\n \nThe nature of this treasure is shrouded in speculation, my friend, but it is said that the ancient hero Arkaine placed the holy armor Valor in a secret vault. Arkaine was the first mortal to turn the tide of the Sin War and chase the legions of darkness back to the Burning Hells.\n \nJust before Arkaine died, his armor was hidden away in a secret vault. It is said that when this holy armor is again needed, a hero will arise to don Valor once more. Perhaps you are that hero..."); const char *TEXT_95 = N_("Every child hears the story of the warrior Arkaine and his mystic armor known as Valor. If you could find its resting place, you would be well protected against the evil in the Labyrinth."); const char *TEXT_96 = N_("Hmm... it sounds like something I should remember, but I've been so busy learning new cures and creating better elixirs that I must have forgotten. Sorry..."); const char *TEXT_97 = N_("The story of the magic armor called Valor is something I often heard the boys talk about. You had better ask one of the men in the village."); const char *TEXT_98 = N_("The armor known as Valor could be what tips the scales in your favor. I will tell you that many have looked for it - including myself. Arkaine hid it well, my friend, and it will take more than a bit of luck to unlock the secrets that have kept it concealed oh, lo these many years."); const char *TEXT_99 = N_("Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."); const char *TEXT_100 = N_("Should you find these Stones of Blood, use them carefully. \n \nThe way is fraught with danger and your only hope rests within your self trust."); const char *TEXT_101 = N_("You intend to find the armor known as Valor? \n \nNo one has ever figured out where Arkaine stashed the stuff, and if my contacts couldn't find it, I seriously doubt you ever will either."); const char *TEXT_102 = N_("I know of only one legend that speaks of such a warrior as you describe. His story is found within the ancient chronicles of the Sin War...\n \nStained by a thousand years of war, blood and death, the Warlord of Blood stands upon a mountain of his tattered victims. His dark blade screams a black curse to the living; a tortured invitation to any who would stand before this Executioner of Hell.\n \nIt is also written that although he was once a mortal who fought beside the Legion of Darkness during the Sin War, he lost his humanity to his insatiable hunger for blood."); const char *TEXT_103 = N_("I am afraid that I haven't heard anything about such a vicious warrior, good master. I hope that you do not have to fight him, for he sounds extremely dangerous."); const char *TEXT_104 = N_("Cain would be able to tell you much more about something like this than I would ever wish to know."); const char *TEXT_105 = N_("If you are to battle such a fierce opponent, may Light be your guide and your defender. I will keep you in my thoughts."); const char *TEXT_106 = N_("Dark and wicked legends surrounds the one Warlord of Blood. Be well prepared, my friend, for he shows no mercy or quarter."); const char *TEXT_107 = N_("Always you gotta talk about Blood? What about flowers, and sunshine, and that pretty girl that brings the drinks. Listen here, friend - you're obsessive, you know that?"); const char *TEXT_108 = N_("His prowess with the blade is awesome, and he has lived for thousands of years knowing only warfare. I am sorry... I can not see if you will defeat him."); const char *TEXT_109 = N_("I haven't ever dealt with this Warlord you speak of, but he sounds like he's going through a lot of swords. Wouldn't mind supplying his armies..."); const char *TEXT_110 = N_("My blade sings for your blood, mortal, and by my dark masters it shall not be denied."); const char *TEXT_111 = N_("Griswold speaks of the Heaven Stone that was destined for the enclave located in the east. It was being taken there for further study. This stone glowed with an energy that somehow granted vision beyond that which a normal man could possess. I do not know what secrets it holds, my friend, but finding this stone would certainly prove most valuable."); const char *TEXT_112 = N_("The caravan stopped here to take on some supplies for their journey to the east. I sold them quite an array of fresh fruits and some excellent sweetbreads that Garda has just finished baking. Shame what happened to them..."); const char *TEXT_113 = N_("I don't know what it is that they thought they could see with that rock, but I will say this. If rocks are falling from the sky, you had better be careful!"); const char *TEXT_114 = N_("Well, a caravan of some very important people did stop here, but that was quite a while ago. They had strange accents and were starting on a long journey, as I recall. \n \nI don't see how you could hope to find anything that they would have been carrying."); const char *TEXT_115 = N_("Stay for a moment - I have a story you might find interesting. A caravan that was bound for the eastern kingdoms passed through here some time ago. It was supposedly carrying a piece of the heavens that had fallen to earth! The caravan was ambushed by cloaked riders just north of here along the roadway. I searched the wreckage for this sky rock, but it was nowhere to be found. If you should find it, I believe that I can fashion something useful from it."); const char *TEXT_116 = N_("I am still waiting for you to bring me that stone from the heavens. I know that I can make something powerful out of it."); const char *TEXT_117 = N_("Let me see that - aye... aye, it is as I believed. Give me a moment...\n \nAh, Here you are. I arranged pieces of the stone within a silver ring that my father left me. I hope it serves you well."); const char *TEXT_118 = N_("I used to have a nice ring; it was a really expensive one, with blue and green and red and silver. Don't remember what happened to it, though. I really miss that ring..."); const char *TEXT_119 = N_("The Heaven Stone is very powerful, and were it any but Griswold who bid you find it, I would prevent it. He will harness its powers and its use will be for the good of us all."); const char *TEXT_120 = N_("If anyone can make something out of that rock, Griswold can. He knows what he is doing, and as much as I try to steal his customers, I respect the quality of his work."); const char *TEXT_121 = N_("The witch Adria seeks a black mushroom? I know as much about Black Mushrooms as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, but this is something that cannot be found in any of my stories or books."); const char *TEXT_122 = N_("Let me just say this. Both Garda and I would never, EVER serve black mushrooms to our honored guests. If Adria wants some mushrooms in her stew, then that is her business, but I can't help you find any. Black mushrooms... disgusting!"); const char *TEXT_123 = N_("The witch told me that you were searching for the brain of a demon to assist me in creating my elixir. It should be of great value to the many who are injured by those foul beasts, if I can just unlock the secrets I suspect that its alchemy holds. If you can remove the brain of a demon when you kill it, I would be grateful if you could bring it to me."); const char *TEXT_124 = N_("Excellent, this is just what I had in mind. I was able to finish the elixir without this, but it can't hurt to have this to study. Would you please carry this to the witch? I believe that she is expecting it."); const char *TEXT_125 = N_("I think Ogden might have some mushrooms in the storage cellar. Why don't you ask him?"); const char *TEXT_126 = N_("If Adria doesn't have one of these, you can bet that's a rare thing indeed. I can offer you no more help than that, but it sounds like... a huge, gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose."); const char *TEXT_127 = N_("Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, listen... here's the secret - moderation is the key!"); const char *TEXT_128 = N_("What do we have here? Interesting, it looks like a book of reagents. Keep your eyes open for a black mushroom. It should be fairly large and easy to identify. If you find it, bring it to me, won't you?"); const char *TEXT_129 = N_("It's a big, black mushroom that I need. Now run off and get it for me so that I can use it for a special concoction that I am working on."); const char *TEXT_130 = N_("Yes, this will be perfect for a brew that I am creating. By the way, the healer is looking for the brain of some demon or another so he can treat those who have been afflicted by their poisonous venom. I believe that he intends to make an elixir from it. If you help him find what he needs, please see if you can get a sample of the elixir for me."); const char *TEXT_131 = N_("Why have you brought that here? I have no need for a demon's brain at this time. I do need some of the elixir that the Healer is working on. He needs that grotesque organ that you are holding, and then bring me the elixir. Simple when you think about it, isn't it?"); const char *TEXT_132 = N_("What? Now you bring me that elixir from the healer? I was able to finish my brew without it. Why don't you just keep it..."); const char *TEXT_133 = N_("I don't have any mushrooms of any size or color for sale. How about something a bit more useful?"); const char *TEXT_134 = N_("So, the legend of the Map is real. Even I never truly believed any of it! I suppose it is time that I told you the truth about who I am, my friend. You see, I am not all that I seem...\n \nMy true name is Deckard Cain the Elder, and I am the last descendant of an ancient Brotherhood that was dedicated to keeping and safeguarding the secrets of a timeless evil. An evil that quite obviously has now been released...\n \nThe evil that you move against is the dark Lord of Terror - known to mortal men as Diablo. It was he who was imprisoned within the Labyrinth many centuries ago. The Map that you hold now was created ages ago to mark the time when Diablo would rise again from his imprisonment. When the two stars on that map align, Diablo will be at the height of his power. He will be all but invincible...\n \nYou are now in a race against time, my friend! Find Diablo and destroy him before the stars align, for we may never have a chance to rid the world of his evil again!"); const char *TEXT_135 = N_("Our time is running short! I sense his dark power building and only you can stop him from attaining his full might."); const char *TEXT_136 = N_("I am sure that you tried your best, but I fear that even your strength and will may not be enough. Diablo is now at the height of his earthly power, and you will need all your courage and strength to defeat him. May the Light protect and guide you, my friend. I will help in any way that I am able."); const char *TEXT_137 = N_("If the witch can't help you and suggests you see Cain, what makes you think that I would know anything? It sounds like this is a very serious matter. You should hurry along and see the storyteller as Adria suggests."); const char *TEXT_138 = N_("I can't make much of the writing on this map, but perhaps Adria or Cain could help you decipher what this refers to. \n \nI can see that it is a map of the stars in our sky, but any more than that is beyond my talents."); const char *TEXT_139 = N_("The best person to ask about that sort of thing would be our storyteller. \n \nCain is very knowledgeable about ancient writings, and that is easily the oldest looking piece of paper that I have ever seen."); const char *TEXT_140 = N_("I have never seen a map of this sort before. Where'd you get it? Although I have no idea how to read this, Cain or Adria may be able to provide the answers that you seek."); const char *TEXT_141 = N_("Listen here, come close. I don't know if you know what I know, but you have really got somethin' here. That's a map."); const char *TEXT_142 = N_("Oh, I'm afraid this does not bode well at all. This map of the stars portends great disaster, but its secrets are not mine to tell. The time has come for you to have a very serious conversation with the Storyteller..."); const char *TEXT_143 = N_("I've been looking for a map, but that certainly isn't it. You should show that to Adria - she can probably tell you what it is. I'll say one thing; it looks old, and old usually means valuable."); const char *TEXT_144 = N_("Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you."); const char *TEXT_145 = N_("Something for you I am making. Again, not kill Gharbad. Live and give good. \n \nYou take this as proof I keep word..."); const char *TEXT_146 = N_("Nothing yet! Almost done. \n \nVery powerful, very strong. Live! Live! \n \nNo pain and promise I keep!"); const char *TEXT_147 = N_("This too good for you. Very Powerful! You want - you take!"); const char *TEXT_148 = N_("What?! Why are you here? All these interruptions are enough to make one insane. Here, take this and leave me to my work. Trouble me no more!"); const char *TEXT_149 = N_("Arrrrgh! Your curiosity will be the death of you!!!"); const char *TEXT_150 = N_("Hello, my friend. Stay awhile and listen..."); const char *TEXT_151 = N_("While you are venturing deeper into the Labyrinth you may find tomes of great knowledge hidden there. \n \nRead them carefully for they can tell you things that even I cannot."); const char *TEXT_152 = N_("I know of many myths and legends that may contain answers to questions that may arise in your journeys into the Labyrinth. If you come across challenges and questions to which you seek knowledge, seek me out and I will tell you what I can."); const char *TEXT_153 = N_("Griswold - a man of great action and great courage. I bet he never told you about the time he went into the Labyrinth to save Wirt, did he? He knows his fair share of the dangers to be found there, but then again - so do you. He is a skilled craftsman, and if he claims to be able to help you in any way, you can count on his honesty and his skill."); const char *TEXT_154 = N_("Ogden has owned and run the Rising Sun Inn and Tavern for almost four years now. He purchased it just a few short months before everything here went to hell. He and his wife Garda do not have the money to leave as they invested all they had in making a life for themselves here. He is a good man with a deep sense of responsibility."); const char *TEXT_155 = N_("Poor Farnham. He is a disquieting reminder of the doomed assembly that entered into the Cathedral with Lazarus on that dark day. He escaped with his life, but his courage and much of his sanity were left in some dark pit. He finds comfort only at the bottom of his tankard nowadays, but there are occasional bits of truth buried within his constant ramblings."); const char *TEXT_156 = N_("The witch, Adria, is an anomaly here in Tristram. She arrived shortly after the Cathedral was desecrated while most everyone else was fleeing. She had a small hut constructed at the edge of town, seemingly overnight, and has access to many strange and arcane artifacts and tomes of knowledge that even I have never seen before."); const char *TEXT_157 = N_("The story of Wirt is a frightening and tragic one. He was taken from the arms of his mother and dragged into the labyrinth by the small, foul demons that wield wicked spears. There were many other children taken that day, including the son of King Leoric. The Knights of the palace went below, but never returned. The Blacksmith found the boy, but only after the foul beasts had begun to torture him for their sadistic pleasures."); const char *TEXT_158 = N_("Ah, Pepin. I count him as a true friend - perhaps the closest I have here. He is a bit addled at times, but never a more caring or considerate soul has existed. His knowledge and skills are equaled by few, and his door is always open."); const char *TEXT_159 = N_("Gillian is a fine woman. Much adored for her high spirits and her quick laugh, she holds a special place in my heart. She stays on at the tavern to support her elderly grandmother who is too sick to travel. I sometimes fear for her safety, but I know that any man in the village would rather die than see her harmed."); const char *TEXT_160 = N_("Greetings, good master. Welcome to the Tavern of the Rising Sun!"); const char *TEXT_161 = N_("Many adventurers have graced the tables of my tavern, and ten times as many stories have been told over as much ale. The only thing that I ever heard any of them agree on was this old axiom. Perhaps it will help you. You can cut the flesh, but you must crush the bone."); const char *TEXT_162 = N_("Griswold the blacksmith is extremely knowledgeable about weapons and armor. If you ever need work done on your gear, he is definitely the man to see."); const char *TEXT_163 = N_("Farnham spends far too much time here, drowning his sorrows in cheap ale. I would make him leave, but he did suffer so during his time in the Labyrinth."); const char *TEXT_164 = N_("Adria is wise beyond her years, but I must admit - she frightens me a little. \n \nWell, no matter. If you ever have need to trade in items of sorcery, she maintains a strangely well-stocked hut just across the river."); const char *TEXT_165 = N_("If you want to know more about the history of our village, the storyteller Cain knows quite a bit about the past."); const char *TEXT_166 = N_("Wirt is a rapscallion and a little scoundrel. He was always getting into trouble, and it's no surprise what happened to him. \n \nHe probably went fooling about someplace that he shouldn't have been. I feel sorry for the boy, but I don't abide the company that he keeps."); const char *TEXT_167 = N_("Pepin is a good man - and certainly the most generous in the village. He is always attending to the needs of others, but trouble of some sort or another does seem to follow him wherever he goes..."); const char *TEXT_168 = N_("Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, she would have fled from here long ago. \n \nGoodness knows I begged her to leave, telling her that I would watch after the old woman, but she is too sweet and caring to have done so."); const char *TEXT_169 = N_("What ails you, my friend?"); const char *TEXT_170 = N_("I have made a very interesting discovery. Unlike us, the creatures in the Labyrinth can heal themselves without the aid of potions or magic. If you hurt one of the monsters, make sure it is dead or it very well may regenerate itself."); const char *TEXT_171 = N_("Before it was taken over by, well, whatever lurks below, the Cathedral was a place of great learning. There are many books to be found there. If you find any, you should read them all, for some may hold secrets to the workings of the Labyrinth."); const char *TEXT_172 = N_("Griswold knows as much about the art of war as I do about the art of healing. He is a shrewd merchant, but his work is second to none. Oh, I suppose that may be because he is the only blacksmith left here."); const char *TEXT_173 = N_("Cain is a true friend and a wise sage. He maintains a vast library and has an innate ability to discern the true nature of many things. If you ever have any questions, he is the person to go to."); const char *TEXT_174 = N_("Even my skills have been unable to fully heal Farnham. Oh, I have been able to mend his body, but his mind and spirit are beyond anything I can do."); const char *TEXT_175 = N_("While I use some limited forms of magic to create the potions and elixirs I store here, Adria is a true sorceress. She never seems to sleep, and she always has access to many mystic tomes and artifacts. I believe her hut may be much more than the hovel it appears to be, but I can never seem to get inside the place."); const char *TEXT_176 = N_("Poor Wirt. I did all that was possible for the child, but I know he despises that wooden peg that I was forced to attach to his leg. His wounds were hideous. No one - and especially such a young child - should have to suffer the way he did."); const char *TEXT_177 = N_("I really don't understand why Ogden stays here in Tristram. He suffers from a slight nervous condition, but he is an intelligent and industrious man who would do very well wherever he went. I suppose it may be the fear of the many murders that happen in the surrounding countryside, or perhaps the wishes of his wife that keep him and his family where they are."); const char *TEXT_178 = N_("Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers from delusions. \n \nShe claims that they are visions, but I have no proof of that one way or the other."); const char *TEXT_179 = N_("Good day! How may I serve you?"); const char *TEXT_180 = N_("My grandmother had a dream that you would come and talk to me. She has visions, you know and can see into the future."); const char *TEXT_181 = N_("The woman at the edge of town is a witch! She seems nice enough, and her name, Adria, is very pleasing to the ear, but I am very afraid of her. \n \nIt would take someone quite brave, like you, to see what she is doing out there."); const char *TEXT_182 = N_("Our Blacksmith is a point of pride to the people of Tristram. Not only is he a master craftsman who has won many contests within his guild, but he received praises from our King Leoric himself - may his soul rest in peace. Griswold is also a great hero; just ask Cain."); const char *TEXT_183 = N_("Cain has been the storyteller of Tristram for as long as I can remember. He knows so much, and can tell you just about anything about almost everything."); const char *TEXT_184 = N_("Farnham is a drunkard who fills his belly with ale and everyone else's ears with nonsense. \n \nI know that both Pepin and Ogden feel sympathy for him, but I get so frustrated watching him slip farther and farther into a befuddled stupor every night."); const char *TEXT_185 = N_("Pepin saved my grandmother's life, and I know that I can never repay him for that. His ability to heal any sickness is more powerful than the mightiest sword and more mysterious than any spell you can name. If you ever are in need of healing, Pepin can help you."); const char *TEXT_186 = N_("I grew up with Wirt's mother, Canace. Although she was only slightly hurt when those hideous creatures stole him, she never recovered. I think she died of a broken heart. Wirt has become a mean-spirited youngster, looking only to profit from the sweat of others. I know that he suffered and has seen horrors that I cannot even imagine, but some of that darkness hangs over him still."); const char *TEXT_187 = N_("Ogden and his wife have taken me and my grandmother into their home and have even let me earn a few gold pieces by working at the inn. I owe so much to them, and hope one day to leave this place and help them start a grand hotel in the east."); const char *TEXT_188 = N_("Well, what can I do for ya?"); const char *TEXT_189 = N_("If you're looking for a good weapon, let me show this to you. Take your basic blunt weapon, such as a mace. Works like a charm against most of those undying horrors down there, and there's nothing better to shatter skinny little skeletons!"); const char *TEXT_190 = N_("The axe? Aye, that's a good weapon, balanced against any foe. Look how it cleaves the air, and then imagine a nice fat demon head in its path. Keep in mind, however, that it is slow to swing - but talk about dealing a heavy blow!"); const char *TEXT_191 = N_("Look at that edge, that balance. A sword in the right hands, and against the right foe, is the master of all weapons. Its keen blade finds little to hack or pierce on the undead, but against a living, breathing enemy, a sword will better slice their flesh!"); const char *TEXT_192 = N_("Your weapons and armor will show the signs of your struggles against the Darkness. If you bring them to me, with a bit of work and a hot forge, I can restore them to top fighting form."); const char *TEXT_193 = N_("While I have to practically smuggle in the metals and tools I need from caravans that skirt the edges of our damned town, that witch, Adria, always seems to get whatever she needs. If I knew even the smallest bit about how to harness magic as she did, I could make some truly incredible things."); const char *TEXT_194 = N_("Gillian is a nice lass. Shame that her gammer is in such poor health or I would arrange to get both of them out of here on one of the trading caravans."); const char *TEXT_195 = N_("Sometimes I think that Cain talks too much, but I guess that is his calling in life. If I could bend steel as well as he can bend your ear, I could make a suit of court plate good enough for an Emperor!"); const char *TEXT_196 = N_("I was with Farnham that night that Lazarus led us into Labyrinth. I never saw the Archbishop again, and I may not have survived if Farnham was not at my side. I fear that the attack left his soul as crippled as, well, another did my leg. I cannot fight this battle for him now, but I would if I could."); const char *TEXT_197 = N_("A good man who puts the needs of others above his own. You won't find anyone left in Tristram - or anywhere else for that matter - who has a bad thing to say about the healer."); const char *TEXT_198 = N_("That lad is going to get himself into serious trouble... or I guess I should say, again. I've tried to interest him in working here and learning an honest trade, but he prefers the high profits of dealing in goods of dubious origin. I cannot hold that against him after what happened to him, but I do wish he would at least be careful."); const char *TEXT_199 = N_("The Innkeeper has little business and no real way of turning a profit. He manages to make ends meet by providing food and lodging for those who occasionally drift through the village, but they are as likely to sneak off into the night as they are to pay him. If it weren't for the stores of grains and dried meats he kept in his cellar, why, most of us would have starved during that first year when the entire countryside was overrun by demons."); const char *TEXT_200 = N_("Can't a fella drink in peace?"); const char *TEXT_201 = N_("The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too."); const char *TEXT_202 = N_("Why don't that old crone do somethin' for a change. Sure, sure, she's got stuff, but you listen to me... she's unnatural. I ain't never seen her eat or drink - and you can't trust somebody who doesn't drink at least a little."); const char *TEXT_203 = N_("Cain isn't what he says he is. Sure, sure, he talks a good story... some of 'em are real scary or funny... but I think he knows more than he knows he knows."); const char *TEXT_204 = N_("Griswold? Good old Griswold. I love him like a brother! We fought together, you know, back when... we... Lazarus... Lazarus... Lazarus!!!"); const char *TEXT_205 = N_("Hehehe, I like Pepin. He really tries, you know. Listen here, you should make sure you get to know him. Good fella like that with people always wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a hero too..."); const char *TEXT_206 = N_("Wirt is a kid with more problems than even me, and I know all about problems. Listen here - that kid is gotta sweet deal, but he's been there, you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..."); const char *TEXT_207 = N_("Ogden is the best man in town. I don't think his wife likes me much, but as long as she keeps tappin' kegs, I'll like her just fine. Seems like I been spendin' more time with Ogden than most, but he's so good to me..."); const char *TEXT_208 = N_("I wanna tell ya sumthin', 'cause I know all about this stuff. It's my specialty. This here is the best... theeeee best! That other ale ain't no good since those stupid dogs..."); const char *TEXT_209 = N_("No one ever lis... listens to me. Somewhere - I ain't too sure - but somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and just waitin' for someone to get it."); const char *TEXT_210 = N_("I know you gots your own ideas, and I know you're not gonna believe this, but that weapon you got there - it just ain't no good against those big brutes! Oh, I don't care what Griswold says, they can't make anything like they used to in the old days..."); const char *TEXT_211 = N_("If I was you... and I ain't... but if I was, I'd sell all that stuff you got and get out of here. That boy out there... He's always got somethin' good, but you gotta give him some gold or he won't even show you what he's got."); const char *TEXT_212 = N_("I sense a soul in search of answers..."); const char *TEXT_213 = N_("Wisdom is earned, not given. If you discover a tome of knowledge, devour its words. Should you already have knowledge of the arcane mysteries scribed within a book, remember - that level of mastery can always increase."); const char *TEXT_214 = N_("The greatest power is often the shortest lived. You may find ancient words of power written upon scrolls of parchment. The strength of these scrolls lies in the ability of either apprentice or adept to cast them with equal ability. Their weakness is that they must first be read aloud and can never be kept at the ready in your mind. Know also that these scrolls can be read but once, so use them with care."); const char *TEXT_215 = N_("Though the heat of the sun is beyond measure, the mere flame of a candle is of greater danger. No energies, no matter how great, can be used without the proper focus. For many spells, ensorcelled Staves may be charged with magical energies many times over. I have the ability to restore their power - but know that nothing is done without a price."); const char *TEXT_216 = N_("The sum of our knowledge is in the sum of its people. Should you find a book or scroll that you cannot decipher, do not hesitate to bring it to me. If I can make sense of it I will share what I find."); const char *TEXT_217 = N_("To a man who only knows Iron, there is no greater magic than Steel. The blacksmith Griswold is more of a sorcerer than he knows. His ability to meld fire and metal is unequaled in this land."); const char *TEXT_218 = N_("Corruption has the strength of deceit, but innocence holds the power of purity. The young woman Gillian has a pure heart, placing the needs of her matriarch over her own. She fears me, but it is only because she does not understand me."); const char *TEXT_219 = N_("A chest opened in darkness holds no greater treasure than when it is opened in the light. The storyteller Cain is an enigma, but only to those who do not look. His knowledge of what lies beneath the cathedral is far greater than even he allows himself to realize."); const char *TEXT_220 = N_("The higher you place your faith in one man, the farther it has to fall. Farnham has lost his soul, but not to any demon. It was lost when he saw his fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to be gleaned, but you must separate fact from fantasy."); const char *TEXT_221 = N_("The hand, the heart and the mind can perform miracles when they are in perfect harmony. The healer Pepin sees into the body in a way that even I cannot. His ability to restore the sick and injured is magnified by his understanding of the creation of elixirs and potions. He is as great an ally as you have in Tristram."); const char *TEXT_222 = N_("There is much about the future we cannot see, but when it comes it will be the children who wield it. The boy Wirt has a blackness upon his soul, but he poses no threat to the town or its people. His secretive dealings with the urchins and unspoken guilds of nearby towns gain him access to many devices that cannot be easily found in Tristram. While his methods may be reproachful, Wirt can provide assistance for your battle against the encroaching Darkness."); const char *TEXT_223 = N_("Earthen walls and thatched canopy do not a home create. The innkeeper Ogden serves more of a purpose in this town than many understand. He provides shelter for Gillian and her matriarch, maintains what life Farnham has left to him, and provides an anchor for all who are left in the town to what Tristram once was. His tavern, and the simple pleasures that can still be found there, provide a glimpse of a life that the people here remember. It is that memory that continues to feed their hopes for your success."); const char *TEXT_224 = N_("Pssst... over here..."); const char *TEXT_225 = N_("Not everyone in Tristram has a use - or a market - for everything you will find in the labyrinth. Not even me, as hard as that is to believe. \n \nSometimes, only you will be able to find a purpose for some things."); const char *TEXT_226 = N_("Don't trust everything the drunk says. Too many ales have fogged his vision and his good sense."); const char *TEXT_227 = N_("In case you haven't noticed, I don't buy anything from Tristram. I am an importer of quality goods. If you want to peddle junk, you'll have to see Griswold, Pepin or that witch, Adria. I'm sure that they will snap up whatever you can bring them..."); const char *TEXT_228 = N_("I guess I owe the blacksmith my life - what there is of it. Sure, Griswold offered me an apprenticeship at the smithy, and he is a nice enough guy, but I'll never get enough money to... well, let's just say that I have definite plans that require a large amount of gold."); const char *TEXT_229 = N_("If I were a few years older, I would shower her with whatever riches I could muster, and let me assure you I can get my hands on some very nice stuff. Gillian is a beautiful girl who should get out of Tristram as soon as it is safe. Hmmm... maybe I'll take her with me when I go..."); const char *TEXT_230 = N_("Cain knows too much. He scares the life out of me - even more than that woman across the river. He keeps telling me about how lucky I am to be alive, and how my story is foretold in legend. I think he's off his crock."); const char *TEXT_231 = N_("Farnham - now there is a man with serious problems, and I know all about how serious problems can be. He trusted too much in the integrity of one man, and Lazarus led him into the very jaws of death. Oh, I know what it's like down there, so don't even start telling me about your plans to destroy the evil that dwells in that Labyrinth. Just watch your legs..."); const char *TEXT_232 = N_("As long as you don't need anything reattached, old Pepin is as good as they come. \n \nIf I'd have had some of those potions he brews, I might still have my leg..."); const char *TEXT_233 = N_("Adria truly bothers me. Sure, Cain is creepy in what he can tell you about the past, but that witch can see into your past. She always has some way to get whatever she needs, too. Adria gets her hands on more merchandise than I've seen pass through the gates of the King's Bazaar during High Festival."); const char *TEXT_234 = N_("Ogden is a fool for staying here. I could get him out of town for a very reasonable price, but he insists on trying to make a go of it with that stupid tavern. I guess at the least he gives Gillian a place to work, and his wife Garda does make a superb Shepherd's pie..."); const char *TEXT_235 = N_("Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any who would seek to steal the treasures secured within this room. So speaks the Lord of Terror, and so it is written."); const char *TEXT_236 = N_("...and so, locked beyond the Gateway of Blood and past the Hall of Fire, Valor awaits for the Hero of Light to awaken..."); const char *TEXT_237 = N_("I can see what you see not.\nVision milky then eyes rot.\nWhen you turn they will be gone,\nWhispering their hidden song.\nThen you see what cannot be,\nShadows move where light should be.\nOut of darkness, out of mind,\nCast down into the Halls of the Blind."); const char *TEXT_238 = N_("The armories of Hell are home to the Warlord of Blood. In his wake lay the mutilated bodies of thousands. Angels and men alike have been cut down to fulfill his endless sacrifices to the Dark ones who scream for one thing - blood."); const char *TEXT_249 = N_("Take heed and bear witness to the truths that lie herein, for they are the last legacy of the Horadrim. There is a war that rages on even now, beyond the fields that we know - between the utopian kingdoms of the High Heavens and the chaotic pits of the Burning Hells. This war is known as the Great Conflict, and it has raged and burned longer than any of the stars in the sky. Neither side ever gains sway for long as the forces of Light and Darkness constantly vie for control over all creation."); const char *TEXT_250 = N_("Take heed and bear witness to the truths that lie herein, for they are the last legacy of the Horadrim. When the Eternal Conflict between the High Heavens and the Burning Hells falls upon mortal soil, it is called the Sin War. Angels and Demons walk amongst humanity in disguise, fighting in secret, away from the prying eyes of mortals. Some daring, powerful mortals have even allied themselves with either side, and helped to dictate the course of the Sin War."); const char *TEXT_251 = N_("Take heed and bear witness to the truths that lie herein, for they are the last legacy of the Horadrim. Nearly three hundred years ago, it came to be known that the Three Prime Evils of the Burning Hells had mysteriously come to our world. The Three Brothers ravaged the lands of the east for decades, while humanity was left trembling in their wake. Our Order - the Horadrim - was founded by a group of secretive magi to hunt down and capture the Three Evils once and for all.\n \nThe original Horadrim captured two of the Three within powerful artifacts known as Soulstones and buried them deep beneath the desolate eastern sands. The third Evil escaped capture and fled to the west with many of the Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - was eventually captured, his essence set in a Soulstone and buried within this Labyrinth.\n \nBe warned that the soulstone must be kept from discovery by those not of the faith. If Diablo were to be released, he would seek a body that is easily controlled as he would be very weak - perhaps that of an old man or a child."); const char *TEXT_252 = N_("So it came to be that there was a great revolution within the Burning Hells known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils and banished their spirit forms to the mortal realm. The demons Belial (the Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of Hell during the absence of the Three Brothers. All of Hell polarized between the factions of Belial and Azmodan while the forces of the High Heavens continually battered upon the very Gates of Hell."); const char *TEXT_253 = N_("Many demons traveled to the mortal realm in search of the Three Brothers. These demons were followed to the mortal plane by Angels who hunted them throughout the vast cities of the East. The Angels allied themselves with a secretive Order of mortal magi named the Horadrim, who quickly became adept at hunting demons. They also made many dark enemies in the underworlds."); const char *TEXT_254 = N_("So it came to be that the Three Prime Evils were banished in spirit form to the mortal realm and after sewing chaos across the East for decades, they were hunted down by the cursed Order of the mortal Horadrim. The Horadrim used artifacts called Soulstones to contain the essence of Mephisto, the Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest brother - Diablo, the Lord of Terror - escaped to the west.\n \nEventually the Horadrim captured Diablo within a Soulstone as well, and buried him under an ancient, forgotten Cathedral. There, the Lord of Terror sleeps and awaits the time of his rebirth. Know ye that he will seek a body of youth and power to possess - one that is innocent and easily controlled. He will then arise to free his Brothers and once more fan the flames of the Sin War..."); const char *TEXT_255 = N_("All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When he awakened from his long slumber, my Lord and Master spoke to me of secrets that few mortals know. He told me the kingdoms of the High Heavens and the pits of the Burning Hells engage in an eternal war. He revealed the powers that have brought this discord to the realms of man. My lord has named the battle for this world and all who exist here the Sin War."); const char *TEXT_256 = N_("Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished to this world long ago. My Lord wishes to bide his time and harness his awesome power so that he may free his captive brothers from their tombs beneath the sands of the east. Once my Lord releases his Brothers, the Sin War will once again know the fury of the Three."); const char *TEXT_257 = N_("Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I awoke my Master from his sleep, he attempted to possess a mortal's form. Diablo attempted to claim the body of King Leoric, but my Master was too weak from his imprisonment. My Lord required a simple and innocent anchor to this world, and so found the boy Albrecht to be perfect for the task. While the good King Leoric was left maddened by Diablo's unsuccessful possession, I kidnapped his son Albrecht and brought him before my Master. I now await Diablo's call and pray that I will be rewarded when he at last emerges as the Lord of this world."); const char *TEXT_258 = N_("Thank goodness you've returned!\nMuch has changed since you lived here, my friend. All was peaceful until the dark riders came and destroyed our village. Many were cut down where they stood, and those who took up arms were slain or dragged away to become slaves - or worse. The church at the edge of town has been desecrated and is being used for dark rituals. The screams that echo in the night are inhuman, but some of our townsfolk may yet survive. Follow the path that lies between my tavern and the blacksmith shop to find the church and save who you can. \n \nPerhaps I can tell you more if we speak again. Good luck."); const char *TEXT_267 = N_("Maintain your quest. Finding a treasure that is lost is not easy. Finding a treasure that is hidden less so. I will leave you with this. Do not let the sands of time confuse your search."); const char *TEXT_268 = N_("A what?! This is foolishness. There's no treasure buried here in Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They don't match our town at all. I'd keep my mind on what lies below the cathedral and not what lies below our topsoil."); const char *TEXT_269 = N_("I really don't have time to discuss some map you are looking for. I have many sick people that require my help and yours as well."); const char *TEXT_270 = N_("The once proud Iswall is trapped deep beneath the surface of this world. His honor stripped and his visage altered. He is trapped in immortal torment. Charged to conceal the very thing that could free him."); const char *TEXT_271 = N_("I'll bet that Wirt saw you coming and put on an act just so he could laugh at you later when you were running around the town with your nose in the dirt. I'd ignore it."); const char *TEXT_272 = N_("There was a time when this town was a frequent stop for travelers from far and wide. Much has changed since then. But hidden caves and buried treasure are common fantasies of any child. Wirt seldom indulges in youthful games. So it may just be his imagination."); const char *TEXT_273 = N_("Listen here. Come close. I don't know if you know what I know, but you've have really got something here. That's a map."); const char *TEXT_274 = N_("My grandmother often tells me stories about the strange forces that inhabit the graveyard outside of the church. And it may well interest you to hear one of them. She said that if you were to leave the proper offering in the cemetery, enter the cathedral to pray for the dead, and then return, the offering would be altered in some strange way. I don't know if this is just the talk of an old sick woman, but anything seems possible these days."); const char *TEXT_275 = N_("Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be interested in picking up a few things from you. Or better yet, don't you need some rare and expensive supplies to get you through this ordeal?"); const char *TEXT_277 = N_("So, you're the hero everyone's been talking about. Perhaps you could help a poor, simple farmer out of a terrible mess? At the edge of my orchard, just south of here, there's a horrible thing swelling out of the ground! I can't get to my crops or my bales of hay, and my poor cows will starve. The witch gave this to me and said that it would blast that thing out of my field. If you could destroy it, I would be forever grateful. I'd do it myself, but someone has to stay here with the cows..."); const char *TEXT_278 = N_("I knew that it couldn't be as simple as that witch made it sound. It's a sad world when you can't even trust your neighbors."); const char *TEXT_279 = N_("Is it gone? Did you send it back to the dark recesses of Hades that spawned it? You what? Oh, don't tell me you lost it! Those things don't come cheap, you know. You've got to find it, and then blast that horror out of our town."); const char *TEXT_280 = N_("I heard the explosion from here! Many thanks to you, kind stranger. What with all these things comin' out of the ground, monsters taking over the church, and so forth, these are trying times. I am but a poor farmer, but here -- take this with my great thanks."); const char *TEXT_281 = N_("Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with all the other troubles. Maybe after you've cleansed the church of some of those creatures you could come back... and spare a little time to help a poor farmer?"); const char *TEXT_282 = N_("Waaaah! (sniff) Waaaah! (sniff)"); const char *TEXT_283 = N_("I lost Theo! I lost my best friend! We were playing over by the river, and Theo said he wanted to go look at the big green thing. I said we shouldn't, but we snuck over there, and then suddenly this BUG came out! We ran away but Theo fell down and the bug GRABBED him and took him away!"); const char *TEXT_284 = N_("Didja find him? You gotta find Theodore, please! He's just little. He can't take care of himself! Please!"); const char *TEXT_285 = N_("You found him! You found him! Thank you! Oh Theo, did those nasty bugs scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, Theo, let's go home! Thanks again, hero person!"); const char *TEXT_286 = N_("We have long lain dormant, and the time to awaken has come. After our long sleep, we are filled with great hunger. Soon, now, we shall feed..."); const char *TEXT_287 = N_("Have you been enjoying yourself, little mammal? How pathetic. Your little world will be no challenge at all."); const char *TEXT_288 = N_("These lands shall be defiled, and our brood shall overrun the fields that men call home. Our tendrils shall envelop this world, and we will feast on the flesh of its denizens. Man shall become our chattel and sustenance."); const char *TEXT_289 = N_("Ah, I can smell you...you are close! Close! Ssss...the scent of blood and fear...how enticing..."); const char *TEXT_296 = N_("And in the year of the Golden Light, it was so decreed that a great Cathedral be raised. The cornerstone of this holy place was to be carved from the translucent stone Antyrael, named for the Angel who shared his power with the Horadrim. \n \nIn the Year of Drawing Shadows, the ground shook and the Cathedral shattered and fell. As the building of catacombs and castles began and man stood against the ravages of the Sin War, the ruins were scavenged for their stones. And so it was that the cornerstone vanished from the eyes of man. \n \nThe stone was of this world -- and of all worlds -- as the Light is both within all things and beyond all things. Light and unity are the products of this holy foundation, a unity of purpose and a unity of possession."); const char *TEXT_297 = N_("Moo."); const char *TEXT_298 = N_("I said, Moo."); const char *TEXT_299 = N_("Look I'm just a cow, OK?"); const char *TEXT_300 = N_("All right, all right. I'm not really a cow. I don't normally go around like this; but, I was sitting at home minding my own business and all of a sudden these bugs & vines & bulbs & stuff started coming out of the floor... it was horrible! If only I had something normal to wear, it wouldn't be so bad. Hey! Could you go back to my place and get my suit for me? The brown one, not the gray one, that's for evening wear. I'd do it myself, but I don't want anyone seeing me like this. Here, take this, you might need it... to kill those things that have overgrown everything. You can't miss my house, it's just south of the fork in the river... you know... the one with the overgrown vegetable garden."); const char *TEXT_301 = N_("What are you wasting time for? Go get my suit! And hurry! That Holstein over there keeps winking at me!"); const char *TEXT_302 = N_("Hey, have you got my suit there? Quick, pass it over! These ears itch like you wouldn't believe!"); const char *TEXT_303 = N_("No no no no! This is my GRAY suit! It's for evening wear! Formal occasions! I can't wear THIS. What are you, some kind of weirdo? I need the BROWN suit."); const char *TEXT_304 = N_("Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on straight? Good. Look, thanks a lot for helping me out. Here, take this as a gift; and, you know... a little fashion tip... you could use a little... you could use a new... yknowwhatImean? The whole adventurer motif is just so... retro. Just a word of advice, eh? Ciao."); const char *TEXT_305 = N_("Look. I'm a cow. And you, you're monster bait. Get some experience under your belt! We'll talk..."); const char *TEXT_307 = N_("It must truly be a fearsome task I've set before you. If there was just some way that I could... would a flagon of some nice, fresh milk help?"); const char *TEXT_308 = N_("Oh, I could use your help, but perhaps after you've saved the catacombs from the desecration of those beasts."); const char *TEXT_309 = N_("I need something done, but I couldn't impose on a perfect stranger. Perhaps after you've been here a while I might feel more comfortable asking a favor."); const char *TEXT_310 = N_("I see in you the potential for greatness. Perhaps sometime while you are fulfilling your destiny, you could stop by and do a little favor for me?"); const char *TEXT_311 = N_("I think you could probably help me, but perhaps after you've gotten a little more powerful. I wouldn't want to injure the village's only chance to destroy the menace in the church!"); const char *TEXT_312 = N_("Me, I'm a self-made cow. Make something of yourself, and... then we'll talk."); const char *TEXT_313 = N_("I don't have to explain myself to every tourist that walks by! Don't you have some monsters to kill? Maybe we'll talk later. If you live..."); const char *TEXT_314 = N_("Quit bugging me. I'm looking for someone really heroic. And you're not it. I can't trust you, you're going to get eaten by monsters any day now... I need someone who's an experienced hero."); const char *TEXT_315 = N_("All right, I'll cut the bull. I didn't mean to steer you wrong. I was sitting at home, feeling moo-dy, when things got really un-stable; a whole stampede of monsters came out of the floor! I just cowed. I just happened to be wearing this Jersey when I ran out the door, and now I look udderly ridiculous. If only I had something normal to wear, it wouldn't be so bad. Hey! Can you go back to my place and get my suit for me? The brown one, not the gray one, that's for evening wear. I'd do it myself, but I don't want anyone seeing me like this. Here, take this, you might need it... to kill those things that have overgrown everything. You can't miss my house, it's just south of the fork in the river... you know... the one with the overgrown vegetable garden."); const char *TEXT_317 = N_("I have tried spells, threats, abjuration and bargaining with this foul creature -- to no avail. My methods of enslaving lesser demons seem to have no effect on this fearsome beast."); const char *TEXT_318 = N_("My home is slowly becoming corrupted by the vileness of this unwanted prisoner. The crypts are full of shadows that move just beyond the corners of my vision. The faint scrabble of claws dances at the edges of my hearing. They are searching, I think, for this journal."); const char *TEXT_319 = N_("In its ranting, the creature has let slip its name -- Na-Krul. I have attempted to research the name, but the smaller demons have somehow destroyed my library. Na-Krul... The name fills me with a cold dread. I prefer to think of it only as The Creature rather than ponder its true name."); const char *TEXT_320 = N_("The entrapped creature's howls of fury keep me from gaining much needed sleep. It rages against the one who sent it to the Void, and it calls foul curses upon me for trapping it here. Its words fill my heart with terror, and yet I cannot block out its voice."); const char *TEXT_321 = N_("My time is quickly running out. I must record the ways to weaken the demon, and then conceal that text, lest his minions find some way to use my knowledge to free their lord. I hope that whoever finds this journal will seek the knowledge."); const char *TEXT_322 = N_("Whoever finds this scroll is charged with stopping the demonic creature that lies within these walls. My time is over. Even now, its hellish minions claw at the frail door behind which I hide. \n \nI have hobbled the demon with arcane magic and encased it within great walls, but I fear that will not be enough. \n \nThe spells found in my three grimoires will provide you protected entrance to his domain, but only if cast in their proper sequence. The levers at the entryway will remove the barriers and free the demon; touch them not! Use only these spells to gain entry or his power may be too great for you to defeat."); const char *TEXT_323 = N_("In Spiritu Sanctum."); const char *TEXT_324 = N_("Praedictum Otium."); const char *TEXT_325 = N_("Efficio Obitus Ut Inimicus."); const char *MT_HELLBOAR_NAME = P_("monster", "Hellboar"); const char *MT_STINGER_NAME = P_("monster", "Stinger"); const char *MT_PSYCHORB_NAME = P_("monster", "Psychorb"); const char *MT_ARACHNON_NAME = P_("monster", "Arachnon"); const char *MT_FELLTWIN_NAME = P_("monster", "Felltwin"); const char *MT_HORKSPWN_NAME = P_("monster", "Hork Spawn"); const char *MT_VENMTAIL_NAME = P_("monster", "Venomtail"); const char *MT_NECRMORB_NAME = P_("monster", "Necromorb"); const char *MT_SPIDLORD_NAME = P_("monster", "Spider Lord"); const char *MT_LASHWORM_NAME = P_("monster", "Lashworm"); const char *MT_TORCHANT_NAME = P_("monster", "Torchant"); const char *MT_DEFILER_NAME = P_("monster", "Hell Bug"); const char *MT_GRAVEDIG_NAME = P_("monster", "Gravedigger"); const char *MT_TOMBRAT_NAME = P_("monster", "Tomb Rat"); const char *MT_FIREBAT_NAME = P_("monster", "Firebat"); const char *MT_SKLWING_NAME = P_("monster", "Skullwing"); const char *MT_LICH_NAME = P_("monster", "Lich"); const char *MT_CRYPTDMN_NAME = P_("monster", "Crypt Demon"); const char *MT_HELLBAT_NAME = P_("monster", "Hellbat"); const char *MT_BONEDEMN_NAME = P_("monster", "Bone Demon"); const char *MT_ARCHLICH_NAME = P_("monster", "Arch Lich"); const char *MT_BICLOPS_NAME = P_("monster", "Biclops"); const char *MT_FLESTHNG_NAME = P_("monster", "Flesh Thing"); const char *MT_REAPER_NAME = P_("monster", "Reaper"); const char *UNIQUE_ITEM_90_NAME = N_("Giant's Knuckle"); const char *UNIQUE_ITEM_91_NAME = N_("Mercurial Ring"); const char *UNIQUE_ITEM_92_NAME = N_("Xorine's Ring"); const char *UNIQUE_ITEM_93_NAME = N_("Karik's Ring"); const char *UNIQUE_ITEM_94_NAME = N_("Ring of Magma"); const char *UNIQUE_ITEM_95_NAME = N_("Ring of the Mystics"); const char *UNIQUE_ITEM_96_NAME = N_("Ring of Thunder"); const char *UNIQUE_ITEM_97_NAME = N_("Amulet of Warding"); const char *UNIQUE_ITEM_98_NAME = N_("Gnat Sting"); const char *UNIQUE_ITEM_99_NAME = N_("Flambeau"); const char *UNIQUE_ITEM_100_NAME = N_("Armor of Gloom"); const char *UNIQUE_ITEM_101_NAME = N_("Blitzen"); const char *UNIQUE_ITEM_102_NAME = N_("Thunderclap"); const char *UNIQUE_ITEM_103_NAME = N_("Shirotachi"); const char *UNIQUE_ITEM_104_NAME = N_("Eater of Souls"); const char *UNIQUE_ITEM_105_NAME = N_("Diamondedge"); const char *UNIQUE_ITEM_106_NAME = N_("Bone Chain Armor"); const char *UNIQUE_ITEM_107_NAME = N_("Demon Plate Armor"); const char *UNIQUE_ITEM_108_NAME = N_("Acolyte's Amulet"); const char *UNIQUE_ITEM_109_NAME = N_("Gladiator's Ring"); const char *ITEM_PREFIX_83_NAME = N_("Jester's"); const char *ITEM_PREFIX_84_NAME = N_("Crystalline"); const char *ITEM_PREFIX_85_NAME = N_("Doppelganger's"); const char *ITEM_SUFFIX_95_NAME = N_("devastation"); const char *ITEM_SUFFIX_96_NAME = N_("decay"); const char *ITEM_SUFFIX_97_NAME = N_("peril"); const char *SPELL_MANA_NAME = P_("spell", "Mana"); const char *SPELL_THE_MAGI_NAME = P_("spell", "the Magi"); const char *SPELL_THE_JESTER_NAME = P_("spell", "the Jester"); const char *SPELL_LIGHTNING_WALL_NAME = P_("spell", "Lightning Wall"); const char *SPELL_IMMOLATION_NAME = P_("spell", "Immolation"); const char *SPELL_WARP_NAME = P_("spell", "Warp"); const char *SPELL_REFLECT_NAME = P_("spell", "Reflect"); const char *SPELL_BERSERK_NAME = P_("spell", "Berserk"); const char *SPELL_RING_OF_FIRE_NAME = P_("spell", "Ring of Fire"); const char *SPELL_SEARCH_NAME = P_("spell", "Search"); const char *SPELL_RUNE_OF_FIRE_NAME = P_("spell", "Rune of Fire"); const char *SPELL_RUNE_OF_LIGHT_NAME = P_("spell", "Rune of Light"); const char *SPELL_RUNE_OF_NOVA_NAME = P_("spell", "Rune of Nova"); const char *SPELL_RUNE_OF_IMMOLATION_NAME = P_("spell", "Rune of Immolation"); const char *SPELL_RUNE_OF_STONE_NAME = P_("spell", "Rune of Stone"); } // namespace ================================================ FILE: Source/utils/algorithm/container.hpp ================================================ #pragma once #include #include #include #include namespace devilution { // Internal namespace that sets up ADL lookup and the container Iterator type. namespace container_internal { using std::begin; using std::end; template using Iterator = decltype(begin(std::declval())); template using Difference = typename std::iterator_traits>::difference_type; template Iterator c_begin(C &c) { return begin(c); } template Iterator c_end(C &c) { return end(c); } } // namespace container_internal template bool c_any_of(const C &c, Predicate &&predicate) { return std::any_of(container_internal::begin(c), container_internal::end(c), std::forward(predicate)); } template bool c_all_of(const C &c, Predicate &&predicate) { return std::all_of(container_internal::begin(c), container_internal::end(c), std::forward(predicate)); } template bool c_none_of(const C &c, Predicate &&predicate) { return std::none_of(container_internal::begin(c), container_internal::end(c), std::forward(predicate)); } template container_internal::Iterator c_find(C &c, T &&value) { return std::find(container_internal::begin(c), container_internal::end(c), std::forward(value)); } template container_internal::Iterator c_find_if(C &c, Predicate &&predicate) { return std::find_if(container_internal::begin(c), container_internal::end(c), std::forward(predicate)); } template container_internal::Difference c_count_if(const C &c, Predicate &&predicate) { return std::count_if(container_internal::c_begin(c), container_internal::c_end(c), std::forward(predicate)); } template container_internal::Difference c_count(const C &c, T &&value) { return std::count(container_internal::c_begin(c), container_internal::c_end(c), std::forward(value)); } template void c_sort(C &c) { std::sort(container_internal::c_begin(c), container_internal::c_end(c)); } template void c_sort(C &c, LessThan &&comp) { std::sort(container_internal::c_begin(c), container_internal::c_end(c), std::forward(comp)); } template container_internal::Iterator c_lower_bound(C &c, T &&value) { return std::lower_bound(container_internal::c_begin(c), container_internal::c_end(c), std::forward(value)); } template container_internal::Iterator c_unique(C &c) { return std::unique(container_internal::c_begin(c), container_internal::c_end(c)); } } // namespace devilution ================================================ FILE: Source/utils/attributes.h ================================================ /** * @file attributes.h * * Macros for attributes on functions, variables, etc. */ #pragma once #ifdef __has_attribute #define DVL_HAVE_ATTRIBUTE(x) __has_attribute(x) #else #define DVL_HAVE_ATTRIBUTE(x) 0 #endif #ifdef __has_builtin #define DVL_HAVE_BUILTIN(x) __has_builtin(x) #else #define DVL_HAVE_BUILTIN(x) 0 #endif #if DVL_HAVE_ATTRIBUTE(format) || (defined(__GNUC__) && !defined(__clang__)) #define DVL_PRINTF_ATTRIBUTE(fmtargnum, firstarg) \ __attribute__((__format__(__printf__, fmtargnum, firstarg))) #else #define DVL_PRINTF_ATTRIBUTE(fmtargnum, firstarg) #endif #if DVL_HAVE_ATTRIBUTE(pure) #define DVL_PURE __attribute__((pure)) #else #define DVL_PURE #endif #if DVL_HAVE_ATTRIBUTE(always_inline) #define DVL_ALWAYS_INLINE inline __attribute__((always_inline)) #elif defined(_MSC_VER) #define DVL_ALWAYS_INLINE __forceinline #else #define DVL_ALWAYS_INLINE inline #endif #if DVL_HAVE_ATTRIBUTE(noinline) #define DVL_NO_INLINE __attribute__((noinline)) #elif defined(_MSC_VER) #define DVL_NO_INLINE __declspec(noinline) #else #define DVL_NO_INLINE #endif #if DVL_HAVE_ATTRIBUTE(hot) #define DVL_ATTRIBUTE_HOT __attribute__((hot)) #else #define DVL_ATTRIBUTE_HOT #endif // Any global data used by tests must be marked with `DVL_API_FOR_TEST`. #if defined(_MSC_VER) && defined(BUILD_TESTING) #ifdef _DVL_EXPORTING #define DVL_API_FOR_TEST __declspec(dllexport) #else #define DVL_API_FOR_TEST __declspec(dllimport) #endif #else #define DVL_API_FOR_TEST #endif #if defined(__clang__) #define DVL_REINITIALIZES [[clang::reinitializes]] #elif DVL_HAVE_ATTRIBUTE(reinitializes) #define DVL_REINITIALIZES __attribute__((reinitializes)) #else #define DVL_REINITIALIZES #endif #if ((defined(__GNUC__) || defined(__clang__)) && !defined(__EXCEPTIONS)) || defined(_MSC_VER) && !_HAS_EXCEPTIONS // NOLINTNEXTLINE(modernize-macro-to-enum) #define DVL_EXCEPTIONS 0 #else // NOLINTNEXTLINE(modernize-macro-to-enum) #define DVL_EXCEPTIONS 1 #endif #if defined(_MSC_VER) #define DVL_RESTRICT __restrict #else #define DVL_RESTRICT __restrict__ #endif #ifdef __has_cpp_attribute #if __has_cpp_attribute(assume) >= 202207L #define DVL_ASSUME(...) [[assume(__VA_ARGS__)]] #endif #endif #ifndef DVL_ASSUME #if defined(__clang__) #define DVL_ASSUME(...) \ do { \ __builtin_assume(__VA_ARGS__); \ } while (false) #elif defined(_MSC_VER) #define DVL_ASSUME(...) \ do { \ __assume(__VA_ARGS__); \ } while (false) #elif defined(__GNUC__) #if __GNUC__ >= 13 #define DVL_ASSUME(...) __attribute__((__assume__(__VA_ARGS__))) #endif #endif #endif #ifndef DVL_ASSUME #define DVL_ASSUME(...) #endif #if defined(__clang__) || defined(__GNUC__) #define DVL_UNREACHABLE() __builtin_unreachable() #elif defined(_MSC_VER) #define DVL_UNREACHABLE() __assume(false) #else #define DVL_UNREACHABLE() #endif #if DVL_HAVE_BUILTIN(__builtin_expect) #define DVL_PREDICT_FALSE(x) (__builtin_expect(false || (x), false)) #define DVL_PREDICT_TRUE(x) (__builtin_expect(false || (x), true)) #else #define DVL_PREDICT_FALSE(x) (x) #define DVL_PREDICT_TRUE(x) (x) #endif ================================================ FILE: Source/utils/aulib.hpp ================================================ #pragma once #ifndef NOSOUND #include #include #include "engine/sound_defs.hpp" // for DVL_AULIB_SUPPORTS_SDL_RESAMPLER #ifdef DEVILUTIONX_RESAMPLER_SPEEX #include #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER #include #endif #include "options.h" namespace devilution { inline std::unique_ptr CreateAulibResampler(int sourceRate) { if (Aulib::sampleRate() == sourceRate) return nullptr; switch (*GetOptions().Audio.resampler) { #ifdef DEVILUTIONX_RESAMPLER_SPEEX case Resampler::Speex: return std::make_unique(*GetOptions().Audio.resamplingQuality); #endif #ifdef DVL_AULIB_SUPPORTS_SDL_RESAMPLER case Resampler::SDL: return std::make_unique(); #endif } return nullptr; } } // namespace devilution #endif // !NOSOUND ================================================ FILE: Source/utils/bitset2d.hpp ================================================ #pragma once #include #include namespace devilution { /** * @brief A 2D variant of std::bitset. * * For function documentation, see `std::bitset` * * @tparam Width * @tparam Height */ template class Bitset2d { public: bool test(size_t x, size_t y) const { return data_.test(index(x, y)); } void set(size_t x, size_t y, bool value = true) { data_.set(index(x, y), value); } void reset(size_t x, size_t y) { data_.reset(index(x, y)); } void reset() { data_.reset(); } [[nodiscard]] size_t count() const { return data_.count(); } private: static size_t index(size_t x, size_t y) { return y * Width + x; } std::bitset data_; }; } // namespace devilution ================================================ FILE: Source/utils/cel_to_clx.cpp ================================================ #include "utils/cel_to_clx.hpp" #include #include #include #ifdef DEBUG_CEL_TO_CL2_SIZE #include #include #endif #include "appfat.h" #include "utils/clx_encode.hpp" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" namespace devilution { namespace { constexpr bool IsCelTransparent(uint8_t control) { constexpr uint8_t CelTransparentMin = 0x80; return control >= CelTransparentMin; } constexpr uint8_t GetCelTransparentWidth(uint8_t control) { return -static_cast(control); } } // namespace OwnedClxSpriteListOrSheet CelToClx(const uint8_t *data, size_t size, PointerOrValue widthOrWidths) { // A CEL file either begins with: // 1. A CEL header. // 2. A list of offsets to frame groups (each group is a CEL file). size_t groupsHeaderSize = 0; uint32_t numGroups = 1; const uint32_t maybeNumFrames = LoadLE32(data); std::vector cl2Data; // Most files become smaller with CL2. Allocate exactly enough bytes to avoid reallocation. // The only file that becomes larger is data\hf_logo3.cel, by exactly 4445 bytes. cl2Data.reserve(size + 4445); // If it is a number of frames, then the last frame offset will be equal to the size of the file. if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) { // maybeNumFrames is the address of the first group, right after // the list of group offsets. numGroups = maybeNumFrames / 4; groupsHeaderSize = maybeNumFrames; data += groupsHeaderSize; cl2Data.resize(groupsHeaderSize); } for (size_t group = 0; group < numGroups; ++group) { uint32_t numFrames; if (numGroups == 1) { numFrames = maybeNumFrames; } else { numFrames = LoadLE32(data); WriteLE32(&cl2Data[4 * group], static_cast(cl2Data.size())); } // CL2 header: frame count, frame offset for each frame, file size const size_t cl2DataOffset = cl2Data.size(); cl2Data.resize(cl2Data.size() + 4 * (2 + static_cast(numFrames))); WriteLE32(&cl2Data[cl2DataOffset], numFrames); const uint8_t *srcEnd = &data[LoadLE32(&data[4])]; for (size_t frame = 1; frame <= numFrames; ++frame) { const uint8_t *src = srcEnd; srcEnd = &data[LoadLE32(&data[4 * (frame + 1)])]; WriteLE32(&cl2Data[cl2DataOffset + 4 * frame], static_cast(cl2Data.size() - cl2DataOffset)); // Skip CEL frame header if there is one. constexpr size_t CelFrameHeaderSize = 10; const bool celFrameHasHeader = LoadLE16(src) == CelFrameHeaderSize; if (celFrameHasHeader) src += CelFrameHeaderSize; const unsigned frameWidth = widthOrWidths.HoldsPointer() ? widthOrWidths.AsPointer()[frame - 1] : widthOrWidths.AsValue(); // CLX frame header. const size_t frameHeaderPos = cl2Data.size(); cl2Data.resize(cl2Data.size() + ClxFrameHeaderSize); WriteLE16(&cl2Data[frameHeaderPos], ClxFrameHeaderSize); WriteLE16(&cl2Data[frameHeaderPos + 2], frameWidth); unsigned transparentRunWidth = 0; size_t frameHeight = 0; while (src != srcEnd) { // Process line: for (unsigned remainingCelWidth = frameWidth; remainingCelWidth != 0;) { uint8_t val = *src++; if (IsCelTransparent(val)) { val = GetCelTransparentWidth(val); transparentRunWidth += val; } else { AppendClxTransparentRun(transparentRunWidth, cl2Data); transparentRunWidth = 0; AppendClxPixelsOrFillRun(src, val, cl2Data); src += val; } remainingCelWidth -= val; } ++frameHeight; } WriteLE16(&cl2Data[frameHeaderPos + 4], static_cast(frameHeight)); AppendClxTransparentRun(transparentRunWidth, cl2Data); } WriteLE32(&cl2Data[cl2DataOffset + 4 * (1 + static_cast(numFrames))], static_cast(cl2Data.size() - cl2DataOffset)); data = srcEnd; } auto out = std::unique_ptr(new uint8_t[cl2Data.size()]); memcpy(&out[0], cl2Data.data(), cl2Data.size()); #ifdef DEBUG_CEL_TO_CL2_SIZE std::cout << "\t" << size << "\t" << cl2Data.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast(cl2Data.size()) - static_cast(size)) / ((float)size) * 100 << "%" << std::endl; #endif return OwnedClxSpriteListOrSheet { std::move(out), static_cast(numGroups == 1 ? 0 : numGroups) }; } } // namespace devilution ================================================ FILE: Source/utils/cel_to_clx.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "utils/pointer_value_union.hpp" namespace devilution { OwnedClxSpriteListOrSheet CelToClx(const uint8_t *data, size_t size, PointerOrValue widthOrWidths); } // namespace devilution ================================================ FILE: Source/utils/cl2_to_clx.cpp ================================================ #include "utils/cl2_to_clx.hpp" #include #include #include #include "utils/clx_decode.hpp" #include "utils/clx_encode.hpp" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" namespace devilution { uint16_t Cl2ToClx(const uint8_t *data, size_t size, PointerOrValue widthOrWidths, std::vector &clxData) { uint32_t numGroups = 1; const uint32_t maybeNumFrames = LoadLE32(data); const uint8_t *groupBegin = data; // If it is a number of frames, then the last frame offset will be equal to the size of the file. if (LoadLE32(&data[maybeNumFrames * 4 + 4]) != size) { // maybeNumFrames is the address of the first group, right after // the list of group offsets. numGroups = maybeNumFrames / 4; clxData.resize(maybeNumFrames); } // Transient buffer for a contiguous run of non-transparent pixels. std::vector pixels; pixels.reserve(4096); for (size_t group = 0; group < numGroups; ++group) { uint32_t numFrames; if (numGroups == 1) { numFrames = maybeNumFrames; } else { groupBegin = &data[LoadLE32(&data[group * 4])]; numFrames = LoadLE32(groupBegin); WriteLE32(&clxData[4 * group], static_cast(clxData.size())); } // CLX header: frame count, frame offset for each frame, file size const size_t clxDataOffset = clxData.size(); clxData.resize(clxData.size() + 4 * (2 + static_cast(numFrames))); WriteLE32(&clxData[clxDataOffset], numFrames); const uint8_t *frameEnd = &groupBegin[LoadLE32(&groupBegin[4])]; for (size_t frame = 1; frame <= numFrames; ++frame) { WriteLE32(&clxData[clxDataOffset + 4 * frame], static_cast(clxData.size() - clxDataOffset)); const uint8_t *frameBegin = frameEnd; frameEnd = &groupBegin[LoadLE32(&groupBegin[4 * (frame + 1)])]; const uint16_t frameWidth = widthOrWidths.HoldsPointer() ? widthOrWidths.AsPointer()[frame - 1] : widthOrWidths.AsValue(); const size_t frameHeaderPos = clxData.size(); clxData.resize(clxData.size() + ClxFrameHeaderSize); WriteLE16(&clxData[frameHeaderPos], ClxFrameHeaderSize); WriteLE16(&clxData[frameHeaderPos + 2], frameWidth); unsigned transparentRunWidth = 0; int_fast16_t xOffset = 0; size_t frameHeight = 0; const uint8_t *src = frameBegin + LoadLE16(frameBegin); while (src != frameEnd) { auto remainingWidth = static_cast(frameWidth) - xOffset; while (remainingWidth > 0) { const uint8_t control = *src++; if (!IsClxOpaque(control)) { if (!pixels.empty()) { AppendClxPixelsOrFillRun(pixels.data(), pixels.size(), clxData); pixels.clear(); } transparentRunWidth += control; remainingWidth -= control; } else if (IsClxOpaqueFill(control)) { AppendClxTransparentRun(transparentRunWidth, clxData); transparentRunWidth = 0; const uint8_t width = GetClxOpaqueFillWidth(control); const uint8_t color = *src++; pixels.insert(pixels.end(), width, color); remainingWidth -= width; } else { AppendClxTransparentRun(transparentRunWidth, clxData); transparentRunWidth = 0; const uint8_t width = GetClxOpaquePixelsWidth(control); pixels.insert(pixels.end(), src, src + width); src += width; remainingWidth -= width; } } const auto skipSize = GetSkipSize(remainingWidth, static_cast(frameWidth)); xOffset = skipSize.xOffset; frameHeight += skipSize.wholeLines; } if (!pixels.empty()) { AppendClxPixelsOrFillRun(pixels.data(), pixels.size(), clxData); pixels.clear(); } AppendClxTransparentRun(transparentRunWidth, clxData); WriteLE16(&clxData[frameHeaderPos + 4], static_cast(frameHeight)); } WriteLE32(&clxData[clxDataOffset + 4 * (1 + static_cast(numFrames))], static_cast(clxData.size() - clxDataOffset)); } return numGroups == 1 ? 0 : numGroups; } } // namespace devilution ================================================ FILE: Source/utils/cl2_to_clx.hpp ================================================ #pragma once #include #include #include #include #include #include "engine/clx_sprite.hpp" #include "utils/pointer_value_union.hpp" namespace devilution { /** * @brief Converts CL2 to CLX in-place. * * @return uint16_t The number of lists in a sheet if it is a sheet, 0 otherwise. */ uint16_t Cl2ToClx(const uint8_t *data, size_t size, PointerOrValue widthOrWidths, std::vector &clxData); inline OwnedClxSpriteListOrSheet Cl2ToClx(std::unique_ptr &&data, size_t size, PointerOrValue widthOrWidths) { std::vector clxData; const uint16_t numLists = Cl2ToClx(data.get(), size, widthOrWidths, clxData); data = nullptr; data = std::unique_ptr(new uint8_t[clxData.size()]); memcpy(&data[0], clxData.data(), clxData.size()); return OwnedClxSpriteListOrSheet { std::move(data), numLists }; } } // namespace devilution ================================================ FILE: Source/utils/clx_decode.hpp ================================================ #pragma once #include #include "appfat.h" #include "utils/attributes.h" namespace devilution { [[nodiscard]] constexpr bool IsClxOpaque(uint8_t control) { constexpr uint8_t ClxOpaqueMin = 0x80; return control >= ClxOpaqueMin; } [[nodiscard]] constexpr uint8_t GetClxOpaquePixelsWidth(uint8_t control) { return -static_cast(control); } [[nodiscard]] constexpr bool IsClxOpaqueFill(uint8_t control) { constexpr uint8_t ClxFillMax = 0xBE; return control <= ClxFillMax; } [[nodiscard]] constexpr uint8_t GetClxOpaqueFillWidth(uint8_t control) { constexpr uint8_t ClxFillEnd = 0xBF; return ClxFillEnd - control; } struct SkipSize { int_fast16_t wholeLines; int_fast16_t xOffset; }; // Returns the number of lines and the x-offset by which the rendering has overrun // the current line (when a CLX command overruns the current line). // // Requires: remainingWidth <= 0. DVL_ALWAYS_INLINE SkipSize GetSkipSize(int_fast16_t remainingWidth, int_fast16_t srcWidth) { // If `remainingWidth` is negative, `-remainingWidth` is the overrun. // Otherwise, `remainingWidth` is always 0. // Remaining width of 0 (= no overrun) is a common case. // The calculation below would result in the same result. // However, checking for 0 and skipping it entirely turns out to be faster. if (remainingWidth == 0) return { 1, 0 }; const auto overrun = static_cast(-remainingWidth); const uint_fast16_t overrunLines = overrun / srcWidth + 1; const uint_fast16_t xOffset = overrun % srcWidth; return { static_cast(overrunLines), static_cast(xOffset) }; } } // namespace devilution ================================================ FILE: Source/utils/clx_encode.hpp ================================================ #pragma once #include #include #include namespace devilution { /** * CLX frame header is 6 bytes: * * Bytes | Type | Value * :-----:|:--------:|------------- * 0..2 | uint16_t | header size * 2..4 | uint16_t | width * 4..6 | uint16_t | height */ constexpr size_t ClxFrameHeaderSize = 6; inline void AppendClxTransparentRun(unsigned width, std::vector &out) { while (width >= 0x7F) { out.push_back(0x7F); width -= 0x7F; } if (width == 0) return; out.push_back(width); } inline void AppendClxFillRun(uint8_t color, unsigned width, std::vector &out) { while (width >= 0x3F) { out.push_back(0x80); out.push_back(color); width -= 0x3F; } if (width == 0) return; out.push_back(0xBF - width); out.push_back(color); } inline void AppendClxPixelsRun(const uint8_t *src, unsigned width, std::vector &out) { while (width >= 0x41) { out.push_back(0xBF); for (size_t i = 0; i < 0x41; ++i) out.push_back(src[i]); width -= 0x41; src += 0x41; } if (width == 0) return; out.push_back(256 - width); for (size_t i = 0; i < width; ++i) out.push_back(src[i]); } inline void AppendClxPixelsOrFillRun(const uint8_t *src, size_t length, std::vector &out) { const uint8_t *begin = src; const uint8_t *prevColorBegin = src; unsigned prevColorRunLength = 1; uint8_t prevColor = *src++; while (--length > 0) { const uint8_t color = *src; if (prevColor == color) { ++prevColorRunLength; } else { // A tunable parameter that decides at which minimum length we encode a fill run. // 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4). constexpr unsigned MinFillRunLength = 3; if (prevColorRunLength >= MinFillRunLength) { AppendClxPixelsRun(begin, static_cast(prevColorBegin - begin), out); AppendClxFillRun(prevColor, prevColorRunLength, out); begin = src; } prevColorBegin = src; prevColorRunLength = 1; prevColor = color; } ++src; } // Here we use 2 instead of `MinFillRunLength` because we know that this run // is followed by transparent pixels. // Width=2 Fill command takes 2 bytes, while the Pixels command is 3 bytes. if (prevColorRunLength >= 2) { AppendClxPixelsRun(begin, static_cast(prevColorBegin - begin), out); AppendClxFillRun(prevColor, prevColorRunLength, out); } else { AppendClxPixelsRun(begin, static_cast(prevColorBegin - begin + prevColorRunLength), out); } } } // namespace devilution ================================================ FILE: Source/utils/console.cpp ================================================ #include "./console.h" #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include #include #include // Suppress definitions of `min` and `max` macros by : #define NOMINMAX 1 #define WIN32_LEAN_AND_MEAN #include namespace devilution { namespace { HANDLE GetStderrHandle() { static HANDLE handle = NULL; if (handle == NULL) { if (AttachConsole(ATTACH_PARENT_PROCESS)) { handle = GetStdHandle(STD_ERROR_HANDLE); } } return handle; } void WriteToStderr(std::string_view str) { HANDLE handle = GetStderrHandle(); if (handle == NULL) return; WriteConsole(handle, str.data(), static_cast(str.size()), NULL, NULL); } } // namespace void printInConsole(std::string_view str) { OutputDebugString(std::string(str).c_str()); WriteToStderr(str); } void printNewlineInConsole() { OutputDebugString("\r\n"); WriteToStderr("\r\n"); } void printfInConsole(const char *fmt, ...) { char message[4096]; va_list ap; va_start(ap, fmt); std::vsnprintf(message, sizeof(message), fmt, ap); va_end(ap); OutputDebugString(message); WriteToStderr(message); } void vprintfInConsole(const char *fmt, va_list ap) { char message[4096]; std::vsnprintf(message, sizeof(message), fmt, ap); OutputDebugString(message); WriteToStderr(message); } } // namespace devilution #else #include namespace devilution { void printInConsole(std::string_view str) { std::fwrite(str.data(), sizeof(char), str.size(), stderr); } void printNewlineInConsole() { std::fputs("\n", stderr); } void printfInConsole(const char *fmt, ...) { std::va_list ap; va_start(ap, fmt); std::vfprintf(stderr, fmt, ap); va_end(ap); } void vprintfInConsole(const char *fmt, std::va_list ap) { std::vfprintf(stderr, fmt, ap); } } // namespace devilution #endif ================================================ FILE: Source/utils/console.h ================================================ #pragma once #include #include #include #include "utils/attributes.h" namespace devilution { void printInConsole(std::string_view str); void printNewlineInConsole(); void printfInConsole(const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(1, 2); void vprintfInConsole(const char *fmt, std::va_list ap) DVL_PRINTF_ATTRIBUTE(1, 0); } // namespace devilution ================================================ FILE: Source/utils/display.cpp ================================================ #include "utils/display.h" #include #include #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif #ifdef __vita__ #include #endif #ifdef __3DS__ #include "platform/ctr/display.hpp" #endif #ifdef NXDK #include #endif #include "DiabloUI/diabloui.h" #include "config.h" #include "control/control.hpp" #include "controls/controller.h" #ifndef USE_SDL1 #include "controls/devices/game_controller.h" #endif #include "controls/devices/joystick.h" #include "controls/devices/kbcontroller.h" #include "controls/game_controls.h" #include "controls/touch/gamepad.h" #include "engine/backbuffer_state.hpp" #include "engine/dx.h" #include "headless_mode.hpp" #include "options.h" #include "utils/log.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/sdl_wrap.h" #include "utils/str_cat.hpp" #ifdef USE_SDL1 #ifndef SDL1_VIDEO_MODE_BPP #define SDL1_VIDEO_MODE_BPP 0 #endif #ifndef SDL1_VIDEO_MODE_FLAGS #define SDL1_VIDEO_MODE_FLAGS SDL_SWSURFACE #endif #endif namespace devilution { extern SDLSurfaceUniquePtr RendererTextureSurface; /** defined in dx.cpp */ SDL_Window *ghMainWnd; Size forceResolution; uint16_t gnScreenWidth; uint16_t gnScreenHeight; uint16_t gnViewportHeight; uint16_t GetScreenWidth() { return gnScreenWidth; } uint16_t GetScreenHeight() { return gnScreenHeight; } uint16_t GetViewportHeight() { return gnViewportHeight; } Rectangle UIRectangle; const Rectangle &GetUIRectangle() { return UIRectangle; } namespace { #ifndef USE_SDL1 void CalculatePreferredWindowSize(int &width, int &height) { SDL_DisplayMode mode; #ifdef USE_SDL3 int numDisplays = 0; SDL_DisplayID *displays = SDL_GetDisplays(&numDisplays); if (numDisplays <= 0 || displays == nullptr) ErrSdl(); const SDL_DisplayMode *modePtr = SDL_GetDesktopDisplayMode(displays[0]); if (modePtr == nullptr) ErrSdl(); mode = *modePtr; #else if (SDL_GetDesktopDisplayMode(0, &mode) != 0) ErrSdl(); #endif if (mode.w < mode.h) { std::swap(mode.w, mode.h); } if (*GetOptions().Graphics.integerScaling) { const int factor = std::min(mode.w / width, mode.h / height); width = mode.w / factor; height = mode.h / factor; return; } const float wFactor = (float)mode.w / width; const float hFactor = (float)mode.h / height; if (wFactor > hFactor) { width = mode.w * height / mode.h; } else { height = mode.h * width / mode.w; } } #endif void CalculateUIRectangle() { constexpr Size UISize { 640, 480 }; UIRectangle = { { (gnScreenWidth - UISize.width) / 2, (gnScreenHeight - UISize.height) / 2 }, UISize }; } Size GetPreferredWindowSize() { Size windowSize = forceResolution.width != 0 ? forceResolution : *GetOptions().Graphics.resolution; #ifndef USE_SDL1 if (*GetOptions().Graphics.upscale && *GetOptions().Graphics.fitToScreen) { CalculatePreferredWindowSize(windowSize.width, windowSize.height); } #endif AdjustToScreenGeometry(windowSize); return windowSize; } const auto OptionChangeHandlerResolution = (GetOptions().Graphics.resolution.SetValueChangedCallback(ResizeWindow), true); const auto OptionChangeHandlerFullscreen = (GetOptions().Graphics.fullscreen.SetValueChangedCallback(SetFullscreenMode), true); void OptionGrabInputChanged() { #ifdef USE_SDL3 if (ghMainWnd != nullptr) { SDL_SetWindowMouseGrab(ghMainWnd, *GetOptions().Gameplay.grabInput); } #elif !defined(USE_SDL1) if (ghMainWnd != nullptr) { SDL_SetWindowGrab(ghMainWnd, *GetOptions().Gameplay.grabInput ? SDL_TRUE : SDL_FALSE); } #else SDL_WM_GrabInput(*GetOptions().Gameplay.grabInput ? SDL_GRAB_ON : SDL_GRAB_OFF); #endif } const auto OptionChangeHandlerGrabInput = (GetOptions().Gameplay.grabInput.SetValueChangedCallback(OptionGrabInputChanged), true); void UpdateAvailableResolutions() { #ifndef USE_SDL1 if ((SDL_WasInit(SDL_INIT_VIDEO) & SDL_INIT_VIDEO) == 0) { // Called before the video subsystem has been initialized, no-op. return; } #endif GraphicsOptions &graphicsOptions = GetOptions().Graphics; std::vector sizes; const float scaleFactor = GetDpiScalingFactor(); // Add resolutions bool supportsAnyResolution = false; #ifdef USE_SDL3 const SDL_DisplayID displayId = SDL_GetDisplayForWindow(ghMainWnd); if (displayId == 0) ErrSdl(); int modeCount; SDLUniquePtr modes { SDL_GetFullscreenDisplayModes(displayId, &modeCount) }; if (modes == nullptr) return; for (SDL_DisplayMode **it = modes.get(), **end = modes.get() + modeCount; it != end; ++it) { const SDL_DisplayMode &mode = **it; int w = mode.w; int h = mode.h; if (w < h) std::swap(w, h); sizes.emplace_back(Size { static_cast(w * scaleFactor), static_cast(h * scaleFactor) }); } supportsAnyResolution = *GetOptions().Graphics.upscale; #elif !defined(USE_SDL1) int displayModeCount = SDL_GetNumDisplayModes(0); for (int i = 0; i < displayModeCount; i++) { SDL_DisplayMode mode; if (SDL_GetDisplayMode(0, i, &mode) != 0) { ErrSdl(); } if (mode.w < mode.h) { std::swap(mode.w, mode.h); } sizes.emplace_back(Size { static_cast(mode.w * scaleFactor), static_cast(mode.h * scaleFactor) }); } supportsAnyResolution = *GetOptions().Graphics.upscale; #else auto *modes = SDL_ListModes(nullptr, SDL_FULLSCREEN | SDL_HWPALETTE); // SDL_ListModes returns -1 if any resolution is allowed (for example returned on 3DS) if (modes == (SDL_Rect **)-1) { supportsAnyResolution = true; } else if (modes != nullptr) { for (size_t i = 0; modes[i] != nullptr; i++) { if (modes[i]->w < modes[i]->h) { std::swap(modes[i]->w, modes[i]->h); } sizes.emplace_back(Size { static_cast(modes[i]->w * scaleFactor), static_cast(modes[i]->h * scaleFactor) }); } } #endif if (supportsAnyResolution && sizes.size() == 1) { // Attempt to provide sensible options for 4:3 and the native aspect ratio const int width = sizes[0].width; const int height = sizes[0].height; const int commonHeights[] = { 480, 540, 720, 960, 1080, 1440, 2160 }; for (const int commonHeight : commonHeights) { if (commonHeight > height) break; sizes.emplace_back(Size { commonHeight * 4 / 3, commonHeight }); if (commonHeight * width % height == 0) sizes.emplace_back(Size { commonHeight * width / height, commonHeight }); } } const Size configuredSize = *graphicsOptions.resolution; // Ensures that the ini specified resolution is present in resolution list even if it doesn't match a monitor resolution (for example if played in window mode) sizes.push_back(configuredSize); // Ensures that the platform's preferred default resolution is always present sizes.emplace_back(Size { DEFAULT_WIDTH, DEFAULT_HEIGHT }); // Ensures that the vanilla Diablo resolution is present on systems that would support it if (supportsAnyResolution) sizes.emplace_back(Size { 640, 480 }); #ifndef USE_SDL1 if (*graphicsOptions.fitToScreen) { #ifdef USE_SDL3 const SDL_DisplayID displayId = SDL_GetDisplayForWindow(ghMainWnd); if (displayId == 0) ErrSdl(); const SDL_DisplayMode *modePtr = SDL_GetDesktopDisplayMode(displayId); if (modePtr == nullptr) ErrSdl(); const SDL_DisplayMode &mode = *modePtr; #else SDL_DisplayMode mode; if (SDL_GetDesktopDisplayMode(0, &mode) != 0) ErrSdl(); #endif for (auto &size : sizes) { if (mode.h == 0) continue; // Ensure that the ini specified resolution remains present in the resolution list if (size.height == configuredSize.height) size.width = configuredSize.width; else size.width = size.height * mode.w / mode.h; } } #endif // Sort by width then by height c_sort(sizes, [](const Size &x, const Size &y) -> bool { if (x.width == y.width) return x.height > y.height; return x.width > y.width; }); // Remove duplicate entries sizes.erase(std::unique(sizes.begin(), sizes.end()), sizes.end()); std::vector> resolutions; for (auto &size : sizes) { #ifndef USE_SDL1 if (*graphicsOptions.fitToScreen) { resolutions.emplace_back(size, StrCat(size.height, "p")); continue; } #endif resolutions.emplace_back(size, StrCat(size.width, "x", size.height)); } graphicsOptions.resolution.setAvailableResolutions(std::move(resolutions)); } #if !defined(USE_SDL1) || defined(__3DS__) void ResizeWindowAndUpdateResolutionOptions() { ResizeWindow(); #ifndef __3DS__ UpdateAvailableResolutions(); #endif } const auto OptionChangeHandlerFitToScreen = (GetOptions().Graphics.fitToScreen.SetValueChangedCallback(ResizeWindowAndUpdateResolutionOptions), true); #endif #if SDL_VERSION_ATLEAST(2, 0, 0) const auto OptionChangeHandlerScaleQuality = (GetOptions().Graphics.scaleQuality.SetValueChangedCallback(ReinitializeTexture), true); const auto OptionChangeHandlerIntegerScaling = (GetOptions().Graphics.integerScaling.SetValueChangedCallback(ReinitializeIntegerScale), true); const auto OptionChangeHandlerVSync = (GetOptions().Graphics.frameRateControl.SetValueChangedCallback(ReinitializeRenderer), true); struct DisplayModeComparator { Size size; #ifdef USE_SDL3 SDL_PixelFormat pixelFormat; #else SDL_PixelFormatEnum pixelFormat; #endif // Is `a` better than `b`? #ifdef USE_SDL3 [[nodiscard]] bool operator()(const SDL_DisplayMode *aPtr, const SDL_DisplayMode *bPtr) #else [[nodiscard]] bool operator()(const SDL_DisplayMode &a, const SDL_DisplayMode &b) #endif { #ifdef USE_SDL3 const SDL_DisplayMode &a = *aPtr; const SDL_DisplayMode &b = *bPtr; #endif const int dwa = a.w - size.width; const int dha = a.h - size.height; const int dwb = b.w - size.width; const int dhb = b.h - size.height; // A mode that fits the target is always better than one that doesn't: if (dha >= 0 && dwa >= 0 && (dhb < 0 || dwb < 0)) return true; if (dhb >= 0 && dwb >= 0 && (dha < 0 || dwa < 0)) return false; // Either both modes fit or they both don't. // If they're the same size, prefer one with matching pixel format. if (pixelFormat != SDL_PIXELFORMAT_UNKNOWN && a.h == b.h && a.w == b.w) { if (a.format != b.format) { if (a.format == pixelFormat) return true; if (b.format == pixelFormat) return false; } } // Prefer smallest height difference, or width difference if heights are the same. return a.h != b.h ? std::abs(dha) < std::abs(dhb) : std::abs(dwa) < std::abs(dwb); } }; #endif } // namespace #if SDL_VERSION_ATLEAST(2, 0, 0) SDL_DisplayMode GetNearestDisplayMode(Size preferredSize, #ifdef USE_SDL3 SDL_PixelFormat preferredPixelFormat #else SDL_PixelFormatEnum preferredPixelFormat #endif ) { SDL_DisplayMode *nearestDisplayMode = nullptr; #ifdef USE_SDL3 const SDL_DisplayID displayId = SDL_GetDisplayForWindow(ghMainWnd); if (displayId == 0) ErrSdl(); int modeCount; SDLUniquePtr modes { SDL_GetFullscreenDisplayModes(displayId, &modeCount) }; if (modes == nullptr) ErrSdl(); nearestDisplayMode = *std::min_element( modes.get(), modes.get() + modeCount, DisplayModeComparator { preferredSize, preferredPixelFormat }); #else SDL_DisplayMode ownedNearestDisplayMode; if (SDL_GetWindowDisplayMode(ghMainWnd, &ownedNearestDisplayMode) != 0) ErrSdl(); nearestDisplayMode = &ownedNearestDisplayMode; const int displayIndex = SDL_GetWindowDisplayIndex(ghMainWnd); const int modeCount = SDL_GetNumDisplayModes(displayIndex); // First, find the best mode among the modes with the requested pixel format. std::vector modes; modes.reserve(modeCount); for (int modeIndex = 0; modeIndex < modeCount; modeIndex++) { SDL_DisplayMode displayMode; if (SDL_GetDisplayMode(displayIndex, modeIndex, &displayMode) != 0) continue; modes.push_back(displayMode); } if (!modes.empty()) { nearestDisplayMode = &*std::min_element( modes.begin(), modes.end(), DisplayModeComparator { preferredSize, preferredPixelFormat }); } #endif LogVerbose("Nearest display mode to {}x{} is {}x{} {}bpp {}Hz", preferredSize.width, preferredSize.height, nearestDisplayMode->w, nearestDisplayMode->h, SDL_BITSPERPIXEL(nearestDisplayMode->format), nearestDisplayMode->refresh_rate); return *nearestDisplayMode; } #endif void AdjustToScreenGeometry(Size windowSize) { gnScreenWidth = windowSize.width; gnScreenHeight = windowSize.height; CalculateUIRectangle(); CalculatePanelAreas(); } float GetDpiScalingFactor() { #ifdef USE_SDL3 const float dispScale = SDL_GetWindowDisplayScale(ghMainWnd); if (dispScale == 0.0F) { LogError("SDL_GetWindowDisplayScale: {}", SDL_GetError()); SDL_ClearError(); return 1.0F; } return dispScale; #elif !defined(USE_SDL1) if (renderer == nullptr) return 1.0F; int renderWidth; int renderHeight; SDL_GetRendererOutputSize(renderer, &renderWidth, &renderHeight); int windowWidth; int windowHeight; SDL_GetWindowSize(ghMainWnd, &windowWidth, &windowHeight); const float hfactor = static_cast(renderWidth) / windowWidth; const float vhfactor = static_cast(renderHeight) / windowHeight; return std::min(hfactor, vhfactor); #else return 1.0F; #endif } #ifdef USE_SDL1 void SetVideoMode(int width, int height, int bpp, uint32_t flags) { Log("Setting video mode {}x{} bpp={} flags=0x{:08X}", width, height, bpp, flags); ghMainWnd = SDL_SetVideoMode(width, height, bpp, flags); if (ghMainWnd == nullptr) { ErrSdl(); } const SDL_Surface *surface = SDL_GetVideoSurface(); if (surface == nullptr) { ErrSdl(); } Log("Video surface is now {}x{} bpp={} flags=0x{:08X}", surface->w, surface->h, surface->format->BitsPerPixel, surface->flags); } void SetVideoModeToPrimary(bool fullscreen, int width, int height) { int flags = SDL1_VIDEO_MODE_FLAGS | SDL_HWPALETTE; if (fullscreen) flags |= SDL_FULLSCREEN; #ifdef __3DS__ flags &= ~SDL_FULLSCREEN; flags |= Get3DSScalingFlag(*GetOptions().Graphics.fitToScreen, width, height); #endif SetVideoMode(width, height, SDL1_VIDEO_MODE_BPP, flags); if (OutputRequiresScaling()) Log("Using software scaling"); } #endif bool IsFullScreen() { #ifdef USE_SDL3 return (SDL_GetWindowFlags(ghMainWnd) & SDL_WINDOW_FULLSCREEN) != 0; #elif !defined(USE_SDL1) return (SDL_GetWindowFlags(ghMainWnd) & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_FULLSCREEN_DESKTOP)) != 0; #else return (SDL_GetVideoSurface()->flags & SDL_FULLSCREEN) != 0; #endif } bool SpawnWindow(const char *lpWindowName) { #ifdef __vita__ scePowerSetArmClockFrequency(444); #endif #ifdef NXDK { Size windowSize = forceResolution.width != 0 ? forceResolution : *GetOptions().Graphics.resolution; VIDEO_MODE xmode; void *p = nullptr; while (XVideoListModes(&xmode, 0, 0, &p)) { if (windowSize.width >= xmode.width && windowSize.height == xmode.height) break; } XVideoSetMode(xmode.width, xmode.height, xmode.bpp, xmode.refresh); } #endif #ifdef USE_SDL3 SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING, PROJECT_NAME); SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_VERSION_STRING, PROJECT_VERSION); SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING, "org.diasurgical.devilutionx"); SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_URL_STRING, "https://devilutionx.com"); SDL_SetAppMetadataProperty(SDL_PROP_APP_METADATA_TYPE_STRING, "game"); SDL_SetHint(SDL_HINT_RETURN_KEY_HIDES_IME, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 4) && !defined(USE_SDL3) SDL_SetHint(SDL_HINT_IME_INTERNAL_EDITING, "1"); #endif #if SDL_VERSION_ATLEAST(2, 0, 6) && defined(__vita__) SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); #endif #if SDL_VERSION_ATLEAST(2, 0, 10) SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); #endif #ifdef USE_SDL3 SDL_SetHint(SDL_HINT_GAMECONTROLLER_SENSOR_FUSION, "0"); #elif SDL_VERSION_ATLEAST(2, 0, 2) SDL_SetHint(SDL_HINT_ACCELEROMETER_AS_JOYSTICK, "0"); #endif #if SDL_VERSION_ATLEAST(2, 0, 12) && !defined(USE_SDL3) SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); #endif int initFlags = SDL_INIT_VIDEO | SDL_INIT_JOYSTICK; #ifndef NOSOUND initFlags |= SDL_INIT_AUDIO; #endif #ifndef USE_SDL1 #ifdef USE_SDL3 initFlags |= SDL_INIT_GAMEPAD; #else initFlags |= SDL_INIT_GAMECONTROLLER; #endif SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); #endif if ( #ifdef USE_SDL3 !SDL_Init(initFlags) #else SDL_Init(initFlags) <= -1 #endif ) { ErrSdl(); } RegisterCustomEvents(); #ifndef USE_SDL1 if (GetOptions().Controller.szMapping[0] != '\0') { #ifdef USE_SDL3 SDL_AddGamepadMapping(GetOptions().Controller.szMapping); #else SDL_GameControllerAddMapping(GetOptions().Controller.szMapping); #endif } #endif #ifdef USE_SDL1 // On SDL 1, there are no ADDED/REMOVED events. // Always try to initialize the first joystick. Joystick::Add(0); #ifdef __SWITCH__ // TODO: There is a bug in SDL2 on Switch where it does not report controllers on startup (Jan 1, 2020) GameController::Add(0); #endif #endif Size windowSize = GetPreferredWindowSize(); #ifdef USE_SDL1 SDL_WM_SetCaption(lpWindowName, WINDOW_ICON_NAME); SetVideoModeToPrimary(*GetOptions().Graphics.fullscreen, windowSize.width, windowSize.height); if (*GetOptions().Gameplay.grabInput) SDL_WM_GrabInput(SDL_GRAB_ON); atexit(SDL_VideoQuit); // Without this video mode is not restored after fullscreen. #else #ifdef USE_SDL3 SDL_WindowFlags flags = SDL_WINDOW_HIGH_PIXEL_DENSITY; #else int flags = SDL_WINDOW_ALLOW_HIGHDPI; #endif if (*GetOptions().Graphics.upscale) { if (*GetOptions().Graphics.fullscreen) { #ifdef USE_SDL3 flags |= SDL_WINDOW_FULLSCREEN; #else flags |= SDL_WINDOW_FULLSCREEN_DESKTOP; #endif } flags |= SDL_WINDOW_RESIZABLE; } else if (*GetOptions().Graphics.fullscreen) { flags |= SDL_WINDOW_FULLSCREEN; } #ifdef USE_SDL3 if (*GetOptions().Graphics.upscale) { if (!SDL_CreateWindowAndRenderer(lpWindowName, windowSize.width, windowSize.height, flags, &ghMainWnd, &renderer)) ErrSdl(); } else { ghMainWnd = SDL_CreateWindow(lpWindowName, windowSize.width, windowSize.height, flags); } #else ghMainWnd = SDL_CreateWindow(lpWindowName, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowSize.width, windowSize.height, flags); #endif #if defined(DEVILUTIONX_DISPLAY_PIXELFORMAT) SDL_DisplayMode nearestDisplayMode = GetNearestDisplayMode(windowSize, DEVILUTIONX_DISPLAY_PIXELFORMAT); #if USE_SDL3 if (!SDL_SetWindowFullscreenMode(ghMainWnd, &nearestDisplayMode)) ErrSdl(); #else if (SDL_SetWindowDisplayMode(ghMainWnd, &nearestDisplayMode) != 0) ErrSdl(); #endif #endif // Note: https://github.com/libsdl-org/SDL/issues/962 // This is a solution to a problem related to SDL mouse grab. // See https://github.com/diasurgical/devilutionX/issues/4251 #ifdef USE_SDL3 if (ghMainWnd != nullptr) { SDL_SetWindowMouseGrab(ghMainWnd, *GetOptions().Gameplay.grabInput); } #else if (ghMainWnd != nullptr) { SDL_SetWindowGrab(ghMainWnd, *GetOptions().Gameplay.grabInput ? SDL_TRUE : SDL_FALSE); } #endif #endif if (ghMainWnd == nullptr) { ErrSdl(); } int refreshRate = 60; #ifndef USE_SDL1 #ifdef USE_SDL3 const SDL_DisplayID displayId = SDL_GetDisplayForWindow(ghMainWnd); if (displayId == 0) ErrSdl(); const SDL_DisplayMode *displayMode = SDL_GetCurrentDisplayMode(displayId); if (displayMode == nullptr) ErrSdl(); if (displayMode->refresh_rate != 0.F) { refreshRate = static_cast(displayMode->refresh_rate); } #else SDL_DisplayMode mode; SDL_GetDisplayMode(0, 0, &mode); if (mode.refresh_rate != 0) { refreshRate = mode.refresh_rate; } #endif #endif refreshDelay = 1000000 / refreshRate; ReinitializeRenderer(); if (ghMainWnd != nullptr) { UpdateAvailableResolutions(); return true; } return false; } #ifndef USE_SDL1 void ReinitializeTexture() { if (texture) texture.reset(); if (renderer == nullptr) return; #ifdef USE_SDL3 if (!SDL_SetDefaultTextureScaleMode(renderer, *GetOptions().Graphics.scaleQuality == ScalingQuality::NearestPixel ? SDL_SCALEMODE_PIXELART : SDL_SCALEMODE_LINEAR)) { Log("SDL_SetDefaultTextureScaleMode: {}", SDL_GetError()); SDL_ClearError(); } texture = SDLWrap::CreateTexture(renderer, DEVILUTIONX_DISPLAY_TEXTURE_FORMAT, SDL_TEXTUREACCESS_STREAMING, gnScreenWidth, gnScreenHeight); #else auto quality = StrCat(static_cast(*GetOptions().Graphics.scaleQuality)); SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, quality.c_str()); texture = SDLWrap::CreateTexture(renderer, DEVILUTIONX_DISPLAY_TEXTURE_FORMAT, SDL_TEXTUREACCESS_STREAMING, gnScreenWidth, gnScreenHeight); #endif } void ReinitializeIntegerScale() { if (*GetOptions().Graphics.fitToScreen) { ResizeWindow(); return; } if (renderer == nullptr) return; #ifdef USE_SDL3 int w, h; SDL_RendererLogicalPresentation mode; if (!SDL_GetRenderLogicalPresentation(renderer, &w, &h, &mode)) ErrSdl(); const SDL_RendererLogicalPresentation newMode = *GetOptions().Graphics.integerScaling ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX; if (mode != newMode) SDL_SetRenderLogicalPresentation(renderer, w, h, newMode); #else if (SDL_RenderSetIntegerScale(renderer, *GetOptions().Graphics.integerScaling ? SDL_TRUE : SDL_FALSE) < 0) { ErrSdl(); } #endif } #endif void ReinitializeRenderer() { if (ghMainWnd == nullptr) return; #ifdef USE_SDL1 const SDL_Surface *surface = SDL_GetVideoSurface(); if (surface == nullptr) { ErrSdl(); } AdjustToScreenGeometry(Size(surface->w, surface->h)); #else if (*GetOptions().Graphics.upscale) { // We don't recreate the renderer, because this can result in a freezing (not refreshing) rendering if (renderer == nullptr) { #ifdef USE_SDL3 renderer = SDL_CreateRenderer(ghMainWnd, nullptr); #else renderer = SDL_CreateRenderer(ghMainWnd, -1, 0); #endif if (renderer == nullptr) { ErrSdl(); } } #ifdef USE_SDL3 SDL_SetRenderVSync(renderer, *GetOptions().Graphics.frameRateControl == FrameRateControl::VerticalSync ? 1 : 0); #elif SDL_VERSION_ATLEAST(2, 0, 18) SDL_RenderSetVSync(renderer, *GetOptions().Graphics.frameRateControl == FrameRateControl::VerticalSync ? 1 : 0); #endif #ifdef USE_SDL3 if (!SDL_SetRenderLogicalPresentation(renderer, gnScreenWidth, gnScreenHeight, *GetOptions().Graphics.integerScaling ? SDL_LOGICAL_PRESENTATION_INTEGER_SCALE : SDL_LOGICAL_PRESENTATION_LETTERBOX)) { ErrSdl(); } #else if (SDL_RenderSetIntegerScale(renderer, *GetOptions().Graphics.integerScaling ? SDL_TRUE : SDL_FALSE) < 0) { ErrSdl(); } if (SDL_RenderSetLogicalSize(renderer, gnScreenWidth, gnScreenHeight) <= -1) { ErrSdl(); } #endif ReinitializeTexture(); #ifdef USE_SDL3 RendererTextureSurface = SDLSurfaceUniquePtr { SDL_CreateSurface(gnScreenWidth, gnScreenHeight, texture->format) }; if (RendererTextureSurface == nullptr) ErrSdl(); #else Uint32 format; if (SDL_QueryTexture(texture.get(), &format, nullptr, nullptr, nullptr) < 0) ErrSdl(); RendererTextureSurface = SDLWrap::CreateRGBSurfaceWithFormat(0, gnScreenWidth, gnScreenHeight, SDL_BITSPERPIXEL(format), format); #endif } else { Size windowSize = {}; SDL_GetWindowSize(ghMainWnd, &windowSize.width, &windowSize.height); AdjustToScreenGeometry(windowSize); } #endif } void SetFullscreenMode() { #ifdef USE_SDL1 Uint32 flags = ghMainWnd->flags ^ SDL_FULLSCREEN; if (*GetOptions().Graphics.fullscreen) { flags |= SDL_FULLSCREEN; } ghMainWnd = SDL_SetVideoMode(0, 0, 0, flags); if (ghMainWnd == NULL) { ErrSdl(); } #else // When switching from windowed to "true fullscreen", // update the display mode of the window before changing the // fullscreen mode so that the display mode only has to change once if (*GetOptions().Graphics.fullscreen && !*GetOptions().Graphics.upscale) { const Size windowSize = GetPreferredWindowSize(); const SDL_DisplayMode displayMode = GetNearestDisplayMode(windowSize); #ifdef USE_SDL3 if (!SDL_SetWindowFullscreenMode(ghMainWnd, &displayMode)) ErrSdl(); #else if (SDL_SetWindowDisplayMode(ghMainWnd, &displayMode) != 0) ErrSdl(); #endif } #if USE_SDL3 if (!SDL_SetWindowFullscreen(ghMainWnd, *GetOptions().Graphics.fullscreen)) ErrSdl(); #else const Uint32 flags = *GetOptions().Graphics.upscale ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN; if (SDL_SetWindowFullscreen(ghMainWnd, *GetOptions().Graphics.fullscreen ? flags : 0) != 0) ErrSdl(); #endif if (!*GetOptions().Graphics.fullscreen) { SDL_RestoreWindow(ghMainWnd); // Avoid window being maximized before resizing const Size windowSize = GetPreferredWindowSize(); SDL_SetWindowSize(ghMainWnd, windowSize.width, windowSize.height); } if (!*GetOptions().Graphics.upscale) { // Because "true fullscreen" is locked into specific resolutions based on the modes // supported by the display, the resolution may have changed when fullscreen was toggled ReinitializeRenderer(); CreateBackBuffer(); } InitializeVirtualGamepad(); #endif RedrawEverything(); } void ResizeWindow() { if (ghMainWnd == nullptr) return; const Size windowSize = GetPreferredWindowSize(); #ifdef USE_SDL1 SetVideoModeToPrimary(*GetOptions().Graphics.fullscreen, windowSize.width, windowSize.height); #else // For "true fullscreen" windows, the window resizes automatically based on the display mode const bool trueFullscreen = *GetOptions().Graphics.fullscreen && !*GetOptions().Graphics.upscale; if (trueFullscreen) { const SDL_DisplayMode displayMode = GetNearestDisplayMode(windowSize); #ifdef USE_SDL3 if (!SDL_SetWindowFullscreenMode(ghMainWnd, &displayMode)) ErrSdl(); #else if (SDL_SetWindowDisplayMode(ghMainWnd, &displayMode) != 0) ErrSdl(); #endif } // Handle switching between "fake fullscreen" and "true fullscreen" when upscale is toggled const bool upscaleChanged = *GetOptions().Graphics.upscale != (renderer != nullptr); if (upscaleChanged && *GetOptions().Graphics.fullscreen) { #ifdef USE_SDL3 if (!SDL_SetWindowFullscreen(ghMainWnd, *GetOptions().Graphics.fullscreen)) ErrSdl(); #else const Uint32 flags = *GetOptions().Graphics.upscale ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_FULLSCREEN; if (SDL_SetWindowFullscreen(ghMainWnd, flags) != 0) ErrSdl(); #endif if (!*GetOptions().Graphics.fullscreen) SDL_RestoreWindow(ghMainWnd); // Avoid window being maximized before resizing } if (!trueFullscreen) SDL_SetWindowSize(ghMainWnd, windowSize.width, windowSize.height); #endif ReinitializeRenderer(); #ifndef USE_SDL1 #ifdef USE_SDL3 SDL_SetWindowResizable(ghMainWnd, renderer != nullptr); #else SDL_SetWindowResizable(ghMainWnd, renderer != nullptr ? SDL_TRUE : SDL_FALSE); #endif InitializeVirtualGamepad(); #endif CreateBackBuffer(); RedrawEverything(); } SDL_Surface *GetOutputSurface() { #ifdef USE_SDL1 SDL_Surface *ret = SDL_GetVideoSurface(); if (ret == nullptr) ErrSdl(); return ret; #else if (renderer != nullptr) return RendererTextureSurface.get(); SDL_Surface *ret = SDL_GetWindowSurface(ghMainWnd); if (ret == nullptr) ErrSdl(); return ret; #endif } bool OutputRequiresScaling() { #ifdef USE_SDL1 if (HeadlessMode) return false; return gnScreenWidth != GetOutputSurface()->w || gnScreenHeight != GetOutputSurface()->h; #else // SDL2, scaling handled by renderer. return false; #endif } void ScaleOutputRect(SDL_Rect *rect) { if (!OutputRequiresScaling()) return; const SDL_Surface *surface = GetOutputSurface(); rect->x = rect->x * surface->w / gnScreenWidth; rect->y = rect->y * surface->h / gnScreenHeight; rect->w = rect->w * surface->w / gnScreenWidth; rect->h = rect->h * surface->h / gnScreenHeight; } #ifdef USE_SDL1 namespace { SDLSurfaceUniquePtr CreateScaledSurface(SDL_Surface *src) { SDL_Rect stretched_rect = MakeSdlRect(0, 0, src->w, src->h); ScaleOutputRect(&stretched_rect); SDLSurfaceUniquePtr stretched = SDLWrap::CreateRGBSurface( SDL_SWSURFACE, stretched_rect.w, stretched_rect.h, src->format->BitsPerPixel, src->format->Rmask, src->format->Gmask, src->format->Bmask, src->format->Amask); if (SDL_HasColorKey(src)) { SDL_SetColorKey(stretched.get(), SDL_SRCCOLORKEY, src->format->colorkey); if (src->format->palette != NULL) SDL_SetPalette(stretched.get(), SDL_LOGPAL, src->format->palette->colors, 0, src->format->palette->ncolors); } if (SDL_SoftStretch((src), NULL, stretched.get(), &stretched_rect) < 0) ErrSdl(); return stretched; } } // namespace #endif // USE_SDL1 SDLSurfaceUniquePtr ScaleSurfaceToOutput(SDLSurfaceUniquePtr surface) { #ifdef USE_SDL1 if (OutputRequiresScaling()) return CreateScaledSurface(surface.get()); #endif return surface; } } // namespace devilution ================================================ FILE: Source/utils/display.h ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif #include "utils/attributes.h" #include "utils/log.hpp" #include "utils/sdl_ptrs.h" #include "utils/ui_fwd.h" namespace devilution { extern int refreshDelay; // Screen refresh rate in nanoseconds extern SDL_Window *window; extern SDL_Window *ghMainWnd; extern SDL_Renderer *renderer; #ifndef USE_SDL1 extern SDLTextureUniquePtr texture; #endif extern SDLPaletteUniquePtr Palette; extern SDL_Surface *PalSurface; extern DVL_API_FOR_TEST Size forceResolution; #ifdef USE_SDL1 void SetVideoMode(int width, int height, int bpp, uint32_t flags); void SetVideoModeToPrimary(bool fullscreen, int width, int height); #endif bool IsFullScreen(); // Returns: // SDL1: Video surface. // SDL2, no upscale: Window surface. // SDL2, upscale: Renderer texture surface. SDL_Surface *GetOutputSurface(); // Whether the output surface requires software scaling. // Always returns false on SDL2. bool OutputRequiresScaling(); // Scales rect if necessary. void ScaleOutputRect(SDL_Rect *rect); // If the output requires software scaling, replaces the given surface with a scaled one. SDLSurfaceUniquePtr ScaleSurfaceToOutput(SDLSurfaceUniquePtr surface); // Convert from output coordinates to logical (resolution-independent) coordinates. template < typename T, typename = typename std::enable_if::value, T>::type> void OutputToLogical(T *x, T *y) { #ifdef USE_SDL3 if (renderer == nullptr) return; float outX, outY; if (!SDL_RenderCoordinatesFromWindow(renderer, *x, *y, &outX, &outY)) { LogError("SDL_RenderCoordinatesFromWindow: {}", SDL_GetError()); SDL_ClearError(); return; } *x = static_cast(outX); *y = static_cast(outY); #elif !defined(USE_SDL1) if (renderer == nullptr) return; float scaleX; SDL_RenderGetScale(renderer, &scaleX, nullptr); float scaleDpi = GetDpiScalingFactor(); float scale = scaleX / scaleDpi; *x = static_cast(*x / scale); *y = static_cast(*y / scale); SDL_Rect view; SDL_RenderGetViewport(renderer, &view); *x -= view.x; *y -= view.y; #else if (!OutputRequiresScaling()) return; const SDL_Surface *surface = GetOutputSurface(); *x = *x * gnScreenWidth / surface->w; *y = *y * gnScreenHeight / surface->h; #endif } template < typename T, typename = typename std::enable_if::value, T>::type> void LogicalToOutput(T *x, T *y) { #ifdef USE_SDL3 if (renderer == nullptr) return; float outX, outY; if (!SDL_RenderCoordinatesToWindow(renderer, *x, *y, &outX, &outY)) { LogError("SDL_RenderCoordinatesFromWindow: {}", SDL_GetError()); SDL_ClearError(); return; } *x = static_cast(outX); *y = static_cast(outY); #elif !defined(USE_SDL1) if (renderer == nullptr) return; SDL_Rect view; SDL_RenderGetViewport(renderer, &view); *x += view.x; *y += view.y; float scaleX; SDL_RenderGetScale(renderer, &scaleX, nullptr); float scaleDpi = GetDpiScalingFactor(); float scale = scaleX / scaleDpi; *x = static_cast(*x * scale); *y = static_cast(*y * scale); #else if (!OutputRequiresScaling()) return; const SDL_Surface *surface = GetOutputSurface(); *x = *x * surface->w / gnScreenWidth; *y = *y * surface->h / gnScreenHeight; #endif } #if SDL_VERSION_ATLEAST(2, 0, 0) SDL_DisplayMode GetNearestDisplayMode(Size preferredSize, #ifdef USE_SDL3 SDL_PixelFormat preferredPixelFormat = SDL_PIXELFORMAT_UNKNOWN #else SDL_PixelFormatEnum preferredPixelFormat = SDL_PIXELFORMAT_UNKNOWN #endif ); #endif } // namespace devilution ================================================ FILE: Source/utils/endian_read.hpp ================================================ #pragma once #include namespace devilution { template constexpr uint16_t LoadLE16(const T *b) { static_assert(sizeof(T) == 1, "invalid argument"); // NOLINTNEXTLINE(readability-magic-numbers) return (static_cast(b[1]) << 8) | static_cast(b[0]); } template constexpr uint16_t LoadBE16(const T *b) { static_assert(sizeof(T) == 1, "invalid argument"); // NOLINTNEXTLINE(readability-magic-numbers) return (static_cast(b[0]) << 8) | static_cast(b[1]); } template constexpr uint32_t LoadLE32(const T *b) { static_assert(sizeof(T) == 1, "invalid argument"); // NOLINTNEXTLINE(readability-magic-numbers) return (static_cast(b[3]) << 24) | (static_cast(b[2]) << 16) | (static_cast(b[1]) << 8) | static_cast(b[0]); } template constexpr uint32_t LoadBE32(const T *b) { static_assert(sizeof(T) == 1, "invalid argument"); // NOLINTNEXTLINE(readability-magic-numbers) return (static_cast(b[0]) << 24) | (static_cast(b[1]) << 16) | (static_cast(b[2]) << 8) | static_cast(b[3]); } } // namespace devilution ================================================ FILE: Source/utils/endian_stream.hpp ================================================ #pragma once #include #include #include #include #include #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" #include "utils/log.hpp" namespace devilution { inline void LoggedFread(void *buffer, size_t size, FILE *stream) { if (std::fread(buffer, size, 1, stream) != 1 && !std::feof(stream)) { LogError("fread failed: {}", std::strerror(errno)); } } inline void LoggedFwrite(const void *buffer, size_t size, FILE *stream) { if (std::fwrite(buffer, size, 1, stream) != 1) { LogError("fwrite failed: {}", std::strerror(errno)); } } template T ReadByte(FILE *stream) { static_assert(sizeof(T) == 1, "invalid argument"); char buf; LoggedFread(&buf, sizeof(buf), stream); return static_cast(buf); } template T ReadLE16(FILE *stream) { static_assert(sizeof(T) == 2, "invalid argument"); char buf[2]; LoggedFread(&buf, sizeof(buf), stream); return static_cast(LoadLE16(buf)); } template T ReadLE32(FILE *stream) { static_assert(sizeof(T) == 4, "invalid argument"); char buf[4]; LoggedFread(&buf, sizeof(buf), stream); return static_cast(LoadLE32(buf)); } inline float ReadLEFloat(FILE *stream) { static_assert(sizeof(float) == sizeof(uint32_t), "invalid float size"); const uint32_t val = ReadLE32(stream); float result; memcpy(&result, &val, sizeof(float)); return result; } inline void WriteByte(FILE *out, uint8_t val) { LoggedFwrite(&val, sizeof(val), out); } inline void WriteLE16(FILE *out, uint16_t val) { char data[2]; WriteLE16(data, val); LoggedFwrite(data, sizeof(data), out); } inline void WriteLE32(FILE *out, uint32_t val) { char data[4]; WriteLE32(data, val); LoggedFwrite(data, sizeof(data), out); } inline void WriteLEFloat(FILE *out, float val) { static_assert(sizeof(float) == sizeof(uint32_t), "invalid float size"); uint32_t intVal; memcpy(&intVal, &val, sizeof(uint32_t)); char data[4]; WriteLE32(data, intVal); LoggedFwrite(data, sizeof(data), out); } } // namespace devilution ================================================ FILE: Source/utils/endian_swap.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { inline uint16_t Swap16LE(uint16_t val) { #ifdef USE_SDL3 return SDL_Swap16LE(val); #else return SDL_SwapLE16(val); #endif } inline uint16_t Swap16BE(uint16_t val) { #ifdef USE_SDL3 return SDL_Swap16BE(val); #else return SDL_SwapBE16(val); #endif } inline uint32_t Swap32LE(uint32_t val) { #ifdef USE_SDL3 return SDL_Swap32LE(val); #else return SDL_SwapLE32(val); #endif } inline int32_t SwapSigned32LE(int32_t val) { return static_cast(Swap32LE(static_cast(val))); } inline uint32_t Swap32BE(uint32_t val) { #ifdef USE_SDL3 return SDL_Swap32BE(val); #else return SDL_SwapBE32(val); #endif } inline uint64_t Swap64LE(uint64_t val) { #ifdef USE_SDL3 return SDL_Swap64LE(val); #else return SDL_SwapLE64(val); #endif } inline uint64_t Swap64BE(uint64_t val) { #ifdef USE_SDL3 return SDL_Swap64BE(val); #else return SDL_SwapBE64(val); #endif } } // namespace devilution ================================================ FILE: Source/utils/endian_write.hpp ================================================ #pragma once #include #include #include "utils/endian_swap.hpp" namespace devilution { inline void WriteLE16(void *out, uint16_t val) { const uint16_t littleEndian = Swap16LE(val); memcpy(out, &littleEndian, 2); } inline void WriteLE32(void *out, uint32_t val) { const uint32_t littleEndian = Swap32LE(val); memcpy(out, &littleEndian, 4); } } // namespace devilution ================================================ FILE: Source/utils/enum_traits.h ================================================ /** * @file enum_traits.h * * Base template for 'enum_traits' which allow us to access static information about an enum. */ #pragma once #include #include #include "utils/attributes.h" namespace devilution { template struct enum_size { constexpr static const std::size_t value = static_cast(T::LAST) + 1; }; template class enum_values { public: class Iterator { typename std::underlying_type::type m_value; public: Iterator(typename std::underlying_type::type value) : m_value(value) { } [[nodiscard]] DVL_ALWAYS_INLINE const T operator*() const { return static_cast(m_value); } DVL_ALWAYS_INLINE void operator++() { m_value++; } [[nodiscard]] DVL_ALWAYS_INLINE bool operator!=(Iterator rhs) const { return m_value != rhs.m_value; } }; }; template [[nodiscard]] DVL_ALWAYS_INLINE typename enum_values::Iterator begin(enum_values) { return typename enum_values::Iterator(static_cast::type>(T::FIRST)); } template [[nodiscard]] DVL_ALWAYS_INLINE typename enum_values::Iterator end(enum_values) { return typename enum_values::Iterator(static_cast::type>(T::LAST) + 1); } template struct is_flags_enum : std::false_type { }; #define use_enum_as_flags(Type) \ template <> \ struct is_flags_enum : std::true_type { \ }; template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr EnumType operator|(EnumType lhs, EnumType rhs) { using T = std::underlying_type_t; return static_cast(static_cast(lhs) | static_cast(rhs)); } template ::value && is_flags_enum::value, bool> = true> DVL_ALWAYS_INLINE constexpr EnumType operator|=(EnumType &lhs, EnumType rhs) { lhs = lhs | rhs; return lhs; } template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr EnumType operator&(EnumType lhs, EnumType rhs) { using T = std::underlying_type_t; return static_cast(static_cast(lhs) & static_cast(rhs)); } template ::value && is_flags_enum::value, bool> = true> DVL_ALWAYS_INLINE constexpr EnumType operator&=(EnumType &lhs, EnumType rhs) { lhs = lhs & rhs; return lhs; } template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr EnumType operator~(EnumType value) { using T = std::underlying_type_t; return static_cast(~static_cast(value)); } template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr bool HasAnyOf(EnumType lhs, EnumType test) { return (lhs & test) != static_cast(0); // Some flags enums may not use a None value outside this check so we don't require an EnumType::None definition here. } template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr bool HasAllOf(EnumType lhs, EnumType test) { return (lhs & test) == test; } template ::value && is_flags_enum::value, bool> = true> [[nodiscard]] DVL_ALWAYS_INLINE constexpr bool HasNoneOf(EnumType lhs, EnumType test) { return !HasAnyOf(lhs, test); } } // namespace devilution ================================================ FILE: Source/utils/file_name_generator.hpp ================================================ #pragma once #include #include #include #include "utils/str_cat.hpp" namespace devilution { class BaseFileNameGenerator { public: BaseFileNameGenerator(std::initializer_list prefixes, std::string_view suffix) : suffix_(suffix) , prefixEnd_(Append(buf_, prefixes)) { } const char *operator()() const { *BufCopy(prefixEnd_, suffix_) = '\0'; return buf_; } protected: static char *Append(char *buf, std::initializer_list strings) { for (std::string_view str : strings) buf = BufCopy(buf, str); return buf; } [[nodiscard]] std::string_view Suffix() const { return suffix_; } [[nodiscard]] char *PrefixEnd() const { return prefixEnd_; } [[nodiscard]] const char *Buffer() const { return buf_; } private: std::string_view suffix_; char *prefixEnd_; char buf_[256]; }; /** * @brief Generates file names from prefixes, a suffix, and an index. * * FileNameGenerator f({"a/", "b"}, ".txt", 1); * f() // "a/b.txt" * f(0) // "a/b1.txt" * f(1) // "a/b2.txt" */ class FileNameGenerator : public BaseFileNameGenerator { public: FileNameGenerator(std::initializer_list prefixes, std::string_view suffix, unsigned min = 1) : BaseFileNameGenerator(prefixes, suffix) , min_(min) { } using BaseFileNameGenerator::operator(); const char *operator()(size_t i) const { *BufCopy(PrefixEnd(), static_cast(min_ + i), Suffix()) = '\0'; return Buffer(); } private: unsigned min_; }; /** * @brief Generates file names from prefixes, a suffix, a char array and an index into it. * * FileNameWithCharAffixGenerator f({"a/", "b"}, ".txt", "ABC"); * f(0) // "a/bA.txt" * f(1) // "a/bB.txt" */ class FileNameWithCharAffixGenerator : public BaseFileNameGenerator { public: FileNameWithCharAffixGenerator(std::initializer_list prefixes, std::string_view suffix, const char *chars) : BaseFileNameGenerator(prefixes, suffix) , chars_(chars) { *BufCopy(PrefixEnd() + 1, Suffix()) = '\0'; } const char *operator()(size_t i) const { PrefixEnd()[0] = chars_[i]; return Buffer(); } private: const char *chars_; }; } // namespace devilution ================================================ FILE: Source/utils/file_util.cpp ================================================ #include "utils/file_util.h" #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #include "utils/sdl_compat.h" #endif #include "utils/log.hpp" #include "utils/stdcompat/filesystem.hpp" #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #ifdef _WIN32 #include // Suppress definitions of `min` and `max` macros by : #define NOMINMAX 1 #define WIN32_LEAN_AND_MEAN #include #ifndef DEVILUTIONX_WINDOWS_NO_WCHAR #include #endif #endif #if _POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__) || defined(__DJGPP__) #define DVL_HAS_POSIX_2001 #endif #if defined(DVL_HAS_POSIX_2001) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) #include #include #ifndef DVL_HAS_FILESYSTEM #include #endif #endif #if defined(__APPLE__) && DARWIN_MAJOR_VERSION >= 9 #include #endif // We include sys/stat.h for mkdir where available. #if !defined(DVL_HAS_FILESYSTEM) && defined(__has_include) && !defined(_WIN32) #if __has_include() #include #endif #endif namespace devilution { #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) std::unique_ptr ToWideChar(std::string_view path) { constexpr std::uint32_t flags = MB_ERR_INVALID_CHARS; const int utf16Size = ::MultiByteToWideChar(CP_UTF8, flags, path.data(), static_cast(path.size()), nullptr, 0); if (utf16Size == 0) return nullptr; std::unique_ptr utf16 { new wchar_t[utf16Size + 1] }; if (::MultiByteToWideChar(CP_UTF8, flags, path.data(), static_cast(path.size()), &utf16[0], utf16Size) != utf16Size) return nullptr; utf16[utf16Size] = L'\0'; return utf16; } #endif std::string_view Dirname(std::string_view path) { while (path.size() > 1 && path.back() == DirectorySeparator) path.remove_suffix(1); if (path.size() == 1 && path.back() == DirectorySeparator) return DIRECTORY_SEPARATOR_STR; const size_t sep = path.find_last_of(DIRECTORY_SEPARATOR_STR); if (sep == std::string_view::npos) return "."; return std::string_view { path.data(), sep }; } bool FileExists(const char *path) { #ifdef _WIN32 #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR const bool exists = ::GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES; #else const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return false; } const bool exists = ::PathFileExistsW(&pathUtf16[0]); #endif if (!exists) { if (::GetLastError() == ERROR_FILE_NOT_FOUND || ::GetLastError() == ERROR_PATH_NOT_FOUND) { ::SetLastError(ERROR_SUCCESS); } else { #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR LogError("GetFileAttributesA({}): error code {}", path, ::GetLastError()); #else LogError("PathFileExistsW({}): error code {}", path, ::GetLastError()); #endif } return false; } return true; #elif defined(DVL_HAS_POSIX_2001) && !defined(__ANDROID__) return ::access(path, F_OK) == 0; #elif defined(DVL_HAS_FILESYSTEM) std::error_code ec; return std::filesystem::exists(reinterpret_cast(path), ec); #else SDL_IOStream *file = SDL_IOFromFile(path, "rb"); if (file == nullptr) return false; SDL_CloseIO(file); return true; #endif } #ifdef _WIN32 namespace { DWORD WindowsGetFileAttributes(const char *path) { #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR const DWORD attr = ::GetFileAttributesA(path); #else const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return false; } const DWORD attr = ::GetFileAttributesW(&pathUtf16[0]); #endif if (attr == INVALID_FILE_ATTRIBUTES) { if (::GetLastError() == ERROR_FILE_NOT_FOUND || ::GetLastError() == ERROR_PATH_NOT_FOUND) { ::SetLastError(ERROR_SUCCESS); } else { #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR LogError("GetFileAttributesA: error code {}", ::GetLastError()); #else LogError("GetFileAttributesW: error code {}", ::GetLastError()); #endif } } return attr; } } // namespace #endif bool DirectoryExists(const char *path) { #ifdef _WIN32 const DWORD attr = WindowsGetFileAttributes(path); return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY) != 0; #elif defined(DVL_HAS_FILESYSTEM) std::error_code error; return std::filesystem::is_directory(reinterpret_cast(path), error); #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) && !defined(__ANDROID__) struct ::stat statResult; return ::stat(path, &statResult) == 0 && S_ISDIR(statResult.st_mode); #endif } bool FileExistsAndIsWriteable(const char *path) { #ifdef _WIN32 const DWORD attr = WindowsGetFileAttributes(path); return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_READONLY) == 0; #elif defined(DVL_HAS_POSIX_2001) && !defined(__ANDROID__) return ::access(path, W_OK) == 0; #else if (!FileExists(path)) return false; SDL_IOStream *file = SDL_IOFromFile(path, "ab"); if (file == nullptr) return false; SDL_CloseIO(file); return true; #endif } bool GetFileSize(const char *path, std::uintmax_t *size) { #ifdef _WIN32 #if defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0) HANDLE handle = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (handle == INVALID_HANDLE_VALUE) { LogError("File not found: {}", GetLastError()); return false; } DWORD fileSizeHigh; const DWORD fileSizeLow = ::GetFileSize(handle, &fileSizeHigh); if (fileSizeLow == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) { LogError("GetFileSize failed for {}: {}", path, GetLastError()); ::CloseHandle(handle); return false; } *size = (static_cast(fileSizeHigh) << 32) | fileSizeLow; ::CloseHandle(handle); return true; #else WIN32_FILE_ATTRIBUTE_DATA attr; #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR if (!GetFileAttributesExA(path, GetFileExInfoStandard, &attr)) { return false; } #else const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return false; } if (!GetFileAttributesExW(&pathUtf16[0], GetFileExInfoStandard, &attr)) { return false; } #endif // C4293 in msvc when shifting a 32 bit type by 32 bits. *size = static_cast(attr.nFileSizeHigh) << (sizeof(attr.nFileSizeHigh) * 8) | attr.nFileSizeLow; return true; #endif #else struct ::stat statResult; if (::stat(path, &statResult) == -1) return false; *size = static_cast(statResult.st_size); return true; #endif } bool CreateDir(const char *path) { #ifdef DVL_HAS_FILESYSTEM std::error_code error; std::filesystem::create_directory(reinterpret_cast(path), error); if (error) { LogError("failed to create directory {}: {}", path, error.message()); return false; } return true; #elif defined(_WIN32) #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR if (::CreateDirectoryA(path, /*lpSecurityAttributes=*/nullptr) != 0) return true; if (::GetLastError() == ERROR_ALREADY_EXISTS) return true; LogError("failed to create directory {}", path); return false; #else const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return false; } if (::CreateDirectoryW(&pathUtf16[0], /*lpSecurityAttributes=*/nullptr) != 0) return true; if (::GetLastError() == ERROR_ALREADY_EXISTS) return true; LogError("failed to create directory {}", path); return false; #endif #else const int result = ::mkdir(path, 0777); if (result != 0 && result != EEXIST) { LogError("failed to create directory {}", path); return false; } return true; #endif } void RecursivelyCreateDir(const char *path) { #ifdef DVL_HAS_FILESYSTEM std::error_code error; std::filesystem::create_directories(reinterpret_cast(path), error); if (error) { LogError("failed to create directory {}: {}", path, error.message()); } #else if (DirectoryExists(path)) return; std::vector paths; std::string cur { path }; do { paths.push_back(cur); std::string_view parent = Dirname(cur); if (parent == cur) break; cur.assign(parent.data(), parent.size()); } while (!DirectoryExists(cur.c_str())); for (auto it = std::rbegin(paths); it != std::rend(paths); ++it) { if (!CreateDir(it->c_str())) return; } #endif } bool ResizeFile(const char *path, std::uintmax_t size) { #ifdef _WIN32 #if defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0) if (size > std::numeric_limits::max()) { return false; } auto lisize = static_cast(size); #else LARGE_INTEGER lisize; lisize.QuadPart = static_cast(size); if (lisize.QuadPart < 0) { return false; } #endif #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR HANDLE file = ::CreateFileA(path, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (file == INVALID_HANDLE_VALUE) { LogError("CreateFileA({}) failed: {}", path, ::GetLastError()); return false; } #else const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return false; } HANDLE file = ::CreateFileW(&pathUtf16[0], GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (file == INVALID_HANDLE_VALUE) { LogError("CreateFileW({}) failed: {}", path, ::GetLastError()); return false; } #endif #if defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0) if (::SetFilePointer(file, lisize, NULL, FILE_BEGIN) == INVALID_SET_FILE_POINTER) { LogError("SetFilePointer(file, {}, NULL, FILE_BEGIN) failed: {}", lisize, ::GetLastError()); ::CloseHandle(file); return false; } #else if (::SetFilePointerEx(file, lisize, NULL, FILE_BEGIN) == 0) { LogError("SetFilePointerEx(file, {}, NULL, FILE_BEGIN) failed: {}", size, ::GetLastError()); ::CloseHandle(file); return false; } #endif if (::SetEndOfFile(file) == 0) { LogError("SetEndOfFile failed: {}", ::GetLastError()); ::CloseHandle(file); return false; } ::CloseHandle(file); return true; #elif defined(DVL_HAS_POSIX_2001) return ::truncate(path, static_cast(size)) == 0; #else static_assert(false, "truncate not implemented for the current platform"); #endif } void RenameFile(const char *from, const char *to) { #ifdef _WIN32 #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR ::MoveFile(from, to); #else const auto fromUtf16 = ToWideChar(from); const auto toUtf16 = ToWideChar(to); if (fromUtf16 == nullptr || toUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return; } ::MoveFileW(&fromUtf16[0], &toUtf16[0]); #endif // _WIN32 #elif defined(DVL_HAS_FILESYSTEM) std::error_code ec; std::filesystem::rename(reinterpret_cast(from), reinterpret_cast(to), ec); #else ::rename(from, to); #endif } void CopyFileOverwrite(const char *from, const char *to) { #ifdef _WIN32 #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR if (!::CopyFile(from, to, /*bFailIfExists=*/false)) { LogError("Failed to copy {} to {}", from, to); } #else const auto fromUtf16 = ToWideChar(from); const auto toUtf16 = ToWideChar(to); if (fromUtf16 == nullptr || toUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return; } if (!::CopyFileW(&fromUtf16[0], &toUtf16[0], /*bFailIfExists=*/false)) { LogError("Failed to copy {} to {}", from, to); } #endif // _WIN32 #elif defined(__APPLE__) && DARWIN_MAJOR_VERSION >= 9 ::copyfile(from, to, nullptr, COPYFILE_ALL); #elif defined(DVL_HAS_FILESYSTEM) std::error_code error; std::filesystem::copy_file(reinterpret_cast(from), reinterpret_cast(to), std::filesystem::copy_options::overwrite_existing, error); if (error) { LogError("Failed to copy {} to {}: {}", from, to, error.message()); } #else FILE *infile = OpenFile(from, "rb"); if (infile == nullptr) { LogError("Failed to open {} for reading: {}", from, std::strerror(errno)); return; } FILE *outfile = OpenFile(to, "wb"); if (outfile == nullptr) { LogError("Failed to open {} for writing: {}", to, std::strerror(errno)); std::fclose(infile); return; } char buffer[4096]; size_t numRead; while ((numRead = std::fread(buffer, sizeof(char), sizeof(buffer), infile)) > 0) { if (std::fwrite(buffer, sizeof(char), numRead, outfile) != numRead) { LogError("Write failed {}: {}", to, std::strerror(errno)); break; } } std::fclose(infile); std::fclose(outfile); #endif } void RemoveFile(const char *path) { #ifdef DEVILUTIONX_WINDOWS_NO_WCHAR ::DeleteFileA(path); #elif defined(_WIN32) const auto pathUtf16 = ToWideChar(path); if (pathUtf16 == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return; } ::DeleteFileW(&pathUtf16[0]); #else std::string name { path }; std::replace(name.begin(), name.end(), '\\', '/'); FILE *f = fopen(name.c_str(), "r+"); if (f != nullptr) { fclose(f); remove(name.c_str()); f = nullptr; LogVerbose("Removed file: {}", name); } else { LogVerbose("Failed to remove file: {}", name); } #endif } FILE *OpenFile(const char *path, const char *mode) { #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) std::unique_ptr pathUtf16; std::unique_ptr modeUtf16; if ((pathUtf16 = ToWideChar(path)) == nullptr || (modeUtf16 = ToWideChar(mode)) == nullptr) { LogError("UTF-8 -> UTF-16 conversion error code {}", ::GetLastError()); return {}; } return _wfopen(pathUtf16.get(), modeUtf16.get()); #else return std::fopen(path, mode); #endif } std::vector ListDirectories(const char *path) { std::vector dirs; #ifdef DVL_HAS_FILESYSTEM std::error_code ec; for (const auto &entry : std::filesystem::directory_iterator(reinterpret_cast(path), ec)) { if (!entry.is_directory()) continue; std::u8string filename = entry.path().filename().u8string(); dirs.emplace_back(filename.begin(), filename.end()); } #elif defined(_WIN32) WIN32_FIND_DATAA findData; // Construct the search path by appending the directory separator and wildcard. std::string searchPath = std::string(path) + DIRECTORY_SEPARATOR_STR + "*"; HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); if (hFind == INVALID_HANDLE_VALUE) return dirs; do { std::string folder = findData.cFileName; // Skip the special entries "." and ".." if (folder == "." || folder == "..") continue; if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) dirs.push_back(folder); } while (FindNextFileA(hFind, &findData)); FindClose(hFind); #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) DIR *d = ::opendir(path); if (d != nullptr) { struct dirent *dir; while ((dir = ::readdir(d)) != nullptr) { if (dir->d_type != DT_DIR) continue; const std::string_view name = dir->d_name; if (name == "." || name == "..") continue; dirs.emplace_back(name); } ::closedir(d); } #else static_assert(false, "ListDirectories not implemented for the current platform"); #endif return dirs; } std::vector ListFiles(const char *path) { std::vector files; #ifdef DVL_HAS_FILESYSTEM std::error_code ec; for (const auto &entry : std::filesystem::directory_iterator(reinterpret_cast(path), ec)) { if (!entry.is_regular_file()) continue; std::u8string filename = entry.path().filename().u8string(); files.emplace_back(filename.begin(), filename.end()); } #elif defined(_WIN32) WIN32_FIND_DATAA findData; // Construct the search path by appending the directory separator and wildcard. std::string searchPath = std::string(path) + DIRECTORY_SEPARATOR_STR + "*"; HANDLE hFind = FindFirstFileA(searchPath.c_str(), &findData); if (hFind == INVALID_HANDLE_VALUE) return files; do { std::string file = findData.cFileName; // Skip the special entries "." and ".." if (file == "." || file == "..") continue; if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) files.push_back(file); } while (FindNextFileA(hFind, &findData)); FindClose(hFind); #elif (_POSIX_C_SOURCE >= 200112L || defined(_BSD_SOURCE) || defined(__APPLE__)) DIR *d = ::opendir(path); if (d != nullptr) { struct dirent *dir; while ((dir = ::readdir(d)) != nullptr) { if (dir->d_type != DT_REG) continue; const std::string_view name = dir->d_name; if (name == "." || name == "..") continue; files.emplace_back(name); } ::closedir(d); } #else static_assert(false, "ListFiles not implemented for the current platform"); #endif return files; } } // namespace devilution ================================================ FILE: Source/utils/file_util.h ================================================ #pragma once #include #include #include #include #include #include namespace devilution { #if defined(_WIN32) || defined(__DJGPP__) constexpr char DirectorySeparator = '\\'; #define DIRECTORY_SEPARATOR_STR "\\" #else constexpr char DirectorySeparator = '/'; #define DIRECTORY_SEPARATOR_STR "/" #endif bool FileExists(const char *path); inline bool FileExists(const std::string &str) { return FileExists(str.c_str()); } bool DirectoryExists(const char *path); std::string_view Dirname(std::string_view path); bool FileExistsAndIsWriteable(const char *path); bool GetFileSize(const char *path, std::uintmax_t *size); /** * @brief Creates a single directory (non-recursively). * * @return True if the directory already existed or has been created successfully. */ bool CreateDir(const char *path); void RecursivelyCreateDir(const char *path); bool ResizeFile(const char *path, std::uintmax_t size); void RenameFile(const char *from, const char *to); void CopyFileOverwrite(const char *from, const char *to); void RemoveFile(const char *path); FILE *OpenFile(const char *path, const char *mode); #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) std::unique_ptr ToWideChar(std::string_view path); #endif std::vector ListDirectories(const char *path); std::vector ListFiles(const char *path); } // namespace devilution ================================================ FILE: Source/utils/format_int.cpp ================================================ #include "utils/format_int.hpp" #include #include #include "utils/language.h" #include "utils/str_cat.hpp" namespace devilution { std::string FormatInteger(int n) { constexpr size_t GroupSize = 3; char buf[40]; char *begin = buf; const char *end = BufCopy(buf, n); const size_t len = end - begin; std::string out; const size_t prefixLen = n < 0 ? 1 : 0; const size_t numLen = len - prefixLen; if (numLen <= GroupSize) { out.append(begin, len); return out; } const std::string_view separator = _(/* TRANSLATORS: Thousands separator */ ","); out.reserve(len + separator.size() * (numLen - 1) / GroupSize); if (n < 0) { out += '-'; ++begin; } size_t mlen = numLen % GroupSize; if (mlen == 0) mlen = GroupSize; out.append(begin, mlen); begin += mlen; for (; begin != end; begin += GroupSize) { out.append(separator); out.append(begin, GroupSize); } return out; } std::string FormatInteger(uint32_t n) { constexpr size_t GroupSize = 3; char buf[40]; char *begin = buf; const char *end = BufCopy(buf, n); const size_t len = end - begin; std::string out; const size_t numLen = len; if (numLen <= GroupSize) { out.append(begin, len); return out; } const std::string_view separator = _(/* TRANSLATORS: Thousands separator */ ","); out.reserve(len + separator.size() * (numLen - 1) / GroupSize); size_t mlen = numLen % GroupSize; if (mlen == 0) mlen = GroupSize; out.append(begin, mlen); begin += mlen; for (; begin != end; begin += GroupSize) { out.append(separator); out.append(begin, GroupSize); } return out; } } // namespace devilution ================================================ FILE: Source/utils/format_int.hpp ================================================ #pragma once #include #include namespace devilution { /** * @brief Formats integer with thousands separator. */ std::string FormatInteger(int n); std::string FormatInteger(uint32_t n); } // namespace devilution ================================================ FILE: Source/utils/hp_mana_units.hpp ================================================ #pragma once namespace devilution { constexpr int HpManaFracBits = 6; constexpr int HpManaScale = 1 << HpManaFracBits; constexpr int HpManaToFrac(int whole) { return whole * HpManaScale; } constexpr int HpManaToWhole(int frac) { return frac / HpManaScale; } constexpr int HpManaFromParts(int whole, int frac) { return HpManaToFrac(whole) + frac; } } // namespace devilution ================================================ FILE: Source/utils/ini.cpp ================================================ #include "utils/ini.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "utils/algorithm/container.hpp" #include "utils/str_cat.hpp" #include "utils/string_view_hash.hpp" #include "utils/utf8.hpp" namespace devilution { // We avoid including the "appfat.h" to avoid depending on SDL in tests. [[noreturn]] extern void app_fatal(std::string_view str); namespace { template bool OrderByValueIndex(const std::pair &a, const std::pair &b) { return a.second.index < b.second.index; }; // Returns a pointer to the first non-leading whitespace. // Only ' ' and '\t' are considered whitespace. // Requires: begin <= end. const char *SkipLeadingWhitespace(const char *begin, const char *end) { while (begin != end && (*begin == ' ' || *begin == '\t')) { ++begin; } return begin; } // Returns a pointer to the last non-whitespace. // Only ' ' and '\t' are considered whitespace. // Requires: begin <= end. const char *SkipTrailingWhitespace(const char *begin, const char *end) { while (begin != end && (*(end - 1) == ' ' || *(end - 1) == '\t')) { --end; } return end; } // Skips UTF-8 byte order mark. // See https://en.wikipedia.org/wiki/Byte_order_mark const char *SkipUtf8Bom(const char *begin, const char *end) { if (end - begin >= 3 && begin[0] == '\xEF' && begin[1] == '\xBB' && begin[2] == '\xBF') { return begin + 3; } return begin; } } // namespace Ini::Values::Values() : rep_(std::vector {}) { } Ini::Values::Values(const Value &data) : rep_(data) { } std::vector Ini::getKeys(std::string_view section) const { const auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) return {}; std::vector> entries(sectionIt->second.entries.begin(), sectionIt->second.entries.end()); c_sort(entries, OrderByValueIndex); std::vector keys; keys.reserve(entries.size()); for (const auto &[key, _] : entries) { keys.push_back(key); } return keys; } std::span Ini::Values::get() const { if (std::holds_alternative(rep_)) { return { &std::get(rep_), 1 }; } return std::get>(rep_); } std::span Ini::Values::get() { if (std::holds_alternative(rep_)) { return { &std::get(rep_), 1 }; } return std::get>(rep_); } void Ini::Values::append(const Ini::Value &value) { if (std::holds_alternative(rep_)) { rep_ = std::vector { std::get(rep_), value }; return; } std::get>(rep_).push_back(value); } tl::expected Ini::parse(std::string_view buffer) { Ini::FileData fileData; ankerl::unordered_dense::map *sectionEntries = nullptr; const char *eof = buffer.data() + buffer.size(); const char *lineBegin = SkipUtf8Bom(buffer.data(), eof); size_t lineNum = 0; const char *commentBegin = nullptr; const char *nextLineBegin; for (; lineBegin < eof; lineBegin = nextLineBegin) { ++lineNum; const char *lineEnd = static_cast(memchr(lineBegin, '\n', eof - lineBegin)); if (lineEnd == nullptr) { lineEnd = eof; nextLineBegin = eof; } else { nextLineBegin = lineEnd + 1; if (lineBegin + 1 <= lineEnd && *(lineEnd - 1) == '\r') --lineEnd; } const char *keyBegin = SkipLeadingWhitespace(lineBegin, lineEnd); if (keyBegin == lineEnd) continue; if (*keyBegin == ';') { if (commentBegin == nullptr) commentBegin = lineBegin; continue; } std::string_view comment; if (commentBegin != nullptr) { comment = std::string_view(commentBegin, lineBegin - commentBegin); } if (*keyBegin == '[') { const char *keyEnd = ++keyBegin; while (keyEnd < lineEnd && *keyEnd != ']') { ++keyEnd; } if (keyEnd == lineEnd) { return tl::make_unexpected(fmt::format("line {}: unclosed section name {}", lineNum, std::string_view(keyBegin, keyEnd - keyBegin))); } if (const char *after = SkipTrailingWhitespace(keyEnd + 1, lineEnd); after != lineEnd) { return tl::make_unexpected(fmt::format("line {}: content after section [{}]: {}", lineNum, std::string_view(keyBegin, keyEnd - keyBegin), std::string_view(after, lineEnd - after))); } const std::string_view sectionName = std::string_view(keyBegin, keyEnd - keyBegin); auto it = fileData.sections.find(sectionName); if (it == fileData.sections.end()) { it = fileData.sections.emplace_hint(it, sectionName, SectionData { .comment = std::string(comment), .entries = {}, .index = static_cast(fileData.sections.size()), }); } sectionEntries = &it->second.entries; commentBegin = nullptr; continue; } if (sectionEntries == nullptr) return tl::unexpected(fmt::format("line {}: key not in any section", lineNum)); const char *eqPos = static_cast(memchr(keyBegin, '=', lineEnd - keyBegin)); if (eqPos == nullptr) { return tl::make_unexpected(fmt::format("line {}: key {} has no value", lineNum, std::string_view(keyBegin, lineEnd - keyBegin))); } const char *keyEnd = SkipTrailingWhitespace(keyBegin, eqPos); const std::string_view key = std::string_view(keyBegin, keyEnd - keyBegin); const char *valueBegin = SkipLeadingWhitespace(eqPos + 1, lineEnd); const std::string_view value = std::string_view(valueBegin, lineEnd - valueBegin); if (const auto it = sectionEntries->find(key); it != sectionEntries->end()) { it->second.values.append(Value { std::string(comment), std::string(value) }); } else { sectionEntries->emplace_hint(it, key, ValuesData { .values = Values { Value { .comment = std::string(comment), .value = std::string(value), } }, .index = static_cast(sectionEntries->size()), }); } commentBegin = nullptr; } return Ini(std::move(fileData)); } std::span Ini::get(std::string_view section, std::string_view key) const { const auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) return {}; const auto it = sectionIt->second.entries.find(key); if (it == sectionIt->second.entries.end()) return {}; return it->second.values.get(); } std::string_view Ini::getString(std::string_view section, std::string_view key, std::string_view defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; return xs.back().value; } int Ini::getInt(std::string_view section, std::string_view key, int defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string_view str = xs.back().value; int value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); if (result.ec != std::errc()) { app_fatal(fmt::format("ini: Failed to parse {}.{}={} as int", section, key, str)); return defaultValue; } return value; } bool Ini::getBool(std::string_view section, std::string_view key, bool defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string_view str = xs.back().value; if (str == "0") return false; if (str == "1") return true; app_fatal(fmt::format("ini: Failed to parse {}.{}={} as bool", section, key, str)); } float Ini::getFloat(std::string_view section, std::string_view key, float defaultValue) const { const std::span xs = get(section, key); if (xs.empty() || xs.back().value.empty()) return defaultValue; const std::string &str = xs.back().value; #if __cpp_lib_to_chars >= 201611L float value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); if (result.ec != std::errc()) { app_fatal(fmt::format("ini: Failed to parse {}.{}={} as float", section, key, str)); return defaultValue; } return value; #else return strtof(str.data(), nullptr); #endif } void Ini::getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const { CopyUtf8(dst, getString(section, key, defaultValue), dstSize); } void Ini::set(std::string_view section, std::string_view key, Ini::Values &&values) { const std::span updated = values.get(); auto sectionIt = data_.sections.find(section); if (sectionIt == data_.sections.end()) { // Deleting a key from a non-existing section if (updated.empty()) return; // Adding a new section and key data_.sections.emplace_hint(sectionIt, section, SectionData { .comment = {}, .entries = { { std::string(key), ValuesData { .values = std::move(values), .index = 0 } } }, .index = static_cast(data_.sections.size()), }); changed_ = true; return; } const auto it = sectionIt->second.entries.find(key); if (it == sectionIt->second.entries.end()) { // Deleting a non-existing key if (updated.empty()) return; // Adding a new key to an existing section sectionIt->second.entries.emplace(key, ValuesData { .values = std::move(values), .index = static_cast(sectionIt->second.entries.size()), }); changed_ = true; return; } // Deleting an existing key if (updated.empty()) { sectionIt->second.entries.erase(it); if (sectionIt->second.entries.empty()) data_.sections.erase(sectionIt); changed_ = true; return; } // Overriding an existing key const std::span original = it->second.values.get(); if (original.size() == updated.size()) { bool equal = true; for (size_t i = 0; i < original.size(); ++i) { if (original[i].value != updated[i].value) { equal = false; break; } } if (equal) return; } // Preserve existing comments where not overriden. for (size_t i = 0, n = std::min(original.size(), updated.size()); i < n; ++i) { if (!updated[i].comment.has_value() && original[i].comment.has_value()) { updated[i].comment = std::move(original[i].comment); } } it->second.values = std::move(values); changed_ = true; } void Ini::set(std::string_view section, std::string_view key, std::span strings) { if (strings.empty()) { set(section, key, Values {}); } else if (strings.size() == 1) { set(section, key, Values { Value { .comment = {}, .value = strings[0] } }); } else { Values values; auto &items = std::get>(values.rep_); items.reserve(strings.size()); for (const std::string &str : strings) { items.push_back(Value { .comment = {}, .value = str }); } set(section, key, std::move(values)); } } void Ini::set(std::string_view section, std::string_view key, std::string &&value) { set(section, key, Values { Value { .comment = {}, .value = std::move(value) } }); } void Ini::set(std::string_view section, std::string_view key, std::string_view value) { set(section, key, std::string(value)); } void Ini::set(std::string_view section, std::string_view key, int value) { set(section, key, StrCat(value)); } void Ini::set(std::string_view section, std::string_view key, bool value) { set(section, key, std::string(value ? "1" : "0")); } void Ini::set(std::string_view section, std::string_view key, float value) { #if __cpp_lib_to_chars >= 201611L constexpr size_t BufSize = 64; char buf[BufSize] {}; const std::to_chars_result result = std::to_chars(buf, buf + BufSize, value); if (result.ec != std::errc()) { app_fatal("float->string failed"); // should never happen } set(section, key, std::string_view(buf, result.ptr - buf)); #else set(section, key, fmt::format("{}", value)); #endif } namespace { // Appends a possibly multi-line comment, converting \n to \r\n. void AppendComment(std::string_view comment, std::string &out) { bool prevR = false; for (const char c : comment) { if (c == '\r') { prevR = true; } else { if (c == '\n' && !prevR) out += '\r'; prevR = false; } out += c; } } void AppendSection(std::string_view sectionName, std::string &out) { out.append("[").append(sectionName).append("]\r\n"); } void AppendKeyValue(std::string_view key, std::string_view value, std::string &out) { out.append(key).append("=").append(value).append("\r\n"); } } // namespace std::string Ini::serialize() const { std::string result; std::vector> sections(data_.sections.begin(), data_.sections.end()); c_sort(sections, OrderByValueIndex); std::vector> entries; for (auto &[sectionName, section] : sections) { if (!result.empty()) result.append("\r\n"); if (!section.comment.empty()) AppendComment(section.comment, result); AppendSection(sectionName, result); entries.assign(section.entries.begin(), section.entries.end()); c_sort(entries, OrderByValueIndex); for (const auto &[key, entry] : entries) { for (const auto &[comment, value] : entry.values.get()) { if (comment.has_value() && !comment->empty()) AppendComment(*comment, result); AppendKeyValue(key, value, result); } } } return result; } } // namespace devilution ================================================ FILE: Source/utils/ini.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include "utils/string_view_hash.hpp" namespace devilution { class Ini { public: // A single value associated with a section and key. struct Value { // When setting a value, `nullopt` results // in preserving the existing comment if any. std::optional comment; std::string value; }; // All the values associated with a section and key. class Values { public: /** * @brief Constructs an empty set of values. * * If passed to `set`, the key is deleted. */ Values(); explicit Values(const Value &data); [[nodiscard]] std::span get() const; [[nodiscard]] std::span get(); void append(const Value &value); private: // Most keys only have a single value, so we use // a representation that avoids allocations in that case. std::variant> rep_; friend class Ini; }; static tl::expected parse(std::string_view buffer); [[nodiscard]] std::string serialize() const; /** @return all the keys associated with this section in the ini */ [[nodiscard]] std::vector getKeys(std::string_view section) const; /** @return all the values associated with this section and key in the ini */ [[nodiscard]] std::span get(std::string_view section, std::string_view key) const; /** @return the default value if the ini value is unset or empty */ [[nodiscard]] std::string_view getString(std::string_view section, std::string_view key, std::string_view defaultValue = {}) const; /** @return the default value if the ini value is unset or empty */ [[nodiscard]] bool getBool(std::string_view section, std::string_view key, bool defaultValue) const; /** @return the default value if the ini value is unset or empty */ [[nodiscard]] int getInt(std::string_view section, std::string_view key, int defaultValue) const; /** @return the default value if the ini value is unset or empty */ [[nodiscard]] float getFloat(std::string_view section, std::string_view key, float defaultValue) const; void getUtf8Buf(std::string_view section, std::string_view key, std::string_view defaultValue, char *dst, size_t dstSize) const; void getUtf8Buf(std::string_view section, std::string_view key, char *dst, size_t dstSize) const { getUtf8Buf(section, key, /*defaultValue=*/ {}, dst, dstSize); } [[nodiscard]] bool changed() const { return changed_; } void markAsUnchanged() { changed_ = false; } // If values are empty, deletes the entry. void set(std::string_view section, std::string_view key, Values &&values); void set(std::string_view section, std::string_view key, std::span value); void set(std::string_view section, std::string_view key, std::string &&value); void set(std::string_view section, std::string_view key, std::string_view value); void set(std::string_view section, std::string_view key, const char *value) { set(section, key, std::string_view(value)); } void set(std::string_view section, std::string_view key, bool value); void set(std::string_view section, std::string_view key, int value); void set(std::string_view section, std::string_view key, float value); private: struct ValuesData { Values values; uint32_t index; }; struct SectionData { std::string comment; ankerl::unordered_dense::map entries; uint32_t index; }; struct FileData { ankerl::unordered_dense::map sections; }; explicit Ini(FileData &&data) : data_(std::move(data)) { } FileData data_; bool changed_ = false; }; } // namespace devilution ================================================ FILE: Source/utils/intrusive_optional.hpp ================================================ #pragma once #include #include #include #include "appfat.h" /// An optional that uses a field of the stored class and some value to store nullopt. #define DEFINE_INTRUSIVE_OPTIONAL_IMPL(OPTIONAL_CLASS, VALUE_CLASS, FIELD, NULL_VALUE, CONSTEXPR) \ public: \ CONSTEXPR OPTIONAL_CLASS() = default; \ \ template \ CONSTEXPR OPTIONAL_CLASS(VALUE_CLASS &&value) \ : value_(std::forward(value)) \ { \ } \ \ CONSTEXPR OPTIONAL_CLASS(std::nullopt_t) \ : OPTIONAL_CLASS() \ { \ } \ \ template \ CONSTEXPR VALUE_CLASS &emplace(Args &&...args) \ { \ value_ = VALUE_CLASS(std::forward(args)...); \ return value_; \ } \ \ template \ CONSTEXPR std::enable_if_t< \ !std::is_same_v>>, \ OPTIONAL_CLASS> & \ operator=(U &&value) noexcept \ { \ value_ = std::forward(value); \ return *this; \ } \ \ CONSTEXPR OPTIONAL_CLASS &operator=(std::nullopt_t) \ { \ value_ = VALUE_CLASS {}; \ return *this; \ } \ \ CONSTEXPR const VALUE_CLASS &operator*() const \ { \ assert(value_.FIELD != NULL_VALUE); \ return value_; \ } \ \ CONSTEXPR VALUE_CLASS &operator*() \ { \ assert(value_.FIELD != NULL_VALUE); \ return value_; \ } \ \ CONSTEXPR const VALUE_CLASS *operator->() const \ { \ assert(value_.FIELD != NULL_VALUE); \ return &value_; \ } \ \ CONSTEXPR VALUE_CLASS *operator->() \ { \ assert(value_.FIELD != NULL_VALUE); \ return &value_; \ } \ \ [[nodiscard]] CONSTEXPR bool has_value() const \ { \ return value_.FIELD != NULL_VALUE; \ } \ \ CONSTEXPR operator bool() const \ { \ return has_value(); \ } \ \ private: \ VALUE_CLASS value_; #define DEFINE_CONSTEXPR_INTRUSIVE_OPTIONAL(OPTIONAL_CLASS, VALUE_CLASS, FIELD, NULL_VALUE) \ DEFINE_INTRUSIVE_OPTIONAL_IMPL(OPTIONAL_CLASS, VALUE_CLASS, FIELD, NULL_VALUE, constexpr) #define DEFINE_INTRUSIVE_OPTIONAL(OPTIONAL_CLASS, VALUE_CLASS, FIELD, NULL_VALUE) \ DEFINE_INTRUSIVE_OPTIONAL_IMPL(OPTIONAL_CLASS, VALUE_CLASS, FIELD, NULL_VALUE, ) ================================================ FILE: Source/utils/is_of.hpp ================================================ #pragma once #include "utils/attributes.h" namespace devilution { template DVL_ALWAYS_INLINE constexpr bool IsAnyOf(const V &v, X x, Xs... xs) { return v == x || ((v == xs) || ...); } template DVL_ALWAYS_INLINE constexpr bool IsNoneOf(const V &v, X x, Xs... xs) { return v != x && ((v != xs) && ...); } } // namespace devilution ================================================ FILE: Source/utils/language.cpp ================================================ #include "utils/language.h" #include #include #include #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include #include #include "engine/assets.hpp" #include "options.h" #include "utils/algorithm/container.hpp" #include "utils/endian_swap.hpp" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/paths.h" #include "utils/string_view_hash.hpp" #define MO_MAGIC 0x950412de std::string forceLocale; namespace { using namespace devilution; // Translations normally come in ".gmo" files. // We also support ".mo" because that is what poedit generates // and what translators use to test their work. constexpr std::array Extensions { ".mo", ".gmo" }; std::unique_ptr translationKeys; std::unique_ptr translationValues; using TranslationRef = uint32_t; std::vector> translation = { {}, {} }; constexpr uint32_t TranslationRefOffsetBits = 19; constexpr uint32_t TranslationRefSizeBits = 32 - TranslationRefOffsetBits; // 13 constexpr uint32_t TranslationRefSizeMask = (1 << TranslationRefSizeBits) - 1; TranslationRef EncodeTranslationRef(uint32_t offset, uint32_t size) { return (offset << TranslationRefSizeBits) | size; } std::string_view GetTranslation(TranslationRef ref) { return { &translationValues[ref >> TranslationRefSizeBits], ref & TranslationRefSizeMask }; } } // namespace namespace { struct MoHead { uint32_t magic; struct { uint16_t major; uint16_t minor; } revision; uint32_t nbMappings; uint32_t srcOffset; uint32_t dstOffset; }; void SwapLE(MoHead &head) { head.magic = Swap32LE(head.magic); head.revision.major = Swap16LE(head.revision.major); head.revision.minor = Swap16LE(head.revision.minor); head.nbMappings = Swap32LE(head.nbMappings); head.srcOffset = Swap32LE(head.srcOffset); head.dstOffset = Swap32LE(head.dstOffset); } struct MoEntry { uint32_t length; uint32_t offset; }; void SwapLE(MoEntry &entry) { entry.length = Swap32LE(entry.length); entry.offset = Swap32LE(entry.offset); } std::string_view TrimLeft(std::string_view str) { str.remove_prefix(std::min(str.find_first_not_of(" \t"), str.size())); return str; } std::string_view TrimRight(std::string_view str) { str.remove_suffix(str.size() - (str.find_last_not_of(" \t") + 1)); return str; } /** plural=(n != 1); */ int PluralIfNotOne(int n) { return n != 1 ? 1 : 0; } // English, Danish, Spanish, Italian, Swedish unsigned PluralForms = 2; tl::function_ref GetLocalPluralId = PluralIfNotOne; /** * Match plural=(n != 1);" */ void SetPluralForm(std::string_view expression) { const std::string_view key = "plural="; const std::string_view::size_type keyPos = expression.find(key); if (keyPos == std::string_view::npos) return; expression.remove_prefix(keyPos + key.size()); const std::string_view::size_type semicolonPos = expression.find(';'); if (semicolonPos != std::string_view::npos) { expression.remove_suffix(expression.size() - semicolonPos); } expression = TrimLeft(TrimRight(expression)); // ko, zh_CN, zh_TW if (expression == "0") { GetLocalPluralId = [](int /*n*/) -> int { return 0; }; return; } // en, bg, da, de, es, fi, it, sv if (expression == "(n != 1)") { GetLocalPluralId = PluralIfNotOne; return; } // fr, pt_BR if (expression == "(n > 1)") { GetLocalPluralId = [](int n) -> int { return n > 1 ? 1 : 0; }; return; } // be, hr, ru if (expression == "(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") { GetLocalPluralId = [](int n) -> int { if (n % 10 == 1 && n % 100 != 11) return 0; if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) return 1; return 2; }; return; } // pl if (expression == "(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2)") { GetLocalPluralId = [](int n) -> int { if (n == 1) return 0; if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) return 1; return 2; }; return; } // ro if (expression == "(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2)") { GetLocalPluralId = [](int n) -> int { if (n == 1) return 0; if (n == 0 || (n != 1 && n % 100 >= 1 && n % 100 <= 19)) return 1; return 2; }; return; } // cs if (expression == "(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2") { GetLocalPluralId = [](int n) -> int { if (n == 1) return 0; if (n >= 2 && n <= 4) return 1; return 2; }; return; } LogError("Unknown plural expression: '{}'", expression); } /** * Parse "nplurals=2;" */ void ParsePluralForms(std::string_view string) { const std::string_view pluralsKey = "nplurals"; const std::string_view::size_type pluralsPos = string.find(pluralsKey); if (pluralsPos == std::string_view::npos) return; string.remove_prefix(pluralsPos + pluralsKey.size()); const std::string_view::size_type eqPos = string.find('='); if (eqPos == std::string_view::npos) return; const std::string_view value = string.substr(eqPos + 1); if (value.empty() || value[0] < '0') return; const unsigned nplurals = value[0] - '0'; if (nplurals == 0) return; PluralForms = nplurals; SetPluralForm(value); } void ParseMetadata(std::string_view metadata) { std::string_view::size_type delim; while (!metadata.empty() && ((delim = metadata.find(':')) != std::string_view::npos)) { const std::string_view key = TrimLeft(std::string_view(metadata.data(), delim)); std::string_view val = TrimLeft(std::string_view(metadata.data() + delim + 1, metadata.size() - delim - 1)); if ((delim = val.find('\n')) != std::string_view::npos) { val = std::string_view(val.data(), delim); metadata.remove_prefix(val.data() - metadata.data() + val.size() + 1); } else { metadata.remove_prefix(metadata.size()); } // Match "Plural-Forms: nplurals=2; plural=(n != 1);" if (key == "Plural-Forms") { ParsePluralForms(val); break; } } } bool ReadEntry(AssetHandle &handle, const MoEntry &e, char *result) { if (!handle.seek(e.offset)) return false; result[e.length] = '\0'; return handle.read(result, e.length); } bool CopyData(void *dst, const std::byte *data, size_t dataSize, size_t offset, size_t length) { if (offset + length > dataSize) return false; memcpy(dst, data + offset, length); return true; } bool ReadEntry(const std::byte *data, size_t dataSize, const MoEntry &e, char *result) { if (!CopyData(result, data, dataSize, e.offset, e.length)) return false; result[e.length] = '\0'; return true; } } // namespace std::string_view LanguageParticularTranslate(std::string_view context, std::string_view message) { constexpr const char Glue = '\004'; std::string key = std::string(context); key.reserve(key.size() + 1 + message.size()); key += Glue; key.append(message); auto it = translation[0].find(key.c_str()); if (it == translation[0].end()) { return message; } return GetTranslation(it->second); } std::string_view LanguagePluralTranslate(const char *singular, std::string_view plural, int count) { const int n = GetLocalPluralId(count); auto it = translation[n].find(singular); if (it == translation[n].end()) { if (count != 1) return plural; return singular; } return GetTranslation(it->second); } std::string_view LanguageTranslate(const char *key) { auto it = translation[0].find(key); if (it == translation[0].end()) { return key; } return GetTranslation(it->second); } bool HasTranslation(const std::string &locale) { if (locale == "en") { // the base translation is en (really en_US). No translation file will be present for this code but we want // the check to succeed to prevent further searches. return true; } return c_any_of(Extensions, [locale](const char *extension) { return FindAsset((locale + extension).c_str()).ok(); }); } std::string_view GetLanguageCode() { if (!forceLocale.empty()) return forceLocale; return *GetOptions().Language.code; } void LanguageInitialize() { translation = { {}, {} }; translationKeys = nullptr; translationValues = nullptr; const std::string lang(GetLanguageCode()); if (lang == "en") { // English does not have a translation file. GetLocalPluralId = PluralIfNotOne; return; } if (IsSmallFontTall() && !HaveExtraFonts()) { UiErrorOkDialog( "Missing fonts.mpq", StrCat("fonts.mpq is required for locale \"", GetLanguageCode(), "\"\n\n" "Please download fonts.mpq from:\n" "github.com/diasurgical/\ndevilutionx-assets/releases")); forceLocale = "en"; GetLocalPluralId = PluralIfNotOne; return; } AssetHandle handle; const uint32_t loadTranslationsStart = SDL_GetTicks(); std::string translationsPath; size_t fileSize; for (const char *ext : Extensions) { translationsPath = lang + ext; handle = OpenAsset(translationsPath.c_str(), fileSize); if (handle.ok()) break; } if (!handle.ok()) { // Reset to English, which is always available: forceLocale = "en"; GetLocalPluralId = PluralIfNotOne; return; } #ifdef UNPACKED_MPQS const bool readWholeFile = false; #else #ifdef USE_SDL3 // In SDL3, we don't have a way to check if the handle is to an MPQ file. // Assume that it is. const bool readWholeFile = true; #else // If reading from an MPQ, it is much faster to // load the whole file instead of seeking. const bool readWholeFile = handle.handle->type == SDL_RWOPS_UNKNOWN; #endif #endif std::unique_ptr data; if (readWholeFile) { data.reset(new std::byte[fileSize]); if (!handle.read(data.get(), fileSize)) return; handle = {}; } // Read header and do sanity checks MoHead head; if (readWholeFile ? !CopyData(&head, data.get(), fileSize, 0, sizeof(MoHead)) : !handle.read(&head, sizeof(MoHead))) { return; } SwapLE(head); if (head.magic != MO_MAGIC) { return; // not a MO file } if (head.revision.major > 1 || head.revision.minor > 1) { return; // unsupported revision } // Read entries of source strings const std::unique_ptr src { new MoEntry[head.nbMappings] }; if (readWholeFile ? !CopyData(src.get(), data.get(), fileSize, head.srcOffset, head.nbMappings * sizeof(MoEntry)) : !handle.seek(head.srcOffset) || !handle.read(src.get(), head.nbMappings * sizeof(MoEntry))) { return; } for (size_t i = 0; i < head.nbMappings; ++i) { SwapLE(src[i]); } // Read entries of target strings const std::unique_ptr dst { new MoEntry[head.nbMappings] }; if (readWholeFile ? !CopyData(dst.get(), data.get(), fileSize, head.dstOffset, head.nbMappings * sizeof(MoEntry)) : !handle.seek(head.dstOffset) || !handle.read(dst.get(), head.nbMappings * sizeof(MoEntry))) { return; } for (size_t i = 0; i < head.nbMappings; ++i) { SwapLE(dst[i]); } // MO header if (src[0].length != 0) { return; } { auto headerValue = std::unique_ptr { new char[dst[0].length + 1] }; if (readWholeFile ? !ReadEntry(data.get(), fileSize, dst[0], &headerValue[0]) : !ReadEntry(handle, dst[0], &headerValue[0])) { return; } ParseMetadata(&headerValue[0]); } translation.resize(PluralForms); for (unsigned i = 0; i < PluralForms; i++) translation[i] = {}; // Read strings described by entries size_t keysSize = 0; size_t valuesSize = 0; for (uint32_t i = 1; i < head.nbMappings; i++) { keysSize += src[i].length + 1; valuesSize += dst[i].length + 1; } translationKeys = std::unique_ptr { new char[keysSize] }; translationValues = std::unique_ptr { new char[valuesSize] }; char *keyPtr = &translationKeys[0]; char *valuePtr = &translationValues[0]; translation[0].reserve(head.nbMappings - 1); for (uint32_t i = 1; i < head.nbMappings; i++) { if (readWholeFile ? ReadEntry(data.get(), fileSize, src[i], keyPtr) && ReadEntry(data.get(), fileSize, dst[i], valuePtr) : ReadEntry(handle, src[i], keyPtr) && ReadEntry(handle, dst[i], valuePtr)) { // Plural keys also have a plural form but it does not participate in lookup. // Plural values are \0-terminated. std::string_view value { valuePtr, dst[i].length + 1 }; for (size_t j = 0; j < PluralForms && !value.empty(); j++) { const size_t formValueEnd = value.find('\0'); translation[j].emplace(keyPtr, EncodeTranslationRef(static_cast(value.data() - &translationValues[0]), static_cast(formValueEnd))); value.remove_prefix(formValueEnd + 1); } keyPtr += src[i].length + 1; valuePtr += dst[i].length + 1; } } LogVerbose(StrCat("Loaded translations from ", translationsPath, " in ", SDL_GetTicks() - loadTranslationsStart, "ms")); } ================================================ FILE: Source/utils/language.h ================================================ #pragma once #include #include #define _(x) LanguageTranslate(x) #define ngettext(x, y, z) LanguagePluralTranslate(x, y, z) #define pgettext(context, x) LanguageParticularTranslate(context, x) #define N_(x) (x) #define P_(context, x) (x) extern std::string forceLocale; std::string_view GetLanguageCode(); bool HasTranslation(const std::string &locale); void LanguageInitialize(); /** * @brief Returns the translation for the given key. * * @return guaranteed to be null-terminated. */ std::string_view LanguageTranslate(const char *key); inline std::string_view LanguageTranslate(const std::string &key) { return LanguageTranslate(key.c_str()); } /** * @brief Returns a singular or plural translation for the given keys and count. * * @return guaranteed to be null-terminated if `plural` is. */ std::string_view LanguagePluralTranslate(const char *singular, std::string_view plural, int count); /** * @brief Returns the translation for the given key and context identifier. * * @return guaranteed to be null-terminated. */ std::string_view LanguageParticularTranslate(std::string_view context, std::string_view message); // Chinese and Japanese, and Korean small font is 16px instead of a 12px one for readability. inline bool IsSmallFontTall() { const std::string_view code = GetLanguageCode().substr(0, 2); return code == "zh" || code == "ja" || code == "ko"; } ================================================ FILE: Source/utils/log.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif #include #include #include #include "utils/str_cat.hpp" #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { // Local definition to fix compilation issue due to header conflict. [[noreturn]] extern void app_fatal(std::string_view); enum class LogCategory { Application = SDL_LOG_CATEGORY_APPLICATION, Error = SDL_LOG_CATEGORY_ERROR, Assert = SDL_LOG_CATEGORY_ASSERT, System = SDL_LOG_CATEGORY_SYSTEM, Audio = SDL_LOG_CATEGORY_AUDIO, Video = SDL_LOG_CATEGORY_VIDEO, Render = SDL_LOG_CATEGORY_RENDER, Input = SDL_LOG_CATEGORY_INPUT, Test = SDL_LOG_CATEGORY_TEST, }; constexpr auto defaultCategory = LogCategory::Application; enum class LogPriority { Verbose = SDL_LOG_PRIORITY_VERBOSE, Debug = SDL_LOG_PRIORITY_DEBUG, Info = SDL_LOG_PRIORITY_INFO, Warn = SDL_LOG_PRIORITY_WARN, Error = SDL_LOG_PRIORITY_ERROR, Critical = SDL_LOG_PRIORITY_CRITICAL, }; namespace detail { template std::string format(std::string_view fmt, Args &&...args) { FMT_TRY { return fmt::format(fmt::runtime(fmt), std::forward(args)...); } #if FMT_EXCEPTIONS FMT_CATCH(const fmt::format_error &e) { // e.what() is undefined if exceptions are disabled, so we wrap it // with an `FMT_EXCEPTIONS` check. std::string error = e.what(); #else FMT_CATCH(const fmt::format_error &) { std::string error = "unknown (FMT_EXCEPTIONS disabled)"; #endif std::string fullError = StrCat("Format error, fmt: ", fmt, " error: ", error); SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, "%s", error.c_str()); app_fatal(error); } } } // namespace detail inline void Log(std::string_view str) { SDL_Log("%.*s", static_cast(str.size()), str.data()); } template void Log(std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_Log("%s", str.c_str()); } inline void LogVerbose(LogCategory category, std::string_view str) { SDL_LogVerbose(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } inline bool IsLogLevel(LogCategory category, SDL_LogPriority priority) { #ifdef USE_SDL3 return SDL_GetLogPriority(static_cast(category)) <= priority; #else return SDL_LogGetPriority(static_cast(category)) <= priority; #endif } template void LogVerbose(LogCategory category, std::string_view fmt, Args &&...args) { if (!IsLogLevel(category, SDL_LOG_PRIORITY_VERBOSE)) return; auto str = detail::format(fmt, std::forward(args)...); SDL_LogVerbose(static_cast(category), "%s", str.c_str()); } template void LogVerbose(std::string_view fmt, Args &&...args) { LogVerbose(defaultCategory, fmt, std::forward(args)...); } inline void LogDebug(LogCategory category, std::string_view str) { SDL_LogDebug(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } template void LogDebug(LogCategory category, std::string_view fmt, Args &&...args) { if (!IsLogLevel(category, SDL_LOG_PRIORITY_DEBUG)) return; auto str = detail::format(fmt, std::forward(args)...); SDL_LogDebug(static_cast(category), "%s", str.c_str()); } template void LogDebug(std::string_view fmt, Args &&...args) { LogDebug(defaultCategory, fmt, std::forward(args)...); } inline void LogInfo(LogCategory category, std::string_view str) { SDL_LogInfo(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } template void LogInfo(LogCategory category, std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_LogInfo(static_cast(category), "%s", str.c_str()); } template void LogInfo(std::string_view fmt, Args &&...args) { LogInfo(defaultCategory, fmt, std::forward(args)...); } inline void LogWarn(LogCategory category, std::string_view str) { SDL_LogWarn(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } template void LogWarn(LogCategory category, std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_LogWarn(static_cast(category), "%s", str.c_str()); } template void LogWarn(std::string_view fmt, Args &&...args) { LogWarn(defaultCategory, fmt, std::forward(args)...); } inline void LogError(LogCategory category, std::string_view str) { SDL_LogError(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } template void LogError(LogCategory category, std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_LogError(static_cast(category), "%s", str.c_str()); } template void LogError(std::string_view fmt, Args &&...args) { LogError(defaultCategory, fmt, std::forward(args)...); } inline void LogCritical(LogCategory category, std::string_view str) { SDL_LogCritical(static_cast(category), "%.*s", static_cast(str.size()), str.data()); } template void LogCritical(LogCategory category, std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_LogCritical(static_cast(category), "%s", str.c_str()); } template void LogCritical(std::string_view fmt, Args &&...args) { LogCritical(defaultCategory, fmt, std::forward(args)...); } inline void LogMessageV(LogCategory category, LogPriority priority, std::string_view str) { SDL_LogMessage(static_cast(category), static_cast(priority), "%.*s", static_cast(str.size()), str.data()); } template void LogMessageV(LogCategory category, LogPriority priority, std::string_view fmt, Args &&...args) { auto str = detail::format(fmt, std::forward(args)...); SDL_LogMessageV(static_cast(category), static_cast(priority), "%s", str.c_str()); } template void LogMessageV(std::string_view fmt, Args &&...args) { LogMessageV(defaultCategory, fmt, std::forward(args)...); } } // namespace devilution ================================================ FILE: Source/utils/logged_fstream.cpp ================================================ #include "utils/logged_fstream.hpp" namespace devilution { const char *LoggedFStream::DirToString(int dir) { switch (dir) { case SEEK_SET: return "SEEK_SET"; case SEEK_END: return "SEEK_END"; case SEEK_CUR: return "SEEK_CUR"; default: return "invalid"; } } } // namespace devilution ================================================ FILE: Source/utils/logged_fstream.hpp ================================================ #pragma once #include #include #include #include #include #include "utils/file_util.h" #include "utils/log.hpp" namespace devilution { // A wrapper around `FILE *` that logs errors. struct LoggedFStream { public: bool Open(const char *path, const char *mode) { s_ = OpenFile(path, mode); return CheckError(s_ != nullptr, "fopen(\"{}\", \"{}\")", path, mode); } void Close() { if (s_ != nullptr) { std::fclose(s_); s_ = nullptr; } } [[nodiscard]] bool IsOpen() const { return s_ != nullptr; } bool Seekp(long pos, int dir = SEEK_SET) { return CheckError(std::fseek(s_, pos, dir) == 0, "fseek({}, {})", pos, DirToString(dir)); } bool Tellp(long *result) { *result = std::ftell(s_); return CheckError(*result != -1L, "ftell() = {}", *result); } bool Write(const char *data, size_t size) { return CheckError(std::fwrite(data, size, 1, s_) == 1, "fwrite(data, {})", size); } bool Read(char *out, size_t size) { return CheckError(std::fread(out, size, 1, s_) == 1, "fread(out, {})", size); } private: static const char *DirToString(int dir); template bool CheckError(bool ok, const char *fmt, PrintFArgs... args) { if (!ok) { std::string fmtWithError = fmt; fmtWithError.append(": failed with \"{}\""); const char *errorMessage = std::strerror(errno); if (errorMessage == nullptr) errorMessage = ""; LogError(LogCategory::System, fmtWithError.c_str(), args..., errorMessage); } else { LogVerbose(LogCategory::System, fmt, args...); } return ok; } FILE *s_ = nullptr; }; } // namespace devilution ================================================ FILE: Source/utils/math.h ================================================ /** * @file math.h * * Math utility functions */ #pragma once namespace devilution { namespace math { /** * @brief Compute sign of t * @tparam T Any arithmetic type * @param t Value to compute sign of * @return -1 if t < 0, 1 if t > 0, 0 if t == 0 */ template int Sign(T t) { return (t > T(0)) - (t < T(0)); } /** * @brief Linearly interpolate from a towards b using mixing value t * @tparam V Any arithmetic type, used for interpolants and return value * @tparam T Any arithmetic type, used for interpolator * @param a Low interpolation value (returned when t == 0) * @param b High interpolation value (returned when t == 1) * @param t Interpolator, commonly in range [0..1], values outside this range will extrapolate * @return a + (b - a) * t */ template V Lerp(V a, V b, T t) { return a + (b - a) * t; } /** * @brief Inverse lerp, given two key values a and b, and a free value v, determine mixing factor t so that v = Lerp(a, b, t) * @tparam T Any arithmetic type * @param a Low key value (function returns 0 if v == a) * @param b High key value (function returns 1 if v == b) * @param v Mixing factor, commonly in range [a..b] to get a return [0..1] * @return Value t so that v = Lerp(a, b, t); or 0 if b == a */ template T InvLerp(T a, T b, T v) { if (b == a) return T(0); return (v - a) / (b - a); } /** * @brief Remaps value v from range [inMin, inMax] to [outMin, outMax] * @tparam T Any arithmetic type * @param inMin First bound of input range * @param inMax Second bound of input range * @param outMin First bound of output range * @param outMax Second bound of output range * @param v Value to remap * @return Transformed value so that InvLerp(inMin, inMax, v) == InvLerp(outMin, outMax, return) */ template T Remap(T inMin, T inMax, T outMin, T outMax, T v) { auto t = InvLerp(inMin, inMax, v); return Lerp(outMin, outMax, t); } } // namespace math } // namespace devilution ================================================ FILE: Source/utils/palette_blending.cpp ================================================ #include "utils/palette_blending.hpp" #include #include #ifdef USE_SDL3 #include #else #include #endif #include "utils/palette_kd_tree.hpp" namespace devilution { // This array is read from a lot on every frame. // We do not use `std::array` here to improve debug build performance. // In a debug build, `std::array` accesses are function calls. uint8_t paletteTransparencyLookup[256][256]; #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT uint16_t paletteTransparencyLookupBlack16[65536]; #endif namespace { PaletteKdTree CurrentPaletteKdTree; using RGB = std::array; RGB BlendColors(const SDL_Color &a, const SDL_Color &b) { return RGB { static_cast((static_cast(a.r) + static_cast(b.r)) / 2), static_cast((static_cast(a.g) + static_cast(b.g)) / 2), static_cast((static_cast(a.b) + static_cast(b.b)) / 2), }; } #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT void SetPaletteTransparencyLookupBlack16(unsigned i, unsigned j) { paletteTransparencyLookupBlack16[i | (j << 8U)] = paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8U); } #endif } // namespace void GenerateBlendedLookupTable(const SDL_Color *palette, int skipFrom, int skipTo) { CurrentPaletteKdTree = PaletteKdTree { palette, skipFrom, skipTo }; for (unsigned i = 0; i < 256; i++) { paletteTransparencyLookup[i][i] = i; unsigned j = 0; for (; j < i; j++) { paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i]; } ++j; for (; j < 256; j++) { paletteTransparencyLookup[i][j] = CurrentPaletteKdTree.findNearestNeighbor(BlendColors(palette[i], palette[j])); } } #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT for (unsigned i = 0; i < 256; ++i) { SetPaletteTransparencyLookupBlack16(i, i); for (unsigned j = 0; j < i; ++j) { SetPaletteTransparencyLookupBlack16(i, j); SetPaletteTransparencyLookupBlack16(j, i); } } #endif } void UpdateBlendedLookupTableSingleColor(const SDL_Color *palette, unsigned i) { for (unsigned j = 0; j < 256; j++) { if (i == j) { // No need to calculate transparency between 2 identical colors paletteTransparencyLookup[i][j] = j; continue; } const uint8_t best = CurrentPaletteKdTree.findNearestNeighbor(BlendColors(palette[i], palette[j])); paletteTransparencyLookup[i][j] = paletteTransparencyLookup[j][i] = best; } #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT UpdateTransparencyLookupBlack16(i, i); #endif } #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT void UpdateTransparencyLookupBlack16(unsigned from, unsigned to) { for (unsigned i = from; i <= to; i++) { for (unsigned j = 0; j < 256; j++) { SetPaletteTransparencyLookupBlack16(i, j); SetPaletteTransparencyLookupBlack16(j, i); } } } #endif } // namespace devilution ================================================ FILE: Source/utils/palette_blending.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { /** * @brief Lookup table for the average of two colors in `logical_palette`. */ extern uint8_t paletteTransparencyLookup[256][256]; /** * @brief Generates `paletteTransparencyLookup` table. * * This is based of the same technique found in Quake2. * * To mimic 50% transparency we figure out what colors in the existing palette are the best match for the combination of any 2 colors. * We save this into a lookup table for use during rendering. * * @param skipFrom Do not use colors between this index and skipTo * @param skipTo Do not use colors between skipFrom and this index */ void GenerateBlendedLookupTable(const SDL_Color *palette, int skipFrom = -1, int skipTo = -1); /** * @brief Updates the transparency lookup table for a single color. */ void UpdateBlendedLookupTableSingleColor(const SDL_Color *palette, unsigned i); #if DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT /** * A lookup table from black for a pair of colors in `logical_palette`. * * For a pair of colors i and j, the index `i | (j << 8)` contains * `paletteTransparencyLookup[0][i] | (paletteTransparencyLookup[0][j] << 8)`. * * On big-endian platforms, the indices are encoded as `j | (i << 8)`, while the * value order remains the same. */ extern uint16_t paletteTransparencyLookupBlack16[65536]; void UpdateTransparencyLookupBlack16(unsigned from, unsigned to); #endif } // namespace devilution ================================================ FILE: Source/utils/palette_kd_tree.cpp ================================================ #include "utils/palette_kd_tree.hpp" #include #include #include #include #include #include #ifdef USE_SDL3 #include #elif defined(USE_SDL1) #include #else #include #endif #include "utils/static_vector.hpp" #include "utils/str_cat.hpp" #if DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ #include #endif namespace devilution { namespace { template uint8_t GetColorComponent(const SDL_Color &); template <> inline uint8_t GetColorComponent<0>(const SDL_Color &c) { return c.r; } template <> inline uint8_t GetColorComponent<1>(const SDL_Color &c) { return c.g; } template <> inline uint8_t GetColorComponent<2>(const SDL_Color &c) { return c.b; } template [[nodiscard]] PaletteKdTreeNode<0> &LeafByIndex(PaletteKdTreeNode &node, uint8_t index) { if constexpr (RemainingDepth == 1) { return node.child(index % 2 == 0); } else { return LeafByIndex(node.child(index % 2 == 0), index / 2); } } template [[nodiscard]] uint8_t LeafIndexForColor(const PaletteKdTreeNode &node, const SDL_Color &color, uint8_t result = 0) { const bool isLeft = GetColorComponent::Coord>(color) < node.pivot; if constexpr (RemainingDepth == 1) { return (2 * result) + (isLeft ? 0 : 1); } else { return (2 * LeafIndexForColor(node.child(isLeft), color, result)) + (isLeft ? 0 : 1); } } struct MedianInfo { std::array counts = {}; uint16_t numValues = 0; }; [[nodiscard]] static uint8_t GetMedian(const MedianInfo &medianInfo) { const std::span counts = medianInfo.counts; const uint_fast16_t numValues = medianInfo.numValues; const auto medianTarget = static_cast((medianInfo.numValues + 1) / 2); uint_fast16_t partialSum = 0; uint_fast16_t i = 0; for (; partialSum < medianTarget && partialSum != numValues; ++i) { partialSum += counts[i]; } // Special cases: // 1. If the elements are empty, this will return 0. // 2. If all the elements are the same, this will be `value + 1` (rolling over to 0 if value is 256). // This means all the elements will be on one side of the pivot (left unless the value is 255). return static_cast(i); } template void MaybeAddToSubdivisionForMedian( const PaletteKdTreeNode &node, const SDL_Color palette[256], unsigned paletteIndex, std::span medianInfos) { const uint8_t color = GetColorComponent::Coord>(palette[paletteIndex]); if constexpr (N == 1) { MedianInfo &medianInfo = medianInfos[0]; ++medianInfo.counts[color]; ++medianInfo.numValues; } else { const bool isLeft = color < node.pivot; MaybeAddToSubdivisionForMedian(node.child(isLeft), palette, paletteIndex, isLeft ? medianInfos.template subspan<0, N / 2>() : medianInfos.template subspan()); } } template void SetPivotsRecursively( PaletteKdTreeNode &node, std::span medianInfos) { if constexpr (N == 1) { node.pivot = GetMedian(medianInfos[0]); } else { SetPivotsRecursively(node.left, medianInfos.template subspan<0, N / 2>()); SetPivotsRecursively(node.right, medianInfos.template subspan()); } } template void PopulatePivotsForTargetDepth(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo) { constexpr size_t NumSubdivisions = 1U << TargetDepth; std::array subdivisions = {}; const std::span subdivisionsSpan { subdivisions }; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; MaybeAddToSubdivisionForMedian(root, palette, i, subdivisionsSpan); } SetPivotsRecursively(root, subdivisionsSpan); } template void PopulatePivotsImpl(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo, std::index_sequence intSeq) // NOLINT(misc-unused-parameters) { (PopulatePivotsForTargetDepth(root, palette, skipFrom, skipTo), ...); } void PopulatePivots(PaletteKdTreeNode &root, const SDL_Color palette[256], int skipFrom, int skipTo) { PopulatePivotsImpl(root, palette, skipFrom, skipTo, std::make_index_sequence {}); } } // namespace PaletteKdTree::PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo) { PopulatePivots(tree_, palette, skipFrom, skipTo); StaticVector leafValues[NumLeaves]; for (int i = 0; i < 256; ++i) { if (i >= skipFrom && i <= skipTo) continue; leafValues[LeafIndexForColor(tree_, palette[i])].emplace_back(i); } size_t totalLen = 0; for (uint8_t leafIndex = 0; leafIndex < NumLeaves; ++leafIndex) { PaletteKdTreeNode<0> &leaf = LeafByIndex(tree_, leafIndex); const std::span values = leafValues[leafIndex]; if (values.empty()) { leaf.valuesBegin = 1; leaf.valuesEndInclusive = 0; } else { leaf.valuesBegin = static_cast(totalLen); leaf.valuesEndInclusive = static_cast(totalLen - 1 + values.size()); for (size_t i = 0; i < values.size(); ++i) { const uint8_t value = values[i]; values_[totalLen + i] = std::make_pair(RGB { palette[value].r, palette[value].g, palette[value].b }, value); } totalLen += values.size(); } } #if DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ // To generate palette.dot.svg, run: // dot -O -Tsvg palette.dot FILE *out = std::fopen("palette.dot", "w"); std::string dot = toGraphvizDot(); std::fwrite(dot.data(), dot.size(), 1, out); std::fclose(out); #endif } std::string PaletteKdTree::toGraphvizDot() const { std::string dot = "graph palette_tree {\n rankdir=LR\n"; tree_.toGraphvizDot(0, values_, dot); dot.append("}\n"); return dot; } void PaletteKdTreeNode<0>::toGraphvizDot( size_t id, std::span::RGB, uint8_t>, 256> values, std::string &dot) const { StrAppend(dot, " node_", id, R"( [shape=plain label=< )"); const std::pair *const end = values.data() + valuesEndInclusive; for (const std::pair *it = values.data() + valuesBegin; it <= end; ++it) { const auto &[rgb, paletteIndex] = *it; StrAppend(dot, R"("); } if (valuesBegin > valuesEndInclusive) StrAppend(dot, ""); StrAppend(dot, "\n
"); const bool useWhiteText = rgb[0] + rgb[1] + rgb[2] < 350; if (useWhiteText) StrAppend(dot, R"()"); StrAppend(dot, static_cast(rgb[0]), " ", static_cast(rgb[1]), " ", static_cast(rgb[2]), R"(
)", static_cast(paletteIndex)); if (useWhiteText) StrAppend(dot, "
"); StrAppend(dot, "
>]\n"); } } // namespace devilution ================================================ FILE: Source/utils/palette_kd_tree.hpp ================================================ #pragma once #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #elif defined(USE_SDL1) #include #else #include #endif #include "utils/str_cat.hpp" #define DEVILUTIONX_PRINT_PALETTE_BLENDING_TREE_GRAPHVIZ 0 // NOLINT(modernize-macro-to-enum) namespace devilution { /** * @brief Depth (number of levels) of the tree. */ constexpr size_t PaletteKdTreeDepth = 5; /** * @brief A node in the k-d tree. * * @tparam RemainingDepth distance to the leaf nodes. */ template struct PaletteKdTreeNode { using RGB = std::array; static constexpr unsigned Coord = (PaletteKdTreeDepth - RemainingDepth) % 3; PaletteKdTreeNode left; PaletteKdTreeNode right; uint8_t pivot; [[nodiscard]] const PaletteKdTreeNode &child(bool isLeft) const { return isLeft ? left : right; } [[nodiscard]] PaletteKdTreeNode &child(bool isLeft) { return isLeft ? left : right; } [[maybe_unused]] void toGraphvizDot(size_t id, std::span, 256> values, std::string &dot) const { StrAppend(dot, " node_", id, " [label=\"", "rgb"[Coord], ": ", pivot, "\"]\n"); const size_t leftId = (2 * id) + 1; const size_t rightId = (2 * id) + 2; left.toGraphvizDot(leftId, values, dot); right.toGraphvizDot(rightId, values, dot); StrAppend(dot, " node_", id, " -- node_", leftId, "\n node_", id, " -- node_", rightId, "\n"); } }; /** * @brief A leaf node in the k-d tree. */ template <> struct PaletteKdTreeNode { using RGB = std::array; // We use inclusive indices to allow for representing the full [0, 255] range. // An empty node is represented as [1, 0]. uint8_t valuesBegin; uint8_t valuesEndInclusive; void toGraphvizDot(size_t id, std::span, 256> values, std::string &dot) const; }; /** * @brief A kd-tree used to find the nearest neighbor in the color space. * * Each level splits the space in half by red, green, and blue respectively. */ class PaletteKdTree { private: using RGB = std::array; static constexpr unsigned NumLeaves = 1U << PaletteKdTreeDepth; public: PaletteKdTree() = default; /** * @brief Constructs a PaletteKdTree * * The palette is used as points in the tree. * Colors between skipFrom and skipTo (inclusive) are skipped. */ PaletteKdTree(const SDL_Color palette[256], int skipFrom, int skipTo); struct VisitState { uint8_t best; uint32_t bestDiff; }; [[nodiscard]] uint8_t findNearestNeighbor(const RGB &rgb) const { VisitState visitState; visitState.bestDiff = std::numeric_limits::max(); findNearestNeighborVisit(tree_, rgb, visitState); return visitState.best; } [[maybe_unused]] [[nodiscard]] std::string toGraphvizDot() const; private: [[nodiscard]] static constexpr uint32_t getColorDistance(const std::array &a, const std::array &b) { const int diffr = a[0] - b[0]; const int diffg = a[1] - b[1]; const int diffb = a[2] - b[2]; return (diffr * diffr) + (diffg * diffg) + (diffb * diffb); } [[nodiscard]] static constexpr uint32_t getColorDistanceToPlane(int x1, int x2) { // Our planes are axis-aligned, so a distance from a point to a plane // can be calculated based on just the axis coordinate. const int delta = x1 - x2; return static_cast(delta * delta); } template void findNearestNeighborVisit(const PaletteKdTreeNode &node, const RGB &rgb, VisitState &visitState) const { const uint8_t coord = rgb[PaletteKdTreeNode::Coord]; findNearestNeighborVisit(node.child(coord < node.pivot), rgb, visitState); // To see if we need to check a node's subtree, we compare the distance from the query // to the current best candidate vs the distance to the edge of the half-space represented // by the node. if (getColorDistanceToPlane(node.pivot, coord) < visitState.bestDiff) { findNearestNeighborVisit(node.child(coord >= node.pivot), rgb, visitState); } } void findNearestNeighborVisit(const PaletteKdTreeNode<0> &node, const RGB &rgb, VisitState &visitState) const { // Nodes are almost never empty. // Separating the empty check from the loop makes this faster, // probaly because of better branch prediction. if (node.valuesBegin > node.valuesEndInclusive) return; const std::pair *it = values_.data() + node.valuesBegin; const std::pair *const end = values_.data() + node.valuesEndInclusive; do { const auto &[paletteColor, paletteIndex] = *it++; const uint32_t diff = getColorDistance(paletteColor, rgb); if (diff < visitState.bestDiff) { visitState.best = paletteIndex; visitState.bestDiff = diff; } } while (it <= end); } PaletteKdTreeNode tree_; std::array, 256> values_; }; } // namespace devilution ================================================ FILE: Source/utils/parse_int.cpp ================================================ #include "parse_int.hpp" #include namespace devilution { uint8_t ParseFixed6Fraction(std::string_view str, const char **endOfParse) { unsigned numDigits = 0; uint32_t decimalFraction = 0; // Read at most 7 digits, at that threshold we're able to determine an exact rounding for 6 bit fixed point numbers while (!str.empty() && numDigits < 7) { if (str[0] < '0' || str[0] > '9') { break; } decimalFraction = decimalFraction * 10 + str[0] - '0'; ++numDigits; str.remove_prefix(1); } if (endOfParse != nullptr) { // to mimic the behaviour of std::from_chars consume all remaining digits in case the value was overly precise. *endOfParse = std::find_if_not(str.data(), str.data() + str.size(), [](char character) { return character >= '0' && character <= '9'; }); } // to ensure rounding to nearest we normalise all values to 7 decimal places while (numDigits < 7) { decimalFraction *= 10; ++numDigits; } // we add half the step between representable values to use integer truncation as a substitute for rounding to nearest. return (decimalFraction + 78125) / 156250; } } // namespace devilution ================================================ FILE: Source/utils/parse_int.hpp ================================================ #pragma once #include #include #include #include #include namespace devilution { enum class ParseIntError { ParseError = 1, OutOfRange }; template using ParseIntResult = tl::expected; template ParseIntResult ParseInt( std::string_view str, IntT min = std::numeric_limits::min(), IntT max = std::numeric_limits::max(), const char **endOfParse = nullptr) { IntT value; const std::from_chars_result result = std::from_chars(str.data(), str.data() + str.size(), value); if (endOfParse != nullptr) { *endOfParse = result.ptr; } if (result.ec == std::errc::invalid_argument) return tl::unexpected(ParseIntError::ParseError); if (result.ec == std::errc::result_out_of_range || value < min || value > max) return tl::unexpected(ParseIntError::OutOfRange); if (result.ec != std::errc()) return tl::unexpected(ParseIntError::ParseError); return value; } /** * @brief Parses a sequence of decimal characters into a 6 bit fixed point number in the range [0, 1.0] * @param str a potentially empty string of base 10 digits, optionally followed by non-digit characters * @param[out] endOfParse equivalent to std::from_chars_result::ptr, used to tell where parsing stopped * @return a value in the range [0, 64], representing a 2.6 fixed value in the range [0, 1.0] */ uint8_t ParseFixed6Fraction(std::string_view str, const char **endOfParse = nullptr); template ParseIntResult ParseFixed6(std::string_view str, const char **endOfParse = nullptr) { if (endOfParse != nullptr) { // To allow for early returns we set the end pointer to the start of the string, which is the common case for errors. *endOfParse = str.data(); } if (str.empty()) { return tl::unexpected { ParseIntError::ParseError }; } constexpr IntT minIntegerValue = std::numeric_limits::min() >> 6; constexpr IntT maxIntegerValue = std::numeric_limits::max() >> 6; const char *currentChar; // will be set by the call to parseInt ParseIntResult integerParseResult = ParseInt(str, minIntegerValue, maxIntegerValue, ¤tChar); bool isNegative = std::is_signed_v && str[0] == '-'; bool haveDigits = integerParseResult.has_value() || integerParseResult.error() == ParseIntError::OutOfRange; if (haveDigits) { str.remove_prefix(static_cast(std::distance(str.data(), currentChar))); } else if (isNegative) { str.remove_prefix(1); } // if the string has no leading digits we still need to try parse the fraction part uint8_t fractionPart = 0; if (!str.empty() && str[0] == '.') { // got a fractional part to read too str.remove_prefix(1); // skip past the decimal point fractionPart = ParseFixed6Fraction(str, ¤tChar); haveDigits = haveDigits || str.data() != currentChar; } if (!haveDigits) { // early return in case we got a string like "-.abc", don't want to set the end pointer in this case return tl::unexpected { ParseIntError::ParseError }; } if (endOfParse != nullptr) { *endOfParse = currentChar; } if (!integerParseResult.has_value() && integerParseResult.error() == ParseIntError::OutOfRange) { // if the integer parsing gave us an out of range value then we've done a bit of unnecessary // work parsing the fraction part, but it saves duplicating code. return integerParseResult; } // integerParseResult could be a ParseError at this point because of a string like ".123" or "-.1" // so we need to default to 0 (and use the result of the minus sign check when it's relevant) IntT integerPart = integerParseResult.value_or(0); // rounding could give us a value of 64 for the fraction part (e.g. 0.993 rounds to 1.0) so we need to ensure this doesn't overflow if (fractionPart >= 64 && (integerPart >= maxIntegerValue || (std::is_signed_v && integerPart <= minIntegerValue))) { return tl::unexpected { ParseIntError::OutOfRange }; } else { IntT fixedValue = integerPart << 6; if (isNegative) { fixedValue -= fractionPart; } else { fixedValue += fractionPart; } return fixedValue; } } } // namespace devilution ================================================ FILE: Source/utils/paths.cpp ================================================ #include "utils/paths.h" #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include "appfat.h" #include "utils/file_util.h" #include "utils/log.hpp" #include "utils/sdl_ptrs.h" #ifdef __IPHONEOS__ #include "platform/ios/ios_paths.h" #endif #ifdef NXDK #define NOMINMAX 1 #define WIN32_LEAN_AND_MEAN #include #endif #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif namespace devilution { namespace paths { namespace { std::optional basePath; std::optional prefPath; std::optional configPath; std::optional assetsPath; void AddTrailingSlash(std::string &path) { if (!path.empty() && path.back() != DirectorySeparator) path += DirectorySeparator; } [[maybe_unused]] std::string FromSDL(char *s) { const SDLUniquePtr pinned(s); std::string result = (s != nullptr ? s : ""); if (s == nullptr) { Log("{}", SDL_GetError()); SDL_ClearError(); } return result; } #ifdef NXDK const std::string &NxdkGetPrefPath() { static const std::string Path = []() { const char *path = "E:\\UDATA\\devilutionx\\"; if (CreateDirectoryA(path, nullptr) == FALSE && ::GetLastError() != ERROR_ALREADY_EXISTS) { DirErrorDlg(path); } return path; }(); return Path; } #endif std::string GetSdlBasePath() { std::string result; #if defined(__DJGPP__) // In DOS, use an empty base path. #elif defined(USE_SDL3) const char *s = SDL_GetBasePath(); if (s == nullptr) { LogError("{}", SDL_GetError()); SDL_ClearError(); } else { result = s; } #else result = FromSDL(SDL_GetBasePath()); #endif return result; } } // namespace const std::string &BasePath() { if (!basePath) { basePath = GetSdlBasePath(); } return *basePath; } const std::string &PrefPath() { if (!prefPath) { #if defined(__DJGPP__) prefPath = std::string(); #elif defined(__IPHONEOS__) prefPath = FromSDL(IOSGetPrefPath()); #elif defined(NXDK) prefPath = NxdkGetPrefPath(); #else prefPath = FromSDL(SDL_GetPrefPath("diasurgical", "devilution")); #if !defined(__amigaos__) && !defined(__DJGPP__) if (FileExistsAndIsWriteable("diablo.ini")) { prefPath = std::string(); } #endif #endif } return *prefPath; } const std::string &ConfigPath() { if (!configPath) { #if defined(__DJGPP__) configPath = std::string(); #elif defined(__IPHONEOS__) configPath = FromSDL(IOSGetPrefPath()); #elif defined(NXDK) configPath = NxdkGetPrefPath(); #else configPath = FromSDL(SDL_GetPrefPath("diasurgical", "devilution")); #if !defined(__amigaos__) && !defined(__DJGPP__) if (FileExistsAndIsWriteable("diablo.ini")) { configPath = std::string(); } #endif #endif } return *configPath; } const std::string &AssetsPath() { if (!assetsPath) { #if __EMSCRIPTEN__ || defined(__DJGPP__) assetsPath.emplace("assets" DIRECTORY_SEPARATOR_STR); #elif defined(NXDK) assetsPath.emplace("D:\\assets\\"); #elif defined(__3DS__) || defined(__SWITCH__) assetsPath.emplace("romfs:/"); #elif defined(__APPLE__) && defined(USE_SDL1) // In `Info.plist` we have // // SDL_FILESYSTEM_BASE_DIR_TYPE // resource // // This means `SDL_GetBasePath()` returns exedir for non-bundled // and the `app_dir.app/Resources/` for bundles. // // Our built-in resources are directly in the `devilutionx.app/Resources` directory // and are normally looked up via a relative lookup in `FindAsset`. // In SDL2, this is implemented by calling `SDL_OpenFPFromBundleOrFallback` // from `SDL_RWFromFile` but SDL1 doesn't do it, so we set the directory explicitly. // // Note that SDL3 reverts to SDL1 behaviour! // https://github.com/libsdl-org/SDL/blob/962268ca21ed10b9cee31198c22681099293f20a/docs/README-migration.md?plain=1#L1623 assetsPath.emplace(GetSdlBasePath()); #else assetsPath.emplace(GetSdlBasePath() + ("assets" DIRECTORY_SEPARATOR_STR)); #endif } return *assetsPath; } void SetBasePath(const std::string &path) { basePath = path; AddTrailingSlash(*basePath); } void SetPrefPath(const std::string &path) { prefPath = path; AddTrailingSlash(*prefPath); } void SetConfigPath(const std::string &path) { configPath = path; AddTrailingSlash(*configPath); } void SetAssetsPath(const std::string &path) { assetsPath = path; AddTrailingSlash(*assetsPath); } } // namespace paths } // namespace devilution ================================================ FILE: Source/utils/paths.h ================================================ #pragma once #include namespace devilution { namespace paths { const std::string &BasePath(); const std::string &PrefPath(); const std::string &ConfigPath(); const std::string &AssetsPath(); void SetBasePath(const std::string &path); void SetPrefPath(const std::string &path); void SetConfigPath(const std::string &path); void SetAssetsPath(const std::string &path); } // namespace paths } // namespace devilution ================================================ FILE: Source/utils/pcx.hpp ================================================ #pragma once #include #include namespace devilution { struct PCXHeader { uint8_t Manufacturer; uint8_t Version; uint8_t Encoding; uint8_t BitsPerPixel; uint16_t Xmin; uint16_t Ymin; uint16_t Xmax; uint16_t Ymax; uint16_t HDpi; uint16_t VDpi; uint8_t Colormap[48]; uint8_t Reserved; uint8_t NPlanes; uint16_t BytesPerLine; uint16_t PaletteInfo; uint16_t HscreenSize; uint16_t VscreenSize; uint8_t Filler[54]; }; static constexpr size_t PcxHeaderSize = 128; } // namespace devilution ================================================ FILE: Source/utils/pcx_to_clx.cpp ================================================ #include "utils/pcx_to_clx.hpp" #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "appfat.h" #include "utils/clx_encode.hpp" #include "utils/endian_read.hpp" #include "utils/endian_swap.hpp" #include "utils/endian_write.hpp" #include "utils/pcx.hpp" #ifdef DEBUG_PCX_TO_CL2_SIZE #include #include #endif namespace devilution { namespace { size_t GetReservationSize(size_t pcxSize) { // For the most part, CL2 is smaller than PCX, with a few exceptions. switch (pcxSize) { case 2352187: // ui_art\hf_logo1.pcx return 2464867; case 172347: // ui_art\creditsw.pcx return 172347; case 157275: // ui_art\credits.pcx return 173367; default: return pcxSize; } } bool LoadPcxMeta(AssetHandle &handle, int &width, int &height, uint8_t &bpp) { PCXHeader pcxhdr; if (!handle.read(&pcxhdr, PcxHeaderSize)) { return false; } width = Swap16LE(pcxhdr.Xmax) - Swap16LE(pcxhdr.Xmin) + 1; height = Swap16LE(pcxhdr.Ymax) - Swap16LE(pcxhdr.Ymin) + 1; bpp = pcxhdr.BitsPerPixel; return true; } } // namespace OptionalOwnedClxSpriteList PcxToClx(AssetHandle &handle, size_t fileSize, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette) { int width; int height; uint8_t bpp; if (!LoadPcxMeta(handle, width, height, bpp)) { return std::nullopt; } assert(bpp == 8); unsigned numFrames; unsigned frameHeight; if (numFramesOrFrameHeight > 0) { numFrames = numFramesOrFrameHeight; frameHeight = height / numFrames; } else { frameHeight = -numFramesOrFrameHeight; numFrames = height / frameHeight; } size_t pixelDataSize = fileSize; if (pixelDataSize <= PcxHeaderSize) { return std::nullopt; } pixelDataSize -= PcxHeaderSize; std::unique_ptr fileBuffer { new uint8_t[pixelDataSize] }; if (handle.read(fileBuffer.get(), pixelDataSize) == 0) { return std::nullopt; } // CLX header: frame count, frame offset for each frame, file size std::vector cl2Data; cl2Data.reserve(GetReservationSize(pixelDataSize)); cl2Data.resize(4 * (2 + static_cast(numFrames))); WriteLE32(cl2Data.data(), numFrames); // We process the PCX a whole frame at a time because the lines are reversed in CEL. auto frameBuffer = std::unique_ptr(new uint8_t[static_cast(frameHeight) * width]); const unsigned srcSkip = width % 2; uint8_t *dataPtr = fileBuffer.get(); for (unsigned frame = 1; frame <= numFrames; ++frame) { WriteLE32(&cl2Data[4 * static_cast(frame)], static_cast(cl2Data.size())); const size_t frameHeaderPos = cl2Data.size(); cl2Data.resize(cl2Data.size() + ClxFrameHeaderSize); // Frame header: WriteLE16(&cl2Data[frameHeaderPos], ClxFrameHeaderSize); WriteLE16(&cl2Data[frameHeaderPos + 2], static_cast(width)); WriteLE16(&cl2Data[frameHeaderPos + 4], static_cast(frameHeight)); for (unsigned j = 0; j < frameHeight; ++j) { uint8_t *buffer = &frameBuffer[static_cast(j) * width]; for (unsigned x = 0; x < static_cast(width);) { constexpr uint8_t PcxMaxSinglePixel = 0xBF; const uint8_t byte = *dataPtr++; if (byte <= PcxMaxSinglePixel) { *buffer++ = byte; ++x; continue; } constexpr uint8_t PcxRunLengthMask = 0x3F; const uint8_t runLength = (byte & PcxRunLengthMask); std::memset(buffer, *dataPtr++, runLength); buffer += runLength; x += runLength; } dataPtr += srcSkip; } unsigned transparentRunWidth = 0; size_t line = 0; while (line != frameHeight) { // Process line: const uint8_t *src = &frameBuffer[(frameHeight - (line + 1)) * width]; if (transparentColor) { unsigned solidRunWidth = 0; for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { if (*src == *transparentColor) { if (solidRunWidth != 0) { AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data); solidRunWidth = 0; } ++transparentRunWidth; } else { AppendClxTransparentRun(transparentRunWidth, cl2Data); transparentRunWidth = 0; ++solidRunWidth; } } if (solidRunWidth != 0) { AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data); } } else { AppendClxPixelsOrFillRun(src, width, cl2Data); } ++line; } AppendClxTransparentRun(transparentRunWidth, cl2Data); } WriteLE32(&cl2Data[4 * (1 + static_cast(numFrames))], static_cast(cl2Data.size())); if (outPalette != nullptr) { [[maybe_unused]] constexpr unsigned PcxPaletteSeparator = 0x0C; assert(*dataPtr == PcxPaletteSeparator); // PCX may not have a palette ++dataPtr; for (unsigned i = 0; i < 256; ++i) { outPalette->r = *dataPtr++; outPalette->g = *dataPtr++; outPalette->b = *dataPtr++; #ifndef USE_SDL1 outPalette->a = SDL_ALPHA_OPAQUE; #endif ++outPalette; } } // Release buffers before allocating the result array to reduce peak memory use. frameBuffer = nullptr; fileBuffer = nullptr; auto out = std::unique_ptr(new uint8_t[cl2Data.size()]); memcpy(&out[0], cl2Data.data(), cl2Data.size()); #ifdef DEBUG_PCX_TO_CL2_SIZE std::cout << "\t" << pixelDataSize << "\t" << cl2Data.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast(cl2Data.size()) - static_cast(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl; #endif return OwnedClxSpriteList { std::move(out) }; } } // namespace devilution ================================================ FILE: Source/utils/pcx_to_clx.hpp ================================================ #pragma once #include #include #ifdef USE_SDL3 #include #else #include #endif #include "engine/assets.hpp" #include "engine/clx_sprite.hpp" namespace devilution { /** * @brief Loads a PCX file as a CLX sprite. * * @param handle A non-null SDL_RWops handle. Closed by this function. * @param numFramesOrFrameHeight Pass a positive value with the number of frames, or the frame height as a negative value. * @param transparentColor The PCX palette index of the transparent color. */ OptionalOwnedClxSpriteList PcxToClx(AssetHandle &handle, size_t fileSize, int numFramesOrFrameHeight = 1, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); } // namespace devilution ================================================ FILE: Source/utils/png.h ================================================ #pragma once #ifdef USE_SDL3 #include #include #else #include #endif #include "engine/assets.hpp" #ifndef USE_SDL3 #ifdef __cplusplus extern "C" { #endif const int IMG_INIT_PNG = 0x00000002; int IMG_Init(int flags); void IMG_Quit(void); int IMG_isPNG(SDL_RWops *src); SDL_Surface *IMG_LoadPNG_RW(SDL_RWops *src); int IMG_SavePNG(SDL_Surface *surface, const char *file); int IMG_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst); inline SDL_Surface *IMG_LoadPNG(const char *file) { SDL_RWops *src = SDL_RWFromFile(file, "rb"); return IMG_LoadPNG_RW(src); } #ifdef __cplusplus } #endif #endif namespace devilution { #ifndef USE_SDL3 inline int InitPNG() { return IMG_Init(IMG_INIT_PNG); } inline void QuitPNG() { IMG_Quit(); } #endif inline SDL_Surface *LoadPNG(const char *file) { auto *rwops = OpenAssetAsSdlRwOps(file); #ifdef USE_SDL3 SDL_Surface *surface = IMG_LoadPNG_IO(rwops); SDL_CloseIO(rwops); #else SDL_Surface *surface = IMG_LoadPNG_RW(rwops); SDL_RWclose(rwops); #endif return surface; } } // namespace devilution ================================================ FILE: Source/utils/pointer_value_union.hpp ================================================ #pragma once #include namespace devilution { /** * @brief A tagged union of a pointer and a value in the space of a single pointer. * * Requires the type T to have alignment > 1. * Internally, uses the last bit to hold the tag: * 0 if it is a pointer, guaranteed by T's alignment requirements. * 1 if it is a value. */ template class PointerOrValue { static_assert(alignof(T) > 1, "requires alignof > 1"); static_assert(sizeof(T) < sizeof(T *), "type too large"); public: explicit PointerOrValue(const T *ptr) : repr_(reinterpret_cast(ptr)) { } explicit PointerOrValue(T val) : repr_((static_cast(val) << 1) | 1) { } [[nodiscard]] bool HoldsPointer() const { return (repr_ & 1) == 0; } [[nodiscard]] const T *AsPointer() const { return reinterpret_cast(repr_); } [[nodiscard]] T AsValue() const { return static_cast(repr_ >> 1); } private: uintptr_t repr_; }; } // namespace devilution ================================================ FILE: Source/utils/push_aulib_decoder.cpp ================================================ #include "push_aulib_decoder.h" #include #include #include #include #include #include #include #include #include "appfat.h" namespace devilution { namespace { float SampleToFloat(int16_t sample) { constexpr float Factor = 1.0F / (std::numeric_limits::max() + 1); return sample * Factor; } float SampleToFloat(uint8_t sample) { constexpr float Factor = 2.0F / std::numeric_limits::max(); return (sample * Factor) - 1; } template void ToFloats(const T *samples, float *out, unsigned count) { std::transform(samples, samples + count, out, [](T sample) { return SampleToFloat(sample); }); } } // namespace void PushAulibDecoder::DiscardPendingSamples() noexcept { const auto lock = std::lock_guard(queue_mutex_); queue_ = std::queue(); } bool PushAulibDecoder::open([[maybe_unused]] SDL_IOStream *rwops) { assert(rwops == nullptr); return true; } bool PushAulibDecoder::rewind() { return false; } std::chrono::microseconds PushAulibDecoder::duration() const { return {}; } bool PushAulibDecoder::seekToTime([[maybe_unused]] std::chrono::microseconds pos) { return false; } int PushAulibDecoder::doDecoding(float buf[], int len, bool &callAgain) { callAgain = false; constexpr auto WriteFloats = [](PushAulibDecoder::AudioQueueItem &item, float *out, unsigned count) { std::visit([&](const auto &samples) { ToFloats(&samples[item.pos], out, count); }, item.data); }; unsigned remaining = len; { const auto lock = std::lock_guard(queue_mutex_); AudioQueueItem *item; while ((item = Next()) != nullptr) { if (static_cast(remaining) <= item->len) { WriteFloats(*item, buf, remaining); item->pos += remaining; item->len -= remaining; if (item->len == 0) queue_.pop(); return len; } WriteFloats(*item, buf, item->len); buf += item->len; remaining -= static_cast(item->len); queue_.pop(); } } std::memset(buf, 0, remaining * sizeof(buf[0])); return len; } PushAulibDecoder::AudioQueueItem *PushAulibDecoder::Next() { while (!queue_.empty() && queue_.front().len == 0) queue_.pop(); if (queue_.empty()) return nullptr; return &queue_.front(); } } // namespace devilution ================================================ FILE: Source/utils/push_aulib_decoder.h ================================================ #pragma once #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include "utils/sdl_compat.h" #include "utils/sdl_mutex.h" namespace devilution { /** * @brief A Decoder interface implementations that simply has the samples pushed into it by the user. */ class PushAulibDecoder final : public ::Aulib::Decoder { public: PushAulibDecoder(int numChannels, int sampleRate) : numChannels_(numChannels) , sampleRate_(sampleRate) { } template void PushSamples(const T *data, unsigned size) noexcept { AudioQueueItem item { data, size }; const auto lock = std::lock_guard(queue_mutex_); queue_.push(std::move(item)); } void DiscardPendingSamples() noexcept; bool open(SDL_IOStream *rwops) override; [[nodiscard]] int getChannels() const override { return numChannels_; } [[nodiscard]] int getRate() const override { return sampleRate_; } bool rewind() override; [[nodiscard]] std::chrono::microseconds duration() const override; bool seekToTime(std::chrono::microseconds pos) override; protected: int doDecoding(float buf[], int len, bool &callAgain) override; private: struct AudioQueueItem { std::variant< std::unique_ptr, std::unique_ptr> data; unsigned len; unsigned pos; template AudioQueueItem(const T *data, unsigned size) : data { std::unique_ptr { new T[size] } } , len { size } , pos { 0 } { std::memcpy(std::get>(this->data).get(), data, size * sizeof(T)); } }; const int numChannels_; const int sampleRate_; // Requires holding the queue_mutex_. AudioQueueItem *Next(); std::queue queue_; SdlMutex queue_mutex_; }; } // namespace devilution ================================================ FILE: Source/utils/screen_reader.cpp ================================================ #include "utils/screen_reader.hpp" #include #include #ifdef _WIN32 #include "utils/file_util.h" #include #else #include #endif namespace devilution { #ifndef _WIN32 SPDConnection *Speechd; #endif void InitializeScreenReader() { #ifdef _WIN32 Tolk_Load(); #else Speechd = spd_open("DevilutionX", "DevilutionX", NULL, SPD_MODE_SINGLE); #endif } void ShutDownScreenReader() { #ifdef _WIN32 Tolk_Unload(); #else spd_close(Speechd); #endif } void SpeakText(std::string_view text, bool force) { static std::string SpokenText; if (!force && SpokenText == text) return; SpokenText = text; #ifdef _WIN32 const auto textUtf16 = ToWideChar(SpokenText); if (textUtf16 != nullptr) Tolk_Output(textUtf16.get(), true); #else spd_say(Speechd, SPD_TEXT, SpokenText.c_str()); #endif } } // namespace devilution ================================================ FILE: Source/utils/screen_reader.hpp ================================================ #pragma once #include namespace devilution { #ifdef SCREEN_READER_INTEGRATION void InitializeScreenReader(); void ShutDownScreenReader(); void SpeakText(std::string_view text, bool force = false); #else constexpr void InitializeScreenReader() { } constexpr void ShutDownScreenReader() { } constexpr void SpeakText(std::string_view text, bool force = false) { } #endif } // namespace devilution ================================================ FILE: Source/utils/sdl2_backports.h ================================================ #pragma once // Backports for older versions of SDL 2. #include #ifndef SDL_MAX_UINT32 #define SDL_MAX_UINT32 ((Uint32)0xFFFFFFFFu) #endif #if !SDL_VERSION_ATLEAST(2, 0, 4) inline SDL_bool SDL_PointInRect(const SDL_Point *p, const SDL_Rect *r) { return ((p->x >= r->x) && (p->x < (r->x + r->w)) && (p->y >= r->y) && (p->y < (r->y + r->h))) ? SDL_TRUE : SDL_FALSE; } #endif #if !SDL_VERSION_ATLEAST(2, 0, 5) inline SDL_Surface * SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, Uint32 format) { int bpp; Uint32 rmask, gmask, bmask, amask; if (!SDL_PixelFormatEnumToMasks(format, &bpp, &rmask, &gmask, &bmask, &amask)) return NULL; return SDL_CreateRGBSurface(flags, width, height, bpp, rmask, gmask, bmask, amask); } // From SDL 2.0.9. inline SDL_Surface * SDL_CreateRGBSurfaceWithFormatFrom(void *pixels, int width, int height, int depth, int pitch, Uint32 format) { SDL_Surface *surface; surface = SDL_CreateRGBSurfaceWithFormat(0, 0, 0, depth, format); if (surface != NULL) { surface->flags |= SDL_PREALLOC; surface->pixels = pixels; surface->w = width; surface->h = height; surface->pitch = pitch; SDL_SetClipRect(surface, NULL); } return surface; } #endif #if !SDL_VERSION_ATLEAST(2, 0, 14) inline SDL_bool SDL_GameControllerHasButton(SDL_GameController *gamecontroller, SDL_GameControllerButton button) { SDL_GameControllerButtonBind bind = SDL_GameControllerGetBindForButton(gamecontroller, button); return (bind.bindType != SDL_CONTROLLER_BINDTYPE_NONE) ? SDL_TRUE : SDL_FALSE; } #endif ================================================ FILE: Source/utils/sdl2_to_1_2_backports.cpp ================================================ #include "utils/sdl2_to_1_2_backports.h" #include #include #include #include #if defined(_WIN32) #define WIN32_LEAN_AND_MEAN #define NOMINMAX 1 #ifndef DEVILUTIONX_WINDOWS_NO_WCHAR #define UNICODE 1 #include #endif #include #endif #include "utils/console.h" #include "utils/str_cat.hpp" #define DEFAULT_PRIORITY SDL_LOG_PRIORITY_CRITICAL #define DEFAULT_ASSERT_PRIORITY SDL_LOG_PRIORITY_WARN #define DEFAULT_APPLICATION_PRIORITY SDL_LOG_PRIORITY_INFO #define DEFAULT_TEST_PRIORITY SDL_LOG_PRIORITY_VERBOSE namespace { // We use the same names of these structs as the SDL2 implementation: // NOLINTNEXTLINE(readability-identifier-naming) struct SDL_LogLevel { int category; SDL_LogPriority priority; SDL_LogLevel *next; }; SDL_LogLevel *SDL_loglevels; // NOLINT(readability-identifier-naming) SDL_LogPriority SDL_default_priority = DEFAULT_PRIORITY; // NOLINT(readability-identifier-naming) SDL_LogPriority SDL_assert_priority = DEFAULT_ASSERT_PRIORITY; // NOLINT(readability-identifier-naming) SDL_LogPriority SDL_application_priority = DEFAULT_APPLICATION_PRIORITY; // NOLINT(readability-identifier-naming) SDL_LogPriority SDL_test_priority = DEFAULT_TEST_PRIORITY; // NOLINT(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming) const char *const SDL_priority_prefixes[SDL_NUM_LOG_PRIORITIES] = { nullptr, "VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL" }; } // namespace void SDL_LogSetAllPriority(SDL_LogPriority priority) { for (SDL_LogLevel *entry = SDL_loglevels; entry != nullptr; entry = entry->next) { entry->priority = priority; } SDL_default_priority = priority; SDL_assert_priority = priority; SDL_application_priority = priority; } void SDL_LogSetPriority(int category, SDL_LogPriority priority) { SDL_LogLevel *entry; for (entry = SDL_loglevels; entry != nullptr; entry = entry->next) { if (entry->category == category) { entry->priority = priority; return; } } entry = static_cast(SDL_malloc(sizeof(*entry))); if (entry != nullptr) { entry->category = category; entry->priority = priority; entry->next = SDL_loglevels; SDL_loglevels = entry; } } SDL_LogPriority SDL_LogGetPriority(int category) { for (SDL_LogLevel *entry = SDL_loglevels; entry != nullptr; entry = entry->next) { if (entry->category == category) { return entry->priority; } } switch (category) { case SDL_LOG_CATEGORY_TEST: return SDL_test_priority; case SDL_LOG_CATEGORY_APPLICATION: return SDL_application_priority; case SDL_LOG_CATEGORY_ASSERT: return SDL_assert_priority; default: return SDL_default_priority; } } void SDL_Log(const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_INFO, fmt, ap); va_end(ap); } void SDL_LogVerbose(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_VERBOSE, fmt, ap); va_end(ap); } void SDL_LogDebug(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_DEBUG, fmt, ap); va_end(ap); } void SDL_LogInfo(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_INFO, fmt, ap); va_end(ap); } void SDL_LogWarn(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_WARN, fmt, ap); va_end(ap); } void SDL_LogError(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_ERROR, fmt, ap); va_end(ap); } void SDL_LogCritical(int category, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, SDL_LOG_PRIORITY_CRITICAL, fmt, ap); va_end(ap); } void SDL_LogMessage(int category, SDL_LogPriority priority, const char *fmt, ...) { va_list ap; va_start(ap, fmt); SDL_LogMessageV(category, priority, fmt, ap); va_end(ap); } void SDL_LogMessageV(int category, SDL_LogPriority priority, const char *fmt, va_list ap) { if (static_cast(priority) < 0 || priority >= SDL_NUM_LOG_PRIORITIES || SDL_LogGetPriority(category) > priority) return; ::devilution::printfInConsole("%s: ", SDL_priority_prefixes[priority]); ::devilution::vprintfInConsole(fmt, ap); ::devilution::printNewlineInConsole(); } namespace { #define DEFINE_COPY_ROW(name, type) \ void name(type *src, int src_w, type *dst, int dst_w) \ { \ type pixel = 0; \ \ int pos = 0x10000; \ int inc = (src_w << 16) / dst_w; \ for (int i = dst_w; i > 0; i--) { \ while (pos >= 0x10000L) { \ pixel = *src++; \ pos -= 0x10000L; \ } \ *dst++ = pixel; \ pos += inc; \ } \ } DEFINE_COPY_ROW(copy_row1, Uint8) DEFINE_COPY_ROW(copy_row2, Uint16) DEFINE_COPY_ROW(copy_row4, Uint32) void copy_row3(Uint8 *src, int src_w, Uint8 *dst, int dst_w) { Uint8 pixel[3] = { 0, 0, 0 }; int pos = 0x10000; int inc = (src_w << 16) / dst_w; for (int i = dst_w; i > 0; --i) { while (pos >= 0x10000L) { pixel[0] = *src++; pixel[1] = *src++; pixel[2] = *src++; pos -= 0x10000L; } *dst++ = pixel[0]; *dst++ = pixel[1]; *dst++ = pixel[2]; pos += inc; } } } // namespace int SDL_SoftStretch(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, const SDL_Rect *dstrect) { // All the ASM support has been removed, as the platforms that the ASM // implementation exists for support SDL2 anyway. int src_locked; int dst_locked; int pos, inc; int dst_maxrow; int src_row, dst_row; Uint8 *srcp = NULL; Uint8 *dstp; SDL_Rect full_src; SDL_Rect full_dst; const int bpp = dst->format->BytesPerPixel; if (!SDLBackport_PixelFormatFormatEq(src->format, dst->format)) { SDL_SetError("Only works with same format surfaces"); return -1; } /* Verify the blit rectangles */ if (srcrect) { if ((srcrect->x < 0) || (srcrect->y < 0) || ((srcrect->x + srcrect->w) > src->w) || ((srcrect->y + srcrect->h) > src->h)) { SDL_SetError("Invalid source blit rectangle"); return -1; } } else { full_src.x = 0; full_src.y = 0; full_src.w = src->w; full_src.h = src->h; srcrect = &full_src; } if (dstrect) { if ((dstrect->x < 0) || (dstrect->y < 0) || ((dstrect->x + dstrect->w) > dst->w) || ((dstrect->y + dstrect->h) > dst->h)) { SDL_SetError("Invalid destination blit rectangle"); return -1; } } else { full_dst.x = 0; full_dst.y = 0; full_dst.w = dst->w; full_dst.h = dst->h; dstrect = &full_dst; } /* Lock the destination if it's in hardware */ dst_locked = 0; if (SDL_MUSTLOCK(dst)) { if (SDL_LockSurface(dst) < 0) { SDL_SetError("Unable to lock destination surface"); return -1; } dst_locked = 1; } /* Lock the source if it's in hardware */ src_locked = 0; if (SDL_MUSTLOCK(src)) { if (SDL_LockSurface(src) < 0) { if (dst_locked) { SDL_UnlockSurface(dst); } SDL_SetError("Unable to lock source surface"); return -1; } src_locked = 1; } /* Set up the data... */ pos = 0x10000; inc = (srcrect->h << 16) / dstrect->h; src_row = srcrect->y; dst_row = dstrect->y; /* Perform the stretch blit */ for (dst_maxrow = dst_row + dstrect->h; dst_row < dst_maxrow; ++dst_row) { dstp = (Uint8 *)dst->pixels + (dst_row * dst->pitch) + (dstrect->x * bpp); while (pos >= 0x10000L) { srcp = (Uint8 *)src->pixels + (src_row * src->pitch) + (srcrect->x * bpp); ++src_row; pos -= 0x10000L; } switch (bpp) { case 1: copy_row1(srcp, srcrect->w, dstp, dstrect->w); break; case 2: copy_row2((Uint16 *)srcp, srcrect->w, (Uint16 *)dstp, dstrect->w); break; case 3: copy_row3(srcp, srcrect->w, dstp, dstrect->w); break; case 4: copy_row4((Uint32 *)srcp, srcrect->w, (Uint32 *)dstp, dstrect->w); break; } pos += inc; } /* We need to unlock the surfaces if they're locked */ if (dst_locked) { SDL_UnlockSurface(dst); } if (src_locked) { SDL_UnlockSurface(src); } return 0; } int SDL_BlitScaled(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect) { if (!SDLBackport_PixelFormatFormatEq(src->format, dst->format) || SDLBackport_IsPixelFormatIndexed(src->format)) { return SDL_BlitSurface(src, srcrect, dst, dstrect); } float src_x0, src_y0, src_x1, src_y1; float dst_x0, dst_y0, dst_x1, dst_y1; SDL_Rect final_src, final_dst; float scaling_w, scaling_h; int src_w, src_h; int dst_w, dst_h; /* Make sure the surfaces aren't locked */ if (!src || !dst) { SDL_SetError("SDL_UpperBlitScaled: passed a NULL surface"); return -1; } if (src->locked || dst->locked) { SDL_SetError("Surfaces must not be locked during blit"); return -1; } if (NULL == srcrect) { src_w = src->w; src_h = src->h; } else { src_w = srcrect->w; src_h = srcrect->h; } if (NULL == dstrect) { dst_w = dst->w; dst_h = dst->h; } else { dst_w = dstrect->w; dst_h = dstrect->h; } if (dst_w == src_w && dst_h == src_h) { /* No scaling, defer to regular blit */ return SDL_BlitSurface(src, srcrect, dst, dstrect); } scaling_w = (float)dst_w / src_w; scaling_h = (float)dst_h / src_h; if (NULL == dstrect) { dst_x0 = 0; dst_y0 = 0; dst_x1 = static_cast(dst_w - 1); dst_y1 = static_cast(dst_h - 1); } else { dst_x0 = dstrect->x; dst_y0 = dstrect->y; dst_x1 = dst_x0 + dst_w - 1; dst_y1 = dst_y0 + dst_h - 1; } if (NULL == srcrect) { src_x0 = 0; src_y0 = 0; src_x1 = static_cast(src_w - 1); src_y1 = static_cast(src_h - 1); } else { src_x0 = srcrect->x; src_y0 = srcrect->y; src_x1 = src_x0 + src_w - 1; src_y1 = src_y0 + src_h - 1; /* Clip source rectangle to the source surface */ if (src_x0 < 0) { dst_x0 -= src_x0 * scaling_w; src_x0 = 0; } if (src_x1 >= src->w) { dst_x1 -= (src_x1 - src->w + 1) * scaling_w; src_x1 = static_cast(src->w - 1); } if (src_y0 < 0) { dst_y0 -= src_y0 * scaling_h; src_y0 = 0; } if (src_y1 >= src->h) { dst_y1 -= (src_y1 - src->h + 1) * scaling_h; src_y1 = static_cast(src->h - 1); } } /* Clip destination rectangle to the clip rectangle */ /* Translate to clip space for easier calculations */ dst_x0 -= dst->clip_rect.x; dst_x1 -= dst->clip_rect.x; dst_y0 -= dst->clip_rect.y; dst_y1 -= dst->clip_rect.y; if (dst_x0 < 0) { src_x0 -= dst_x0 / scaling_w; dst_x0 = 0; } if (dst_x1 >= dst->clip_rect.w) { src_x1 -= (dst_x1 - dst->clip_rect.w + 1) / scaling_w; dst_x1 = static_cast(dst->clip_rect.w - 1); } if (dst_y0 < 0) { src_y0 -= dst_y0 / scaling_h; dst_y0 = 0; } if (dst_y1 >= dst->clip_rect.h) { src_y1 -= (dst_y1 - dst->clip_rect.h + 1) / scaling_h; dst_y1 = static_cast(dst->clip_rect.h - 1); } /* Translate back to surface coordinates */ dst_x0 += dst->clip_rect.x; dst_x1 += dst->clip_rect.x; dst_y0 += dst->clip_rect.y; dst_y1 += dst->clip_rect.y; final_src.x = static_cast(SDL_floor(src_x0 + 0.5)); final_src.y = static_cast(SDL_floor(src_y0 + 0.5)); src_w = std::max(static_cast(SDL_floor(src_x1 + 1 + 0.5)) - static_cast(SDL_floor(src_x0 + 0.5)), 0); src_h = std::max(static_cast(SDL_floor(src_y1 + 1 + 0.5)) - static_cast(SDL_floor(src_y0 + 0.5)), 0); final_src.w = static_cast(src_w); final_src.h = static_cast(src_h); final_dst.x = static_cast(SDL_floor(dst_x0 + 0.5)); final_dst.y = static_cast(SDL_floor(dst_y0 + 0.5)); dst_w = std::max(static_cast(SDL_floor(dst_x1 - dst_x0 + 1.5)), 0); dst_h = std::max(static_cast(SDL_floor(dst_y1 - dst_y0 + 1.5)), 0); final_dst.w = static_cast(dst_w); final_dst.h = static_cast(dst_h); if (dstrect) *dstrect = final_dst; if (final_dst.w == 0 || final_dst.h == 0 || final_src.w == 0 || final_src.h == 0) { /* No-op. */ return 0; } return SDL_SoftStretch(src, &final_src, dst, &final_dst); } // = Filesystem Sint64 SDL_RWsize(SDL_RWops *context) { const int current = SDL_RWtell(context); if (current == -1) return -1; const int begin = SDL_RWseek(context, 0, RW_SEEK_SET); if (begin == -1) return -1; const int end = SDL_RWseek(context, 0, RW_SEEK_END); if (end == -1) return -1; if (SDL_RWseek(context, current, RW_SEEK_SET) == -1) return -1; return end - begin; } #if defined(_WIN32) && !defined(DEVILUTIONX_WINDOWS_NO_WCHAR) namespace { // From sdl2-2.0.9/src/core/windows/SDL_windows.h #define WIN_StringToUTF8(S) SDL_iconv_string("UTF-8", "UTF-16LE", (char *)(S), (wcslen(S) + 1) * sizeof(WCHAR)) #define WIN_UTF8ToString(S) (WCHAR *)SDL_iconv_string("UTF-16LE", "UTF-8", (char *)(S), SDL_strlen(S) + 1) /* Sets an error message based on an HRESULT */ int WIN_SetErrorFromHRESULT(const char *prefix, HRESULT hr) { // From sdl2-2.0.9/src/core/windows/SDL_windows.c TCHAR buffer[1024]; char *message; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, hr, 0, buffer, SDL_arraysize(buffer), NULL); message = WIN_StringToUTF8(buffer); SDL_SetError("%s%s%s", prefix ? prefix : "", prefix ? ": " : "", message); SDL_free(message); return -1; } /* Sets an error message based on GetLastError() */ int WIN_SetError(const char *prefix) { // From sdl2-2.0.9/src/core/windows/SDL_windows.c return WIN_SetErrorFromHRESULT(prefix, GetLastError()); } } // namespace extern "C" char *SDL_GetBasePath(void) { // From sdl2-2.0.9/src/filesystem/windows/SDL_sysfilesystem.c typedef DWORD(WINAPI * GetModuleFileNameExW_t)(HANDLE, HMODULE, LPWSTR, DWORD); GetModuleFileNameExW_t pGetModuleFileNameExW; DWORD buflen = 128; WCHAR *path = NULL; HMODULE psapi = LoadLibrary(L"psapi.dll"); char *retval = NULL; DWORD len = 0; int i; if (!psapi) { WIN_SetError("Couldn't load psapi.dll"); return NULL; } pGetModuleFileNameExW = (GetModuleFileNameExW_t)GetProcAddress(psapi, "GetModuleFileNameExW"); if (!pGetModuleFileNameExW) { WIN_SetError("Couldn't find GetModuleFileNameExW"); FreeLibrary(psapi); return NULL; } while (SDL_TRUE) { void *ptr = SDL_realloc(path, buflen * sizeof(WCHAR)); if (!ptr) { SDL_free(path); FreeLibrary(psapi); SDL_OutOfMemory(); return NULL; } path = (WCHAR *)ptr; len = pGetModuleFileNameExW(GetCurrentProcess(), NULL, path, buflen); if (len != buflen) { break; } /* buffer too small? Try again. */ buflen *= 2; } FreeLibrary(psapi); if (len == 0) { SDL_free(path); WIN_SetError("Couldn't locate our .exe"); return NULL; } for (i = len - 1; i > 0; i--) { if (path[i] == '\\') { break; } } path[i + 1] = '\0'; /* chop off filename. */ retval = WIN_StringToUTF8(path); SDL_free(path); return retval; } extern "C" char *SDL_GetPrefPath(const char *org, const char *app) { // From sdl2-2.0.9/src/filesystem/windows/SDL_sysfilesystem.c /* * Vista and later has a new API for this, but SHGetFolderPath works there, * and apparently just wraps the new API. This is the new way to do it: * * SHGetKnownFolderPath(FOLDERID_RoamingAppData, KF_FLAG_CREATE, * NULL, &wszPath); */ WCHAR path[MAX_PATH]; char *retval = NULL; WCHAR *worg = NULL; WCHAR *wapp = NULL; size_t new_wpath_len = 0; BOOL api_result = FALSE; if (!app) { SDL_InvalidParamError("app"); return NULL; } if (!org) { org = ""; } if (!SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA | CSIDL_FLAG_CREATE, NULL, 0, path))) { WIN_SetError("Couldn't locate our prefpath"); return NULL; } worg = WIN_UTF8ToString(org); if (worg == NULL) { SDL_OutOfMemory(); return NULL; } wapp = WIN_UTF8ToString(app); if (wapp == NULL) { SDL_free(worg); SDL_OutOfMemory(); return NULL; } new_wpath_len = lstrlenW(worg) + lstrlenW(wapp) + lstrlenW(path) + 3; if ((new_wpath_len + 1) > MAX_PATH) { SDL_free(worg); SDL_free(wapp); WIN_SetError("Path too long."); return NULL; } if (*worg) { lstrcatW(path, L"\\"); lstrcatW(path, worg); } SDL_free(worg); api_result = CreateDirectoryW(path, NULL); if (api_result == FALSE) { if (GetLastError() != ERROR_ALREADY_EXISTS) { SDL_free(wapp); WIN_SetError("Couldn't create a prefpath."); return NULL; } } lstrcatW(path, L"\\"); lstrcatW(path, wapp); SDL_free(wapp); api_result = CreateDirectoryW(path, NULL); if (api_result == FALSE) { if (GetLastError() != ERROR_ALREADY_EXISTS) { WIN_SetError("Couldn't create a prefpath."); return NULL; } } lstrcatW(path, L"\\"); retval = WIN_StringToUTF8(path); return retval; } // For Apple, definitions are in Source/platform/macos_sdl1/SDL_filesystem.m #elif !defined(__APPLE__) namespace { #if !defined(__QNXNTO__) && !defined(__amigaos__) && !(defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) char *readSymLink(const char *path) { // From sdl2-2.0.9/src/filesystem/unix/SDL_sysfilesystem.c char *retval = NULL; ssize_t len = 64; ssize_t rc = -1; while (1) { char *ptr = (char *)SDL_realloc(retval, (size_t)len); if (ptr == NULL) { SDL_OutOfMemory(); break; } retval = ptr; rc = readlink(path, retval, len); if (rc == -1) { break; /* not a symlink, i/o error, etc. */ } else if (rc < len) { retval[rc] = '\0'; /* readlink doesn't null-terminate. */ return retval; /* we're good to go. */ } len *= 2; /* grow buffer, try again. */ } SDL_free(retval); return NULL; } #endif } // namespace extern "C" char *SDL_GetBasePath() { // From sdl2-2.0.9/src/filesystem/unix/SDL_sysfilesystem.c char *retval = NULL; #if defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0) TCHAR buffer[MAX_PATH] = { 0 }; GetModuleFileName(NULL, buffer, MAX_PATH); size_t len = std::string_view(buffer).size(); while (len > 0) { if (buffer[len - 1] == '\\') { break; } --len; } buffer[len] = '\0'; retval = static_cast(SDL_malloc(len + 1)); if (!retval) { SDL_OutOfMemory(); return NULL; } SDL_memcpy(retval, buffer, len + 1); #elif defined(__FREEBSD__) char fullpath[PATH_MAX]; size_t buflen = sizeof(fullpath); const int mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 }; if (sysctl(mib, SDL_arraysize(mib), fullpath, &buflen, NULL, 0) != -1) { retval = SDL_strdup(fullpath); if (!retval) { SDL_OutOfMemory(); return NULL; } } #elif defined(__OPENBSD__) char **retvalargs; size_t len; const int mib[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; if (sysctl(mib, 4, NULL, &len, NULL, 0) != -1) { retvalargs = SDL_malloc(len); if (!retvalargs) { SDL_OutOfMemory(); return NULL; } sysctl(mib, 4, retvalargs, &len, NULL, 0); retval = SDL_malloc(PATH_MAX + 1); if (retval) realpath(retvalargs[0], retval); SDL_free(retvalargs); } #elif defined(__SOLARIS__) const char *path = getexecname(); if ((path != NULL) && (path[0] == '/')) { /* must be absolute path... */ retval = SDL_strdup(path); if (!retval) { SDL_OutOfMemory(); return NULL; } } #elif defined(__3DS__) retval = SDL_strdup("file:sdmc:/3ds/devilutionx/"); #elif defined(__amigaos__) retval = SDL_strdup("PROGDIR:"); #else /* is a Linux-style /proc filesystem available? */ if (!retval && (access("/proc", F_OK) == 0)) { /* !!! FIXME: after 2.0.6 ships, let's delete this code and just use the /proc/%llu version. There's no reason to have two copies of this plus all the #ifdefs. --ryan. */ #if defined(__FREEBSD__) retval = readSymLink("/proc/curproc/file"); #elif defined(__NETBSD__) retval = readSymLink("/proc/curproc/exe"); #elif defined(__QNXNTO__) retval = SDL_LoadFile("/proc/self/exefile", NULL); #else retval = readSymLink("/proc/self/exe"); /* linux. */ if (retval == NULL) { /* older kernels don't have /proc/self ... try PID version... */ char path[64]; const int rc = (int)SDL_snprintf(path, sizeof(path), "/proc/%llu/exe", (unsigned long long)getpid()); if ((rc > 0) && (static_cast(rc) < sizeof(path))) { retval = readSymLink(path); } } #endif } /* If we had access to argv[0] here, we could check it for a path, or troll through $PATH looking for it, too. */ if (retval != NULL) { /* chop off filename. */ char *ptr = SDL_strrchr(retval, '/'); if (ptr != NULL) { *(ptr + 1) = '\0'; } else { /* shouldn't happen, but just in case... */ SDL_free(retval); retval = NULL; } } if (retval != NULL) { /* try to shrink buffer... */ char *ptr = (char *)SDL_realloc(retval, strlen(retval) + 1); if (ptr != NULL) retval = ptr; /* oh well if it failed. */ } #endif return retval; } extern "C" char *SDL_GetPrefPath(const char *org, const char *app) { // From sdl2-2.0.9/src/filesystem/unix/SDL_sysfilesystem.c /* * We use XDG's base directory spec, even if you're not on Linux. * This isn't strictly correct, but the results are relatively sane * in any case. * * https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html */ #if (defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) || defined(__DJGPP__) // On Windows9x there is no such thing as PrefPath. Simply use the current directory. char *result = (char *)SDL_malloc(1); *result = '\0'; return result; #else const char *envr = SDL_getenv("XDG_DATA_HOME"); const char *append; char *retval = NULL; char *ptr = NULL; size_t len = 0; #if defined(__3DS__) retval = SDL_strdup("sdmc:/3ds/devilutionx/"); return retval; #elif defined(__amigaos__) retval = SDL_strdup("PROGDIR:"); return retval; #endif if (!app) { SDL_InvalidParamError("app"); return NULL; } if (!org) { org = ""; } if (!envr) { /* You end up with "$HOME/.local/share/Game Name 2" */ envr = SDL_getenv("HOME"); if (!envr) { /* we could take heroic measures with /etc/passwd, but oh well. */ SDL_SetError("neither XDG_DATA_HOME nor HOME environment is set"); return NULL; } #if defined(__unix__) || defined(__unix) append = "/.local/share/"; #else append = "/"; #endif } else { append = "/"; } len = SDL_strlen(envr); if (envr[len - 1] == '/') append += 1; len += SDL_strlen(append) + SDL_strlen(org) + SDL_strlen(app) + 3; retval = (char *)SDL_malloc(len); if (!retval) { SDL_OutOfMemory(); return NULL; } if (*org) { *devilution::BufCopy(retval, envr, append, org, "/", app) = '\0'; } else { *devilution::BufCopy(retval, envr, append, app) = '\0'; } for (ptr = retval + 1; *ptr; ptr++) { if (*ptr == '/') { *ptr = '\0'; if (mkdir(retval, 0700) != 0 && errno != EEXIST) goto error; *ptr = '/'; } } if (mkdir(retval, 0700) != 0 && errno != EEXIST) { error: SDL_SetError("Couldn't create directory '%s': '%s'", retval, strerror(errno)); SDL_free(retval); return NULL; } // Append trailing / size_t final_len = SDL_strlen(retval); if (final_len + 1 < len) { retval[final_len++] = '/'; retval[final_len] = '\0'; } return retval; #endif } #endif ================================================ FILE: Source/utils/sdl2_to_1_2_backports.h ================================================ #pragma once #include #include #include #include #include #include #include #include "utils/attributes.h" #include "utils/console.h" #ifndef _WIN32 #include #endif #define WINDOW_ICON_NAME 0 #define SDL_Window SDL_Surface //== Utility #define SDL_zero(x) SDL_memset(&(x), 0, sizeof((x))) #define SDL_InvalidParamError(param) SDL_SetError("Parameter '%s' is invalid", (param)) #define SDL_floor floor #define SDL_MAX_UINT32 ((Uint32)0xFFFFFFFFu) //== Events handling #define SDL_threadID Uint32 #define SDL_Keysym SDL_keysym #define SDL_Keycode SDLKey #define SDL_Keymod SDLMod #define SDLK_PRINTSCREEN SDLK_PRINT #define SDLK_SCROLLLOCK SDLK_SCROLLOCK #define SDLK_NUMLOCKCLEAR SDLK_NUMLOCK #define SDLK_KP_1 SDLK_KP1 #define SDLK_KP_2 SDLK_KP2 #define SDLK_KP_3 SDLK_KP3 #define SDLK_KP_4 SDLK_KP4 #define SDLK_KP_5 SDLK_KP5 #define SDLK_KP_6 SDLK_KP6 #define SDLK_KP_7 SDLK_KP7 #define SDLK_KP_8 SDLK_KP8 #define SDLK_KP_9 SDLK_KP9 #define SDLK_KP_0 SDLK_KP0 #define SDLK_KP_COMMA SDLK_COMMA #define SDLK_LGUI SDLK_LSUPER #define SDLK_RGUI SDLK_RSUPER #define SDL_SCANCODE_GRAVE 53 // Haptic events are not supported in SDL1. #define SDL_INIT_HAPTIC 0 // For now we only process ASCII input when using SDL1. #define SDL_TEXTINPUTEVENT_TEXT_SIZE 2 #define SDL_JoystickID Sint32 #define SDL_JoystickNameForIndex SDL_JoystickName enum SDL_LogCategory { SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_CATEGORY_ERROR, SDL_LOG_CATEGORY_ASSERT, SDL_LOG_CATEGORY_SYSTEM, SDL_LOG_CATEGORY_AUDIO, SDL_LOG_CATEGORY_VIDEO, SDL_LOG_CATEGORY_RENDER, SDL_LOG_CATEGORY_INPUT, SDL_LOG_CATEGORY_TEST, }; enum SDL_LogPriority { SDL_LOG_PRIORITY_VERBOSE = 1, SDL_LOG_PRIORITY_DEBUG, SDL_LOG_PRIORITY_INFO, SDL_LOG_PRIORITY_WARN, SDL_LOG_PRIORITY_ERROR, SDL_LOG_PRIORITY_CRITICAL, SDL_NUM_LOG_PRIORITIES }; void SDL_Log(const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(1, 2); void SDL_LogVerbose(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogDebug(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogInfo(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogWarn(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogError(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogCritical(int category, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(2, 3); void SDL_LogMessage(int category, SDL_LogPriority priority, const char *fmt, ...) DVL_PRINTF_ATTRIBUTE(3, 4); void SDL_LogMessageV(int category, SDL_LogPriority priority, const char *fmt, va_list ap) DVL_PRINTF_ATTRIBUTE(3, 0); void SDL_LogSetAllPriority(SDL_LogPriority priority); void SDL_LogSetPriority(int category, SDL_LogPriority priority); SDL_LogPriority SDL_LogGetPriority(int category); inline void SDL_StartTextInput() { SDL_EnableUNICODE(1); } inline void SDL_StopTextInput() { SDL_EnableUNICODE(0); } inline void SDL_SetTextInputRect(const SDL_Rect *r) { } inline bool SDLC_StartTextInput(SDL_Window *) { SDL_StartTextInput(); return true; } inline bool SDLC_StopTextInput(SDL_Window *) { SDL_StopTextInput(); return true; } inline bool SDL_SetTextInputArea(SDL_Window *, const SDL_Rect *, int) { return true; } //== Graphics helpers typedef struct SDL_Point { int x; int y; } SDL_Point; inline SDL_bool SDL_PointInRect(const SDL_Point *p, const SDL_Rect *r) { return ((p->x >= r->x) && (p->x < (r->x + r->w)) && (p->y >= r->y) && (p->y < (r->y + r->h))) ? SDL_TRUE : SDL_FALSE; } inline bool SDLC_PointInRect(const SDL_Point *p, const SDL_Rect *r) { return SDL_PointInRect(p, r) == SDL_TRUE; } //= Messagebox (simply logged to stderr for now) enum { // clang-format off SDL_MESSAGEBOX_ERROR = 1 << 4, /**< error dialog */ SDL_MESSAGEBOX_WARNING = 1 << 5, /**< warning dialog */ SDL_MESSAGEBOX_INFORMATION = 1 << 6, /**< informational dialog */ // clang-format on }; #ifdef __3DS__ /** Defined in Source/platform/ctr/messagebox.cpp */ int SDL_ShowSimpleMessageBox(Uint32 flags, const char *title, const char *message, SDL_Surface *window); #else inline int SDL_ShowSimpleMessageBox(Uint32 flags, const char *title, const char *message, SDL_Surface *window) { SDL_Log("MSGBOX: %s\n%s", title, message); return 0; } #endif //= Window handling inline void SDL_GetWindowPosition(SDL_Window *window, int *x, int *y) { *x = window->clip_rect.x; *y = window->clip_rect.x; SDL_Log("SDL_GetWindowPosition %i %i", *x, *y); } inline void SDL_GetWindowSize(SDL_Window *window, int *w, int *h) { *w = window->clip_rect.w; *h = window->clip_rect.h; SDL_Log("SDL_GetWindowSize %i %i", *w, *h); } inline void SDL_DestroyWindow(SDL_Window *window) { } inline void SDL_WarpMouseInWindow(SDL_Window *window, int x, int y) { SDL_WarpMouse(x, y); } //= Renderer stubs #define SDL_Renderer void //= Texture stubs #define SDL_Texture void //= Palette handling inline SDL_Palette * SDL_AllocPalette(int ncolors) { SDL_Palette *palette; /* Input validation */ if (ncolors < 1) { SDL_InvalidParamError("ncolors"); return NULL; } palette = (SDL_Palette *)SDL_malloc(sizeof(*palette)); if (!palette) { SDL_OutOfMemory(); return NULL; } palette->colors = (SDL_Color *)SDL_malloc(ncolors * sizeof(*palette->colors)); if (!palette->colors) { SDL_free(palette); return NULL; } palette->ncolors = ncolors; SDL_memset(palette->colors, 0xFF, ncolors * sizeof(*palette->colors)); return palette; } inline void SDL_FreePalette(SDL_Palette *palette) { if (!palette) { SDL_InvalidParamError("palette"); return; } SDL_free(palette->colors); SDL_free(palette); } inline bool SDL_HasColorKey(SDL_Surface *surface) { return (surface->flags & SDL_SRCCOLORKEY) != 0; } //= Pixel formats #define SDL_PIXELFORMAT_INDEX8 1 #define SDL_PIXELFORMAT_RGB888 2 #define SDL_PIXELFORMAT_RGBA8888 3 inline void SDLBackport_PixelformatToMask(int pixelformat, Uint32 *flags, Uint32 *rmask, Uint32 *gmask, Uint32 *bmask, Uint32 *amask) { if (pixelformat == SDL_PIXELFORMAT_RGBA8888) { #if SDL_BYTEORDER == SDL_BIG_ENDIAN *rmask = 0xff000000; *gmask = 0x00ff0000; *bmask = 0x0000ff00; *amask = 0x000000ff; #else *rmask = 0x000000ff; *gmask = 0x0000ff00; *bmask = 0x00ff0000; *amask = 0xff000000; #endif } else if (pixelformat == SDL_PIXELFORMAT_RGB888) { #if SDL_BYTEORDER == SDL_BIG_ENDIAN *rmask = 0xff000000; *gmask = 0x00ff0000; *bmask = 0x0000ff00; #else *rmask = 0x000000ff; *gmask = 0x0000ff00; *bmask = 0x00ff0000; #endif *amask = 0; } else { *rmask = *gmask = *bmask = *amask = 0; } } /** * A limited implementation of `a.format` == `b.format` from SDL2. */ inline bool SDLBackport_PixelFormatFormatEq(const SDL_PixelFormat *a, const SDL_PixelFormat *b) { return a->BitsPerPixel == b->BitsPerPixel && (a->palette != NULL) == (b->palette != NULL) && a->Rmask == b->Rmask && a->Gmask == b->Gmask && a->Bmask == b->Bmask; } /** * Similar to `SDL_ISPIXELFORMAT_INDEXED` from SDL2. */ inline bool SDLBackport_IsPixelFormatIndexed(const SDL_PixelFormat *pf) { return pf->BitsPerPixel == 8 && pf->palette != NULL; } //= Surface creation inline SDL_Surface * SDL_CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, Uint32 format) { Uint32 rmask, gmask, bmask, amask; SDLBackport_PixelformatToMask(format, &flags, &rmask, &gmask, &bmask, &amask); return SDL_CreateRGBSurface(flags, width, height, depth, rmask, gmask, bmask, amask); } inline SDL_Surface * SDL_CreateRGBSurfaceWithFormatFrom(void *pixels, Uint32 flags, int width, int height, int depth, Uint32 format) { Uint32 rmask, gmask, bmask, amask; SDLBackport_PixelformatToMask(format, &flags, &rmask, &gmask, &bmask, &amask); return SDL_CreateRGBSurfaceFrom(pixels, flags, width, height, depth, rmask, gmask, bmask, amask); } //= BlitScaled backport from SDL 2.0.9. // NOTE: Not thread-safe int SDL_SoftStretch(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, const SDL_Rect *dstrect); // NOTE: The second argument is const in SDL2 but not here. int SDL_BlitScaled(SDL_Surface *src, SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect); //== Filesystem #define SDL_RWOPS_UNKNOWN 0U Sint64 SDL_RWsize(SDL_RWops *context); extern "C" char *SDL_GetBasePath(); extern "C" char *SDL_GetPrefPath(const char *org, const char *app); ================================================ FILE: Source/utils/sdl_bilinear_scale.cpp ================================================ #include "utils/sdl_bilinear_scale.hpp" #include #include #include #ifdef USE_SDL3 #include #include #include #else #include #endif #include "appfat.h" // Performs bilinear scaling using fixed-width integer math. namespace devilution { namespace { int Frac(int fixedPoint) { return fixedPoint & 0xffff; } int ToInt(int fixedPoint) { return fixedPoint >> 16; } std::unique_ptr CreateMixFactors(unsigned srcSize, unsigned dstSize) { std::unique_ptr result { new int[dstSize + 1] }; const auto scale = static_cast(65536.0 * static_cast(srcSize - 1) / dstSize); int mix = 0; for (unsigned i = 0; i <= dstSize; ++i) { result[i] = mix; mix = Frac(mix) + scale; } return result; }; uint8_t MixColors(uint8_t first, uint8_t second, int ratio) { return ToInt((second - first) * ratio) + first; } uint8_t MixColorsWithAlpha(uint8_t first, uint8_t firstAlpha, uint8_t second, uint8_t secondAlpha, uint8_t mixedAlpha, int ratio) { if (mixedAlpha == 0) return 0; if (mixedAlpha == 255) return MixColors(first, second, ratio); const int firstWithAlpha = first * firstAlpha; const int secondWithAlpha = second * secondAlpha; // We want to calculate: // // (ToInt((secondWithAlpha - firstWithAlpha) * ratio) + firstWithAlpha) / mixedAlpha // // However, the above written as-is can overflow in the argument to `ToInt`. // To avoid the overflow we divide each term by `mixedAlpha` separately. // // This would be lower precision and could result in a negative overall result, // so we do the rounding-up integer division for each term (instead of a truncating one): // // (a + (b - 1)) / b return ToInt((secondWithAlpha - firstWithAlpha) * ((ratio + (mixedAlpha - 1)) / mixedAlpha)) + (firstWithAlpha + (mixedAlpha - 1)) / mixedAlpha; } } // namespace void BilinearScale32(SDL_Surface *src, SDL_Surface *dst) { const std::unique_ptr mixXs = CreateMixFactors(src->w, dst->w); const std::unique_ptr mixYs = CreateMixFactors(src->h, dst->h); const unsigned dgap = dst->pitch - dst->w * 4; auto *srcPixels = static_cast(src->pixels); auto *dstPixels = static_cast(dst->pixels); int *curMixY = &mixYs[0]; unsigned srcY = 0; for (unsigned y = 0; y < static_cast(dst->h); ++y) { uint8_t *s[4] = { srcPixels, // Self srcPixels + 4, // Right srcPixels + src->pitch, // Bottom srcPixels + src->pitch + 4 // Bottom right }; int *curMixX = &mixXs[0]; unsigned srcX = 0; for (unsigned x = 0; x < static_cast(dst->w); ++x) { const int mixX = Frac(*curMixX); const int mixY = Frac(*curMixY); const uint8_t alpha0 = MixColors(s[0][3], s[1][3], mixX); const uint8_t alpha1 = MixColors(s[2][3], s[3][3], mixX); const uint8_t finalAlpha = MixColors(alpha0, alpha1, mixY); if (finalAlpha == 0) { dstPixels[0] = 0; dstPixels[1] = 0; dstPixels[2] = 0; dstPixels[3] = 0; } else if (finalAlpha == 255) { for (unsigned channel = 0; channel < 3; ++channel) { dstPixels[channel] = MixColors( MixColors(s[0][channel], s[1][channel], mixX), MixColors(s[2][channel], s[3][channel], mixX), mixY); } dstPixels[3] = 255; } else { for (unsigned channel = 0; channel < 3; ++channel) { dstPixels[channel] = MixColorsWithAlpha( MixColorsWithAlpha(s[0][channel], s[0][3], s[1][channel], s[1][3], alpha0, mixX), alpha0, MixColorsWithAlpha(s[2][channel], s[2][3], s[3][channel], s[3][3], alpha1, mixX), alpha1, finalAlpha, mixY); } dstPixels[3] = finalAlpha; } ++curMixX; if (*curMixX > 0) { unsigned step = ToInt(*curMixX); srcX += step; if (srcX <= static_cast(src->w)) { step *= 4; for (auto &v : s) { v += step; } } } dstPixels += 4; } ++curMixY; if (*curMixY > 0) { const unsigned step = ToInt(*curMixY); srcY += step; if (srcY < static_cast(src->h)) { srcPixels += step * src->pitch; } } dstPixels += dgap; } } void BilinearDownscaleByHalf8(SDL_Surface *src, const uint8_t paletteBlendingTable[256][256], SDL_Surface *dst, uint8_t transparentIndex) { SDL_Rect srcClipRect, dstClipRect; #ifdef USE_SDL3 if (!SDL_GetSurfaceClipRect(src, &srcClipRect)) app_fatal(SDL_GetError()); if (!SDL_GetSurfaceClipRect(dst, &dstClipRect)) app_fatal(SDL_GetError()); #else srcClipRect = src->clip_rect; dstClipRect = dst->clip_rect; #endif const auto *const srcPixelsBegin = static_cast(src->pixels) + static_cast((srcClipRect.y * src->pitch) + srcClipRect.x); auto *const dstPixelsBegin = static_cast(dst->pixels) + static_cast((dstClipRect.y * dst->pitch) + dstClipRect.x); for (unsigned y = 0, h = static_cast(dstClipRect.h); y < h; ++y) { const uint8_t *srcPixels = srcPixelsBegin + static_cast(2 * y * src->pitch); uint8_t *dstPixels = dstPixelsBegin + static_cast(y * dst->pitch); for (unsigned x = 0, w = static_cast(dstClipRect.w); x < w; ++x) { uint8_t quad[] = { srcPixels[0], srcPixels[1], srcPixels[src->pitch], srcPixels[src->pitch + 1] }; // Attempt to avoid blending with transparent pixels if (quad[0] == transparentIndex) quad[0] = quad[1]; if (quad[1] == transparentIndex) quad[1] = quad[0]; if (quad[2] == transparentIndex) quad[2] = quad[3]; if (quad[3] == transparentIndex) quad[3] = quad[2]; uint8_t top = paletteBlendingTable[quad[0]][quad[1]]; uint8_t bottom = paletteBlendingTable[quad[2]][quad[3]]; if (top == transparentIndex) top = bottom; if (bottom == transparentIndex) bottom = top; *dstPixels++ = paletteBlendingTable[top][bottom]; srcPixels += 2; } } } } // namespace devilution ================================================ FILE: Source/utils/sdl_bilinear_scale.hpp ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #endif namespace devilution { /** * @brief Bilinear 32-bit scaling. * Requires `src` and `dst` to have the same pixel format (ARGB8888 or RGBA8888). */ void BilinearScale32(SDL_Surface *src, SDL_Surface *dst); /** * @brief Streamlined bilinear downscaling using blended transparency table. * Requires `src` and `dst` to have the same pixel format (INDEX8). */ void BilinearDownscaleByHalf8(SDL_Surface *src, const uint8_t paletteBlendingTable[256][256], SDL_Surface *dst, uint8_t transparentIndex); } // namespace devilution ================================================ FILE: Source/utils/sdl_compat.h ================================================ // Compatibility wrappers for SDL 1 & 2. #pragma once #ifdef USE_SDL3 #include #include #include #include #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/display.h" #else #include "utils/sdl2_backports.h" #endif #endif #ifdef USE_SDL1 #define SDL_Scancode Uint8 #endif #ifdef USE_SDL3 #define SDLC_PushEvent SDL_PushEvent #define SDLC_SetClipboardText SDL_SetClipboardText #define SDLC_HasClipboardText SDL_HasClipboardText inline SDL_Keycode SDLC_EventKey(const SDL_Event &event) { return event.key.key; } inline SDL_Scancode SDLC_EventScancode(const SDL_Event &event) { return event.key.scancode; } inline int SDLC_EventMotionIntX(const SDL_Event &event) { return static_cast(event.motion.x); } inline int SDLC_EventMotionIntY(const SDL_Event &event) { return static_cast(event.motion.y); } inline int SDLC_EventButtonIntX(const SDL_Event &event) { return static_cast(event.button.x); } inline int SDLC_EventButtonIntY(const SDL_Event &event) { return static_cast(event.button.y); } inline int SDLC_EventWheelIntX(const SDL_Event &event) { #if SDL_VERSION_ATLEAST(3, 2, 12) return event.wheel.integer_x; #else return static_cast(event.wheel.x); #endif } inline int SDLC_EventWheelIntY(const SDL_Event &event) { #if SDL_VERSION_ATLEAST(3, 2, 12) return event.wheel.integer_y; #else return static_cast(event.wheel.y); #endif } inline const SDL_GamepadAxisEvent &SDLC_EventGamepadAxis(const SDL_Event &event) { return event.gaxis; } inline const SDL_GamepadButtonEvent &SDLC_EventGamepadButton(const SDL_Event &event) { return event.gbutton; } inline const SDL_GamepadDeviceEvent &SDLC_EventGamepadDevice(const SDL_Event &event) { return event.gdevice; } #define SDLC_SURFACE_BITSPERPIXEL(surface) SDL_BITSPERPIXEL(surface->format) #define SDLC_StartTextInput SDL_StartTextInput #define SDLC_StopTextInput SDL_StopTextInput #define SDLC_SetSurfacePalette SDL_SetSurfacePalette #define SDLC_PointInRect SDL_PointInRect #define SDLC_ShowCursor SDL_ShowCursor #define SDLC_HideCursor SDL_HideCursor #define SDLC_ShowSimpleMessageBox SDL_ShowSimpleMessageBox #else #define SDL_EVENT_AUDIO_DEVICE_ADDED SDL_AUDIODEVICEADDED #define SDL_EVENT_AUDIO_DEVICE_REMOVED SDL_AUDIODEVICEREMOVED #define SDL_EVENT_CLIPBOARD_UPDATE SDL_CLIPBOARDUPDATE #define SDL_EVENT_GAMEPAD_AXIS_MOTION SDL_CONTROLLERAXISMOTION #define SDL_EVENT_GAMEPAD_BUTTON_DOWN SDL_CONTROLLERBUTTONDOWN #define SDL_EVENT_GAMEPAD_BUTTON_UP SDL_CONTROLLERBUTTONUP #define SDL_EVENT_GAMEPAD_ADDED SDL_CONTROLLERDEVICEADDED #define SDL_EVENT_GAMEPAD_REMAPPED SDL_CONTROLLERDEVICEREMAPPED #define SDL_EVENT_GAMEPAD_REMOVED SDL_CONTROLLERDEVICEREMOVED #define SDL_EVENT_GAMEPAD_SENSOR_UPDATE SDL_CONTROLLERSENSORUPDATE #define SDL_EVENT_GAMEPAD_STEAM_HANDLE_UPDATED SDL_CONTROLLERSTEAMHANDLEUPDATED #define SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN SDL_CONTROLLERTOUCHPADDOWN #define SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION SDL_CONTROLLERTOUCHPADMOTION #define SDL_EVENT_GAMEPAD_TOUCHPAD_UP SDL_CONTROLLERTOUCHPADUP #define SDL_GAMEPAD_AXIS_INVALID SDL_CONTROLLER_AXIS_INVALID #define SDL_GAMEPAD_AXIS_LEFTX SDL_CONTROLLER_AXIS_LEFTX #define SDL_GAMEPAD_AXIS_LEFTY SDL_CONTROLLER_AXIS_LEFTY #define SDL_GAMEPAD_AXIS_COUNT SDL_CONTROLLER_AXIS_MAX #define SDL_GAMEPAD_AXIS_RIGHTX SDL_CONTROLLER_AXIS_RIGHTX #define SDL_GAMEPAD_AXIS_RIGHTY SDL_CONTROLLER_AXIS_RIGHTY #define SDL_GAMEPAD_AXIS_LEFT_TRIGGER SDL_CONTROLLER_AXIS_TRIGGERLEFT #define SDL_GAMEPAD_AXIS_RIGHT_TRIGGER SDL_CONTROLLER_AXIS_TRIGGERRIGHT #define SDL_EVENT_FINGER_DOWN SDL_FINGERDOWN #define SDL_EVENT_FINGER_MOTION SDL_FINGERMOTION #define SDL_EVENT_FINGER_UP SDL_FINGERUP #define SDL_EVENT_JOYSTICK_AXIS_MOTION SDL_JOYAXISMOTION #define SDL_EVENT_JOYSTICK_BATTERY_UPDATED SDL_JOYBATTERYUPDATED #define SDL_EVENT_JOYSTICK_BUTTON_DOWN SDL_JOYBUTTONDOWN #define SDL_EVENT_JOYSTICK_BUTTON_UP SDL_JOYBUTTONUP #define SDL_EVENT_JOYSTICK_ADDED SDL_JOYDEVICEADDED #define SDL_EVENT_JOYSTICK_REMOVED SDL_JOYDEVICEREMOVED #define SDL_EVENT_JOYSTICK_BALL_MOTION SDL_JOYBALLMOTION #define SDL_EVENT_JOYSTICK_HAT_MOTION SDL_JOYHATMOTION #define SDL_EVENT_KEY_DOWN SDL_KEYDOWN #define SDL_EVENT_KEYMAP_CHANGED SDL_KEYMAPCHANGED #define SDL_EVENT_KEY_UP SDL_KEYUP #define SDL_EVENT_LAST SDL_LASTEVENT #define SDL_EVENT_LOCALE_CHANGED SDL_LOCALECHANGED #define SDL_EVENT_MOUSE_BUTTON_DOWN SDL_MOUSEBUTTONDOWN #define SDL_EVENT_MOUSE_BUTTON_UP SDL_MOUSEBUTTONUP #define SDL_EVENT_MOUSE_MOTION SDL_MOUSEMOTION #define SDL_EVENT_MOUSE_WHEEL SDL_MOUSEWHEEL #define SDL_EVENT_POLL_SENTINEL SDL_POLLSENTINEL #define SDL_EVENT_QUIT SDL_QUIT #define SDL_EVENT_TEXT_INPUT SDL_TEXTINPUT #define SDL_EVENT_USER SDL_USEREVENT #define SDL_Gamepad SDL_GameController #define SDL_GamepadButton SDL_GameControllerButton #define SDL_GamepadAxisEvent SDL_ControllerAxisEvent #define SDL_GamepadButtonEvent SDL_ControllerButtonEvent #define SDL_GamepadDeviceEvent SDL_ControllerDeviceEvent #define SDL_GAMEPAD_BUTTON_SOUTH SDL_CONTROLLER_BUTTON_A #define SDL_GAMEPAD_BUTTON_EAST SDL_CONTROLLER_BUTTON_B #define SDL_GAMEPAD_BUTTON_BACK SDL_CONTROLLER_BUTTON_BACK #define SDL_GAMEPAD_BUTTON_DPAD_DOWN SDL_CONTROLLER_BUTTON_DPAD_DOWN #define SDL_GAMEPAD_BUTTON_DPAD_LEFT SDL_CONTROLLER_BUTTON_DPAD_LEFT #define SDL_GAMEPAD_BUTTON_DPAD_RIGHT SDL_CONTROLLER_BUTTON_DPAD_RIGHT #define SDL_GAMEPAD_BUTTON_DPAD_UP SDL_CONTROLLER_BUTTON_DPAD_UP #define SDL_GAMEPAD_BUTTON_GUIDE SDL_CONTROLLER_BUTTON_GUIDE #define SDL_GAMEPAD_BUTTON_INVALID SDL_CONTROLLER_BUTTON_INVALID #define SDL_GAMEPAD_BUTTON_LEFT_SHOULDER SDL_CONTROLLER_BUTTON_LEFTSHOULDER #define SDL_GAMEPAD_BUTTON_LEFT_STICK SDL_CONTROLLER_BUTTON_LEFTSTICK #define SDL_GAMEPAD_BUTTON_COUNT SDL_CONTROLLER_BUTTON_MAX #define SDL_GAMEPAD_BUTTON_MISC1 SDL_CONTROLLER_BUTTON_MISC1 #define SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1 SDL_CONTROLLER_BUTTON_PADDLE1 #define SDL_GAMEPAD_BUTTON_LEFT_PADDLE1 SDL_CONTROLLER_BUTTON_PADDLE2 #define SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2 SDL_CONTROLLER_BUTTON_PADDLE3 #define SDL_GAMEPAD_BUTTON_LEFT_PADDLE2 SDL_CONTROLLER_BUTTON_PADDLE4 #define SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER SDL_CONTROLLER_BUTTON_RIGHTSHOULDER #define SDL_GAMEPAD_BUTTON_RIGHT_STICK SDL_CONTROLLER_BUTTON_RIGHTSTICK #define SDL_GAMEPAD_BUTTON_START SDL_CONTROLLER_BUTTON_START #define SDL_GAMEPAD_BUTTON_TOUCHPAD SDL_CONTROLLER_BUTTON_TOUCHPAD #define SDL_GAMEPAD_BUTTON_WEST SDL_CONTROLLER_BUTTON_X #define SDL_GAMEPAD_BUTTON_NORTH SDL_CONTROLLER_BUTTON_Y #define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_LEFT #define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_PAIR #define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT #define SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO #define SDL_GAMEPAD_TYPE_PS3 SDL_CONTROLLER_TYPE_PS3 #define SDL_GAMEPAD_TYPE_PS4 SDL_CONTROLLER_TYPE_PS4 #define SDL_GAMEPAD_TYPE_PS5 SDL_CONTROLLER_TYPE_PS5 #define SDL_GAMEPAD_TYPE_STANDARD SDL_CONTROLLER_TYPE_UNKNOWN #define SDL_GAMEPAD_TYPE_VIRTUAL SDL_CONTROLLER_TYPE_VIRTUAL #define SDL_GAMEPAD_TYPE_XBOX360 SDL_CONTROLLER_TYPE_XBOX360 #define SDL_GAMEPAD_TYPE_XBOXONE SDL_CONTROLLER_TYPE_XBOXONE #define SDL_KMOD_ALT KMOD_ALT #define SDL_KMOD_CAPS KMOD_CAPS #define SDL_KMOD_CTRL KMOD_CTRL #define SDL_KMOD_GUI KMOD_GUI #define SDL_KMOD_LALT KMOD_LALT #define SDL_KMOD_LCTRL KMOD_LCTRL #define SDL_KMOD_LGUI KMOD_LGUI #define SDL_KMOD_LSHIFT KMOD_LSHIFT #define SDL_KMOD_MODE KMOD_MODE #define SDL_KMOD_NONE KMOD_NONE #define SDL_KMOD_NUM KMOD_NUM #define SDL_KMOD_RALT KMOD_RALT #define SDL_KMOD_RCTRL KMOD_RCTRL #define SDL_KMOD_RGUI KMOD_RGUI #define SDL_KMOD_RSHIFT KMOD_RSHIFT #define SDL_KMOD_SCROLL KMOD_SCROLL #define SDL_KMOD_SHIFT KMOD_SHIFT #define SDLK_GRAVE SDLK_BACKQUOTE #define SDLK_APOSTROPHE SDLK_QUOTE #define SDLK_A SDLK_a #define SDLK_B SDLK_b #define SDLK_C SDLK_c #define SDLK_D SDLK_d #define SDLK_E SDLK_e #define SDLK_F SDLK_f #define SDLK_G SDLK_g #define SDLK_H SDLK_h #define SDLK_I SDLK_i #define SDLK_J SDLK_j #define SDLK_K SDLK_k #define SDLK_L SDLK_l #define SDLK_M SDLK_m #define SDLK_N SDLK_n #define SDLK_O SDLK_o #define SDLK_P SDLK_p #define SDLK_Q SDLK_q #define SDLK_R SDLK_r #define SDLK_S SDLK_s #define SDLK_T SDLK_t #define SDLK_U SDLK_u #define SDLK_V SDLK_v #define SDLK_W SDLK_w #define SDLK_X SDLK_x #define SDLK_Y SDLK_y #define SDLK_Z SDLK_z #define SDL_PIXELFORMAT_XBGR4444 SDL_PIXELFORMAT_BGR444 #define SDL_PIXELFORMAT_XBGR1555 SDL_PIXELFORMAT_BGR555 #define SDL_PIXELFORMAT_XBGR8888 SDL_PIXELFORMAT_BGR888 #define SDL_PIXELFORMAT_XRGB4444 SDL_PIXELFORMAT_RGB444 #define SDL_PIXELFORMAT_XRGB1555 SDL_PIXELFORMAT_RGB555 #define SDL_PIXELFORMAT_XRGB8888 SDL_PIXELFORMAT_RGB888 #define SDL_IO_SEEK_CUR RW_SEEK_CUR #define SDL_IO_SEEK_END RW_SEEK_END #define SDL_IO_SEEK_SET RW_SEEK_SET #define SDL_IOFromConstMem SDL_RWFromConstMem #define SDL_IOFromFile SDL_RWFromFile #define SDL_IOFromMem SDL_RWFromMem #define SDL_IOStream SDL_RWops #define SDL_SeekIO SDL_RWseek #define SDL_GetIOSize SDL_RWsize #define SDL_TellIO SDL_RWtell #define SDL_GetLogOutputFunction SDL_LogGetOutputFunction #define SDL_GetLogPriority SDL_LogGetPriority #define SDL_ResetLogPriorities SDL_LogResetPriorities #define SDL_SetLogPriorities SDL_LogSetAllPriority #define SDL_SetLogOutputFunction SDL_LogSetOutputFunction #define SDL_SetLogPriority SDL_LogSetPriority #define SDL_LOG_PRIORITY_COUNT SDL_NUM_LOG_PRIORITIES #define SDL_ThreadID SDL_threadID #define SDLC_SURFACE_BITSPERPIXEL(surface) surface->format->BitsPerPixel inline bool SDLC_PushEvent(SDL_Event *event) { return SDL_PushEvent(event) == 1; } inline #ifdef USE_SDL1 SDLKey #else SDL_Keycode #endif SDLC_EventKey(const SDL_Event &event) { return event.key.keysym.sym; } inline SDL_Scancode SDLC_EventScancode(const SDL_Event &event) { return event.key.keysym.scancode; } inline int SDLC_EventMotionIntX(const SDL_Event &event) { return event.motion.x; } inline int SDLC_EventMotionIntY(const SDL_Event &event) { return event.motion.y; } inline int SDLC_EventButtonIntX(const SDL_Event &event) { return event.button.x; } inline int SDLC_EventButtonIntY(const SDL_Event &event) { return event.button.y; } inline bool SDL_FillSurfaceRect(SDL_Surface *dst, #ifdef USE_SDL1 SDL_Rect *rect, #else const SDL_Rect *rect, #endif Uint32 color) { return SDL_FillRect(dst, rect, color) == 0; } inline bool SDL_SetSurfaceClipRect(SDL_Surface *surface, const SDL_Rect *rect) { return SDL_SetClipRect(surface, rect) == SDL_TRUE; } inline bool SDL_SetSurfaceColorKey(SDL_Surface *surface, bool enabled, Uint32 key) { return SDL_SetColorKey(surface, enabled ? 1 : 0, key) == 0; } #ifndef USE_SDL1 inline bool SDLC_SetClipboardText(const char *text) { return SDL_SetClipboardText(text) == 0; } inline bool SDLC_HasClipboardText() { return SDL_HasClipboardText() == SDL_TRUE; } inline int SDLC_EventWheelIntX(const SDL_Event &event) { return event.wheel.x; } inline int SDLC_EventWheelIntY(const SDL_Event &event) { return event.wheel.y; } inline const SDL_GamepadAxisEvent &SDLC_EventGamepadAxis(const SDL_Event &event) { return event.caxis; } inline const SDL_GamepadButtonEvent &SDLC_EventGamepadButton(const SDL_Event &event) { return event.cbutton; } inline const SDL_GamepadDeviceEvent &SDLC_EventGamepadDevice(const SDL_Event &event) { return event.cdevice; } inline bool SDL_GamepadHasButton(SDL_Gamepad *gamepad, SDL_GamepadButton button) { return SDL_GameControllerHasButton(gamepad, button) == SDL_TRUE; } inline bool SDL_GetGamepadButton(SDL_Gamepad *gamepad, SDL_GamepadButton button) { return SDL_GameControllerGetButton(gamepad, button) != 0; } inline bool SDL_GetRenderScale(SDL_Renderer *renderer, float *scaleX, float *scaleY) { SDL_RenderGetScale(renderer, scaleX, scaleY); return true; } // For SDL1.2, these are defined in sdl2_to_1_2_backports.h inline bool SDLC_StartTextInput([[maybe_unused]] SDL_Window *window) { SDL_StartTextInput(); return true; } inline bool SDLC_StopTextInput([[maybe_unused]] SDL_Window *window) { SDL_StopTextInput(); return true; } inline bool SDL_SetTextInputArea([[maybe_unused]] SDL_Window *window, SDL_Rect *rect, [[maybe_unused]] int cursor) { SDL_SetTextInputRect(rect); return true; } inline bool SDLC_SetSurfacePalette(SDL_Surface *surface, SDL_Palette *palette) { return SDL_SetSurfacePalette(surface, palette) == 0; } inline bool SDL_CursorVisible() { return SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE; } // For SDL1.2, this is defined in sdl2_to_1_2_backports.h inline bool SDLC_PointInRect(const SDL_Point *p, const SDL_Rect *r) { return SDL_PointInRect(p, r) == SDL_TRUE; } #endif inline bool SDLC_ShowCursor() { return SDL_ShowCursor(SDL_ENABLE) >= 0; } inline bool SDLC_HideCursor() { return SDL_ShowCursor(SDL_DISABLE) >= 0; } inline size_t SDL_ReadIO(SDL_IOStream *context, void *ptr, size_t size) { #if SDL_VERSION_ATLEAST(2, 0, 0) return SDL_RWread(context, ptr, 1, size); #else return SDL_RWread(context, ptr, 1, static_cast(size)); #endif } inline size_t SDL_WriteIO(SDL_IOStream *context, const void *ptr, size_t size) { #if SDL_VERSION_ATLEAST(2, 0, 0) return SDL_RWwrite(context, ptr, 1, size); #else return SDL_RWwrite(context, ptr, 1, static_cast(size)); #endif } inline bool SDL_CloseIO(SDL_IOStream *iostr) { return SDL_RWclose(iostr) == 0; } #define SDLC_ShowSimpleMessageBox(flags, title, message, window) SDL_ShowSimpleMessageBox(flags, title, message, window) == 0 #endif #ifndef USE_SDL1 #define SDLC_KEYSTATE_LEFTCTRL SDL_SCANCODE_LCTRL #define SDLC_KEYSTATE_RIGHTCTRL SDL_SCANCODE_RCTRL #define SDLC_KEYSTATE_LEFTSHIFT SDL_SCANCODE_LSHIFT #define SDLC_KEYSTATE_RIGHTSHIFT SDL_SCANCODE_RSHIFT #define SDLC_KEYSTATE_LALT SDL_SCANCODE_LALT #define SDLC_KEYSTATE_RALT SDL_SCANCODE_RALT #define SDLC_KEYSTATE_UP SDL_SCANCODE_UP #define SDLC_KEYSTATE_DOWN SDL_SCANCODE_DOWN #define SDLC_KEYSTATE_LEFT SDL_SCANCODE_LEFT #define SDLC_KEYSTATE_RIGHT SDL_SCANCODE_RIGHT #else #define SDLC_KEYSTATE_LEFTCTRL SDLK_LCTRL #define SDLC_KEYSTATE_RIGHTCTRL SDLK_RCTRL #define SDLC_KEYSTATE_LEFTSHIFT SDLK_LSHIFT #define SDLC_KEYSTATE_RIGHTSHIFT SDLK_LSHIFT #define SDLC_KEYSTATE_LALT SDLK_LALT #define SDLC_KEYSTATE_RALT SDLK_RALT #define SDLC_KEYSTATE_UP SDLK_UP #define SDLC_KEYSTATE_DOWN SDLK_DOWN #define SDLC_KEYSTATE_LEFT SDLK_LEFT #define SDLC_KEYSTATE_RIGHT SDLK_RIGHT #endif #ifdef USE_SDL3 inline const bool *SDLC_GetKeyState() #else inline const Uint8 *SDLC_GetKeyState() #endif { #ifndef USE_SDL1 return SDL_GetKeyboardState(nullptr); #else return SDL_GetKeyState(nullptr); #endif } // Convert events to renderer coordinates. // This is done automatically in SDL2 but not in SDL3 and SDL1.2. inline bool SDLC_ConvertEventToRenderCoordinates( #ifndef USE_SDL1 SDL_Renderer *renderer, #else void *, #endif SDL_Event *event) { #ifdef USE_SDL3 if (renderer != nullptr) { return SDL_ConvertEventToRenderCoordinates(renderer, event); } #elif !defined(USE_SDL1) // No-op in SDL2. #else if (event->type == SDL_MOUSEMOTION) { devilution::OutputToLogical(&event->motion.x, &event->motion.y); } else if (event->type == SDL_MOUSEBUTTONDOWN || event->type == SDL_MOUSEBUTTONUP) { devilution::OutputToLogical(&event->button.x, &event->button.y); } #endif return true; } // Sets the palette's colors and: // SDL3 and SDL2: Points the surface's palette to the given palette if necessary. // SDL1: Sets the surface's colors. inline bool SDLC_SetSurfaceAndPaletteColors(SDL_Surface *surface, SDL_Palette *palette, SDL_Color *colors, int firstcolor, int ncolors) { #if defined(USE_SDL1) if (ncolors > (palette->ncolors - firstcolor)) { SDL_SetError("ncolors > (palette->ncolors - firstcolor)"); return false; } if (colors != (palette->colors + firstcolor)) SDL_memcpy(palette->colors + firstcolor, colors, ncolors * sizeof(*colors)); #if SDL1_VIDEO_MODE_BPP == 8 // When the video surface is 8bit, we need to set the output palette as well. SDL_Surface *videoSurface = SDL_GetVideoSurface(); SDL_SetColors(videoSurface, colors, firstcolor, ncolors); if (videoSurface == surface) return true; #endif // In SDL1, the surface always has its own distinct palette, so we need to // update it as well. return SDL_SetPalette(surface, SDL_LOGPAL, colors, firstcolor, ncolors) != 0; #elif defined(USE_SDL3) if (!SDL_SetPaletteColors(palette, colors, firstcolor, ncolors)) return false; if (SDL_GetSurfacePalette(surface) != palette) { if (!SDL_SetSurfacePalette(surface, palette)) return false; } return true; #else if (SDL_SetPaletteColors(palette, colors, firstcolor, ncolors) < 0) return false; if (surface->format->palette != palette) return SDL_SetSurfacePalette(surface, palette) == 0; return true; #endif } ================================================ FILE: Source/utils/sdl_geometry.h ================================================ /** * @file sdl_geometry.h * Helpers for SDL geometry types */ #pragma once #ifdef USE_SDL3 #include #else #include #if SDL_VERSION_ATLEAST(2, 0, 0) #include #else #include #endif #endif #include "engine/rectangle.hpp" namespace devilution { /** * @brief Same as constructing directly but avoids type conversion warnings. */ inline SDL_Rect MakeSdlRect( decltype(SDL_Rect {}.x) x, decltype(SDL_Rect {}.y) y, decltype(SDL_Rect {}.w) w, decltype(SDL_Rect {}.h) h) { return SDL_Rect { x, y, w, h }; } inline SDL_Rect MakeSdlRect(Rectangle rect) { return MakeSdlRect(rect.position.x, rect.position.y, rect.size.width, rect.size.height); } constexpr Rectangle MakeRectangle(SDL_Rect sdlRect) { return { Point { sdlRect.x, sdlRect.y }, Size { sdlRect.w, sdlRect.h } }; } } // namespace devilution ================================================ FILE: Source/utils/sdl_mutex.h ================================================ #pragma once #include #ifdef USE_SDL3 #include #include #else #include #include #endif #include "appfat.h" namespace devilution { /* * RAII wrapper for SDL_mutex. Satisfies std's "Lockable" (SDL 2) or "BasicLockable" (SDL 1) * requirements so it can be used with std::lock_guard and friends. */ #ifdef __DJGPP__ class SdlMutex final { public: SdlMutex() noexcept { } ~SdlMutex() noexcept { } SdlMutex(const SdlMutex &) = delete; SdlMutex(SdlMutex &&) = delete; SdlMutex &operator=(const SdlMutex &) = delete; SdlMutex &operator=(SdlMutex &&) = delete; void lock() noexcept { } void unlock() noexcept { } void *get() noexcept { return nullptr; } // Dummy }; #else class SdlMutex final { public: SdlMutex() : mutex_(SDL_CreateMutex()) { if (mutex_ == nullptr) ErrSdl(); } ~SdlMutex() { SDL_DestroyMutex(mutex_); } SdlMutex(const SdlMutex &) = delete; SdlMutex(SdlMutex &&) = delete; SdlMutex &operator=(const SdlMutex &) = delete; SdlMutex &operator=(SdlMutex &&) = delete; void lock() noexcept // NOLINT(readability-identifier-naming) { #ifdef USE_SDL3 SDL_LockMutex(mutex_); #else int err = SDL_LockMutex(mutex_); if (err == -1) ErrSdl(); #endif } #if SDL_VERSION_ATLEAST(2, 0, 0) [[nodiscard]] bool try_lock() noexcept // NOLINT(readability-identifier-naming) { const bool ok = #ifdef USE_SDL3 SDL_TryLockMutex(mutex_); #else SDL_TryLockMutex(mutex_) == 0; #endif return ok; } #endif void unlock() noexcept // NOLINT(readability-identifier-naming) { #ifdef USE_SDL3 SDL_UnlockMutex(mutex_); #else int err = SDL_UnlockMutex(mutex_); if (err == -1) ErrSdl(); #endif } #ifdef USE_SDL3 SDL_Mutex *get() { return mutex_; } #else SDL_mutex *get() { return mutex_; } #endif private: #ifdef USE_SDL3 SDL_Mutex *mutex_; #else SDL_mutex *mutex_; #endif }; #endif } // namespace devilution ================================================ FILE: Source/utils/sdl_ptrs.h ================================================ #pragma once /** * @brief std::unique_ptr specializations for SDL types. */ #include #include #ifdef USE_SDL3 #include #include #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif namespace devilution { /** * @brief Deletes the SDL surface using `SDL_FreeSurface`. */ struct SDLSurfaceDeleter { void operator()(SDL_Surface *surface) const { #ifdef USE_SDL3 SDL_DestroySurface(surface); #else SDL_FreeSurface(surface); #endif } }; using SDLSurfaceUniquePtr = std::unique_ptr; #if SDL_VERSION_ATLEAST(2, 0, 0) struct SDLCursorDeleter { void operator()(SDL_Cursor *cursor) const { #ifdef USE_SDL3 SDL_DestroyCursor(cursor); #else SDL_FreeCursor(cursor); #endif } }; using SDLCursorUniquePtr = std::unique_ptr; struct SDLTextureDeleter { void operator()(SDL_Texture *texture) const { SDL_DestroyTexture(texture); } }; using SDLTextureUniquePtr = std::unique_ptr; #endif struct SDLPaletteDeleter { void operator()(SDL_Palette *palette) const { #ifdef USE_SDL3 SDL_DestroyPalette(palette); #else SDL_FreePalette(palette); #endif } }; using SDLPaletteUniquePtr = std::unique_ptr; /** * @brief Deletes the object using `SDL_free`. */ template struct SDLFreeDeleter { static_assert(!std::is_same::value, "SDL_Surface should use SDLSurfaceUniquePtr instead."); void operator()(T *obj) const { SDL_free(obj); } }; /** * @brief A unique pointer to T that is deleted with SDL_free. */ template using SDLUniquePtr = std::unique_ptr>; } // namespace devilution ================================================ FILE: Source/utils/sdl_thread.cpp ================================================ #include "utils/sdl_thread.h" namespace devilution { #ifndef __DJGPP__ int SDLCALL SdlThread::ThreadTranslate(void *ptr) { auto handler = (void (*)())ptr; handler(); return 0; } void SdlThread::ThreadDeleter(SDL_Thread *thread) { if (thread != nullptr) app_fatal("Joinable thread destroyed"); } #endif } // namespace devilution ================================================ FILE: Source/utils/sdl_thread.h ================================================ #pragma once #include #ifdef USE_SDL3 #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #endif #endif #include "appfat.h" #include "utils/attributes.h" namespace devilution { namespace this_sdl_thread { #ifdef USE_SDL3 inline SDL_ThreadID get_id() #else inline SDL_threadID get_id() #endif { #if defined(__DJGPP__) return 1; #else return SDL_GetThreadID(nullptr); #endif } } // namespace this_sdl_thread #if defined(__DJGPP__) class SdlThread final { public: SdlThread(int(SDLCALL *handler)(void *), void *data) { if (handler != nullptr) handler(data); } explicit SdlThread(void (*handler)(void)) { if (handler != nullptr) handler(); } SdlThread() = default; bool joinable() const { return false; } #ifdef USE_SDL3 SDL_ThreadID get_id() const #else SDL_threadID get_id() const #endif { return this_sdl_thread::get_id(); } void join() { } }; #else class SdlThread final { static int SDLCALL ThreadTranslate(void *ptr); static void ThreadDeleter(SDL_Thread *thread); std::unique_ptr thread { nullptr, ThreadDeleter }; public: SdlThread(int(SDLCALL *handler)(void *), void *data) #ifdef USE_SDL1 : thread(SDL_CreateThread(handler, data), ThreadDeleter) #else : thread(SDL_CreateThread(handler, nullptr, data), ThreadDeleter) #endif { if (thread == nullptr) ErrSdl(); } explicit SdlThread(void (*handler)(void)) : SdlThread(ThreadTranslate, (void *)handler) { } SdlThread() = default; bool joinable() const { return thread != nullptr; } #ifdef USE_SDL3 SDL_ThreadID get_id() const #else SDL_threadID get_id() const #endif { return SDL_GetThreadID(thread.get()); } void join() { if (!joinable()) return; if (get_id() == this_sdl_thread::get_id()) app_fatal("Thread joined from within itself"); SDL_WaitThread(thread.get(), nullptr); thread.release(); } }; #endif } // namespace devilution ================================================ FILE: Source/utils/sdl_wrap.h ================================================ #pragma once #ifdef USE_SDL3 #include #include #include #include #include #else #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #endif #include "appfat.h" #include "utils/sdl_ptrs.h" #define NonNull(x) NullErrDlg(x, __FILE__, __LINE__) namespace devilution { namespace SDLWrap { template T NullErrDlg(T x, const char *file, int line) { if (x == nullptr) ErrDlg("SDL Error", SDL_GetError(), file, line); return x; } inline SDLSurfaceUniquePtr CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask) { #ifdef USE_SDL3 return SDLSurfaceUniquePtr { NonNull(SDL_CreateSurface(width, height, SDL_GetPixelFormatForMasks(depth, Rmask, Gmask, Bmask, Amask))) }; #else return SDLSurfaceUniquePtr { NonNull(SDL_CreateRGBSurface(flags, width, height, depth, Rmask, Gmask, Bmask, Amask)) }; #endif } #ifdef USE_SDL3 inline SDLSurfaceUniquePtr CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, SDL_PixelFormat format) { return SDLSurfaceUniquePtr { NonNull(SDL_CreateSurface(width, height, format)) }; } #else inline SDLSurfaceUniquePtr CreateRGBSurfaceWithFormat(Uint32 flags, int width, int height, int depth, Uint32 format) { return SDLSurfaceUniquePtr { NonNull(SDL_CreateRGBSurfaceWithFormat(flags, width, height, depth, format)) }; } #endif #ifdef USE_SDL3 inline SDLSurfaceUniquePtr CreateRGBSurfaceWithFormatFrom(void *pixels, int width, int height, int depth, int pitch, SDL_PixelFormat format) { return SDLSurfaceUniquePtr { NonNull(SDL_CreateSurfaceFrom(width, height, format, pixels, pitch)) }; } #else inline SDLSurfaceUniquePtr CreateRGBSurfaceWithFormatFrom(void *pixels, int width, int height, int depth, int pitch, Uint32 format) { return SDLSurfaceUniquePtr { NonNull(SDL_CreateRGBSurfaceWithFormatFrom(pixels, width, height, depth, pitch, format)) }; } #endif #ifndef USE_SDL1 inline SDLSurfaceUniquePtr ConvertSurface(SDL_Surface *src, const SDL_PixelFormat *fmt, Uint32 flags) #else inline SDLSurfaceUniquePtr ConvertSurface(SDL_Surface *src, SDL_PixelFormat *fmt, Uint32 flags) #endif { #ifdef USE_SDL3 return SDLSurfaceUniquePtr { NonNull(SDL_ConvertSurface(src, *fmt)) }; #else return SDLSurfaceUniquePtr { NonNull(SDL_ConvertSurface(src, fmt, flags)) }; #endif } #ifndef USE_SDL1 #ifdef USE_SDL3 inline SDLSurfaceUniquePtr ConvertSurfaceFormat(SDL_Surface *src, SDL_PixelFormat format, Uint32 flags) { return SDLSurfaceUniquePtr { NonNull(SDL_ConvertSurface(src, format)) }; } #else inline SDLSurfaceUniquePtr ConvertSurfaceFormat(SDL_Surface *src, Uint32 format, Uint32 flags) { return SDLSurfaceUniquePtr { NonNull(SDL_ConvertSurfaceFormat(src, format, flags)) }; } #endif #ifdef USE_SDL3 inline SDLTextureUniquePtr CreateTexture(SDL_Renderer *renderer, SDL_PixelFormat format, SDL_TextureAccess access, int w, int h) { return SDLTextureUniquePtr { NonNull(SDL_CreateTexture(renderer, format, access, w, h)) }; } #else inline SDLTextureUniquePtr CreateTexture(SDL_Renderer *renderer, Uint32 format, int access, int w, int h) { return SDLTextureUniquePtr { NonNull(SDL_CreateTexture(renderer, format, access, w, h)) }; } #endif #endif inline SDLPaletteUniquePtr AllocPalette(int ncolors = 256) { #ifdef USE_SDL3 return SDLPaletteUniquePtr { NonNull(SDL_CreatePalette(ncolors)) }; #else return SDLPaletteUniquePtr { NonNull(SDL_AllocPalette(ncolors)) }; #endif } } // namespace SDLWrap } // namespace devilution ================================================ FILE: Source/utils/soundsample.cpp ================================================ #include "utils/soundsample.h" #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #include #include #include #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" #else #include "utils/sdl2_backports.h" #endif #include "utils/aulib.hpp" #endif #include "engine/assets.hpp" #include "options.h" #include "utils/log.hpp" #include "utils/math.h" #include "utils/stubs.h" namespace devilution { namespace { constexpr float LogBase = 10.0; /** * Scaling factor for attenuating volume. * Picked so that a volume change of -10 dB results in half perceived loudness. * VolumeScale = -1000 / log(0.5) */ constexpr float VolumeScale = 3321.9281F; /** * Min and max volume range, in millibel. * -100 dB (muted) to 0 dB (max. loudness). */ constexpr float MillibelMin = -10000.F; constexpr float MillibelMax = 0.F; /** * Stereo separation factor for left/right speaker panning. Lower values increase separation, moving * sounds further left/right, while higher values will pull sounds more towards the middle, reducing separation. * Current value is tuned to have ~2:1 mix for sounds that happen on the edge of a 640x480 screen. */ constexpr float StereoSeparation = 6000.F; #ifndef USE_SDL3 float PanLogToLinear(int logPan) { if (logPan == 0) return 0; auto factor = std::pow(LogBase, static_cast(-std::abs(logPan)) / StereoSeparation); return copysign(1.F - factor, static_cast(logPan)); } std::unique_ptr CreateDecoder(bool isMp3) { if (isMp3) return std::make_unique(); return std::make_unique(); } std::unique_ptr CreateStream(SDL_IOStream *handle, bool isMp3) { auto decoder = CreateDecoder(isMp3); if (!decoder->open(handle)) // open for `getRate` return nullptr; auto resampler = CreateAulibResampler(decoder->getRate()); return std::make_unique(handle, std::move(decoder), std::move(resampler), /*closeRw=*/true); } /** * @brief Converts log volume passed in into linear volume. * @param logVolume Logarithmic volume in the range [logMin..logMax] * @param logMin Volume range minimum (usually ATTENUATION_MIN for game sounds and VOLUME_MIN for volume sliders) * @param logMax Volume range maximum (usually 0) * @return Linear volume in the range [0..1] */ float VolumeLogToLinear(int logVolume, int logMin, int logMax) { const auto logScaled = math::Remap(static_cast(logMin), static_cast(logMax), MillibelMin, MillibelMax, static_cast(logVolume)); return std::pow(LogBase, logScaled / VolumeScale); // linVolume } #endif } // namespace ///// SoundSample ///// #ifndef USE_SDL3 void SoundSample::SetFinishCallback(Aulib::Stream::Callback &&callback) { stream_->setFinishCallback(std::forward(callback)); } #endif void SoundSample::Stop() { #ifndef USE_SDL3 stream_->stop(); #endif } void SoundSample::Mute() { #ifndef USE_SDL3 stream_->mute(); #endif } void SoundSample::Unmute() { #ifndef USE_SDL3 stream_->unmute(); #endif } void SoundSample::Release() { #ifndef USE_SDL3 stream_ = nullptr; #endif file_data_ = nullptr; file_data_size_ = 0; } /** * @brief Check if a the sound is being played atm */ bool SoundSample::IsPlaying() { #ifdef USE_SDL3 return false; #else return stream_ && stream_->isPlaying(); #endif } bool SoundSample::Play(int numIterations) { #ifdef USE_SDL3 return false; #else if (!stream_->play(numIterations)) { LogError(LogCategory::Audio, "Aulib::Stream::play (from SoundSample::Play): {}", SDL_GetError()); return false; } return true; #endif } int SoundSample::SetChunkStream(std::string filePath, bool isMp3, bool logErrors) { #ifdef USE_SDL3 return 0; #else SDL_IOStream *handle = OpenAssetAsSdlRwOps(filePath.c_str(), /*threadsafe=*/true); if (handle == nullptr) { if (logErrors) LogError(LogCategory::Audio, "OpenAsset failed (from SoundSample::SetChunkStream) for {}: {}", filePath, SDL_GetError()); return -1; } file_path_ = std::move(filePath); isMp3_ = isMp3; stream_ = CreateStream(handle, isMp3); if (!stream_->open()) { stream_ = nullptr; if (logErrors) LogError(LogCategory::Audio, "Aulib::Stream::open (from SoundSample::SetChunkStream) for {}: {}", file_path_, SDL_GetError()); return -1; } return 0; #endif } int SoundSample::SetChunk(ArraySharedPtr fileData, std::size_t dwBytes, bool isMp3) { #ifdef USE_SDL3 return 0; #else isMp3_ = isMp3; file_data_ = std::move(fileData); file_data_size_ = dwBytes; SDL_IOStream *buf = SDL_IOFromConstMem(file_data_.get(), static_cast(dwBytes)); if (buf == nullptr) { return -1; } stream_ = CreateStream(buf, isMp3_); if (!stream_->open()) { stream_ = nullptr; file_data_ = nullptr; LogError(LogCategory::Audio, "Aulib::Stream::open (from SoundSample::SetChunk): {}", SDL_GetError()); return -1; } return 0; #endif } void SoundSample::SetVolume(int logVolume, int logMin, int logMax) { #ifndef USE_SDL3 stream_->setVolume(VolumeLogToLinear(logVolume, logMin, logMax)); #endif } void SoundSample::SetStereoPosition(int logPan) { #ifndef USE_SDL3 stream_->setStereoPosition(PanLogToLinear(logPan)); #endif } int SoundSample::GetLength() const { #ifdef USE_SDL3 return 0; #else if (!stream_) return 0; return static_cast(std::chrono::duration_cast(stream_->duration()).count()); #endif } } // namespace devilution ================================================ FILE: Source/utils/soundsample.h ================================================ #pragma once #include #include #include #include #include #include "engine/sound_defs.hpp" #include "utils/stdcompat/shared_ptr_array.hpp" #ifndef USE_SDL3 // Forward-declares Aulib::Stream to avoid adding dependencies // on SDL_audiolib to every user of this header. namespace Aulib { class Stream; } // namespace Aulib #endif namespace devilution { class SoundSample final { public: SoundSample() = default; SoundSample(SoundSample &&) noexcept = default; SoundSample &operator=(SoundSample &&) noexcept = default; [[nodiscard]] bool IsLoaded() const { #ifdef USE_SDL3 return false; #else return stream_ != nullptr; #endif } void Release(); bool IsPlaying(); // Returns 0 on success. int SetChunkStream(std::string filePath, bool isMp3, bool logErrors = true); #ifndef USE_SDL3 void SetFinishCallback(std::function &&callback); #endif /** * @brief Sets the sample's WAV, FLAC, or Ogg/Vorbis data. * @param fileData Buffer containing the data * @param dwBytes Length of buffer * @param isMp3 Whether the data is an MP3 * @return 0 on success, -1 otherwise */ int SetChunk(ArraySharedPtr fileData, std::size_t dwBytes, bool isMp3); [[nodiscard]] bool IsStreaming() const { return file_data_ == nullptr; } int DuplicateFrom(const SoundSample &other) { if (other.IsStreaming()) return SetChunkStream(other.file_path_, other.isMp3_); return SetChunk(other.file_data_, other.file_data_size_, other.isMp3_); } /** * @brief Start playing the sound for a given number of iterations (0 means loop). */ bool Play(int numIterations = 1); /** * @brief Start playing the sound with the given sound and user volume, and a stereo position. */ bool PlayWithVolumeAndPan(int logSoundVolume, int logUserVolume, int logPan) { SetVolume(logSoundVolume + logUserVolume * (ATTENUATION_MIN / VOLUME_MIN), ATTENUATION_MIN, 0); SetStereoPosition(logPan); return Play(); } /** * @brief Stop playing the sound */ void Stop(); void SetVolume(int logVolume, int logMin, int logMax); void SetStereoPosition(int logPan); void Mute(); void Unmute(); /** * @return Audio duration in ms */ int GetLength() const; private: // Non-streaming audio fields: ArraySharedPtr file_data_; std::size_t file_data_size_; // Set for streaming audio to allow for duplicating it: std::string file_path_; bool isMp3_; #ifndef USE_SDL3 std::unique_ptr stream_; #endif }; } // namespace devilution ================================================ FILE: Source/utils/static_vector.hpp ================================================ #pragma once #include #include #include #include #include "appfat.h" #include "utils/attributes.h" namespace devilution { /** * @brief A stack-allocated vector with a fixed capacity. * * @tparam T element type. * @tparam N capacity. */ template class StaticVector { public: using value_type = T; using reference = T &; using const_reference = const T &; using pointer = T *; using const_pointer = const T *; using size_type = size_t; using iterator = T *; using const_iterator = const T *; using difference_type = std::ptrdiff_t; StaticVector() = default; template StaticVector(std::initializer_list elements) { for (auto &&element : elements) { emplace_back(element); } } [[nodiscard]] const T *begin() const { return &(*this)[0]; } [[nodiscard]] T *begin() { return &(*this)[0]; } [[nodiscard]] const T *end() const { return begin() + size_; } [[nodiscard]] T *end() { return begin() + size_; } [[nodiscard]] size_t size() const { return size_; } [[nodiscard]] bool empty() const DVL_PURE { return size_ == 0; } [[nodiscard]] const T &front() const { return (*this)[0]; } [[nodiscard]] T &front() { return (*this)[0]; } [[nodiscard]] const T &back() const { return (*this)[size_ - 1]; } [[nodiscard]] T &back() { return (*this)[size_ - 1]; } [[nodiscard]] const T *data() const { return data_[0].ptr(); } [[nodiscard]] T *data() { return data_[0].ptr(); } template void push_back(Args &&...args) // NOLINT(readability-identifier-naming) { emplace_back(std::forward(args)...); } template T &emplace_back(Args &&...args) // NOLINT(readability-identifier-naming) { assert(size_ < N); return *::new (&data_[size_++]) T(std::forward(args)...); } const T &operator[](std::size_t pos) const { return *data_[pos].ptr(); } T &operator[](std::size_t pos) { return *data_[pos].ptr(); } void erase(const T *first, const T *last) { if (last == first) return; assert(first >= begin() && last <= end() && first <= last); const auto count = last - first; auto tail = std::move(const_cast(last), end(), const_cast(first)); std::destroy(tail, end()); size_ -= count; } void erase(const T *element) { assert(element >= begin() && element < end()); erase(element, element + 1); } void pop_back() // NOLINT(readability-identifier-naming) { std::destroy_at(&back()); --size_; } void clear() { erase(begin(), end()); } ~StaticVector() { std::destroy_n(data(), size_); } private: struct AlignedStorage { alignas(alignof(T)) std::byte data[sizeof(T)]; [[nodiscard]] const T *ptr() const { return std::launder(reinterpret_cast(data)); } [[nodiscard]] T *ptr() { return std::launder(reinterpret_cast(data)); } }; AlignedStorage data_[N]; std::size_t size_ = 0; }; } // namespace devilution ================================================ FILE: Source/utils/status_macros.hpp ================================================ #pragma once #include "utils/attributes.h" #define RETURN_IF_ERROR(expr) \ if (auto result = expr; DVL_PREDICT_FALSE(!result.has_value())) { \ return tl::make_unexpected(std::move(result).error()); \ } #define STATUS_MACROS_CONCAT_NAME_INNER(x, y) x##y #define STATUS_MACROS_CONCAT_NAME(x, y) STATUS_MACROS_CONCAT_NAME_INNER(x, y) #define ASSIGN_OR_RETURN_IMPL(result, lhs, rhs) \ auto result = rhs; /* NOLINT(bugprone-macro-parentheses): assignment */ \ if (DVL_PREDICT_FALSE(!result.has_value())) { /* NOLINT(bugprone-macro-parentheses): assignment */ \ return tl::make_unexpected(std::move(result).error()); \ } \ lhs = std::move(result).value(); /* NOLINT(bugprone-macro-parentheses): assignment */ #define ASSIGN_OR_RETURN(lhs, rhs) \ ASSIGN_OR_RETURN_IMPL( \ STATUS_MACROS_CONCAT_NAME(_result, __COUNTER__), lhs, rhs) ================================================ FILE: Source/utils/stdcompat/filesystem.hpp ================================================ #pragma once #if defined(__APPLE__) && DARWIN_MAJOR_VERSION >= 9 #include #if (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500) \ || (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000) #define DVL_NO_FILESYSTEM #endif #elif defined(NXDK) || (defined(_MSVC_LANG) && _MSVC_LANG < 201703L) \ || (defined(WINVER) && WINVER <= 0x0500 && (!defined(_WIN32_WINNT) || _WIN32_WINNT == 0)) #define DVL_NO_FILESYSTEM #endif #ifndef DVL_NO_FILESYSTEM #if defined(__has_include) #if __has_include() #define DVL_HAS_FILESYSTEM #include // IWYU pragma: export #elif __has_include() #define DVL_HAS_FILESYSTEM #include // IWYU pragma: export #define filesystem experimental::filesystem #endif #endif #endif ================================================ FILE: Source/utils/stdcompat/shared_ptr_array.hpp ================================================ #pragma once #include #include namespace devilution { // Apple Clang 12 has a buggy implementation that fails to compile `std::shared_ptr(new T[size])`. #if (__cplusplus >= 201611L && (!defined(__clang_major__) || __clang_major__ >= 13)) && !defined(NXDK) template using ArraySharedPtr = std::shared_ptr; template ArraySharedPtr MakeArraySharedPtr(std::size_t size) { return ArraySharedPtr(new T[size]); } #else template using ArraySharedPtr = std::shared_ptr; template ArraySharedPtr MakeArraySharedPtr(std::size_t size) { return ArraySharedPtr { new T[size], std::default_delete() }; } #endif } // namespace devilution ================================================ FILE: Source/utils/str_case.cpp ================================================ #include "utils/str_case.hpp" namespace devilution { void AsciiStrToLower(std::string &str) { for (char &c : str) { // NOLINT(readability-identifier-length) if (c >= 'A' && c <= 'Z') c += ('a' - 'A'); } } } // namespace devilution ================================================ FILE: Source/utils/str_case.hpp ================================================ #pragma once #include #include namespace devilution { void AsciiStrToLower(std::string &str); [[nodiscard]] inline std::string AsciiStrToLower(std::string_view str) { std::string copy { str.data(), str.size() }; AsciiStrToLower(copy); return copy; } } // namespace devilution ================================================ FILE: Source/utils/str_cat.cpp ================================================ #include "utils/str_cat.hpp" #include #include namespace devilution { namespace { [[nodiscard]] char HexDigit(uint8_t v) { return "0123456789abcdef"[v]; } [[nodiscard]] char HexDigitUpper(uint8_t v) { return "0123456789ABCDEF"[v]; } } // namespace char *BufCopy(char *out, long long value) { const fmt::format_int formatted { value }; std::memcpy(out, formatted.data(), formatted.size()); return out + formatted.size(); } char *BufCopy(char *out, unsigned long long value) { const fmt::format_int formatted { value }; std::memcpy(out, formatted.data(), formatted.size()); return out + formatted.size(); } char *BufCopy(char *out, AsHexU8Pad2 value) { if (value.uppercase) { *out++ = HexDigitUpper(value.value >> 4); *out++ = HexDigitUpper(value.value & 0xf); } else { *out++ = HexDigit(value.value >> 4); *out++ = HexDigit(value.value & 0xf); } return out; } char *BufCopy(char *out, AsHexU16Pad2 value) { if (value.value > 0xff) { if (value.value > 0xfff) { out = BufCopy(out, AsHexU8Pad2 { static_cast(value.value >> 8) }); } else { *out++ = value.uppercase ? HexDigitUpper(value.value >> 8) : HexDigit(value.value >> 8); } } return BufCopy(out, AsHexU8Pad2 { static_cast(value.value & 0xff) }); } void StrAppend(std::string &out, long long value) { const fmt::format_int formatted { value }; out.append(formatted.data(), formatted.size()); } void StrAppend(std::string &out, unsigned long long value) { const fmt::format_int formatted { value }; out.append(formatted.data(), formatted.size()); } void StrAppend(std::string &out, AsHexU8Pad2 value) { char hex[2]; BufCopy(hex, value); out.append(hex, 2); } void StrAppend(std::string &out, AsHexU16Pad2 value) { char hex[4]; const auto len = static_cast(BufCopy(hex, value) - hex); out.append(hex, len); } } // namespace devilution ================================================ FILE: Source/utils/str_cat.hpp ================================================ #pragma once #include #include #include #include #include namespace devilution { struct AsHexU8Pad2 { uint8_t value; bool uppercase = false; }; struct AsHexU16Pad2 { uint16_t value; bool uppercase = false; }; /** @brief Maximum number of digits for a value of type T. * If T is signed, also accounts for a potential '-'. */ template constexpr size_t MaxNumDigits = (8 * sizeof(T) * 28 / 93) + 1 + (std::is_signed_v ? 1 : 0); template struct LeftPadT { T value; unsigned length; char padChar; }; /** * @brief Formats the value as a lowercase zero-padded hexadecimal with at least 2 hex digits (0-padded on the left). */ constexpr AsHexU8Pad2 AsHexPad2(uint8_t value, bool uppercase = false) { return { value, uppercase }; } /** * @brief Formats the value as a lowercase zero-padded hexadecimal with at least 2 hex digits (0-padded on the left). */ constexpr AsHexU16Pad2 AsHexPad2(uint16_t value, bool uppercase = false) { return { value, uppercase }; } /** * @brief Left-pads the value to the specified length with the specified character. */ template constexpr LeftPadT LeftPad(T value, unsigned length, char padChar) { return { value, length, padChar }; } /** * @brief Writes the integer to the given buffer. * @return char* end of the buffer */ char *BufCopy(char *out, long long value); inline char *BufCopy(char *out, long value) { return BufCopy(out, static_cast(value)); } inline char *BufCopy(char *out, int value) { return BufCopy(out, static_cast(value)); } inline char *BufCopy(char *out, short value) { return BufCopy(out, static_cast(value)); } /** * @brief Writes the integer to the given buffer. * @return char* end of the buffer */ char *BufCopy(char *out, unsigned long long value); inline char *BufCopy(char *out, unsigned long value) { return BufCopy(out, static_cast(value)); } inline char *BufCopy(char *out, unsigned int value) { return BufCopy(out, static_cast(value)); } inline char *BufCopy(char *out, unsigned short value) { return BufCopy(out, static_cast(value)); } char *BufCopy(char *out, AsHexU8Pad2 value); char *BufCopy(char *out, AsHexU16Pad2 value); template char *BufCopy(char *out, LeftPadT value) { char buf[MaxNumDigits]; const char *end = BufCopy(buf, value.value); const std::string_view str = std::string_view(buf, end - buf); for (size_t i = str.size(); i < value.length; ++i) { *out++ = value.padChar; } std::memcpy(out, str.data(), str.size()); return out + str.size(); } /** * @brief Appends the integer to the given string. */ void StrAppend(std::string &out, long long value); inline void StrAppend(std::string &out, long value) { StrAppend(out, static_cast(value)); } inline void StrAppend(std::string &out, int value) { StrAppend(out, static_cast(value)); } inline void StrAppend(std::string &out, short value) { StrAppend(out, static_cast(value)); } /** * @brief Appends the integer to the given string. */ void StrAppend(std::string &out, unsigned long long value); inline void StrAppend(std::string &out, unsigned long value) { StrAppend(out, static_cast(value)); } inline void StrAppend(std::string &out, unsigned int value) { StrAppend(out, static_cast(value)); } inline void StrAppend(std::string &out, unsigned short value) { StrAppend(out, static_cast(value)); } void StrAppend(std::string &out, AsHexU8Pad2 value); void StrAppend(std::string &out, AsHexU16Pad2 value); template void StrAppend(std::string &out, LeftPadT value) { char buf[MaxNumDigits]; const auto len = static_cast(BufCopy(buf, value) - buf); out.append(buf, len); } /** * @brief Copies the given std::string_view to the given buffer. */ inline char *BufCopy(char *out, std::string_view value) { std::memcpy(out, value.data(), value.size()); return out + value.size(); } /** * @brief Copies the given std::string_view to the given string. */ inline void StrAppend(std::string &out, std::string_view value) { out.append(value); } /** * @brief Appends the given C string to the given buffer. * * `str` must be a null-terminated C string, `out` will not be null terminated. */ inline char *BufCopy(char *out, const char *str) { return BufCopy(out, std::string_view(str != nullptr ? str : "(nullptr)")); } /** * @brief Appends the given C string to the given string. */ inline void StrAppend(std::string &out, const char *str) { out.append(std::string_view(str != nullptr ? str : "(nullptr)")); } template typename std::enable_if<(sizeof...(Args) > 1), char *>::type BufCopy(char *out, Args &&...args) { return ((out = BufCopy(out, std::forward(args))), ...); } template typename std::enable_if<(sizeof...(Args) > 1), void>::type StrAppend(std::string &out, Args &&...args) { (StrAppend(out, std::forward(args)), ...); } template std::string StrCat(Args &&...args) { std::string result; StrAppend(result, std::forward(args)...); return result; } } // namespace devilution ================================================ FILE: Source/utils/str_split.hpp ================================================ #pragma once #include #include #include namespace devilution { class SplitByCharIterator { public: using iterator_category = std::forward_iterator_tag; using value_type = std::string_view; using reference = std::add_lvalue_reference::type; using pointer = std::add_pointer::type; static SplitByCharIterator begin(std::string_view text, char split_by) // NOLINT(readability-identifier-naming) { return SplitByCharIterator(split_by, text, text.substr(0, text.find(split_by))); } // End iterator SplitByCharIterator() = default; [[nodiscard]] std::string_view operator*() const { return slice_; } [[nodiscard]] const std::string_view *operator->() const { return &slice_; } SplitByCharIterator &operator++() { if (slice_.data() + slice_.size() == text_.data() + text_.size()) { slice_ = {}; return *this; } slice_ = text_.substr(slice_.data() - text_.data() + slice_.size()); if (!slice_.empty()) slice_.remove_prefix(1); // skip the split_by char slice_ = slice_.substr(0, slice_.find(split_by_)); return *this; } SplitByCharIterator operator++(int) { auto copy = *this; ++(*this); return copy; } bool operator==(const SplitByCharIterator &rhs) const { return slice_.data() == rhs.slice_.data(); } bool operator!=(const SplitByCharIterator &rhs) const { return !(*this == rhs); } private: SplitByCharIterator(char split_by, std::string_view text, std::string_view slice) : split_by_(split_by) , text_(text) , slice_(slice) { } const char split_by_ = '\0'; const std::string_view text_; std::string_view slice_; }; class SplitByChar { public: explicit SplitByChar(std::string_view text, char split_by) : text_(text) , split_by_(split_by) { } [[nodiscard]] SplitByCharIterator begin() const // NOLINT(readability-identifier-naming) { return SplitByCharIterator::begin(text_, split_by_); } [[nodiscard]] SplitByCharIterator end() const // NOLINT(readability-identifier-naming) { return {}; } private: const std::string_view text_; const char split_by_; }; } // namespace devilution ================================================ FILE: Source/utils/string_or_view.hpp ================================================ #pragma once #include #include #include #include namespace devilution { class StringOrView { public: StringOrView() : rep_ { std::string_view {} } { } StringOrView(const StringOrView &) = default; StringOrView(StringOrView &&) noexcept = default; StringOrView(std::string &&str) : rep_ { std::move(str) } { } StringOrView(std::string_view str) : rep_ { str } { } StringOrView &operator=(StringOrView &&) noexcept = default; StringOrView &operator=(std::string &&value) noexcept { rep_ = std::move(value); return *this; } StringOrView &operator=(std::string_view value) noexcept { rep_ = value; return *this; } bool empty() const { return std::visit([](auto &&val) -> bool { return val.empty(); }, rep_); } std::string_view str() const { return std::visit([](auto &&val) -> std::string_view { return val; }, rep_); } operator std::string_view() const { return str(); } private: std::variant rep_; }; } // namespace devilution ================================================ FILE: Source/utils/string_view_hash.hpp ================================================ #pragma once #include #include #include #include namespace devilution { // A hash functor that enables heterogenous lookup for `unordered_set/map`. struct StringViewHash { using is_transparent = void; using is_avalanching = void; [[nodiscard]] uint64_t operator()(std::string_view str) const noexcept { return ankerl::unordered_dense::hash {}(str); } [[nodiscard]] uint64_t operator()(const char *str) const noexcept { return (*this)(std::string_view { str }); } [[nodiscard]] uint64_t operator()(const std::string &str) const noexcept { return (*this)(std::string_view { str }); } }; // Usually we'd use `std::equal_to<>` instead but the latter // does not link on the libcxx that comes with Xbox NXDK as of Aug 2024, struct StringViewEquals { using is_transparent = void; [[nodiscard]] bool operator()(std::string_view a, std::string_view b) const { return a == b; } [[nodiscard]] bool operator()(std::string_view a, const std::string &b) const { return a == b; } [[nodiscard]] bool operator()(const std::string &a, std::string_view &b) const { return a == b; } [[nodiscard]] bool operator()(const char *a, const std::string &b) const { return a == b; } [[nodiscard]] bool operator()(const std::string &a, const char *b) const { return a == b; } [[nodiscard]] bool operator()(std::string_view a, const char *b) const { return a == b; } [[nodiscard]] bool operator()(const char *a, std::string_view b) const { return a == b; } [[nodiscard]] bool operator()(const char *a, const char *b) const { return std::string_view { a } == b; } }; } // namespace devilution ================================================ FILE: Source/utils/stubs.h ================================================ #pragma once #include "utils/log.hpp" #define UNIMPLEMENTED() \ do { \ ::devilution::LogDebug("UNIMPLEMENTED: {} @ {}:{}", __FUNCTION__, __FILE__, __LINE__); \ abort(); \ } while (0) #define ABORT() \ do { \ ::devilution::LogCritical("ABORT: {} @ {}:{}", __FUNCTION__, __FILE__, __LINE__); \ abort(); \ } while (0) #define ASSERT(x) \ if (!(x)) { \ ::devilution::LogCritical("Assertion failed in {}:{}: {}", __FILE__, __LINE__, #x); \ abort(); \ } ================================================ FILE: Source/utils/surface_to_clx.cpp ================================================ #include "utils/surface_to_clx.hpp" #include #include #include #include "utils/clx_encode.hpp" #include "utils/endian_read.hpp" #include "utils/endian_write.hpp" #ifdef DEBUG_SURFACE_TO_CLX_SIZE #include #include #endif namespace devilution { OwnedClxSpriteList SurfaceToClx(const Surface &surface, unsigned numFrames, std::optional transparentColor) { // CLX header: frame count, frame offset for each frame, file size std::vector clxData(4 * (2 + static_cast(numFrames))); WriteLE32(clxData.data(), numFrames); const auto height = static_cast(surface.h()); const auto width = static_cast(surface.w()); const auto pitch = static_cast(surface.pitch()); const unsigned frameHeight = height / numFrames; // We process the surface a whole frame at a time because the lines are reversed in CEL. const uint8_t *dataPtr = surface.begin(); for (unsigned frame = 1; frame <= numFrames; ++frame) { WriteLE32(&clxData[4 * static_cast(frame)], static_cast(clxData.size())); const size_t frameHeaderPos = clxData.size(); clxData.resize(clxData.size() + ClxFrameHeaderSize); // Frame header: WriteLE16(&clxData[frameHeaderPos], ClxFrameHeaderSize); WriteLE16(&clxData[frameHeaderPos + 2], static_cast(width)); WriteLE16(&clxData[frameHeaderPos + 4], static_cast(frameHeight)); unsigned transparentRunWidth = 0; size_t line = 0; while (line != frameHeight) { // Process line: const uint8_t *src = &dataPtr[(frameHeight - (line + 1)) * pitch]; if (transparentColor) { unsigned solidRunWidth = 0; for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { if (*src == *transparentColor) { if (solidRunWidth != 0) { AppendClxPixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, clxData); solidRunWidth = 0; } ++transparentRunWidth; } else { AppendClxTransparentRun(transparentRunWidth, clxData); transparentRunWidth = 0; ++solidRunWidth; } } if (solidRunWidth != 0) { AppendClxPixelsOrFillRun(src - solidRunWidth, solidRunWidth, clxData); } } else { AppendClxPixelsOrFillRun(src, width, clxData); } ++line; } AppendClxTransparentRun(transparentRunWidth, clxData); dataPtr += static_cast(pitch * frameHeight); } WriteLE32(&clxData[4 * (1 + static_cast(numFrames))], static_cast(clxData.size())); auto out = std::unique_ptr(new uint8_t[clxData.size()]); memcpy(&out[0], clxData.data(), clxData.size()); #ifdef DEBUG_SURFACE_TO_CLX_SIZE const int surfaceSize = surface.h() * surface.pitch(); std::cout << "Surface(" << surface.w() << ", " << surface.h() << ") -> CLX\t" << surfaceSize << " -> " << clxData.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast(clxData.size()) - surfaceSize) / ((float)surfaceSize) * 100 << "%" << std::endl; #endif return OwnedClxSpriteList { std::move(out) }; } } // namespace devilution ================================================ FILE: Source/utils/surface_to_clx.hpp ================================================ #pragma once #include #include #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" namespace devilution { /** * @brief Converts a Surface to a CLX sprite list. * * @param surface The source surface. * @param numFrames The number of vertically stacked frames in the surface. * @param transparentColor The PCX palette index of the transparent color. */ OwnedClxSpriteList SurfaceToClx(const Surface &surface, unsigned numFrames = 1, std::optional transparentColor = std::nullopt); } // namespace devilution ================================================ FILE: Source/utils/surface_to_pcx.cpp ================================================ #include "utils/surface_to_pcx.hpp" #include #include #include #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "engine/surface.hpp" #include "utils/endian_swap.hpp" #include "utils/pcx.hpp" #include "utils/sdl_compat.h" namespace devilution { namespace { tl::expected CheckedFWrite(const void *ptr, size_t size, SDL_IOStream *out) { if (SDL_WriteIO(out, ptr, size) != size) { const char *errorMessage = SDL_GetError(); if (errorMessage == nullptr) errorMessage = ""; tl::expected result = tl::make_unexpected(std::string("write failed with: ").append(errorMessage)); SDL_ClearError(); return result; } return {}; } /** * @brief Write the PCX-file header * @param width Image width * @param height Image height * @param out File stream to write to * @return True on success */ tl::expected WritePcxHeader(int16_t width, int16_t height, SDL_IOStream *out) { PCXHeader buffer; memset(&buffer, 0, sizeof(buffer)); buffer.Manufacturer = 10; buffer.Version = 5; buffer.Encoding = 1; buffer.BitsPerPixel = 8; buffer.Xmax = Swap16LE(width - 1); buffer.Ymax = Swap16LE(height - 1); buffer.HDpi = Swap16LE(width); buffer.VDpi = Swap16LE(height); buffer.NPlanes = 1; buffer.BytesPerLine = Swap16LE(width); return CheckedFWrite(&buffer, sizeof(buffer), out); } /** * @brief Write the current in-game palette to the PCX file * @param palette Current palette * @param out File stream for the PCX file. * @return True if successful, else false */ tl::expected WritePcxPalette(SDL_Color *palette, SDL_IOStream *out) { uint8_t pcxPalette[1 + 256 * 3]; pcxPalette[0] = 12; for (int i = 0; i < 256; i++) { pcxPalette[1 + 3 * i + 0] = palette[i].r; pcxPalette[1 + 3 * i + 1] = palette[i].g; pcxPalette[1 + 3 * i + 2] = palette[i].b; } return CheckedFWrite(pcxPalette, sizeof(pcxPalette), out); } /** * @brief RLE compress the pixel data * @param src Raw pixel buffer * @param dst Output buffer * @param width Width of pixel buffer * @return Output buffer */ uint8_t *WritePcxLine(uint8_t *src, uint8_t *dst, int width) { int rleLength; do { const uint8_t rlePixel = *src; src++; rleLength = 1; width--; while (rlePixel == *src) { if (rleLength >= 63) break; if (width == 0) break; rleLength++; width--; src++; } if (rleLength > 1 || rlePixel > 0xBF) { *dst = rleLength | 0xC0; dst++; } *dst = rlePixel; dst++; } while (width > 0); return dst; } /** * @brief Write the pixel data to the PCX file * * @param buf Pixel data * @param out File stream for the PCX file. * @return True if successful, else false */ tl::expected WritePcxPixels(const Surface &buf, SDL_IOStream *out) { const int width = buf.w(); const std::unique_ptr pBuffer { new uint8_t[static_cast(2 * width)] }; uint8_t *pixels = buf.begin(); for (int height = buf.h(); height > 0; height--) { const uint8_t *pBufferEnd = WritePcxLine(pixels, pBuffer.get(), width); pixels += buf.pitch(); tl::expected result = CheckedFWrite(pBuffer.get(), pBufferEnd - pBuffer.get(), out); if (!result.has_value()) return result; } return {}; } } // namespace tl::expected WriteSurfaceToFilePcx(const Surface &buf, SDL_IOStream *outStream) { tl::expected result = WritePcxHeader(buf.w(), buf.h(), outStream); if (!result.has_value()) return result; result = WritePcxPixels(buf, outStream); if (!result.has_value()) return result; result = WritePcxPalette(buf.surface->format->palette->colors, outStream); if (!result.has_value()) return result; SDL_CloseIO(outStream); return {}; } } // namespace devilution ================================================ FILE: Source/utils/surface_to_pcx.hpp ================================================ #include #ifdef USE_SDL3 #include #else #include #include "utils/sdl_compat.h" #endif #include #include "engine/surface.hpp" namespace devilution { tl::expected WriteSurfaceToFilePcx(const Surface &buf, SDL_IOStream *outStream); } // namespace devilution ================================================ FILE: Source/utils/surface_to_png.cpp ================================================ #include "utils/surface_to_png.hpp" #include #ifdef USE_SDL3 #include #include #include #else #include #include "utils/sdl_compat.h" #endif #include #include "engine/surface.hpp" namespace devilution { #ifndef USE_SDL3 extern "C" int IMG_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst); #endif tl::expected WriteSurfaceToFilePng(const Surface &buf, SDL_IOStream *dst) { #ifdef USE_SDL3 const bool ok = IMG_SavePNG_IO(buf.surface, dst, /*closeio=*/true); #else const bool ok = IMG_SavePNG_RW(buf.surface, dst, /*freedst=*/1) == 0; #endif if (!ok) { tl::expected result = tl::make_unexpected(std::string(SDL_GetError())); SDL_ClearError(); return result; } return {}; } } // namespace devilution ================================================ FILE: Source/utils/surface_to_png.hpp ================================================ #include #include #ifdef USE_SDL3 #include #include #else #include #endif #include #include "engine/surface.hpp" namespace devilution { /** * @brief Writes the given surface to `dst` as PNG. * * Takes ownership of `dst` and closes it when done. */ tl::expected WriteSurfaceToFilePng(const Surface &buf, #ifdef USE_SDL3 SDL_IOStream * #else SDL_RWops * #endif dst); } // namespace devilution ================================================ FILE: Source/utils/timer.cpp ================================================ #include "engine/demomode.h" #ifdef USE_SDL3 #include #else #include #endif namespace devilution { uint32_t GetMillisecondsSinceStartup() { return (demo::IsRunning() || demo::IsRecording()) ? demo::SimulateMillisecondsSinceStartup() : SDL_GetTicks(); } } // namespace devilution ================================================ FILE: Source/utils/timer.hpp ================================================ #pragma once namespace devilution { uint32_t GetMillisecondsSinceStartup(); } ================================================ FILE: Source/utils/ui_fwd.h ================================================ #pragma once #include #include "engine/rectangle.hpp" #include "utils/attributes.h" namespace devilution { extern DVL_API_FOR_TEST uint16_t gnScreenWidth; extern DVL_API_FOR_TEST uint16_t gnScreenHeight; extern DVL_API_FOR_TEST uint16_t gnViewportHeight; uint16_t GetScreenWidth(); uint16_t GetScreenHeight(); uint16_t GetViewportHeight(); /** @brief Returns the UI (Menus, Messages, Help) can use. Currently this is 640x480 like vanilla. */ const Rectangle &GetUIRectangle(); void AdjustToScreenGeometry(Size windowSize); float GetDpiScalingFactor(); /** * @brief Set the screen to fullscreen or windowe if fullsc */ void SetFullscreenMode(); bool SpawnWindow(const char *lpWindowName); #ifndef USE_SDL1 void ReinitializeTexture(); void ReinitializeIntegerScale(); #endif void ReinitializeRenderer(); void ResizeWindow(); void UiErrorOkDialog(std::string_view caption, std::string_view text, bool error = true); } // namespace devilution ================================================ FILE: Source/utils/utf8.cpp ================================================ #include "utils/utf8.hpp" #include #include #include #include #include namespace devilution { char32_t DecodeFirstUtf8CodePoint(std::string_view input, std::size_t *len) { SBUInteger index = 0; const SBCodepoint result = SBCodepointDecodeNextFromUTF8( reinterpret_cast(input.data()), static_cast(input.size()), &index); *len = index; return result; } std::string_view TruncateUtf8(std::string_view str, std::size_t len) { if (str.size() > len) { std::size_t truncIndex = len; while (truncIndex > 0 && IsTrailUtf8CodeUnit(str[truncIndex])) truncIndex--; str.remove_suffix(str.size() - truncIndex); } return str; } void CopyUtf8(char *dest, std::string_view source, std::size_t bytes) { source = TruncateUtf8(source, bytes - 1); // source.empty() can mean source.data() == nullptr. // It is UB to pass a null pointer to memcpy, so we guard against it. if (!source.empty()) { std::memcpy(dest, source.data(), source.size()); } dest[source.size()] = '\0'; } void AppendUtf8(char32_t codepoint, std::string &out) { if (codepoint <= 0x7F) { out += static_cast(codepoint); return; } char buf[4]; if (codepoint <= 0x7FF) { buf[0] = static_cast(0xC0 | (codepoint >> 6 & 0x3F)); buf[1] = static_cast(0x80 | (codepoint & 0x3F)); out.append(buf, 2); } else if (codepoint <= 0xFFFF) { buf[0] = static_cast(0xE0 | (codepoint >> 12 & 0x3F)); buf[1] = static_cast(0x80 | (codepoint >> 6 & 0x3F)); buf[2] = static_cast(0x80 | (codepoint & 0x3F)); out.append(buf, 3); } else { buf[0] = static_cast(0xF0 | (codepoint >> 18 & 0x3F)); buf[1] = static_cast(0x80 | (codepoint >> 12 & 0x3F)); buf[2] = static_cast(0x80 | (codepoint >> 6 & 0x3F)); buf[3] = static_cast(0x80 | (codepoint & 0x3F)); out.append(buf, 4); } } } // namespace devilution ================================================ FILE: Source/utils/utf8.hpp ================================================ #pragma once #include #include #include namespace devilution { constexpr char32_t Utf8DecodeError = 0xFFFD; /** * Decodes the first code point from UTF8-encoded input. * * Sets `len` to the length of the code point in bytes. * Returns `Utf8DecodeError` on error. */ char32_t DecodeFirstUtf8CodePoint(std::string_view input, std::size_t *len); /** * Decodes and removes the first code point from UTF8-encoded input. */ inline char32_t ConsumeFirstUtf8CodePoint(std::string_view *input) { std::size_t len; const char32_t result = DecodeFirstUtf8CodePoint(*input, &len); input->remove_prefix(len); return result; } /** * Returns true if the character is part of the Basic Latin set. * * This includes ASCII punctuation, symbols, math operators, digits, and both uppercase/lowercase latin alphabets */ constexpr bool IsBasicLatin(char x) { return x >= '\x20' && x <= '\x7E'; } /** * Returns true if this is a trailing byte in a UTF-8 code point encoding. * * Trailing bytes all begin with 10 as the most significant bits, meaning they generally fall in the range 0x80 to * 0xBF. Please note that certain 3 and 4 byte sequences use a narrower range for the second byte, this function is * not intended to guarantee the character is valid within the sequence (or that the sequence is well-formed). */ inline bool IsTrailUtf8CodeUnit(char x) { // The following is equivalent to a bitmask test (x & 0xC0) == 0x80 // On x86_64 architectures it ends up being one instruction shorter return static_cast(x) < static_cast('\xC0'); } /** * @brief Returns the number of code units for a code point starting at *src; * * `src` must not be empty. * If `src` does not begin with a UTF-8 code point start byte, returns 1. */ inline size_t Utf8CodePointLen(const char *src) { // This constant is effectively a lookup table for 2-bit keys, where // values represent code point length - 1. // `-1` is so that this method never returns 0, even for invalid values // (which could lead to infinite loops in some code). // Generated with: // ruby -e 'p "0000000000000000000000001111223".reverse.to_i(4).to_s(16)' return ((0x3a55000000000000ULL >> (2 * (static_cast(*src) >> 3))) & 0x3) + 1; } /** * Returns the start byte index of the last code point in a UTF-8 string. */ inline std::size_t FindLastUtf8Symbols(std::string_view input) { if (input.empty()) return 0; std::size_t pos = input.size() - 1; while (pos > 0 && IsTrailUtf8CodeUnit(input[pos])) --pos; return pos; } /** * @brief Copy up to a given number of bytes from a UTF8 string, and zero terminate string * @param dest The destination buffer * @param source The source string * @param bytes Max number of bytes to copy */ void CopyUtf8(char *dest, std::string_view source, std::size_t bytes); void AppendUtf8(char32_t codepoint, std::string &out); /** @brief Truncates `str` to at most `len` at a code point boundary. */ std::string_view TruncateUtf8(std::string_view str, std::size_t len); } // namespace devilution ================================================ FILE: Source/vision.cpp ================================================ #include "vision.hpp" #include #include #include #include "engine/displacement.hpp" #include "engine/point.hpp" namespace devilution { namespace { /* * XY points of vision rays are cast to trace the visibility of the * surrounding environment. The table represents N rays of M points in * one quadrant (0°-90°) of a circle, so rays for other quadrants will * be created by mirroring. Zero points at the end will be trimmed and * ignored. A similar table can be recreated using Bresenham's line * drawing algorithm, which is suitable for integer arithmetic: * https://en.wikipedia.org/wiki/Bresenham's_line_algorithm */ const DisplacementOf VisionRays[23][15] = { // clang-format off { { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 0 }, { 5, 0 }, { 6, 0 }, { 7, 0 }, { 8, 0 }, { 9, 0 }, { 10, 0 }, { 11, 0 }, { 12, 0 }, { 13, 0 }, { 14, 0 }, { 15, 0 } }, { { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 0 }, { 5, 0 }, { 6, 0 }, { 7, 0 }, { 8, 1 }, { 9, 1 }, { 10, 1 }, { 11, 1 }, { 12, 1 }, { 13, 1 }, { 14, 1 }, { 15, 1 } }, { { 1, 0 }, { 2, 0 }, { 3, 0 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 1 }, { 9, 1 }, { 10, 1 }, { 11, 1 }, { 12, 2 }, { 13, 2 }, { 14, 2 }, { 15, 2 } }, { { 1, 0 }, { 2, 0 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 2 }, { 9, 2 }, { 10, 2 }, { 11, 2 }, { 12, 2 }, { 13, 3 }, { 14, 3 }, { 15, 3 } }, { { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 2 }, { 7, 2 }, { 8, 2 }, { 9, 3 }, { 10, 3 }, { 11, 3 }, { 12, 3 }, { 13, 4 }, { 14, 4 }, { 0, 0 } }, { { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 2 }, { 6, 2 }, { 7, 3 }, { 8, 3 }, { 9, 3 }, { 10, 4 }, { 11, 4 }, { 12, 4 }, { 13, 5 }, { 14, 5 }, { 0, 0 } }, { { 1, 0 }, { 2, 1 }, { 3, 1 }, { 4, 2 }, { 5, 2 }, { 6, 3 }, { 7, 3 }, { 8, 3 }, { 9, 4 }, { 10, 4 }, { 11, 5 }, { 12, 5 }, { 13, 6 }, { 14, 6 }, { 0, 0 } }, { { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 2 }, { 5, 3 }, { 6, 3 }, { 7, 4 }, { 8, 4 }, { 9, 5 }, { 10, 5 }, { 11, 6 }, { 12, 6 }, { 13, 7 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 2 }, { 5, 3 }, { 6, 4 }, { 7, 4 }, { 8, 5 }, { 9, 6 }, { 10, 6 }, { 11, 7 }, { 12, 7 }, { 12, 8 }, { 13, 8 }, { 0, 0 } }, { { 1, 1 }, { 2, 2 }, { 3, 2 }, { 4, 3 }, { 5, 4 }, { 6, 5 }, { 7, 5 }, { 8, 6 }, { 9, 7 }, { 10, 7 }, { 10, 8 }, { 11, 8 }, { 12, 9 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 6, 5 }, { 7, 6 }, { 8, 7 }, { 9, 8 }, { 10, 9 }, { 11, 9 }, { 11, 10 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 6, 6 }, { 7, 7 }, { 8, 8 }, { 9, 9 }, { 10, 10 }, { 11, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 5, 6 }, { 6, 7 }, { 7, 8 }, { 8, 9 }, { 9, 10 }, { 9, 11 }, { 10, 11 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 2, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 }, { 5, 7 }, { 6, 8 }, { 7, 9 }, { 7, 10 }, { 8, 10 }, { 8, 11 }, { 9, 12 }, { 0, 0 }, { 0, 0 } }, { { 1, 1 }, { 1, 2 }, { 2, 3 }, { 2, 4 }, { 3, 5 }, { 4, 6 }, { 4, 7 }, { 5, 8 }, { 6, 9 }, { 6, 10 }, { 7, 11 }, { 7, 12 }, { 8, 12 }, { 8, 13 }, { 0, 0 } }, { { 1, 1 }, { 1, 2 }, { 2, 3 }, { 2, 4 }, { 3, 5 }, { 3, 6 }, { 4, 7 }, { 4, 8 }, { 5, 9 }, { 5, 10 }, { 6, 11 }, { 6, 12 }, { 7, 13 }, { 0, 0 }, { 0, 0 } }, { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 2, 4 }, { 2, 5 }, { 3, 6 }, { 3, 7 }, { 3, 8 }, { 4, 9 }, { 4, 10 }, { 5, 11 }, { 5, 12 }, { 6, 13 }, { 6, 14 }, { 0, 0 } }, { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 2, 5 }, { 2, 6 }, { 3, 7 }, { 3, 8 }, { 3, 9 }, { 4, 10 }, { 4, 11 }, { 4, 12 }, { 5, 13 }, { 5, 14 }, { 0, 0 } }, { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 2, 6 }, { 2, 7 }, { 2, 8 }, { 3, 9 }, { 3, 10 }, { 3, 11 }, { 3, 12 }, { 4, 13 }, { 4, 14 }, { 0, 0 } }, { { 0, 1 }, { 0, 2 }, { 1, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 7 }, { 2, 8 }, { 2, 9 }, { 2, 10 }, { 2, 11 }, { 2, 12 }, { 3, 13 }, { 3, 14 }, { 3, 15 } }, { { 0, 1 }, { 0, 2 }, { 0, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 }, { 1, 7 }, { 1, 8 }, { 1, 9 }, { 1, 10 }, { 1, 11 }, { 2, 12 }, { 2, 13 }, { 2, 14 }, { 2, 15 } }, { { 0, 1 }, { 0, 2 }, { 0, 3 }, { 0, 4 }, { 0, 5 }, { 0, 6 }, { 0, 7 }, { 1, 8 }, { 1, 9 }, { 1, 10 }, { 1, 11 }, { 1, 12 }, { 1, 13 }, { 1, 14 }, { 1, 15 } }, { { 0, 1 }, { 0, 2 }, { 0, 3 }, { 0, 4 }, { 0, 5 }, { 0, 6 }, { 0, 7 }, { 0, 8 }, { 0, 9 }, { 0, 10 }, { 0, 11 }, { 0, 12 }, { 0, 13 }, { 0, 14 }, { 0, 15 } }, // clang-format on }; } // namespace void DoVision(Point position, uint8_t radius, tl::function_ref markVisibleFn, tl::function_ref markTransparentFn, tl::function_ref passesLightFn, tl::function_ref inBoundsFn) { markVisibleFn(position); // Adjustment to a ray length to ensure all rays lie on an // accurate circle constexpr uint8_t RayLenAdj[23] = { 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 4, 3, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0 }; static_assert(std::size(RayLenAdj) == std::size(VisionRays)); // Four quadrants on a circle constexpr Displacement Quadrants[] = { { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 } }; // Loop over quadrants and mirror rays for each one for (const auto &quadrant : Quadrants) { // Cast a ray for a quadrant for (unsigned int j = 0; j < std::size(VisionRays); j++) { const int rayLen = radius - RayLenAdj[j]; for (int k = 0; k < rayLen; k++) { const auto &relRayPoint = VisionRays[j][k]; // Calculate the next point on a ray in the quadrant const Point rayPoint = position + relRayPoint * quadrant; if (!inBoundsFn(rayPoint)) break; // We've cast an approximated ray on an integer 2D // grid, so we need to check if a ray can pass through // the diagonally adjacent tiles. For example, consider // this case: // // #? // ↗ # // x // // The ray is cast from the observer 'x', and reaches // the '?', but diagonally adjacent tiles '#' do not // pass the light, so the '?' should not be visible // for the 2D observer. // // The trick is to perform two additional visibility // checks for the diagonally adjacent tiles, but only // for the rays that are not parallel to the X or Y // coordinate lines. Parallel rays, which have a 0 in // one of their coordinate components, do not require // any additional adjacent visibility checks, and the // tile, hit by the ray, is always considered visible. // if (relRayPoint.deltaX > 0 && relRayPoint.deltaY > 0) { const Displacement adjacent1 = { -quadrant.deltaX, 0 }; const Displacement adjacent2 = { 0, -quadrant.deltaY }; // If diagonally adjacent tiles do not pass the // light further, we are done with this ray. const bool passesLight = (passesLightFn(rayPoint + adjacent1) || passesLightFn(rayPoint + adjacent2)); if (!passesLight) break; } markVisibleFn(rayPoint); // If the tile does not pass the light further, we are // done with this ray. const bool passesLight = passesLightFn(rayPoint); if (!passesLight) break; markTransparentFn(rayPoint); } } } } } // namespace devilution ================================================ FILE: Source/vision.hpp ================================================ #pragma once #include #include #include "engine/point.hpp" namespace devilution { void DoVision(Point position, uint8_t radius, tl::function_ref markVisibleFn, tl::function_ref markTransparentFn, tl::function_ref passesLightFn, tl::function_ref inBoundsFn); } // namespace devilution ================================================ FILE: Translations/be.po ================================================ msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2022-04-11 10:56+0100\n" "PO-Revision-Date: \n" "Last-Translator: Данек \n" "Language-Team: \n" "Language: be_BY\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 3.4.1\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:7 msgid "Game Design" msgstr "Дызайн гульні" #: Source/DiabloUI/credits_lines.cpp:10 msgid "Senior Designers" msgstr "Старшыя дызайнеры" #: Source/DiabloUI/credits_lines.cpp:13 Source/DiabloUI/credits_lines.cpp:232 msgid "Additional Design" msgstr "Дадатковы дызайн" #: Source/DiabloUI/credits_lines.cpp:16 Source/DiabloUI/credits_lines.cpp:215 msgid "Lead Programmer" msgstr "Вядучы праграміст" #: Source/DiabloUI/credits_lines.cpp:19 msgid "Senior Programmers" msgstr "Старшыя праграмісты" #: Source/DiabloUI/credits_lines.cpp:23 msgid "Programming" msgstr "Праграмаванне" #: Source/DiabloUI/credits_lines.cpp:26 msgid "Special Guest Programmers" msgstr "Спецыяльна запрошаныя праграмісты" #: Source/DiabloUI/credits_lines.cpp:29 msgid "Battle.net Programming" msgstr "Праграмаванне Battle.net" #: Source/DiabloUI/credits_lines.cpp:32 msgid "Serial Communications Programming" msgstr "Прагамаванне паслядоўнай передачы дадзеных" #: Source/DiabloUI/credits_lines.cpp:35 msgid "Installer Programming" msgstr "Праграмаванне ўсталёўшчыка" #: Source/DiabloUI/credits_lines.cpp:38 msgid "Art Directors" msgstr "Арт-дырэктары" #: Source/DiabloUI/credits_lines.cpp:41 msgid "Artwork" msgstr "Афармленне" #: Source/DiabloUI/credits_lines.cpp:48 msgid "Technical Artwork" msgstr "Тэхнічнае афармленне" #: Source/DiabloUI/credits_lines.cpp:52 msgid "Cinematic Art Directors" msgstr "Арт-дырэктары катсцэн" #: Source/DiabloUI/credits_lines.cpp:55 msgid "3D Cinematic Artwork" msgstr "Афармленне 3D катсцэн" #: Source/DiabloUI/credits_lines.cpp:61 msgid "Cinematic Technical Artwork" msgstr "Тэчнічнае афармленне катсцэн" #: Source/DiabloUI/credits_lines.cpp:64 msgid "Executive Producer" msgstr "Выканаўчы прадзюсар" #: Source/DiabloUI/credits_lines.cpp:67 msgid "Producer" msgstr "Прадзюсар" #: Source/DiabloUI/credits_lines.cpp:70 msgid "Associate Producer" msgstr "Памочнік прадзюсара" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:73 msgid "Diablo Strike Team" msgstr "Каманда Diablo Strike" #: Source/DiabloUI/credits_lines.cpp:77 Source/gamemenu.cpp:67 msgid "Music" msgstr "Музыка" #: Source/DiabloUI/credits_lines.cpp:80 msgid "Sound Design" msgstr "Гукавы дызайн" #: Source/DiabloUI/credits_lines.cpp:83 msgid "Cinematic Music & Sound" msgstr "Музыка і гук у катсцэнах" #: Source/DiabloUI/credits_lines.cpp:86 msgid "Voice Production, Direction & Casting" msgstr "Агучка, рэжысура, пробы" #: Source/DiabloUI/credits_lines.cpp:89 msgid "Script & Story" msgstr "Сцэнарый і сюжэт" #: Source/DiabloUI/credits_lines.cpp:93 msgid "Voice Editing" msgstr "Апрацоўка голаса" #: Source/DiabloUI/credits_lines.cpp:96 Source/DiabloUI/credits_lines.cpp:250 msgid "Voices" msgstr "Галасы" #: Source/DiabloUI/credits_lines.cpp:101 msgid "Recording Engineer" msgstr "Інжынер гуказапісу" #: Source/DiabloUI/credits_lines.cpp:104 msgid "Manual Design & Layout" msgstr "Дызайн дапаможніка і макета" #: Source/DiabloUI/credits_lines.cpp:108 msgid "Manual Artwork" msgstr "Афармленне дапаможніка" #: Source/DiabloUI/credits_lines.cpp:112 msgid "Provisional Director of QA (Lead Tester)" msgstr "Часовы рэжысёр па праверцы якасці (Вядучы тэсціроўшчык)" #: Source/DiabloUI/credits_lines.cpp:115 msgid "QA Assault Team (Testers)" msgstr "Штурмавая каманда па праверцы якасці (Тэсціроўшчыкі)" #: Source/DiabloUI/credits_lines.cpp:120 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "Спецпадраздзяленне па праверцы якасці (Тэсціроўшчыкі сумяшчальнасці)" #: Source/DiabloUI/credits_lines.cpp:123 msgid "QA Artillery Support (Additional Testers) " msgstr "Артылерыйская падтрымка па праверцы якасці (Дадатковыя тэсціроўшчыкі) " #: Source/DiabloUI/credits_lines.cpp:127 msgid "QA Counterintelligence" msgstr "Каманда па праверцы якасці (Контрразведка)" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:130 msgid "Order of Network Information Services" msgstr "Парадак Інфармацыйнай службы сеткі" #: Source/DiabloUI/credits_lines.cpp:134 msgid "Customer Support" msgstr "Служба падтрымкі" #: Source/DiabloUI/credits_lines.cpp:139 msgid "Sales" msgstr "Продажы" #: Source/DiabloUI/credits_lines.cpp:142 msgid "Dunsel" msgstr "Бескарысны" #: Source/DiabloUI/credits_lines.cpp:145 msgid "Mr. Dabiri's Background Vocalists" msgstr "Бэквакалісты містэра Дэбіры" #: Source/DiabloUI/credits_lines.cpp:149 msgid "Public Relations" msgstr "Сувязі з грамадствам" #: Source/DiabloUI/credits_lines.cpp:152 msgid "Marketing" msgstr "Маркетынг" #: Source/DiabloUI/credits_lines.cpp:155 msgid "International Sales" msgstr "Міжнародныя продажы" #: Source/DiabloUI/credits_lines.cpp:158 msgid "U.S. Sales" msgstr "Продажы ў ЗША" #: Source/DiabloUI/credits_lines.cpp:161 msgid "Manufacturing" msgstr "Вытворчасць" #: Source/DiabloUI/credits_lines.cpp:164 msgid "Legal & Business" msgstr "Юрыспрудэнцыя і бізнес" #: Source/DiabloUI/credits_lines.cpp:167 msgid "Special Thanks To" msgstr "Асаблівая падзяка" #: Source/DiabloUI/credits_lines.cpp:171 msgid "Thanks To" msgstr "Дзякуючы" #: Source/DiabloUI/credits_lines.cpp:200 msgid "In memory of" msgstr "У памяць" #: Source/DiabloUI/credits_lines.cpp:206 msgid "Very Special Thanks to" msgstr "Вельмі асаблівая падзяка" #: Source/DiabloUI/credits_lines.cpp:212 msgid "General Manager" msgstr "Галоўны менеджар" #: Source/DiabloUI/credits_lines.cpp:218 msgid "Software Engineering" msgstr "Распрацоўка праграмнага забеспячэння" #: Source/DiabloUI/credits_lines.cpp:221 msgid "Art Director" msgstr "Арт-дырэктар" #: Source/DiabloUI/credits_lines.cpp:224 msgid "Artists" msgstr "Мастакі" #: Source/DiabloUI/credits_lines.cpp:228 msgid "Design" msgstr "Дызайн" #: Source/DiabloUI/credits_lines.cpp:235 msgid "Sound Design, SFX & Audio Engineering" msgstr "Гукавы дызайн, SFX і гукарэжысура" #: Source/DiabloUI/credits_lines.cpp:238 msgid "Quality Assurance Lead" msgstr "Вядучы па праверцы якасці" #: Source/DiabloUI/credits_lines.cpp:241 msgid "Testers" msgstr "Тэсціроўшчыкі" #: Source/DiabloUI/credits_lines.cpp:246 msgid "Manual" msgstr "Дапаможнік" #: Source/DiabloUI/credits_lines.cpp:255 msgid "\tAdditional Work" msgstr " Дадатковая праца" #: Source/DiabloUI/credits_lines.cpp:257 msgid "Quest Text Writing" msgstr "Напісанне тэксту заданняў" #: Source/DiabloUI/credits_lines.cpp:260 Source/DiabloUI/credits_lines.cpp:295 msgid "Thanks to" msgstr "Дзякуючы" #: Source/DiabloUI/credits_lines.cpp:265 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tАсаблівая падзяка Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:270 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr " Sierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:272 msgid "Quality Assurance Manager" msgstr "Кіраўнік праверкі якасці" #: Source/DiabloUI/credits_lines.cpp:275 msgid "Quality Assurance Lead Tester" msgstr "Вядучы тэсціроўшчык праверкі якасці" #: Source/DiabloUI/credits_lines.cpp:278 msgid "Main Testers" msgstr "Галоўныя тэсціроўшчыкі" #: Source/DiabloUI/credits_lines.cpp:281 msgid "Additional Testers" msgstr "Дадатковыя тэсціроўшчыкі" #: Source/DiabloUI/credits_lines.cpp:286 msgid "Product Marketing Manager" msgstr "Кіраўнік маркетынгу прадукту" #: Source/DiabloUI/credits_lines.cpp:289 msgid "Public Relations Manager" msgstr "Кіраўнік сувязі з грамадствам" #: Source/DiabloUI/credits_lines.cpp:292 msgid "Associate Product Manager" msgstr "Часовы менеджар прадукту" #: Source/DiabloUI/credits_lines.cpp:301 msgid "The Ring of One Thousand" msgstr "Пярсцёнак Тысячы" #: Source/DiabloUI/credits_lines.cpp:547 msgid "\tNo souls were sold in the making of this game." msgstr "\tПры стварэнні гульні ніводнай душы не было прададзена." #: Source/DiabloUI/dialogs.cpp:196 Source/DiabloUI/dialogs.cpp:208 #: Source/DiabloUI/selconn.cpp:80 Source/DiabloUI/selgame.cpp:168 #: Source/DiabloUI/selgame.cpp:306 Source/DiabloUI/selgame.cpp:332 #: Source/DiabloUI/selgame.cpp:472 Source/DiabloUI/selgame.cpp:547 #: Source/DiabloUI/selhero.cpp:154 Source/DiabloUI/selhero.cpp:179 #: Source/DiabloUI/selhero.cpp:249 Source/DiabloUI/selhero.cpp:493 #: Source/DiabloUI/selok.cpp:69 msgid "OK" msgstr "ОК" #: Source/DiabloUI/mainmenu.cpp:38 msgid "Single Player" msgstr "Адзіночная гульня" #: Source/DiabloUI/mainmenu.cpp:39 msgid "Multi Player" msgstr "Шматкарыстальніцкая гульня" #: Source/DiabloUI/mainmenu.cpp:40 Source/DiabloUI/settingsmenu.cpp:259 msgid "Settings" msgstr "Налады" #: Source/DiabloUI/mainmenu.cpp:41 msgid "Support" msgstr "Падтрымка" #: Source/DiabloUI/mainmenu.cpp:42 msgid "Show Credits" msgstr "Паказаць удзельнікаў" #: Source/DiabloUI/mainmenu.cpp:44 msgid "Exit Hellfire" msgstr "Выйсці з Hellfire" #: Source/DiabloUI/mainmenu.cpp:44 msgid "Exit Diablo" msgstr "Выйсці з Diablo" #: Source/DiabloUI/mainmenu.cpp:60 msgid "Shareware" msgstr "Дэма" #: Source/DiabloUI/progress.cpp:37 Source/DiabloUI/selconn.cpp:83 #: Source/DiabloUI/selhero.cpp:157 Source/DiabloUI/selhero.cpp:182 #: Source/DiabloUI/selhero.cpp:252 Source/DiabloUI/selhero.cpp:501 msgid "Cancel" msgstr "Адмяніць" #: Source/DiabloUI/selconn.cpp:14 msgid "Client-Server (TCP)" msgstr "Кліент-сервер (TCP)" #: Source/DiabloUI/selconn.cpp:15 msgid "Loopback" msgstr "Loopback" #: Source/DiabloUI/selconn.cpp:54 Source/DiabloUI/selgame.cpp:604 #: Source/DiabloUI/selgame.cpp:625 msgid "Multi Player Game" msgstr "Шматкарыстальніцкая гульня" #: Source/DiabloUI/selconn.cpp:60 msgid "Requirements:" msgstr "Патрабаванні:" #: Source/DiabloUI/selconn.cpp:66 msgid "no gateway needed" msgstr "шлюз не патрэбен" #: Source/DiabloUI/selconn.cpp:72 msgid "Select Connection" msgstr "Выбраць злучэнне" #: Source/DiabloUI/selconn.cpp:75 msgid "Change Gateway" msgstr "Змяніць шлюз" #: Source/DiabloUI/selconn.cpp:108 msgid "All computers must be connected to a TCP-compatible network." msgstr "Усе камп'ютары павінны быць падключанымі да сеткі, сумяшчальнай з TCP." #: Source/DiabloUI/selconn.cpp:112 msgid "All computers must be connected to the internet." msgstr "Усе камп'ютары павінны быць падключанымі да Інтэрнэта." #: Source/DiabloUI/selconn.cpp:116 msgid "Play by yourself with no network exposure." msgstr "Гуляць самому без кантакта з сеткаю." #: Source/DiabloUI/selconn.cpp:121 msgid "Players Supported: {:d}" msgstr "Гульцоў падтрымліваецца: {:d}" #: Source/DiabloUI/selgame.cpp:83 Source/options.cpp:542 Source/options.cpp:581 #: Source/quests.cpp:49 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/selgame.cpp:86 msgid "Diablo Shareware" msgstr "Дэма-версія Diablo" #: Source/DiabloUI/selgame.cpp:89 Source/options.cpp:544 Source/options.cpp:595 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/selgame.cpp:92 msgid "Hellfire Shareware" msgstr "Дэма-версія Hellfire" #: Source/DiabloUI/selgame.cpp:95 msgid "The host is running a different game than you." msgstr "Хост зараз гуляе ў другую гульню." #: Source/DiabloUI/selgame.cpp:97 msgid "The host is running a different game mode ({:s}) than you." msgstr "Хост зараз гуляе ў другую ({:s}) гульню." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/selgame.cpp:99 msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "Ваша версія {:s} не падыходзіць да хоста {:d}.{:d}.{:d}." #: Source/DiabloUI/selgame.cpp:134 Source/DiabloUI/selgame.cpp:533 msgid "Description:" msgstr "Апісанне:" #: Source/DiabloUI/selgame.cpp:140 msgid "Select Action" msgstr "Выбраць дзеянне" #: Source/DiabloUI/selgame.cpp:143 Source/DiabloUI/selgame.cpp:294 #: Source/DiabloUI/selgame.cpp:453 msgid "Create Game" msgstr "Стварыць гульню" #: Source/DiabloUI/selgame.cpp:145 msgid "Create Public Game" msgstr "Стварыць публічную гульню" #: Source/DiabloUI/selgame.cpp:146 msgid "Join Game" msgstr "Далучыцца да гульні" #: Source/DiabloUI/selgame.cpp:150 msgid "Public Games" msgstr "Публічныя гульні" #: Source/DiabloUI/selgame.cpp:155 Source/error.cpp:62 msgid "Loading..." msgstr "Загрузка..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/selgame.cpp:157 Source/discord/discord.cpp:71 #: Source/options.cpp:563 Source/panels/charpanel.cpp:137 msgid "None" msgstr "Ніякі" #: Source/DiabloUI/selgame.cpp:171 Source/DiabloUI/selgame.cpp:309 #: Source/DiabloUI/selgame.cpp:335 Source/DiabloUI/selgame.cpp:475 #: Source/DiabloUI/selgame.cpp:550 msgid "CANCEL" msgstr "Адмяніць" #: Source/DiabloUI/selgame.cpp:187 msgid "Create a new game with a difficulty setting of your choice." msgstr "Стварыць новую гульню са складанасцю на ваш выбар." #: Source/DiabloUI/selgame.cpp:190 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" "Стварыць новую публічную гульню, да якой могуць далучыцца са складанасцю на " "ваш выбар." #: Source/DiabloUI/selgame.cpp:194 msgid "Enter Game ID to join a game already in progress." msgstr "Напішыце ID гульні, каб далучыцца да ўжо запушчанай гульні." #: Source/DiabloUI/selgame.cpp:196 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "Напішыце IP ці імя хоста, каб далучыцца да ўжо запушчанай гульні." #: Source/DiabloUI/selgame.cpp:201 msgid "Join the public game already in progress." msgstr "Далучыцца да ўжо запушчанай гульні." #: Source/DiabloUI/selgame.cpp:207 Source/DiabloUI/selgame.cpp:299 #: Source/DiabloUI/selgame.cpp:360 Source/DiabloUI/selgame.cpp:464 #: Source/DiabloUI/selgame.cpp:484 Source/automap.cpp:522 #: Source/discord/discord.cpp:100 msgid "Normal" msgstr "Нармальная" #: Source/DiabloUI/selgame.cpp:210 Source/DiabloUI/selgame.cpp:300 #: Source/DiabloUI/selgame.cpp:364 Source/automap.cpp:525 #: Source/discord/discord.cpp:100 msgid "Nightmare" msgstr "Кашмар" #: Source/DiabloUI/selgame.cpp:213 Source/DiabloUI/selgame.cpp:301 #: Source/DiabloUI/selgame.cpp:368 Source/automap.cpp:528 #: Source/discord/discord.cpp:66 Source/discord/discord.cpp:100 msgid "Hell" msgstr "Пекла" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/selgame.cpp:216 Source/automap.cpp:532 msgid "Difficulty: {:s}" msgstr "Складанасць: {:s}" #: Source/DiabloUI/selgame.cpp:220 Source/gamemenu.cpp:161 msgid "Speed: Normal" msgstr "Хуткасць: Нармальная" #: Source/DiabloUI/selgame.cpp:223 Source/gamemenu.cpp:159 msgid "Speed: Fast" msgstr "Хуткасць: Шпарка" #: Source/DiabloUI/selgame.cpp:226 Source/gamemenu.cpp:157 msgid "Speed: Faster" msgstr "Хуткасць: Шпарчэй" #: Source/DiabloUI/selgame.cpp:229 Source/gamemenu.cpp:155 msgid "Speed: Fastest" msgstr "Хуткасць: Найшпарчэй" #: Source/DiabloUI/selgame.cpp:237 msgid "Players: " msgstr "Гульцы: " #: Source/DiabloUI/selgame.cpp:297 msgid "Select Difficulty" msgstr "Выбраць складанасць" #: Source/DiabloUI/selgame.cpp:315 msgid "Join {:s} Games" msgstr "Далучыцца да {:s} гульняў" #: Source/DiabloUI/selgame.cpp:320 msgid "Enter Game ID" msgstr "Напішыце ID гульні" #: Source/DiabloUI/selgame.cpp:322 msgid "Enter address" msgstr "Напішыце адрас" #: Source/DiabloUI/selgame.cpp:361 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Нармальная складанасць\n" "Пачаткоўцу трэба пачынаць шлях да перамогі над Д'яблам тут." #: Source/DiabloUI/selgame.cpp:365 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Складанасць Кашмар\n" "Жыхары Лабірынту ўзмацніліся, выпрабаванне стане складнейшым. Рэкамендуецца " "толькі для дасведчаных гульцоў." #: Source/DiabloUI/selgame.cpp:369 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Складанасць Пекла\n" "Наймацнейшыя стварэнні падземнага свету пільнуюць вас каля варот Пекла. " "Толькі самыя дасведчаныя адважацца зайсці ў гэтае царства." #: Source/DiabloUI/selgame.cpp:384 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Ваш персанаж павінен быць 20 ўзроўню, каб зайсці ў шматкарыстальніцкую " "гульню на складанасці Кашмар." #: Source/DiabloUI/selgame.cpp:386 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Ваш персанаж павінен быць 30 ўзроўню, каб зайсці ў шматкарыстальніцкую " "гульню на складанасці Пекла." #: Source/DiabloUI/selgame.cpp:462 msgid "Select Game Speed" msgstr "Выбраць хуткасць гульні" #: Source/DiabloUI/selgame.cpp:465 Source/DiabloUI/selgame.cpp:488 msgid "Fast" msgstr "Шпаркая" #: Source/DiabloUI/selgame.cpp:466 Source/DiabloUI/selgame.cpp:492 msgid "Faster" msgstr "Шпарчэйшая" #: Source/DiabloUI/selgame.cpp:467 Source/DiabloUI/selgame.cpp:496 msgid "Fastest" msgstr "Найшпарчэйшая" #: Source/DiabloUI/selgame.cpp:485 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Нармальная хуткасць\n" "Пачаткоўцу трэба пачынаць шлях да перамогі над Д'яблам тут." #: Source/DiabloUI/selgame.cpp:489 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Шпаркая хуткасць\n" "Жыхароў Лабірынту прыспешылі, выпрабаванне стане складнейшым. Рэкамендуецца " "толькі для дасведчаных гульцоў." #: Source/DiabloUI/selgame.cpp:493 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "Шпарчэйшая хуткасць\n" "Большасць пачвар цяпер адшукае вас шпарчэй, чым калі-небудзь раней. Толькі " "дасведчаны асілак можа пакаштаваць шчасця на такой хуткасці." #: Source/DiabloUI/selgame.cpp:497 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Найшпарчэйшая хутскасць\n" "Памагатыя падзем'я рынуцца ў бой без вагання. Толькі сапраўды хуткі як чорт " "можа увайсці такой хадою." #: Source/DiabloUI/selgame.cpp:539 Source/DiabloUI/selgame.cpp:544 msgid "Enter Password" msgstr "Увесці пароль" #: Source/DiabloUI/selhero.cpp:132 msgid "Choose Class" msgstr "Выбраць клас" #: Source/DiabloUI/selhero.cpp:136 Source/panels/charpanel.cpp:23 msgid "Warrior" msgstr "Ваяр" #: Source/DiabloUI/selhero.cpp:137 Source/panels/charpanel.cpp:24 msgid "Rogue" msgstr "Шэльма" #: Source/DiabloUI/selhero.cpp:138 Source/panels/charpanel.cpp:25 msgid "Sorcerer" msgstr "Чараўнік" #: Source/DiabloUI/selhero.cpp:140 Source/panels/charpanel.cpp:26 msgid "Monk" msgstr "Манах" #: Source/DiabloUI/selhero.cpp:143 Source/panels/charpanel.cpp:27 msgid "Bard" msgstr "Бард" #: Source/DiabloUI/selhero.cpp:146 Source/panels/charpanel.cpp:28 msgid "Barbarian" msgstr "Варвар" #: Source/DiabloUI/selhero.cpp:163 Source/DiabloUI/selhero.cpp:237 msgid "New Multi Player Hero" msgstr "Новы герой для Мультыплэера" #: Source/DiabloUI/selhero.cpp:163 Source/DiabloUI/selhero.cpp:237 msgid "New Single Player Hero" msgstr "Новы адзіночны герой" #: Source/DiabloUI/selhero.cpp:171 msgid "Save File Exists" msgstr "Захаванне існуе" #: Source/DiabloUI/selhero.cpp:174 Source/gamemenu.cpp:38 msgid "Load Game" msgstr "Загрузіць гульню" #: Source/DiabloUI/selhero.cpp:175 Source/gamemenu.cpp:37 #: Source/gamemenu.cpp:48 Source/multi.cpp:737 msgid "New Game" msgstr "Новая гульня" #: Source/DiabloUI/selhero.cpp:185 Source/DiabloUI/selhero.cpp:507 msgid "Single Player Characters" msgstr "Персанажы для адзіночнай гульні" #: Source/DiabloUI/selhero.cpp:231 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Шэльма і Чараўнік даступныя толькі ў поўнай рознічнай версіі Diablo. " "Наведайце https://www.gog.com/game/diablo, каб набыць." #: Source/DiabloUI/selhero.cpp:243 Source/DiabloUI/selhero.cpp:246 msgid "Enter Name" msgstr "Увесці імя" #: Source/DiabloUI/selhero.cpp:275 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Імя не падыходзіць. У імені не павінна быць прабелаў, зарэзерваваных " "сімвалаў і слоў.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/selhero.cpp:282 msgid "Unable to create character." msgstr "Немагчыма стварыць персанажа." #: Source/DiabloUI/selhero.cpp:436 Source/DiabloUI/selhero.cpp:439 msgid "Level:" msgstr "Узровень:" #: Source/DiabloUI/selhero.cpp:444 msgid "Strength:" msgstr "Сіла:" #: Source/DiabloUI/selhero.cpp:449 msgid "Magic:" msgstr "Магія:" #: Source/DiabloUI/selhero.cpp:454 msgid "Dexterity:" msgstr "Спрыт:" #: Source/DiabloUI/selhero.cpp:459 msgid "Vitality:" msgstr "Жывучасць:" #: Source/DiabloUI/selhero.cpp:465 msgid "Savegame:" msgstr "Захаванне:" #: Source/DiabloUI/selhero.cpp:477 msgid "Select Hero" msgstr "Выбраць героя" #: Source/DiabloUI/selhero.cpp:485 msgid "New Hero" msgstr "Новы герой" #: Source/DiabloUI/selhero.cpp:496 msgid "Delete" msgstr "Выдаліць" #: Source/DiabloUI/selhero.cpp:505 msgid "Multi Player Characters" msgstr "Героі для мультыплэера" #: Source/DiabloUI/selhero.cpp:555 msgid "Delete Multi Player Hero" msgstr "Выдаліць мультыплэернага героя" #: Source/DiabloUI/selhero.cpp:557 msgid "Delete Single Player Hero" msgstr "Выдаліць адзіночнага героя" #: Source/DiabloUI/selhero.cpp:559 msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Вы ўпэўненныя ў выдаленні персанажа \"{:s}\"?" #: Source/DiabloUI/selstart.cpp:43 msgid "Enter Hellfire" msgstr "Зайсці ў Hellfire" #: Source/DiabloUI/selstart.cpp:44 msgid "Switch to Diablo" msgstr "Вярнуцца да Diablo" #: Source/DiabloUI/selyesno.cpp:55 Source/stores.cpp:991 msgid "Yes" msgstr "Так" #: Source/DiabloUI/selyesno.cpp:56 Source/stores.cpp:992 msgid "No" msgstr "Не" #: Source/DiabloUI/settingsmenu.cpp:304 msgid "Bound key:" msgstr "Прывязаная клавіша:" #: Source/DiabloUI/settingsmenu.cpp:339 msgid "Press any key to change." msgstr "Націснеце любую клавішу каб замяніць." #: Source/DiabloUI/settingsmenu.cpp:341 msgid "Unbind key" msgstr "Адвязаць клавішу" #: Source/DiabloUI/settingsmenu.cpp:347 Source/gamemenu.cpp:61 msgid "Previous Menu" msgstr "Папярэдняе меню" #: Source/DiabloUI/support_lines.cpp:8 msgid "" "We maintain a chat server at Discord.gg/YQKCAYQ Follow the links to join our " "community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "У нас ёсць чат у Discord.gg/YQKCAYQ Прайдзіе па спасылках, каб далучыцца да " "нашай суполкі, дзе мы абмяркоўваем усё звязанае з Diablo і дапаўненнем " "Hellfire." #: Source/DiabloUI/support_lines.cpp:10 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX падтрымліваецца Diasurgical, аб праблемах і памылках паведаміць " "па адрасе https://github.com/diasurgical/devilutionX Каб дапамагчы нам лепей " "вас абслугоўваць, калі ласка не забывайце дадаваць нумар версіі, аперацыйную " "сістэму і сутнасць праблемы." #: Source/DiabloUI/support_lines.cpp:13 msgid "Disclaimer:" msgstr "Дысклеймер:" #: Source/DiabloUI/support_lines.cpp:14 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX не падтрымліваецца ні Blizzard Entertainment ні GOG.com. Ні " "Blizzard Entertainment, ні GOG.com не тэсціравалі ці сведчылі якасць ці " "сумяшчальнасць DevilutionX. Усе запытанні, датычныя DevilutionX, трэба " "накіроўваць да Diasurgical, не Blizzard Entertainment ці GOG.com." #: Source/DiabloUI/support_lines.cpp:17 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tГэты порт карыстаецца Charis SIL, New Athena Unicode, Unifont і Noto, якія " "ліцэнзаваныя SIL Open Font, гэтак жа сама і Twitmoj ліцэнзавана CC-BY 4.0. " "Порт таксама карыстаецца SDL, ліцэнзаванай zlib-ліцэнзіяй. Паглядзіце ReadMe " "для дадатковых дэталяў." #: Source/DiabloUI/title.cpp:46 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Copyright © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:38 msgid "Error" msgstr "Памылка" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:100 msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Адбылася памылка на {:s} радку {:d}" #: Source/appfat.cpp:109 msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Немагчыма адчыніць галоўны архіў ({:s}).\n" "\n" "Праверце, ці яна ў папке з гульнёй." #: Source/appfat.cpp:114 msgid "Data File Error" msgstr "Памылка з файлам" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:120 msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Не ўдалося запісаць месцазнаходжанне:\n" "{:s}" #: Source/appfat.cpp:122 msgid "Read-Only Directory Error" msgstr "Памылка у Read-Only дапаможніку" #: Source/automap.cpp:484 msgid "Game: " msgstr "Гульня: " #: Source/automap.cpp:492 msgid "Password: " msgstr "Пароль: " #: Source/automap.cpp:495 msgid "Public Game" msgstr "Публічная гульня" #: Source/automap.cpp:509 msgid "Level: Nest {:d}" msgstr "Узровень: Гняздо {:d}" #: Source/automap.cpp:511 msgid "Level: Crypt {:d}" msgstr "Узровень: Крыпта {:d}" #: Source/automap.cpp:513 msgid "Level: {:d}" msgstr "Узровень: {:d}" #: Source/control.cpp:155 msgid "Tab" msgstr "Tab" #: Source/control.cpp:155 msgid "Esc" msgstr "Esc" #: Source/control.cpp:155 msgid "Enter" msgstr "Enter" #: Source/control.cpp:158 msgid "Character Information" msgstr "Аб персанажы" #: Source/control.cpp:159 msgid "Quests log" msgstr "Заданні" #: Source/control.cpp:160 msgid "Automap" msgstr "Аўтамапа" #: Source/control.cpp:161 msgid "Main Menu" msgstr "Меню" #: Source/control.cpp:162 Source/diablo.cpp:1537 msgid "Inventory" msgstr "Інвентар" #: Source/control.cpp:163 msgid "Spell book" msgstr "Кніга чараў" #: Source/control.cpp:164 msgid "Send Message" msgstr "Даслаць паведамленне" #: Source/control.cpp:694 msgid "Player friendly" msgstr "Плэер-фрэндлі" #: Source/control.cpp:696 msgid "Player attack" msgstr "Гулец атакуе" #: Source/control.cpp:699 msgid "Hotkey: {:s}" msgstr "Гарачая клавіша: {:s}" #: Source/control.cpp:706 msgid "Select current spell button" msgstr "Выбраць клавішу цяперашняй чары" #: Source/control.cpp:709 msgid "Hotkey: 's'" msgstr "Гарачая клавіша: 's'" #: Source/control.cpp:715 Source/panels/spell_list.cpp:163 msgid "{:s} Skill" msgstr "{:s} Уменне" #: Source/control.cpp:718 Source/panels/spell_list.cpp:170 msgid "{:s} Spell" msgstr "{:s} Чара" #: Source/control.cpp:720 Source/panels/spell_list.cpp:175 msgid "Spell Level 0 - Unusable" msgstr "Чара Узровень 0 – Няздатная" #: Source/control.cpp:720 Source/panels/spell_list.cpp:177 msgid "Spell Level {:d}" msgstr "Узровень Чары {:d}" #: Source/control.cpp:723 Source/panels/spell_list.cpp:184 msgid "Scroll of {:s}" msgstr "Скрутак {:s}" #: Source/control.cpp:728 Source/panels/spell_list.cpp:189 msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Скрутак" msgstr[1] "{:d} Скруткі" msgstr[2] "{:d} Скруткаў" #: Source/control.cpp:731 Source/panels/spell_list.cpp:196 msgid "Staff of {:s}" msgstr "Посах {:s}" #: Source/control.cpp:732 Source/panels/spell_list.cpp:198 msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Зарад" msgstr[1] "{:d} Зарады" msgstr[2] "{:d} Зарадаў" #: Source/control.cpp:860 Source/inv.cpp:2004 Source/items.cpp:3486 msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} манета" msgstr[1] "{:s} манеты" msgstr[2] "{:s} манет" #: Source/control.cpp:862 msgid "Requirements not met" msgstr "Патрабаванні не выкананыя" #: Source/control.cpp:896 msgid "{:s}, Level: {:d}" msgstr "{:s}, Узровень: {:d}" #: Source/control.cpp:897 msgid "Hit Points {:d} of {:d}" msgstr "Ачкі здароўя {:d} of {:d}" #: Source/control.cpp:931 msgid "Level Up" msgstr "Новы ўзровень" #. TRANSLATORS: {:d} is a number. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1039 msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "У вас ёсць {:s} манета. Колькі хочаце пакінуць?" msgstr[1] "У вас ёсць {:s} манеты. Колькі хочаце пакінуць?" msgstr[2] "У вас ёсць {:s} манет. Колькі хочаце пакінуць?" #: Source/controls/modifier_hints.cpp:177 Source/qol/monhealthbar.cpp:36 #: Source/qol/xpbar.cpp:76 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Не атрымалася загрузіць рэсурсы інтэрфэйса.\n" "\n" "Праверце, ці devilutionx.mpq у папке з гульнёю і ці абноўлены ён." #: Source/cursor.cpp:234 msgid "Town Portal" msgstr "Партал у горад" #: Source/cursor.cpp:235 msgid "from {:s}" msgstr "з {:s}" #: Source/cursor.cpp:249 msgid "Portal to" msgstr "Партал у" #: Source/cursor.cpp:250 msgid "The Unholy Altar" msgstr "Паганы алтар" #: Source/cursor.cpp:250 msgid "level 15" msgstr "узровень 15" #: Source/diablo.cpp:120 msgid "I need help! Come Here!" msgstr "Дапамажыце! Сюды!" #: Source/diablo.cpp:121 msgid "Follow me." msgstr "За мною." #: Source/diablo.cpp:122 msgid "Here's something for you." msgstr "Вось, на." #: Source/diablo.cpp:123 msgid "Now you DIE!" msgstr "Здохні!" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:822 msgid "Options:" msgstr "Варыянты:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:823 msgid "Print this message and exit" msgstr "Пакінуць паведамленне і выйсці" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:824 msgid "Print the version and exit" msgstr "Пакінуць версію і выйсці" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:825 msgid "Specify the folder of diabdat.mpq" msgstr "Вызначыць папку diabdat.mpq" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:826 msgid "Specify the folder of save files" msgstr "Вызначаць папку захаванняў" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:827 msgid "Specify the location of diablo.ini" msgstr "Вызначыць знаходжанне diablo.ini" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:828 msgid "Skip startup videos" msgstr "Прапусціць відэа" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:829 msgid "Display frames per second" msgstr "Паказваць кадры ў секунду" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:830 msgid "Enable verbose logging" msgstr "Дазволіць падрабязнае апісанне" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:831 msgid "Record a demo file" msgstr "Запісаць дэма-файл" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:832 msgid "Play a demo file" msgstr "Прайграць дэма-файл" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:833 msgid "Disable all frame limiting during demo playback" msgstr "Адмяніць усё абмежаванне кадраў падчас прайгравання дэма" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:834 msgid "Game selection:" msgstr "Выбар гульні:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:835 msgid "Force Shareware mode" msgstr "Уключыць рэжым Дэма" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:836 msgid "Force Diablo mode" msgstr "Уключыць рэжым Diablo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:837 msgid "Force Hellfire mode" msgstr "Уключыць рэжым Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:838 msgid "Hellfire options:" msgstr "Налады Hellfire:" #: Source/diablo.cpp:844 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "Паведаміць аб памылках https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:956 msgid "version {:s}" msgstr "версія {:s}" #: Source/diablo.cpp:1277 msgid "-- Network timeout --" msgstr "-- Перапынак сеткі --" #: Source/diablo.cpp:1278 msgid "-- Waiting for players --" msgstr "-- Чакаем гульцоў --" #: Source/diablo.cpp:1297 msgid "No help available" msgstr "Дапамога не даступна" #: Source/diablo.cpp:1298 msgid "while in stores" msgstr "пакуль у чаканні" #: Source/diablo.cpp:1439 msgid "Belt item {}" msgstr "Рэч на поясе {}" #: Source/diablo.cpp:1440 msgid "Use Belt item." msgstr "Скарыстаць рэч на поясе." #: Source/diablo.cpp:1455 msgid "Quick spell {}" msgstr "Быстрае закляцце {}" #: Source/diablo.cpp:1456 msgid "Hotkey for skill or spell." msgstr "Гарачая клавіша пад уменне ці чару." #: Source/diablo.cpp:1474 msgid "Speedbook" msgstr "Кніга прыткасці" #: Source/diablo.cpp:1475 msgid "Open Speedbook." msgstr "Адчыніць кнігу прыткасці." #: Source/diablo.cpp:1482 msgid "Quick save" msgstr "Хутка захавацца" #: Source/diablo.cpp:1483 msgid "Saves the game." msgstr "Захоўвае гульню." #: Source/diablo.cpp:1490 msgid "Quick load" msgstr "Хутка загрузіцца" #: Source/diablo.cpp:1491 msgid "Loads the game." msgstr "Загрузіць гульню." #: Source/diablo.cpp:1499 msgid "Quit game" msgstr "Выйсці" #: Source/diablo.cpp:1500 msgid "Closes the game." msgstr "Зачыніць гульню." #: Source/diablo.cpp:1506 msgid "Stop hero" msgstr "Спыніць героя" #: Source/diablo.cpp:1507 msgid "Stops walking and cancel pending actions." msgstr "Спыніць ход і адмяніць дзеянне." #: Source/diablo.cpp:1514 msgid "Item highlighting" msgstr "Падсвечванне рэчаў" #: Source/diablo.cpp:1515 msgid "Show/hide items on ground." msgstr "Паказваць/хаваць рэчы на зямлі." #: Source/diablo.cpp:1521 msgid "Toggle item highlighting" msgstr "Пераключыць падсвечванне рэчаў" #: Source/diablo.cpp:1522 msgid "Permanent show/hide items on ground." msgstr "Імгненна паказваць/хаваць рэчы на зямлі." #: Source/diablo.cpp:1528 msgid "Toggle automap" msgstr "Пераключыць аўтамапу" #: Source/diablo.cpp:1529 msgid "Toggles if automap is displayed." msgstr "Пераключае калі аўтамапа ўключана." #: Source/diablo.cpp:1538 msgid "Open Inventory screen." msgstr "Адчыніць інвентар." #: Source/diablo.cpp:1545 msgid "Character" msgstr "Персанаж" #: Source/diablo.cpp:1546 msgid "Open Character screen." msgstr "Адчыніць экран персанажа." #: Source/diablo.cpp:1553 msgid "Quest log" msgstr "Заданні" #: Source/diablo.cpp:1554 msgid "Open Quest log." msgstr "Паглядзець заданні." #: Source/diablo.cpp:1561 msgid "Spellbook" msgstr "Кніга чараў" #: Source/diablo.cpp:1562 msgid "Open Spellbook." msgstr "Адчыніць кнігу чараў." #: Source/diablo.cpp:1570 msgid "Quick Message {}" msgstr "Хутка паведаміць {}" #: Source/diablo.cpp:1571 msgid "Use Quick Message in chat." msgstr "Хутка паведаміць у чат." #: Source/diablo.cpp:1580 msgid "Hide Info Screens" msgstr "Схаваць экраны інфармацыі" #: Source/diablo.cpp:1581 msgid "Hide all info screens." msgstr "Схаваць усе экраны інфармацыі." #: Source/diablo.cpp:1601 msgid "Zoom" msgstr "Наблізіць" #: Source/diablo.cpp:1602 msgid "Zoom Game Screen." msgstr "Наблізіць экран гульні." #: Source/diablo.cpp:1612 msgid "Pause Game" msgstr "Спыніць гульню" #: Source/diablo.cpp:1613 msgid "Pauses the game." msgstr "Спыняе гульню." #: Source/diablo.cpp:1618 msgid "Decrease Gamma" msgstr "Паменшыць гаму" #: Source/diablo.cpp:1619 msgid "Reduce screen brightness." msgstr "Знізіць яркасць экрана." #: Source/diablo.cpp:1626 msgid "Increase Gamma" msgstr "Павысіць гаму" #: Source/diablo.cpp:1627 msgid "Increase screen brightness." msgstr "Павысіць яркасць экрана." #: Source/diablo.cpp:1634 msgid "Help" msgstr "Дапамога" #: Source/diablo.cpp:1635 msgid "Open Help Screen." msgstr "Адчыніць экран дапамогі." #: Source/diablo.cpp:1642 msgid "Screenshot" msgstr "Здымак экрана" #: Source/diablo.cpp:1643 msgid "Takes a screenshot." msgstr "Зрабіць здымак экрана." #: Source/diablo.cpp:1649 msgid "Game info" msgstr "Аб гульні" #: Source/diablo.cpp:1650 msgid "Displays game infos." msgstr "Паказвае інфармацыю аб гульні." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:1654 msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:1663 msgid "Chat Log" msgstr "Чат" #: Source/diablo.cpp:1664 msgid "Displays chat log." msgstr "Паказвае чат." #: Source/discord/discord.cpp:66 Source/objects.cpp:136 msgid "Town" msgstr "Горад" #: Source/discord/discord.cpp:66 msgid "Cathedral" msgstr "Сабор" #: Source/discord/discord.cpp:66 msgid "Catacombs" msgstr "Катакомбы" #: Source/discord/discord.cpp:66 msgid "Caves" msgstr "Пячоры" #: Source/discord/discord.cpp:66 msgid "Nest" msgstr "Гняздо" #: Source/discord/discord.cpp:66 msgid "Crypt" msgstr "Крыпта" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:82 msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:90 msgid "Lv {} {}" msgstr "Уз {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:102 msgid "{} difficulty" msgstr "{} складанасць" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:182 msgid "In Menu" msgstr "У меню" #: Source/dvlnet/loopback.cpp:113 msgid "loopback" msgstr "loopback" #: Source/dvlnet/tcp_client.cpp:65 msgid "Unable to connect" msgstr "Няма як звязацца" #: Source/dvlnet/tcp_client.cpp:91 msgid "error: read 0 bytes from server" msgstr "памылка: счытана 0 байт з сервера" #: Source/error.cpp:53 msgid "No automap available in town" msgstr "Мапа не даступна ў горадзе" #: Source/error.cpp:54 msgid "No multiplayer functions in demo" msgstr "У дэма няма шматкарыстальніцкай гульні" #: Source/error.cpp:55 msgid "Direct Sound Creation Failed" msgstr "Стварэнне гука правалілася" #: Source/error.cpp:56 msgid "Not available in shareware version" msgstr "Недаступна ў дэма версіі" #: Source/error.cpp:57 msgid "Not enough space to save" msgstr "Няма месца каб захаваць" #: Source/error.cpp:58 msgid "No Pause in town" msgstr "Нельга ставіць паўзу ў горадзе" #: Source/error.cpp:59 msgid "Copying to a hard disk is recommended" msgstr "Рэкамендуем захваць на цвёрдым дыску" #: Source/error.cpp:60 msgid "Multiplayer sync problem" msgstr "Праблема з сінхранізацыяй у мультыплэеры" #: Source/error.cpp:61 msgid "No pause in multiplayer" msgstr "Нельга ставіць на паўзу ў мультыплэеры" #: Source/error.cpp:63 msgid "Saving..." msgstr "Захоўвае..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:64 msgid "Some are weakened as one grows strong" msgstr "Як нехта мацнее другія слабеюць" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:65 msgid "New strength is forged through destruction" msgstr "Моц новая куецца разбурэннем" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:66 msgid "Those who defend seldom attack" msgstr "Хто бароніцца рэдка нападае" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:67 msgid "The sword of justice is swift and sharp" msgstr "Меч справядлівасці хуткі ды востры" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:68 msgid "While the spirit is vigilant the body thrives" msgstr "Пакуль дух пільнуе, цела квітнее" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:69 msgid "The powers of mana refocused renews" msgstr "Перакіраваўшыся моц маны аднаўляецца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:70 msgid "Time cannot diminish the power of steel" msgstr "Час сталі моцы не паслабіць" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:71 msgid "Magic is not always what it seems to be" msgstr "Магія не заўжды тое, чым здаецца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:72 msgid "What once was opened now is closed" msgstr "Што адчынілі цяпер зачынілі" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:73 msgid "Intensity comes at the cost of wisdom" msgstr "Напружанне каштуе мудрасці" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:74 msgid "Arcane power brings destruction" msgstr "Таемная моц нясе разбурэнне" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:75 msgid "That which cannot be held cannot be harmed" msgstr "За што ні ўзяцца, няма як параніць" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:76 msgid "Crimson and Azure become as the sun" msgstr "Чырвань ды блакіт стануць нібы сонца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:77 msgid "Knowledge and wisdom at the cost of self" msgstr "Веды ды мудрасць па кошту сябе самога" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:78 msgid "Drink and be refreshed" msgstr "Пі ды асвяжыся" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:79 msgid "Wherever you go, there you are" msgstr "Дзе б ты ні быў, вось і ты" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:80 msgid "Energy comes at the cost of wisdom" msgstr "Сіла жыццёвая мудрасці каштуе" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:81 msgid "Riches abound when least expected" msgstr "Багацця там многа, дзе менш за ўсё думаеш" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:82 msgid "Where avarice fails, patience gains reward" msgstr "Дзе прайграе сквапнасць, там цярпенне ўзнагародзіцца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:83 msgid "Blessed by a benevolent companion!" msgstr "Благаславёны добразычлівым спадарожнікам!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:84 msgid "The hands of men may be guided by fate" msgstr "Няхай рукі чалавечыя лёсам накіруюцца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:85 msgid "Strength is bolstered by heavenly faith" msgstr "Сіла ўмацавана вераю нябеснай" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:86 msgid "The essence of life flows from within" msgstr "Жыццёвая сутнасць ліецца знутры" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:87 msgid "The way is made clear when viewed from above" msgstr "Зверху глядзіш – шлях лягчэй пабачыць" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:88 msgid "Salvation comes at the cost of wisdom" msgstr "Ратунак прыйдзе цаною мудрасці" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:89 msgid "Mysteries are revealed in the light of reason" msgstr "Таямніцы раскрыюцца пры святле розуму" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:90 msgid "Those who are last may yet be first" msgstr "Апошнія могуць быць яшчэ і першымі" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:91 msgid "Generosity brings its own rewards" msgstr "Шчодрасць узнагародзіць па-свойму" #: Source/error.cpp:92 msgid "You must be at least level 8 to use this." msgstr "Трэба быць хаця б 8 ўзроўню." #: Source/error.cpp:93 msgid "You must be at least level 13 to use this." msgstr "Трэба быць хаця б 13 ўзроўню." #: Source/error.cpp:94 msgid "You must be at least level 17 to use this." msgstr "Трэба быць хаця б 8 ўзроўню." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:95 msgid "Arcane knowledge gained!" msgstr "Таемныя веды вывучаны!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:96 msgid "That which does not kill you..." msgstr "Тое што вас не забівае..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:97 msgid "Knowledge is power." msgstr "Веды ёсць сіла." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:98 msgid "Give and you shall receive." msgstr "Давайце і вам будзе дадзена." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:99 msgid "Some experience is gained by touch." msgstr "Досвед можна і крануўшыся атрымаць." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:100 msgid "There's no place like home." msgstr "Усюды добра, а дома лепей." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:101 msgid "Spiritual energy is restored." msgstr "Сіла духа вернута." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:102 msgid "You feel more agile." msgstr "Вы нібыта пажвавелі." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:103 msgid "You feel stronger." msgstr "Вы нібыта узмацнелі." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:104 msgid "You feel wiser." msgstr "Вы нібыта памудрэлі." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:105 msgid "You feel refreshed." msgstr "Вы нібыта пасвяжэлі." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/error.cpp:106 msgid "That which can break will." msgstr "Тое, што можа зламіць волю." #: Source/gamemenu.cpp:35 msgid "Save Game" msgstr "Захаваць гульню" #: Source/gamemenu.cpp:36 Source/gamemenu.cpp:47 msgid "Options" msgstr "Налады" #: Source/gamemenu.cpp:39 Source/gamemenu.cpp:50 msgid "Quit Game" msgstr "Выйсці" #: Source/gamemenu.cpp:49 msgid "Restart In Town" msgstr "Пачаць зноў у Горадзе" #: Source/gamemenu.cpp:59 msgid "Gamma" msgstr "Гама" #: Source/gamemenu.cpp:60 Source/gamemenu.cpp:167 msgid "Speed" msgstr "Хуткасць" #: Source/gamemenu.cpp:68 msgid "Music Disabled" msgstr "Музыка адключана" #: Source/gamemenu.cpp:72 msgid "Sound" msgstr "Гук" #: Source/gamemenu.cpp:73 msgid "Sound Disabled" msgstr "Гук адключаны" #: Source/gmenu.cpp:168 msgid "Pause" msgstr "Паўза" #: Source/help.cpp:27 msgid "$Keyboard Shortcuts:" msgstr "$Скарачэнні клавіш:" #: Source/help.cpp:28 msgid "F1: Open Help Screen" msgstr "F1:: Адчыніць экран дапамогі" #: Source/help.cpp:29 msgid "Esc: Display Main Menu" msgstr "Esc: Паказаць Галоўнае Меню" #: Source/help.cpp:30 msgid "Tab: Display Auto-map" msgstr "Tab: Паказаць аўтамапу" #: Source/help.cpp:31 msgid "Space: Hide all info screens" msgstr "Прабел: Схаваць усе экраны інфармацыі" #: Source/help.cpp:32 msgid "S: Open Speedbook" msgstr "S: Адчыцніь кнігу прыткасці" #: Source/help.cpp:33 msgid "B: Open Spellbook" msgstr "B: Адчыцніць кнігу чараў" #: Source/help.cpp:34 msgid "I: Open Inventory screen" msgstr "I: Адчыніць інвентар" #: Source/help.cpp:35 msgid "C: Open Character screen" msgstr "C: Адчыніць экран персанажа" #: Source/help.cpp:36 msgid "Q: Open Quest log" msgstr "Q: Паглядзець заданні" #: Source/help.cpp:37 msgid "F: Reduce screen brightness" msgstr "F: Знізіць яркасць экрана" #: Source/help.cpp:38 msgid "G: Increase screen brightness" msgstr "G: Павысіць яркасць экрана" #: Source/help.cpp:39 msgid "Z: Zoom Game Screen" msgstr "Z: Наблізіць экран гульні" #: Source/help.cpp:40 msgid "+ / -: Zoom Automap" msgstr "+ / -: Маштабаванне аўтамапы" #: Source/help.cpp:41 msgid "1 - 8: Use Belt item" msgstr "1 – 8: Выкарыстаць рэч з пояса" #: Source/help.cpp:42 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Устанавіць гарачую клавішу" #: Source/help.cpp:43 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Левая кнопка мышы: Біць не рухаючыся" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Левая кнопка мышы (на экране персанажа): Прызначыць ачкі статыстыкі" #: Source/help.cpp:45 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Левая кнопка мышы (у інвентары): Павесіць нешта на пояс ці зняць/" "надзець рэч" #: Source/help.cpp:46 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "Shift + Левая кнопка мышы (на поясе): Убраць рэч у інвентар" #: Source/help.cpp:48 msgid "$Movement:" msgstr "$Рух:" #: Source/help.cpp:49 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" "Калі трымаеце кнопку мышы падчас руху, персанаж будзе працягваць рухацца ў " "гэтым кірунку." #: Source/help.cpp:52 msgid "$Combat:" msgstr "$Бой:" #: Source/help.cpp:53 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Трымаючы клавішу shift і пасля націскаючы левую кнопку мышы, персанаж можа " "біцца стоячы на месцы." #: Source/help.cpp:56 msgid "$Auto-map:" msgstr "$Аўтамапа:" #: Source/help.cpp:57 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "Каб адкрыць аўтамапу, націснеце \"МАПА\" на панэлі інфармацыі ці клавішу " "'TAB' на клавіатуры. Маштабаванне карты ажыццяўляецца клавішамі + ды -. " "Пракручвайце карту стрэлкамі на клавіатуры." #: Source/help.cpp:62 msgid "$Picking up Objects:" msgstr "$Падыманне рэчаў:" #: Source/help.cpp:63 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Розныя маленькія рэчы, якімі можна карыстацца, як зеллі ці скруткі, " "аўтаматычна змяшчаюцца ў ваш \"пояс\", наверсе панэлі інтэрфейсу. Калі " "прадмет вешаецца на пояс, невялікая лічба з'яўляецца ў яго полі. Каб " "скарыстацца рэччу, трэба націснуць адпаведную лічбу на клавіатуры або " "націснуць па ім правай кнопкай мышы." #: Source/help.cpp:69 msgid "$Gold:" msgstr "$Золата:" #: Source/help.cpp:70 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Вы можаце выбраць, колькі золата кінуць, націснуўшы правай кнопкай мышы па " "жменьцы золота ў інвентары." #: Source/help.cpp:73 msgid "$Skills & Spells:" msgstr "$Уменні ды Чары:" #: Source/help.cpp:74 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "Вы можаце адкрыць спіс уменняў ды чараў, націснуўшы левай кнопкай мышы па " "'ЧАРЫ' на панэлі інтэрфейсу. Тут пералічаны вывучаныя ды даступныя праз " "посах чары. Каб падрыхтаваць патрэбныя чары, клікніце па іх левай кнопкай " "мышы. Чары можна наслаць націснуўшы правай кнопкаю мышы." #: Source/help.cpp:80 msgid "$Using the Speedbook for Spells:" msgstr "$Кніга прыткасці для чараў:" #: Source/help.cpp:81 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Калі націснуць на \"гатовыя чары\", то адкрыецца \"Кніга прыткасці\", якая " "дае выбраць чары ці уменне для хуткага карыстання. Каб скарыстацца гатовым " "уменнем ці чарамі, проста націснеце правай кнопкаю мышы." #: Source/help.cpp:85 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Shift + Левая кнопка мышы па \"выбраць цяперашнія чары\" прыбярэ гатовыя " "чары." #: Source/help.cpp:87 msgid "$Setting Spell Hotkeys:" msgstr "$Устаноўка гарачых клавішаў для чараў:" #: Source/help.cpp:88 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Вы можаце прызначыць да чатырох гарачых клавіш для навыкаў, чараў ці " "скруткаў. Пачніце адкрыўшы \"кнігу прыткасці\", як апісана вышэй. Націснеце " "F5, F6, F7 ці F8 пасля вылучэння чараў, якія хочаце прызначыць." #: Source/help.cpp:93 msgid "$Spell Books:" msgstr "$Кнігі чараў:" #: Source/help.cpp:94 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Чытанне больш за адну кнігу павышае вашыя веды аб гэтых чарах, дазваляючы " "карыстацца імі эфектыўней." #: Source/help.cpp:177 msgid "Shareware Hellfire Help" msgstr "Дамапога з дэма Hellfire" #: Source/help.cpp:177 msgid "Hellfire Help" msgstr "Дапамога з Hellfire" #: Source/help.cpp:179 msgid "Shareware Diablo Help" msgstr "Дапамога з дэма Diablo" #: Source/help.cpp:179 msgid "Diablo Help" msgstr "Дапамога з Diablo" #: Source/help.cpp:209 Source/qol/chatlog.cpp:180 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Націснеце ESC каб скончыць ці пракруціце стрэлачкамі." #: Source/init.cpp:194 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq ці spawn.mpq" #: Source/init.cpp:215 msgid "Some Hellfire MPQs are missing" msgstr "Нейкіх MPQ файлаў Hellfire бракуе" #: Source/init.cpp:215 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Не ўсе MPQ файлы Hellfire знойдзеныя.\n" "Скапіюйце ўсе hf*.mpq файлы, калі ласка." #: Source/init.cpp:223 msgid "Unable to create main window" msgstr "Немагчыма стварыць галоўнае акно" #: Source/itemdat.cpp:53 Source/itemdat.cpp:235 Source/panels/charpanel.cpp:164 msgid "Gold" msgstr "Золата" #: Source/itemdat.cpp:54 Source/itemdat.cpp:172 msgid "Short Sword" msgstr "Корд" #: Source/itemdat.cpp:55 Source/itemdat.cpp:124 msgid "Buckler" msgstr "Пуклер" #: Source/itemdat.cpp:56 Source/itemdat.cpp:192 Source/itemdat.cpp:193 msgid "Club" msgstr "Дубіна" #: Source/itemdat.cpp:57 Source/itemdat.cpp:196 msgid "Short Bow" msgstr "Кароткі лук" #: Source/itemdat.cpp:58 msgid "Short Staff of Mana" msgstr "Кароткі посах маны" #: Source/itemdat.cpp:59 msgid "Cleaver" msgstr "Сякач" #: Source/itemdat.cpp:60 Source/itemdat.cpp:434 msgid "The Undead Crown" msgstr "Нябожчыкава карона" #: Source/itemdat.cpp:61 Source/itemdat.cpp:435 msgid "Empyrean Band" msgstr "Незямная стужка" #: Source/itemdat.cpp:62 msgid "Magic Rock" msgstr "Чарадзейны камень" #: Source/itemdat.cpp:63 Source/itemdat.cpp:436 msgid "Optic Amulet" msgstr "Аптычны амулет" #: Source/itemdat.cpp:64 Source/itemdat.cpp:437 msgid "Ring of Truth" msgstr "Пярцсцёнак праўды" #: Source/itemdat.cpp:65 msgid "Tavern Sign" msgstr "Знак карчмы" #: Source/itemdat.cpp:66 Source/itemdat.cpp:438 msgid "Harlequin Crest" msgstr "Арлекінаў грабянец" #: Source/itemdat.cpp:67 Source/itemdat.cpp:439 msgid "Veil of Steel" msgstr "Стальны вэлюм" #: Source/itemdat.cpp:68 msgid "Golden Elixir" msgstr "Залаты эліксір" #: Source/itemdat.cpp:69 Source/quests.cpp:54 msgid "Anvil of Fury" msgstr "Кавадла ярасці" #: Source/itemdat.cpp:70 Source/quests.cpp:45 msgid "Black Mushroom" msgstr "Чорны грыб" #: Source/itemdat.cpp:71 msgid "Brain" msgstr "Мозг" #: Source/itemdat.cpp:72 msgid "Fungal Tome" msgstr "Грыбны фаліянт" #: Source/itemdat.cpp:73 msgid "Spectral Elixir" msgstr "Прывідны эліксір" #: Source/itemdat.cpp:74 msgid "Blood Stone" msgstr "Камень крыві" #: Source/itemdat.cpp:75 msgid "Cathedral Map" msgstr "Мапа сабору" #: Source/itemdat.cpp:76 msgid "Heart" msgstr "Сэрца" #: Source/itemdat.cpp:77 Source/itemdat.cpp:130 msgid "Potion of Healing" msgstr "Зелле лячэння" #: Source/itemdat.cpp:78 Source/itemdat.cpp:132 msgid "Potion of Mana" msgstr "Зелле маны" #: Source/itemdat.cpp:79 Source/itemdat.cpp:147 msgid "Scroll of Identify" msgstr "Скрутак выяўлення" #: Source/itemdat.cpp:80 Source/itemdat.cpp:151 msgid "Scroll of Town Portal" msgstr "Скрутак партала ў Горад" #: Source/itemdat.cpp:81 Source/itemdat.cpp:440 msgid "Arkaine's Valor" msgstr "Адвага Аркейна" #: Source/itemdat.cpp:82 Source/itemdat.cpp:131 msgid "Potion of Full Healing" msgstr "Зелле поўнага лячэння" #: Source/itemdat.cpp:83 Source/itemdat.cpp:133 msgid "Potion of Full Mana" msgstr "Зелле поўнай маны" #: Source/itemdat.cpp:84 Source/itemdat.cpp:441 msgid "Griswold's Edge" msgstr "Вастрыё Грызвальда" #: Source/itemdat.cpp:85 Source/itemdat.cpp:442 msgid "Bovine Plate" msgstr "Бычыная пласціна" #: Source/itemdat.cpp:86 msgid "Staff of Lazarus" msgstr "Посах Лазара" #: Source/itemdat.cpp:87 Source/itemdat.cpp:148 msgid "Scroll of Resurrect" msgstr "Скрутак Уваскрэсення" #: Source/itemdat.cpp:88 Source/itemdat.cpp:136 Source/items.cpp:172 msgid "Blacksmith Oil" msgstr "Кавалёва масла" #: Source/itemdat.cpp:89 Source/itemdat.cpp:204 msgid "Short Staff" msgstr "Кароткі посах" #: Source/itemdat.cpp:90 Source/itemdat.cpp:172 Source/itemdat.cpp:173 #: Source/itemdat.cpp:174 Source/itemdat.cpp:175 Source/itemdat.cpp:178 #: Source/itemdat.cpp:179 Source/itemdat.cpp:180 Source/itemdat.cpp:181 #: Source/itemdat.cpp:182 msgid "Sword" msgstr "Меч" #: Source/itemdat.cpp:91 Source/itemdat.cpp:171 msgid "Dagger" msgstr "Кінжал" #: Source/itemdat.cpp:92 msgid "Rune Bomb" msgstr "Рунічная бомба" #: Source/itemdat.cpp:93 msgid "Theodore" msgstr "Тэадор" #: Source/itemdat.cpp:94 msgid "Auric Amulet" msgstr "Амулет аўры" #: Source/itemdat.cpp:95 msgid "Torn Note 1" msgstr "Абрывак 1" #: Source/itemdat.cpp:96 msgid "Torn Note 2" msgstr "Абрывак 2" #: Source/itemdat.cpp:97 msgid "Torn Note 3" msgstr "Абрывак 3" #: Source/itemdat.cpp:98 msgid "Reconstructed Note" msgstr "Узноўлены запіс" #: Source/itemdat.cpp:99 msgid "Brown Suit" msgstr "Руды ўбор" #: Source/itemdat.cpp:100 msgid "Grey Suit" msgstr "Шэры ўбор" #: Source/itemdat.cpp:101 Source/itemdat.cpp:102 msgid "Cap" msgstr "Шапка" #: Source/itemdat.cpp:102 msgid "Skull Cap" msgstr "Шышак" #: Source/itemdat.cpp:103 Source/itemdat.cpp:104 Source/itemdat.cpp:106 msgid "Helm" msgstr "Шалом" #: Source/itemdat.cpp:104 msgid "Full Helm" msgstr "Закрыты шалом" #: Source/itemdat.cpp:105 msgid "Crown" msgstr "Карона" #: Source/itemdat.cpp:106 msgid "Great Helm" msgstr "Вялікі шалом" #: Source/itemdat.cpp:107 msgid "Cape" msgstr "Накідка" #: Source/itemdat.cpp:108 msgid "Rags" msgstr "Рыззё" #: Source/itemdat.cpp:109 msgid "Cloak" msgstr "Плашч" #: Source/itemdat.cpp:110 msgid "Robe" msgstr "Мантыя" #: Source/itemdat.cpp:111 msgid "Quilted Armor" msgstr "Падшыванка" #: Source/itemdat.cpp:111 Source/itemdat.cpp:112 Source/itemdat.cpp:113 #: Source/itemdat.cpp:114 Source/objects.cpp:5476 msgid "Armor" msgstr "Браня" #: Source/itemdat.cpp:112 msgid "Leather Armor" msgstr "Кожаны панцыр" #: Source/itemdat.cpp:113 msgid "Hard Leather Armor" msgstr "Жорсткі скураны панцыр" #: Source/itemdat.cpp:114 msgid "Studded Leather Armor" msgstr "Нітаваны скураны панцыр" #: Source/itemdat.cpp:115 msgid "Ring Mail" msgstr "Кольчаты панцыр" #: Source/itemdat.cpp:115 Source/itemdat.cpp:116 Source/itemdat.cpp:117 #: Source/itemdat.cpp:119 msgid "Mail" msgstr "Кальчуга" #: Source/itemdat.cpp:116 msgid "Chain Mail" msgstr "Кальчуга" #: Source/itemdat.cpp:117 msgid "Scale Mail" msgstr "Лускаваты панцыр" #: Source/itemdat.cpp:118 msgid "Breast Plate" msgstr "Нагруднік" #: Source/itemdat.cpp:118 Source/itemdat.cpp:120 Source/itemdat.cpp:121 #: Source/itemdat.cpp:122 Source/itemdat.cpp:123 msgid "Plate" msgstr "Латы" #: Source/itemdat.cpp:119 msgid "Splint Mail" msgstr "Бехцер" #: Source/itemdat.cpp:120 msgid "Plate Mail" msgstr "Латы" #: Source/itemdat.cpp:121 msgid "Field Plate" msgstr "Баявы даспех" #: Source/itemdat.cpp:122 msgid "Gothic Plate" msgstr "Гатычны даспех" #: Source/itemdat.cpp:123 msgid "Full Plate Mail" msgstr "Поўны латны даспех" #: Source/itemdat.cpp:124 Source/itemdat.cpp:125 Source/itemdat.cpp:126 #: Source/itemdat.cpp:127 Source/itemdat.cpp:128 Source/itemdat.cpp:129 msgid "Shield" msgstr "Шчыт" #: Source/itemdat.cpp:125 msgid "Small Shield" msgstr "Малы шчыт" #: Source/itemdat.cpp:126 msgid "Large Shield" msgstr "Вялізны шчыт" #: Source/itemdat.cpp:127 msgid "Kite Shield" msgstr "Міндалепадобны шчыт" #: Source/itemdat.cpp:128 msgid "Tower Shield" msgstr "Павеза" #: Source/itemdat.cpp:129 msgid "Gothic Shield" msgstr "Гатычны шчыт" #: Source/itemdat.cpp:134 msgid "Potion of Rejuvenation" msgstr "Зелле аднаўлення" #: Source/itemdat.cpp:135 msgid "Potion of Full Rejuvenation" msgstr "Зелле поўнага аднаўлення" #: Source/itemdat.cpp:137 Source/items.cpp:167 msgid "Oil of Accuracy" msgstr "Алей дакладнасці" #: Source/itemdat.cpp:138 Source/items.cpp:169 msgid "Oil of Sharpness" msgstr "Алей вастрыні" #: Source/itemdat.cpp:139 msgid "Oil" msgstr "Алей" #: Source/itemdat.cpp:140 msgid "Elixir of Strength" msgstr "Эліксір моцы" #: Source/itemdat.cpp:141 msgid "Elixir of Magic" msgstr "Эліксір магіі" #: Source/itemdat.cpp:142 msgid "Elixir of Dexterity" msgstr "Эліксір спрыту" #: Source/itemdat.cpp:143 msgid "Elixir of Vitality" msgstr "Эліксір жывучасці" #: Source/itemdat.cpp:144 msgid "Scroll of Healing" msgstr "Скрутак лячэння" #: Source/itemdat.cpp:145 msgid "Scroll of Search" msgstr "Скрутак пошуку" #: Source/itemdat.cpp:146 msgid "Scroll of Lightning" msgstr "Скрутак маланкі" #: Source/itemdat.cpp:149 msgid "Scroll of Fire Wall" msgstr "Скрутак вогеннай сцяны" #: Source/itemdat.cpp:150 msgid "Scroll of Inferno" msgstr "Скрутак апраметнай" #: Source/itemdat.cpp:152 msgid "Scroll of Flash" msgstr "Скрутак бліску" #: Source/itemdat.cpp:153 msgid "Scroll of Infravision" msgstr "Скрутак скрозьбачання" #: Source/itemdat.cpp:154 msgid "Scroll of Phasing" msgstr "Скрутак перамяшчэння" #: Source/itemdat.cpp:155 msgid "Scroll of Mana Shield" msgstr "Скрутак шчыта маны" #: Source/itemdat.cpp:156 msgid "Scroll of Flame Wave" msgstr "Скрутак хвалі полымя" #: Source/itemdat.cpp:157 msgid "Scroll of Fireball" msgstr "Скрутак вогненнага шара" #: Source/itemdat.cpp:158 msgid "Scroll of Stone Curse" msgstr "Скрутак праклёну каменя" #: Source/itemdat.cpp:159 msgid "Scroll of Chain Lightning" msgstr "Скрутак ланцуговай маланкі" #: Source/itemdat.cpp:160 msgid "Scroll of Guardian" msgstr "Скрутак вартавога" #: Source/itemdat.cpp:162 msgid "Scroll of Nova" msgstr "Скрутак новай зоркі" #: Source/itemdat.cpp:163 msgid "Scroll of Golem" msgstr "Скрутак голема" #: Source/itemdat.cpp:165 msgid "Scroll of Teleport" msgstr "Скрутак тэлепартацыі" #: Source/itemdat.cpp:166 msgid "Scroll of Apocalypse" msgstr "Скрутак апакаліпсіса" #: Source/itemdat.cpp:167 Source/itemdat.cpp:168 Source/itemdat.cpp:169 #: Source/itemdat.cpp:170 msgid "Book of " msgstr "Кніга " #: Source/itemdat.cpp:173 msgid "Falchion" msgstr "Фальшыён" #: Source/itemdat.cpp:174 msgid "Scimitar" msgstr "Крывая шабля" #: Source/itemdat.cpp:175 msgid "Claymore" msgstr "Клеймор" #: Source/itemdat.cpp:176 msgid "Blade" msgstr "Клінок" #: Source/itemdat.cpp:177 msgid "Sabre" msgstr "Шабля" #: Source/itemdat.cpp:178 msgid "Long Sword" msgstr "Доўгі меч" #: Source/itemdat.cpp:179 msgid "Broad Sword" msgstr "Палаш" #: Source/itemdat.cpp:180 msgid "Bastard Sword" msgstr "Паўтараручны меч" #: Source/itemdat.cpp:181 msgid "Two-Handed Sword" msgstr "Двухручны меч" #: Source/itemdat.cpp:182 msgid "Great Sword" msgstr "Аберучны меч" #: Source/itemdat.cpp:183 msgid "Small Axe" msgstr "Сякерка" #: Source/itemdat.cpp:183 Source/itemdat.cpp:184 Source/itemdat.cpp:185 #: Source/itemdat.cpp:186 Source/itemdat.cpp:187 Source/itemdat.cpp:188 msgid "Axe" msgstr "Сякера" #: Source/itemdat.cpp:185 msgid "Large Axe" msgstr "Бярдыш" #: Source/itemdat.cpp:186 msgid "Broad Axe" msgstr "Склюд" #: Source/itemdat.cpp:187 msgid "Battle Axe" msgstr "Лабрыс" #: Source/itemdat.cpp:188 msgid "Great Axe" msgstr "Вялікая сякера" #: Source/itemdat.cpp:189 Source/itemdat.cpp:190 msgid "Mace" msgstr "Булава" #: Source/itemdat.cpp:190 msgid "Morning Star" msgstr "Маргенштэрн" #: Source/itemdat.cpp:191 msgid "War Hammer" msgstr "Баявы молат" #: Source/itemdat.cpp:191 msgid "Hammer" msgstr "Молат" #: Source/itemdat.cpp:192 msgid "Spiked Club" msgstr "Шыпастая дубіна" #: Source/itemdat.cpp:194 msgid "Flail" msgstr "Кісцень" #: Source/itemdat.cpp:195 msgid "Maul" msgstr "Кувалда" #: Source/itemdat.cpp:196 Source/itemdat.cpp:197 Source/itemdat.cpp:198 #: Source/itemdat.cpp:199 Source/itemdat.cpp:200 Source/itemdat.cpp:201 #: Source/itemdat.cpp:202 Source/itemdat.cpp:203 msgid "Bow" msgstr "Лук" #: Source/itemdat.cpp:197 msgid "Hunter's Bow" msgstr "Паляўнічы лук" #: Source/itemdat.cpp:198 msgid "Long Bow" msgstr "Доўгі лук" #: Source/itemdat.cpp:199 msgid "Composite Bow" msgstr "Складаны лук" #: Source/itemdat.cpp:200 msgid "Short Battle Bow" msgstr "Кароткі баявы лук" #: Source/itemdat.cpp:201 msgid "Long Battle Bow" msgstr "Доўгі баявы лук" #: Source/itemdat.cpp:202 msgid "Short War Bow" msgstr "Кароткі ваенны лук" #: Source/itemdat.cpp:203 msgid "Long War Bow" msgstr "Доўгі ваенны лук" #: Source/itemdat.cpp:204 Source/itemdat.cpp:205 Source/itemdat.cpp:206 #: Source/itemdat.cpp:207 Source/itemdat.cpp:208 #: Source/panels/spell_list.cpp:195 msgid "Staff" msgstr "Посах" #: Source/itemdat.cpp:205 msgid "Long Staff" msgstr "Доўгі посах" #: Source/itemdat.cpp:206 msgid "Composite Staff" msgstr "Складаны посах" #: Source/itemdat.cpp:207 msgid "Quarter Staff" msgstr "Посах міласэрнасці" #: Source/itemdat.cpp:208 msgid "War Staff" msgstr "Посах вайны" #: Source/itemdat.cpp:209 Source/itemdat.cpp:210 Source/itemdat.cpp:211 msgid "Ring" msgstr "Пярсцёнак" #: Source/itemdat.cpp:212 Source/itemdat.cpp:213 msgid "Amulet" msgstr "Амулет" #: Source/itemdat.cpp:214 msgid "Rune of Fire" msgstr "Руна агню" #: Source/itemdat.cpp:214 Source/itemdat.cpp:215 Source/itemdat.cpp:216 #: Source/itemdat.cpp:217 Source/itemdat.cpp:218 msgid "Rune" msgstr "Руна" #: Source/itemdat.cpp:215 msgid "Rune of Lightning" msgstr "Руна маланкі" #: Source/itemdat.cpp:216 msgid "Greater Rune of Fire" msgstr "Большая руна агню" #: Source/itemdat.cpp:217 msgid "Greater Rune of Lightning" msgstr "Большая руна маланкі" #: Source/itemdat.cpp:218 msgid "Rune of Stone" msgstr "Руна каменя" #: Source/itemdat.cpp:219 msgid "Short Staff of Charged Bolt" msgstr "Кароткі посах заражанай маланкі" #. TRANSLATORS: Item prefix section. #: Source/itemdat.cpp:229 msgid "Tin" msgstr "Алавянны" #: Source/itemdat.cpp:230 msgid "Brass" msgstr "Латуневы" #: Source/itemdat.cpp:231 msgid "Bronze" msgstr "Бронзавы" #: Source/itemdat.cpp:232 msgid "Iron" msgstr "Жалезны" #: Source/itemdat.cpp:233 msgid "Steel" msgstr "Стальны" #: Source/itemdat.cpp:234 msgid "Silver" msgstr "Срэбраны" #: Source/itemdat.cpp:236 msgid "Platinum" msgstr "Плацінавы" #: Source/itemdat.cpp:237 msgid "Mithril" msgstr "Мітрылявы" #: Source/itemdat.cpp:238 msgid "Meteoric" msgstr "Метэорны" #: Source/itemdat.cpp:239 Source/objects.cpp:109 msgid "Weird" msgstr "Дзіўны" #: Source/itemdat.cpp:240 msgid "Strange" msgstr "Невядомы" #: Source/itemdat.cpp:241 msgid "Useless" msgstr "Дарэмны" #: Source/itemdat.cpp:242 msgid "Bent" msgstr "Крывы" #: Source/itemdat.cpp:243 msgid "Weak" msgstr "Слабы" #: Source/itemdat.cpp:244 msgid "Jagged" msgstr "Вышчарблены" #: Source/itemdat.cpp:245 msgid "Deadly" msgstr "Смяротны" #: Source/itemdat.cpp:246 msgid "Heavy" msgstr "Важкі" #: Source/itemdat.cpp:247 msgid "Vicious" msgstr "Ліхі" #: Source/itemdat.cpp:248 msgid "Brutal" msgstr "Люты" #: Source/itemdat.cpp:249 msgid "Massive" msgstr "Велізарны" #: Source/itemdat.cpp:250 msgid "Savage" msgstr "Зверскі" #: Source/itemdat.cpp:251 msgid "Ruthless" msgstr "Жорсткі" #: Source/itemdat.cpp:252 msgid "Merciless" msgstr "Бязлітасны" #: Source/itemdat.cpp:253 msgid "Clumsy" msgstr "Няспраўны" #: Source/itemdat.cpp:254 msgid "Dull" msgstr "Тупы" #: Source/itemdat.cpp:255 msgid "Sharp" msgstr "Востры" #: Source/itemdat.cpp:256 Source/itemdat.cpp:266 msgid "Fine" msgstr "Файны" #: Source/itemdat.cpp:257 msgid "Warrior's" msgstr "Ваярскі" #: Source/itemdat.cpp:258 msgid "Soldier's" msgstr "Жаўнерскі" #: Source/itemdat.cpp:259 msgid "Lord's" msgstr "Валадарскі" #: Source/itemdat.cpp:260 msgid "Knight's" msgstr "Рыцарскі" #: Source/itemdat.cpp:261 msgid "Master's" msgstr "Майстроўскі" #: Source/itemdat.cpp:262 msgid "Champion's" msgstr "Чэмпіёнскі" #: Source/itemdat.cpp:263 msgid "King's" msgstr "Каралеўскі" #: Source/itemdat.cpp:264 msgid "Vulnerable" msgstr "Прыступны" #: Source/itemdat.cpp:265 msgid "Rusted" msgstr "Заіржавелы" #: Source/itemdat.cpp:267 msgid "Strong" msgstr "Моцны" #: Source/itemdat.cpp:268 msgid "Grand" msgstr "Велічэзны" #: Source/itemdat.cpp:269 msgid "Valiant" msgstr "Адважны" #: Source/itemdat.cpp:270 msgid "Glorious" msgstr "Славуты" #: Source/itemdat.cpp:271 msgid "Blessed" msgstr "Дабраславёны" #: Source/itemdat.cpp:272 msgid "Saintly" msgstr "Праведны" #: Source/itemdat.cpp:273 msgid "Awesome" msgstr "Жахлівы" #: Source/itemdat.cpp:274 Source/objects.cpp:121 msgid "Holy" msgstr "Святы" #: Source/itemdat.cpp:275 msgid "Godly" msgstr "Боскі" #: Source/itemdat.cpp:276 msgid "Red" msgstr "Чырвоны" #: Source/itemdat.cpp:277 Source/itemdat.cpp:278 msgid "Crimson" msgstr "Барвовы" #: Source/itemdat.cpp:279 msgid "Garnet" msgstr "Гранатавы" #: Source/itemdat.cpp:280 msgid "Ruby" msgstr "Рубінавы" #: Source/itemdat.cpp:281 msgid "Blue" msgstr "Сіні" #: Source/itemdat.cpp:282 msgid "Azure" msgstr "Блакітны" #: Source/itemdat.cpp:283 msgid "Lapis" msgstr "Ляпісны" #: Source/itemdat.cpp:284 msgid "Cobalt" msgstr "Кобальтавы" #: Source/itemdat.cpp:285 msgid "Sapphire" msgstr "Сапфіравы" #: Source/itemdat.cpp:286 msgid "White" msgstr "Белы" #: Source/itemdat.cpp:287 msgid "Pearl" msgstr "Жамчужны" #: Source/itemdat.cpp:288 msgid "Ivory" msgstr "Слановай косці" #: Source/itemdat.cpp:289 msgid "Crystal" msgstr "Крыштальны" #: Source/itemdat.cpp:290 msgid "Diamond" msgstr "Дыямантавы" #: Source/itemdat.cpp:291 msgid "Topaz" msgstr "Тапазавы" #: Source/itemdat.cpp:292 msgid "Amber" msgstr "Бурштынавы" #: Source/itemdat.cpp:293 msgid "Jade" msgstr "Нефрытавы" #: Source/itemdat.cpp:294 msgid "Obsidian" msgstr "Абсідыянавы" #: Source/itemdat.cpp:295 msgid "Emerald" msgstr "Смарагдавы" #: Source/itemdat.cpp:296 msgid "Hyena's" msgstr "Гіенін" #: Source/itemdat.cpp:297 msgid "Frog's" msgstr "Жабін" #: Source/itemdat.cpp:298 msgid "Spider's" msgstr "Павукоў" #: Source/itemdat.cpp:299 msgid "Raven's" msgstr "Крумкачоў" #: Source/itemdat.cpp:300 msgid "Snake's" msgstr "Змеяў" #: Source/itemdat.cpp:301 msgid "Serpent's" msgstr "Вужаў" #: Source/itemdat.cpp:302 msgid "Drake's" msgstr "Смокаў" #: Source/itemdat.cpp:303 msgid "Dragon's" msgstr "Цмокаў" #: Source/itemdat.cpp:304 msgid "Wyrm's" msgstr "Гадскі" #: Source/itemdat.cpp:305 msgid "Hydra's" msgstr "Гідрын" #: Source/itemdat.cpp:306 msgid "Angel's" msgstr "Анёльскі" #: Source/itemdat.cpp:307 msgid "Arch-Angel's" msgstr "Арханёльскі" #: Source/itemdat.cpp:308 msgid "Plentiful" msgstr "Шчодры" #: Source/itemdat.cpp:309 msgid "Bountiful" msgstr "Дастатны" #: Source/itemdat.cpp:310 msgid "Flaming" msgstr "Агністы" #: Source/itemdat.cpp:311 msgid "Lightning" msgstr "Бліскавічны" #: Source/itemdat.cpp:312 msgid "Jester's" msgstr "Блазанскі" #: Source/itemdat.cpp:313 msgid "Crystalline" msgstr "Празрасты" #. TRANSLATORS: Item prefix section end. #: Source/itemdat.cpp:315 msgid "Doppelganger's" msgstr "Двайніковы" #. TRANSLATORS: Item suffix section. All items will have a word binding word. (Format: {:s} of {:s} - e.g. Rags of Valor) #: Source/itemdat.cpp:325 msgid "quality" msgstr "якасці" #: Source/itemdat.cpp:326 msgid "maiming" msgstr "нявечання" #: Source/itemdat.cpp:327 msgid "slaying" msgstr "забойства" #: Source/itemdat.cpp:328 msgid "gore" msgstr "запечанай крыві" #: Source/itemdat.cpp:329 msgid "carnage" msgstr "бойні" #: Source/itemdat.cpp:330 msgid "slaughter" msgstr "разні" #: Source/itemdat.cpp:331 msgid "pain" msgstr "болю" #: Source/itemdat.cpp:332 msgid "tears" msgstr "слёзаў" #: Source/itemdat.cpp:333 msgid "health" msgstr "здароўя" #: Source/itemdat.cpp:334 msgid "protection" msgstr "аховы" #: Source/itemdat.cpp:335 msgid "absorption" msgstr "паглынання" #: Source/itemdat.cpp:336 msgid "deflection" msgstr "адхілення" #: Source/itemdat.cpp:337 msgid "osmosis" msgstr "осмасу" #: Source/itemdat.cpp:338 msgid "frailty" msgstr "кволасці" #: Source/itemdat.cpp:339 msgid "weakness" msgstr "слабасці" #: Source/itemdat.cpp:340 msgid "strength" msgstr "моцы" #: Source/itemdat.cpp:341 msgid "might" msgstr "магутнасці" #: Source/itemdat.cpp:342 msgid "power" msgstr "сілы" #: Source/itemdat.cpp:343 msgid "giants" msgstr "волатаў" #: Source/itemdat.cpp:344 msgid "titans" msgstr "тытанаў" #: Source/itemdat.cpp:345 msgid "paralysis" msgstr "паралюшу" #: Source/itemdat.cpp:346 msgid "atrophy" msgstr "адмірання" #: Source/itemdat.cpp:347 msgid "dexterity" msgstr "спрыту" #: Source/itemdat.cpp:348 msgid "skill" msgstr "спраўнасці" #: Source/itemdat.cpp:349 msgid "accuracy" msgstr "дакладнасці" #: Source/itemdat.cpp:350 msgid "precision" msgstr "зладжанасці" #: Source/itemdat.cpp:351 msgid "perfection" msgstr "беззаганнасці" #: Source/itemdat.cpp:352 msgid "the fool" msgstr "дурня" #: Source/itemdat.cpp:353 msgid "dyslexia" msgstr "дыслексіі" #: Source/itemdat.cpp:354 msgid "magic" msgstr "магіі" #: Source/itemdat.cpp:355 msgid "the mind" msgstr "розуму" #: Source/itemdat.cpp:356 msgid "brilliance" msgstr "выдатнасці" #: Source/itemdat.cpp:357 msgid "sorcery" msgstr "вядзьмарства" #: Source/itemdat.cpp:358 msgid "wizardry" msgstr "чарадзейства" #: Source/itemdat.cpp:359 msgid "illness" msgstr "немачы" #: Source/itemdat.cpp:360 msgid "disease" msgstr "хваробы" #: Source/itemdat.cpp:361 msgid "vitality" msgstr "жывучасці" #: Source/itemdat.cpp:362 msgid "zest" msgstr "запалу" #: Source/itemdat.cpp:363 msgid "vim" msgstr "энергіі" #: Source/itemdat.cpp:364 msgid "vigor" msgstr "дужасці" #: Source/itemdat.cpp:365 msgid "life" msgstr "жыцця" #: Source/itemdat.cpp:366 msgid "trouble" msgstr "бяды" #: Source/itemdat.cpp:367 msgid "the pit" msgstr "ямы" #: Source/itemdat.cpp:368 msgid "the sky" msgstr "неба" #: Source/itemdat.cpp:369 msgid "the moon" msgstr "месяца" #: Source/itemdat.cpp:370 msgid "the stars" msgstr "зорак" #: Source/itemdat.cpp:371 msgid "the heavens" msgstr "нябёс" #: Source/itemdat.cpp:372 msgid "the zodiac" msgstr "задыяку" #: Source/itemdat.cpp:373 msgid "the vulture" msgstr "драпежніка" #: Source/itemdat.cpp:374 msgid "the jackal" msgstr "шакала" #: Source/itemdat.cpp:375 msgid "the fox" msgstr "ліса" #: Source/itemdat.cpp:376 msgid "the jaguar" msgstr "ягуара" #: Source/itemdat.cpp:377 msgid "the eagle" msgstr "арла" #: Source/itemdat.cpp:378 msgid "the wolf" msgstr "ваўка" #: Source/itemdat.cpp:379 msgid "the tiger" msgstr "тыгра" #: Source/itemdat.cpp:380 msgid "the lion" msgstr "льва" #: Source/itemdat.cpp:381 msgid "the mammoth" msgstr "маманта" #: Source/itemdat.cpp:382 msgid "the whale" msgstr "кіта" #: Source/itemdat.cpp:383 msgid "fragility" msgstr "кволасці" #: Source/itemdat.cpp:384 msgid "brittleness" msgstr "ломкасці" #: Source/itemdat.cpp:385 msgid "sturdiness" msgstr "непахіснасці" #: Source/itemdat.cpp:386 msgid "craftsmanship" msgstr "майстэрства" #: Source/itemdat.cpp:387 msgid "structure" msgstr "будовы" #: Source/itemdat.cpp:388 msgid "the ages" msgstr "вякоў" #: Source/itemdat.cpp:389 msgid "the dark" msgstr "цемры" #: Source/itemdat.cpp:390 msgid "the night" msgstr "ночы" #: Source/itemdat.cpp:391 msgid "light" msgstr "святла" #: Source/itemdat.cpp:392 msgid "radiance" msgstr "ззяння" #: Source/itemdat.cpp:393 msgid "flame" msgstr "полымя" #: Source/itemdat.cpp:394 msgid "fire" msgstr "агню" #: Source/itemdat.cpp:395 msgid "burning" msgstr "спякоты" #: Source/itemdat.cpp:396 msgid "shock" msgstr "шоку" #: Source/itemdat.cpp:397 msgid "lightning" msgstr "маланкі" #: Source/itemdat.cpp:398 msgid "thunder" msgstr "грымот" #: Source/itemdat.cpp:399 msgid "many" msgstr "шматлікіх" #: Source/itemdat.cpp:400 msgid "plenty" msgstr "мноства" #: Source/itemdat.cpp:401 msgid "thorns" msgstr "церняў" #: Source/itemdat.cpp:402 msgid "corruption" msgstr "скажэння" #: Source/itemdat.cpp:403 msgid "thieves" msgstr "зладзеяў" #: Source/itemdat.cpp:404 msgid "the bear" msgstr "мядзведзя" #: Source/itemdat.cpp:405 msgid "the bat" msgstr "кажана" #: Source/itemdat.cpp:406 msgid "vampires" msgstr "вупараў" #: Source/itemdat.cpp:407 msgid "the leech" msgstr "п'яўкі" #: Source/itemdat.cpp:408 msgid "blood" msgstr "крыві" #: Source/itemdat.cpp:409 msgid "piercing" msgstr "працінання" #: Source/itemdat.cpp:410 msgid "puncturing" msgstr "праколу" #: Source/itemdat.cpp:411 msgid "bashing" msgstr "збівання" #: Source/itemdat.cpp:412 msgid "readiness" msgstr "падрыхтаванасці" #: Source/itemdat.cpp:413 msgid "swiftness" msgstr "імклівасці" #: Source/itemdat.cpp:414 msgid "speed" msgstr "хуткасці" #: Source/itemdat.cpp:415 msgid "haste" msgstr "паспеху" #: Source/itemdat.cpp:416 msgid "balance" msgstr "раўнавагі" #: Source/itemdat.cpp:417 msgid "stability" msgstr "стойкасці" #: Source/itemdat.cpp:418 msgid "harmony" msgstr "суладнасці" #: Source/itemdat.cpp:419 msgid "blocking" msgstr "блакавання" #: Source/itemdat.cpp:420 msgid "devastation" msgstr "спустошанасці" #: Source/itemdat.cpp:421 msgid "decay" msgstr "гніцця" #. TRANSLATORS: Item suffix section end. #: Source/itemdat.cpp:423 msgid "peril" msgstr "небяспекі" #. TRANSLATORS: Unique Item section #: Source/itemdat.cpp:433 msgid "The Butcher's Cleaver" msgstr "Сякач Мясніка" #: Source/itemdat.cpp:443 msgid "The Rift Bow" msgstr "Лук разлому" #: Source/itemdat.cpp:444 msgid "The Needler" msgstr "Ігольшчык" #: Source/itemdat.cpp:445 msgid "The Celestial Bow" msgstr "Нябескі лук" #: Source/itemdat.cpp:446 msgid "Deadly Hunter" msgstr "Смяротны паляўнічы" #: Source/itemdat.cpp:447 msgid "Bow of the Dead" msgstr "Лук памерлых" #: Source/itemdat.cpp:448 msgid "The Blackoak Bow" msgstr "Лук з чорнага дубу" #: Source/itemdat.cpp:449 msgid "Flamedart" msgstr "Палымяністы дроцік" #: Source/itemdat.cpp:450 msgid "Fleshstinger" msgstr "Джганіплоць" #: Source/itemdat.cpp:451 msgid "Windforce" msgstr "Сіла ветру" #: Source/itemdat.cpp:452 msgid "Eaglehorn" msgstr "Рог арла" #: Source/itemdat.cpp:453 msgid "Gonnagal's Dirk" msgstr "Кордзік Гонагала" #: Source/itemdat.cpp:454 msgid "The Defender" msgstr "Абаронца" #: Source/itemdat.cpp:455 msgid "Gryphon's Claw" msgstr "Кіпцюр грыфона" #: Source/itemdat.cpp:456 msgid "Black Razor" msgstr "Чорная брытва" #: Source/itemdat.cpp:457 msgid "Gibbous Moon" msgstr "Позні маладзік" #: Source/itemdat.cpp:458 msgid "Ice Shank" msgstr "Лядзяны прэнт" #: Source/itemdat.cpp:459 msgid "The Executioner's Blade" msgstr "Катаў меч" #: Source/itemdat.cpp:460 msgid "The Bonesaw" msgstr "Касцярэз" #: Source/itemdat.cpp:461 msgid "Shadowhawk" msgstr "Ястраб ценю" #: Source/itemdat.cpp:462 msgid "Wizardspike" msgstr "Вастрыё чараўніка" #: Source/itemdat.cpp:463 msgid "Lightsabre" msgstr "Шабля святла" #: Source/itemdat.cpp:464 msgid "The Falcon's Talon" msgstr "Кіпцюр сокала" #: Source/itemdat.cpp:465 msgid "Inferno" msgstr "Апраметны" #: Source/itemdat.cpp:466 msgid "Doombringer" msgstr "Веснік пагібелі" #: Source/itemdat.cpp:467 msgid "The Grizzly" msgstr "Грызлі" #: Source/itemdat.cpp:468 msgid "The Grandfather" msgstr "Дзед" #: Source/itemdat.cpp:469 msgid "The Mangler" msgstr "Рубайла" #: Source/itemdat.cpp:470 msgid "Sharp Beak" msgstr "Вострая дзюба" #: Source/itemdat.cpp:471 msgid "BloodSlayer" msgstr "Крывалівень" #: Source/itemdat.cpp:472 msgid "The Celestial Axe" msgstr "Нябеская сякера" #: Source/itemdat.cpp:473 msgid "Wicked Axe" msgstr "Ліхая сякера" #: Source/itemdat.cpp:474 msgid "Stonecleaver" msgstr "Каменясек" #: Source/itemdat.cpp:475 msgid "Aguinara's Hatchet" msgstr "Сякерка Агінары" #: Source/itemdat.cpp:476 msgid "Hellslayer" msgstr "Пагібель аекла" #: Source/itemdat.cpp:477 msgid "Messerschmidt's Reaver" msgstr "Разаральнік Мэссершміта" #: Source/itemdat.cpp:478 msgid "Crackrust" msgstr "Іржатрэск" #: Source/itemdat.cpp:479 msgid "Hammer of Jholm" msgstr "Молат Джольма" #: Source/itemdat.cpp:480 msgid "Civerb's Cudgel" msgstr "Кій Сіверба" #: Source/itemdat.cpp:481 msgid "The Celestial Star" msgstr "Нябеская зорка" #: Source/itemdat.cpp:482 msgid "Baranar's Star" msgstr "Зорка Баранара" #: Source/itemdat.cpp:483 msgid "Gnarled Root" msgstr "Скрыўлены корань" #: Source/itemdat.cpp:484 msgid "The Cranium Basher" msgstr "Разбівальнік чарапоў" #: Source/itemdat.cpp:485 msgid "Schaefer's Hammer" msgstr "Молат Шэфера" #: Source/itemdat.cpp:486 msgid "Dreamflange" msgstr "Грэбень сноў" #: Source/itemdat.cpp:487 msgid "Staff of Shadows" msgstr "Посах ценяў" #: Source/itemdat.cpp:488 msgid "Immolator" msgstr "Спаліцель" #: Source/itemdat.cpp:489 msgid "Storm Spire" msgstr "Шпіль Буры" #: Source/itemdat.cpp:490 msgid "Gleamsong" msgstr "Спеў промня" #: Source/itemdat.cpp:491 msgid "Thundercall" msgstr "Вокліч грому" #: Source/itemdat.cpp:492 msgid "The Protector" msgstr "Заступнік" #: Source/itemdat.cpp:493 msgid "Naj's Puzzler" msgstr "Загваздка Наж" #: Source/itemdat.cpp:494 msgid "Mindcry" msgstr "Выкрык розуму" #: Source/itemdat.cpp:495 msgid "Rod of Onan" msgstr "Прут Онана" #: Source/itemdat.cpp:496 msgid "Helm of Spirits" msgstr "Шалом духаў" #: Source/itemdat.cpp:497 msgid "Thinking Cap" msgstr "Талковая шапка" #: Source/itemdat.cpp:498 msgid "OverLord's Helm" msgstr "Шалом усеўладара" #: Source/itemdat.cpp:499 msgid "Fool's Crest" msgstr "Грыва блазна" #: Source/itemdat.cpp:500 msgid "Gotterdamerung" msgstr "Змрок багоў" #: Source/itemdat.cpp:501 msgid "Royal Circlet" msgstr "Каралеўскі абруч" #: Source/itemdat.cpp:502 msgid "Torn Flesh of Souls" msgstr "Драная плоць душ" #: Source/itemdat.cpp:503 msgid "The Gladiator's Bane" msgstr "Згуба гладыятара" #: Source/itemdat.cpp:504 msgid "The Rainbow Cloak" msgstr "Плашч вясёлкі" #: Source/itemdat.cpp:505 msgid "Leather of Aut" msgstr "Скура Ота" #: Source/itemdat.cpp:506 msgid "Wisdom's Wrap" msgstr "Шаль мудрасці" #: Source/itemdat.cpp:507 msgid "Sparking Mail" msgstr "Іскрыстая кальчуга" #: Source/itemdat.cpp:508 msgid "Scavenger Carapace" msgstr "Панцыр трупаеда" #: Source/itemdat.cpp:509 msgid "Nightscape" msgstr "Накідка ночы" #: Source/itemdat.cpp:510 msgid "Naj's Light Plate" msgstr "Лёгкія латы Наж" #: Source/itemdat.cpp:511 msgid "Demonspike Coat" msgstr "Курта чортавых шыпоў" #: Source/itemdat.cpp:512 msgid "The Deflector" msgstr "Адхіляльнік" #: Source/itemdat.cpp:513 msgid "Split Skull Shield" msgstr "Шчыт з расколатага чэрапа" #: Source/itemdat.cpp:514 msgid "Dragon's Breach" msgstr "Цмокава адтуліна" #: Source/itemdat.cpp:515 msgid "Blackoak Shield" msgstr "Шчыт з чорнага дубу" #: Source/itemdat.cpp:516 msgid "Holy Defender" msgstr "Святы абаронца" #: Source/itemdat.cpp:517 msgid "Stormshield" msgstr "Шчыт буры" #: Source/itemdat.cpp:518 msgid "Bramble" msgstr "Ажына" #: Source/itemdat.cpp:519 msgid "Ring of Regha" msgstr "Пярсцёнак Рэгі" #: Source/itemdat.cpp:520 msgid "The Bleeder" msgstr "Крывацёк" #: Source/itemdat.cpp:521 msgid "Constricting Ring" msgstr "Сціскальны пярсцёнак" #: Source/itemdat.cpp:522 msgid "Ring of Engagement" msgstr "Заручальны пярсцёнак" #: Source/itemdat.cpp:523 msgid "Giant's Knuckle" msgstr "Костка велікалюда" #: Source/itemdat.cpp:524 msgid "Mercurial Ring" msgstr "Пярсцёнак Меркурыя" #: Source/itemdat.cpp:525 msgid "Xorine's Ring" msgstr "Пярсцёнак Ксорына" #: Source/itemdat.cpp:526 msgid "Karik's Ring" msgstr "Пярсцёнак Карыка" #: Source/itemdat.cpp:527 msgid "Ring of Magma" msgstr "Пярсцёнак магмы" #: Source/itemdat.cpp:528 msgid "Ring of the Mystics" msgstr "Пярсцёнак містыкі" #: Source/itemdat.cpp:529 msgid "Ring of Thunder" msgstr "Пярсцёнак грому" #: Source/itemdat.cpp:530 msgid "Amulet of Warding" msgstr "Амулет аховы" #: Source/itemdat.cpp:531 msgid "Gnat Sting" msgstr "Джала мошкі" #: Source/itemdat.cpp:532 msgid "Flambeau" msgstr "Светач" #: Source/itemdat.cpp:533 msgid "Armor of Gloom" msgstr "Даспех цямнэчы" #: Source/itemdat.cpp:534 msgid "Blitzen" msgstr "Бляск" #: Source/itemdat.cpp:535 msgid "Thunderclap" msgstr "Пярун" #: Source/itemdat.cpp:536 msgid "Shirotachi" msgstr "Шыратачы" #: Source/itemdat.cpp:537 msgid "Eater of Souls" msgstr "Пажыральнік душ" #: Source/itemdat.cpp:538 msgid "Diamondedge" msgstr "Алмазнае лязо" #: Source/itemdat.cpp:539 msgid "Bone Chain Armor" msgstr "Касцяная кальчуга" #: Source/itemdat.cpp:540 msgid "Demon Plate Armor" msgstr "Латы дэмана" #: Source/itemdat.cpp:541 msgid "Acolyte's Amulet" msgstr "Амулет прыслужніка" #. TRANSLATORS: Unique Item section end. #: Source/itemdat.cpp:543 msgid "Gladiator's Ring" msgstr "Пярсцёнак гладыятара" #: Source/items.cpp:168 msgid "Oil of Mastery" msgstr "Алей улады" #: Source/items.cpp:170 msgid "Oil of Death" msgstr "Алей смерці" #: Source/items.cpp:171 msgid "Oil of Skill" msgstr "Алей спраўнасці" #: Source/items.cpp:173 msgid "Oil of Fortitude" msgstr "Алей трываласці" #: Source/items.cpp:174 msgid "Oil of Permanence" msgstr "Алей нязменнасці" #: Source/items.cpp:175 msgid "Oil of Hardening" msgstr "Алей гарту" #: Source/items.cpp:176 msgid "Oil of Imperviousness" msgstr "Алей непрабіўнасці" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1149 msgctxt "spell" msgid "{0} of {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1157 msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{0} {1} {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1175 msgid "{0} {1} of {2}" msgstr "{0} {1} {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1178 msgid "{0} {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1181 msgid "{0} of {1}" msgstr "{0} {1}" #: Source/items.cpp:1716 Source/items.cpp:1724 msgid "increases a weapon's" msgstr "павялічвае зброі" #: Source/items.cpp:1717 msgid "chance to hit" msgstr "шанец трапіць" #: Source/items.cpp:1720 msgid "greatly increases a" msgstr "моцна павялічвае" #: Source/items.cpp:1721 msgid "weapon's chance to hit" msgstr "шанец зброі трапіць" #: Source/items.cpp:1725 msgid "damage potential" msgstr "патэнцыял шкоды" #: Source/items.cpp:1728 msgid "greatly increases a weapon's" msgstr "моцна павялічвае зброі" #: Source/items.cpp:1729 msgid "damage potential - not bows" msgstr "патэнцыял шкоды – не для лукаў" #: Source/items.cpp:1732 msgid "reduces attributes needed" msgstr "змяншае неабходныя атрыбуты" #: Source/items.cpp:1733 msgid "to use armor or weapons" msgstr "для карыстання брані ці зброі" #: Source/items.cpp:1736 #, no-c-format msgid "restores 20% of an" msgstr "аднаўляе 20%" #: Source/items.cpp:1737 msgid "item's durability" msgstr "мацунак рэчы" #: Source/items.cpp:1740 msgid "increases an item's" msgstr "павялічвае рэчы" #: Source/items.cpp:1741 msgid "current and max durability" msgstr "цяперашні ды максімальны мацунак" #: Source/items.cpp:1744 msgid "makes an item indestructible" msgstr "робіць рэч незнішчальнаю" #: Source/items.cpp:1747 msgid "increases the armor class" msgstr "павялічвае клас брані" #: Source/items.cpp:1748 msgid "of armor and shields" msgstr "брані і шчытоў" #: Source/items.cpp:1751 msgid "greatly increases the armor" msgstr "моцна павялічвае брані" #: Source/items.cpp:1752 msgid "class of armor and shields" msgstr "клас даспехаў ды шчытоў" #: Source/items.cpp:1755 Source/items.cpp:1762 msgid "sets fire trap" msgstr "ставіць вогненную пастку" #: Source/items.cpp:1759 msgid "sets lightning trap" msgstr "ставіць маланкавую пастку" #: Source/items.cpp:1765 msgid "sets petrification trap" msgstr "ставіць пастку акамянення" #: Source/items.cpp:1768 msgid "restore all life" msgstr "аднавіць ўсё жыццё" #: Source/items.cpp:1771 msgid "restore some life" msgstr "аднавіць трохі жыцця" #: Source/items.cpp:1774 msgid "recover life" msgstr "вярнуць жыццё" #: Source/items.cpp:1777 msgid "deadly heal" msgstr "смяротнае лячэнне" #: Source/items.cpp:1780 msgid "restore some mana" msgstr "аднавіць трохі маны" #: Source/items.cpp:1783 msgid "restore all mana" msgstr "аднавіць усю ману" #: Source/items.cpp:1786 msgid "increase strength" msgstr "павялічыць моц" #: Source/items.cpp:1789 msgid "increase magic" msgstr "павялічыць магію" #: Source/items.cpp:1792 msgid "increase dexterity" msgstr "павялічыць спрыт" #: Source/items.cpp:1795 msgid "increase vitality" msgstr "павялічыць жывучасць" #: Source/items.cpp:1799 msgid "decrease strength" msgstr "паменшыць моц" #: Source/items.cpp:1802 msgid "decrease dexterity" msgstr "паменшыць спрыт" #: Source/items.cpp:1805 msgid "decrease vitality" msgstr "паменшыць жывучасць" #: Source/items.cpp:1808 msgid "restore some life and mana" msgstr "аднавіць трохі жыцця ды маны" #: Source/items.cpp:1811 msgid "restore all life and mana" msgstr "аднавіць ўсё жыццё ды ману" #: Source/items.cpp:1826 Source/items.cpp:1866 msgid "Right-click to read" msgstr "Націснуць правай кнопкай мышы каб прачытаць" #: Source/items.cpp:1829 Source/items.cpp:1844 Source/items.cpp:1858 msgid "Open inventory to use" msgstr "Адчыніце інвентар каб выкараставаць" #: Source/items.cpp:1831 Source/items.cpp:1846 Source/items.cpp:1868 msgid "Activate to read" msgstr "Актывізуйце каб прачытаць" #: Source/items.cpp:1837 msgid "Right-click to read, then" msgstr "Націснуць правай кнопкай мышы каб прачытаць, пасля" #: Source/items.cpp:1838 msgid "left-click to target" msgstr "націснуць левай кнопкай мышы па цэлі" #: Source/items.cpp:1841 msgid "Select from spell book, then" msgstr "Выберыце з кнігі чараў, пасля" #: Source/items.cpp:1842 msgid "cast spell to read" msgstr "накладзіце чары каб прачытаць" #: Source/items.cpp:1855 msgid "Right-click to use" msgstr "Правай кнопкай мышы каб выкараставаць" #: Source/items.cpp:1860 msgid "Activate to use" msgstr "Актывізуйце каб выкараставаць" #: Source/items.cpp:1873 msgid "Right-click to view" msgstr "Правай кнопкай мышы каб пабачыць" #: Source/items.cpp:1875 msgid "Activate to view" msgstr "Актывуйце каб пабачыць" #: Source/items.cpp:1879 msgctxt "player" msgid "Level: {:d}" msgstr "Узровень {:d}" #: Source/items.cpp:1882 msgid "Doubles gold capacity" msgstr "Падвойвае ёмістасць золата" #: Source/items.cpp:1893 Source/stores.cpp:282 msgid "Required:" msgstr "Патрэбна:" #: Source/items.cpp:1895 Source/stores.cpp:284 msgid " {:d} Str" msgstr " {:d} Моц" #: Source/items.cpp:1897 Source/stores.cpp:286 msgid " {:d} Mag" msgstr " {:d} Маг" #: Source/items.cpp:1899 Source/stores.cpp:288 msgid " {:d} Dex" msgstr " {:d} Спрт" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:3256 Source/player.cpp:3145 msgid "Ear of {:s}" msgstr "Вуха {:s}" #: Source/items.cpp:3552 msgid "chance to hit: {:+d}%" msgstr "шанец трапіць: {:+d}%" #: Source/items.cpp:3555 #, no-c-format msgid "{:+d}% damage" msgstr "{:+d}% шкоды" #: Source/items.cpp:3558 Source/items.cpp:3759 msgid "to hit: {:+d}%, {:+d}% damage" msgstr "трапіць: {:+d}%, {:+d}% шкоды" #: Source/items.cpp:3561 #, no-c-format msgid "{:+d}% armor" msgstr "{:+d}% брані" #: Source/items.cpp:3564 msgid "armor class: {:d}" msgstr "клас даспехаў: {:d}" #: Source/items.cpp:3568 Source/items.cpp:3745 msgid "Resist Fire: {:+d}%" msgstr "Супраціў агню: {:+d}%" #: Source/items.cpp:3570 msgid "Resist Fire: {:+d}% MAX" msgstr "Супраціў агню: {:+d}% МАКС" #: Source/items.cpp:3574 msgid "Resist Lightning: {:+d}%" msgstr "Супраціў маланцы: {:+d}%" #: Source/items.cpp:3576 msgid "Resist Lightning: {:+d}% MAX" msgstr "Супраціў маланцы: {:+d}% МАКС" #: Source/items.cpp:3580 msgid "Resist Magic: {:+d}%" msgstr "Супраціў магіі: {:+d}%" #: Source/items.cpp:3582 msgid "Resist Magic: {:+d}% MAX" msgstr "Супраціў магіі: {:+d}% МАКС" #: Source/items.cpp:3586 msgid "Resist All: {:+d}%" msgstr "Супраціў усяму: {:+d}%" #: Source/items.cpp:3588 msgid "Resist All: {:+d}% MAX" msgstr "Супраціў усяму: {:+d}% МАКС" #: Source/items.cpp:3591 msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "чары павышаныя на {:d} узровень" msgstr[1] "чары павашанныя на {:d} ўзроўні" msgstr[2] "чары павышаныя на {:d} узроўнюў" #: Source/items.cpp:3593 msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "чары паменшаныя на {:d} узровень" msgstr[1] "чары паменьшаныя на {:d} узроўні" msgstr[2] "чары паменьшаныя на {:d} узроўнюў" #: Source/items.cpp:3595 msgid "spell levels unchanged (?)" msgstr "узроўні чараў нязменныя (?)" #: Source/items.cpp:3597 msgid "Extra charges" msgstr "Дадатковыя зарады" #: Source/items.cpp:3599 msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} зарад" msgstr[1] "{:d} {:s} зарады" msgstr[2] "{:d} {:s} зарадаў" #: Source/items.cpp:3602 msgid "Fire hit damage: {:d}" msgstr "Шкода агнём: {:d}" #: Source/items.cpp:3604 msgid "Fire hit damage: {:d}-{:d}" msgstr "Шкода агнём: {:d}-{:d}" #: Source/items.cpp:3607 msgid "Lightning hit damage: {:d}" msgstr "Шкода маланкаю: {:d}" #: Source/items.cpp:3609 msgid "Lightning hit damage: {:d}-{:d}" msgstr "Шкода маланкаю: {:d}-{:d}" #: Source/items.cpp:3612 msgid "{:+d} to strength" msgstr "{:+d} да моцы" #: Source/items.cpp:3615 msgid "{:+d} to magic" msgstr "{:+d} да магіі" #: Source/items.cpp:3618 msgid "{:+d} to dexterity" msgstr "{:+d} да спрыту" #: Source/items.cpp:3621 msgid "{:+d} to vitality" msgstr "{:+d} да жывучасці" #: Source/items.cpp:3624 msgid "{:+d} to all attributes" msgstr "{:+d} да ўсіх атрыбутаў" #: Source/items.cpp:3627 msgid "{:+d} damage from enemies" msgstr "{:+d} шкоды ад ворагаў" #: Source/items.cpp:3630 msgid "Hit Points: {:+d}" msgstr "Здароўе: {:+d}" #: Source/items.cpp:3633 msgid "Mana: {:+d}" msgstr "Мана: {:+d}" #: Source/items.cpp:3635 msgid "high durability" msgstr "вялікі мацунак" #: Source/items.cpp:3637 msgid "decreased durability" msgstr "паменшаны мацунак" #: Source/items.cpp:3639 msgid "indestructible" msgstr "незнішчальны" #: Source/items.cpp:3641 #, no-c-format msgid "+{:d}% light radius" msgstr "+{:d}% радыуса святла" #: Source/items.cpp:3643 #, no-c-format msgid "-{:d}% light radius" msgstr "-{:d}% радыуса святла" #: Source/items.cpp:3645 msgid "multiple arrows per shot" msgstr "некалькі стрэл за адзін стрэл" #: Source/items.cpp:3648 msgid "fire arrows damage: {:d}" msgstr "шкода вогненнымі стрэламі: {:d}" #: Source/items.cpp:3650 msgid "fire arrows damage: {:d}-{:d}" msgstr "шкода вогненнымі стрэламі: {:d}-{:d}" #: Source/items.cpp:3653 msgid "lightning arrows damage {:d}" msgstr "шкода маланкавымі стрэламі: {:d}" #: Source/items.cpp:3655 msgid "lightning arrows damage {:d}-{:d}" msgstr "шкода маланкавымі стрэламі: {:d}-{:d}" #: Source/items.cpp:3658 msgid "fireball damage: {:d}" msgstr "шкода вогненным шарам: {:d}" #: Source/items.cpp:3660 msgid "fireball damage: {:d}-{:d}" msgstr "шкода вогненным шарам: {:d}-{:d}" #: Source/items.cpp:3662 msgid "attacker takes 1-3 damage" msgstr "нападаючы атрымлівае 1-3 шкоды" #: Source/items.cpp:3664 msgid "user loses all mana" msgstr "карыстальнік страчвае ўсю ману" #: Source/items.cpp:3666 msgid "you can't heal" msgstr "не можаце лячыцца" #: Source/items.cpp:3668 msgid "absorbs half of trap damage" msgstr "паглынае палову шкоды пасткі" #: Source/items.cpp:3670 msgid "knocks target back" msgstr "адкідае цэль узад" #: Source/items.cpp:3672 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% шкоды супраць дэманаў" #: Source/items.cpp:3674 msgid "All Resistance equals 0" msgstr "Увесь супраціў роўны 0" #: Source/items.cpp:3676 msgid "hit monster doesn't heal" msgstr "удар па пачвары не лечыць" #: Source/items.cpp:3679 #, no-c-format msgid "hit steals 3% mana" msgstr "удар крадзе 3% маны" #: Source/items.cpp:3681 #, no-c-format msgid "hit steals 5% mana" msgstr "удар крадзе 5% маны" #: Source/items.cpp:3685 #, no-c-format msgid "hit steals 3% life" msgstr "удар крадзе 3% жыцця" #: Source/items.cpp:3687 #, no-c-format msgid "hit steals 5% life" msgstr "удар крадзе 5% жыцця" #: Source/items.cpp:3691 Source/items.cpp:3693 msgid "penetrates target's armor" msgstr "прабівае браню цэлі" #: Source/items.cpp:3696 msgid "quick attack" msgstr "хуткі напад" #: Source/items.cpp:3698 msgid "fast attack" msgstr "шпаркі напад" #: Source/items.cpp:3700 msgid "faster attack" msgstr "шпарчэйшы напад" #: Source/items.cpp:3702 msgid "fastest attack" msgstr "найшпарчэйшы напад" #: Source/items.cpp:3703 Source/items.cpp:3711 Source/items.cpp:3769 msgid "Another ability (NW)" msgstr "Іншая здольнасць (NW)" #: Source/items.cpp:3706 msgid "fast hit recovery" msgstr "хуткае ачуньванне па ўдары" #: Source/items.cpp:3708 msgid "faster hit recovery" msgstr "хутчэйшае ачуньванне па ўдары" #: Source/items.cpp:3710 msgid "fastest hit recovery" msgstr "найхутчэйшае ачуньванне па ўдары" #: Source/items.cpp:3713 msgid "fast block" msgstr "хуткі блок" #: Source/items.cpp:3715 msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "дадае {:d} ачко шкодзе" msgstr[1] "дадае {:d} ачкі шкодзе" msgstr[2] "дадае {:d} ачкоў шкодзе" #: Source/items.cpp:3717 msgid "fires random speed arrows" msgstr "пускае некалькі хуткіх стрэл" #: Source/items.cpp:3719 msgid "unusual item damage" msgstr "незвычайная шкода рэчай" #: Source/items.cpp:3721 msgid "altered durability" msgstr "зменены мацунак" #: Source/items.cpp:3723 msgid "Faster attack swing" msgstr "Узмах шпарчэйшай атакі" #: Source/items.cpp:3725 msgid "one handed sword" msgstr "аднаручны меч" #: Source/items.cpp:3727 msgid "constantly lose hit points" msgstr "безупынна губляць здароўе" #: Source/items.cpp:3729 msgid "life stealing" msgstr "крадзеж жыцця" #: Source/items.cpp:3731 msgid "no strength requirement" msgstr "няма патрабавання моцы" #: Source/items.cpp:3733 msgid "see with infravision" msgstr "глядзець скрозьбачаннем" #: Source/items.cpp:3738 msgid "lightning damage: {:d}" msgstr "шкода маланкаю: {:d}" #: Source/items.cpp:3740 msgid "lightning damage: {:d}-{:d}" msgstr "шкода маланкаю: {:d}-{:d}" #: Source/items.cpp:3742 msgid "charged bolts on hits" msgstr "выпускае зараджаныя стрэлы пры ўдарах" #: Source/items.cpp:3749 msgid "occasional triple damage" msgstr "выпадковая патройная шкода" #: Source/items.cpp:3751 #, no-c-format msgid "decaying {:+d}% damage" msgstr "занепадае {:+d}% шкоды" #: Source/items.cpp:3753 msgid "2x dmg to monst, 1x to you" msgstr "2х шкд пачвр, 1х вам" #: Source/items.cpp:3755 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Выпадковая 0 – 600% шкоды" #: Source/items.cpp:3757 #, no-c-format msgid "low dur, {:+d}% damage" msgstr "нізкі спрыт, {:+d}% шкоды" #: Source/items.cpp:3761 msgid "extra AC vs demons" msgstr "дадат. абарона ад дэманаў" #: Source/items.cpp:3763 msgid "extra AC vs undead" msgstr "дадат. абарона ад наўцоў" #: Source/items.cpp:3765 msgid "50% Mana moved to Health" msgstr "50% маны перайшло ў Здароўе" #: Source/items.cpp:3767 msgid "40% Health moved to Mana" msgstr "40% Здароўя перайшло ў Ману" #: Source/items.cpp:3804 Source/items.cpp:3842 msgid "damage: {:d} Indestructible" msgstr "шкода {:d} Незнішчальна" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:3806 Source/items.cpp:3844 msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "шкода {:d} мац: {:d}/{:d}" #: Source/items.cpp:3809 Source/items.cpp:3847 msgid "damage: {:d}-{:d} Indestructible" msgstr "шкода {:d}-{:d} Незнішчальна" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:3811 Source/items.cpp:3849 msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "шкода: {:d}-{:d} мац: {:d}/{:d}" #: Source/items.cpp:3816 Source/items.cpp:3859 msgid "armor: {:d} Indestructible" msgstr "браня: {:d} Незнішчальна" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:3818 Source/items.cpp:3861 msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "браня: {:d} мац: {:d}/{:d}" #: Source/items.cpp:3821 Source/items.cpp:3852 Source/items.cpp:3865 #: Source/stores.cpp:256 msgid "Charges: {:d}/{:d}" msgstr "Зарады: {:d}/{:d}" #: Source/items.cpp:3830 msgid "unique item" msgstr "унікальная рэч" #: Source/items.cpp:3855 Source/items.cpp:3863 Source/items.cpp:3869 msgid "Not Identified" msgstr "Не выяўлена" #: Source/loadsave.cpp:1870 Source/loadsave.cpp:2388 msgid "Unable to open save file archive" msgstr "Немагчыма адкрыць архіў захаванняў" #: Source/loadsave.cpp:1873 msgid "Invalid save file" msgstr "Няправільны файл захавання" #: Source/loadsave.cpp:1904 msgid "Player is on a Hellfire only level" msgstr "Гулец толькі на ўзроўні Hellfire" #: Source/loadsave.cpp:2150 msgid "Invalid game state" msgstr "Няправільны стан гульні" #: Source/menu.cpp:149 msgid "Unable to display mainmenu" msgstr "Няма як паказаць галоўнае меню" #. TRANSLATORS: Monster Block start #. MT_NZOMBIE #: Source/monstdat.cpp:20 msgctxt "monster" msgid "Zombie" msgstr "Зомбі" #: Source/monstdat.cpp:21 msgctxt "monster" msgid "Ghoul" msgstr "Гуль" #: Source/monstdat.cpp:22 msgctxt "monster" msgid "Rotting Carcass" msgstr "Гнілая туша" #: Source/monstdat.cpp:23 msgctxt "monster" msgid "Black Death" msgstr "Чорная смерць" #: Source/monstdat.cpp:24 Source/monstdat.cpp:32 msgctxt "monster" msgid "Fallen One" msgstr "Загінулы" #: Source/monstdat.cpp:25 Source/monstdat.cpp:33 msgctxt "monster" msgid "Carver" msgstr "Рэзчык" #: Source/monstdat.cpp:26 Source/monstdat.cpp:34 msgctxt "monster" msgid "Devil Kin" msgstr "Нячысцік" #: Source/monstdat.cpp:27 Source/monstdat.cpp:35 msgctxt "monster" msgid "Dark One" msgstr "Цёмны" #: Source/monstdat.cpp:28 Source/monstdat.cpp:40 msgctxt "monster" msgid "Skeleton" msgstr "Шкілет" #: Source/monstdat.cpp:29 msgctxt "monster" msgid "Corpse Axe" msgstr "Мёртвы сякернік" #: Source/monstdat.cpp:30 Source/monstdat.cpp:42 msgctxt "monster" msgid "Burning Dead" msgstr "Палымнеючы мрэц" #: Source/monstdat.cpp:31 Source/monstdat.cpp:43 msgctxt "monster" msgid "Horror" msgstr "Жах" #: Source/monstdat.cpp:36 msgctxt "monster" msgid "Scavenger" msgstr "Трупаед" #: Source/monstdat.cpp:37 msgctxt "monster" msgid "Plague Eater" msgstr "Чумаед" #: Source/monstdat.cpp:38 msgctxt "monster" msgid "Shadow Beast" msgstr "Звер цемры" #: Source/monstdat.cpp:39 msgctxt "monster" msgid "Bone Gasher" msgstr "Касцяны разрэзак" #: Source/monstdat.cpp:41 msgctxt "monster" msgid "Corpse Bow" msgstr "Мёртвы лучнік" #: Source/monstdat.cpp:44 msgctxt "monster" msgid "Skeleton Captain" msgstr "Шкілет капітана" #: Source/monstdat.cpp:45 msgctxt "monster" msgid "Corpse Captain" msgstr "Труп капітана" #: Source/monstdat.cpp:46 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Палымнеючы мёртвы капітан" #: Source/monstdat.cpp:47 msgctxt "monster" msgid "Horror Captain" msgstr "Жахлівы капітан" #: Source/monstdat.cpp:48 msgctxt "monster" msgid "Invisible Lord" msgstr "Нябачны ўладар" #: Source/monstdat.cpp:49 msgctxt "monster" msgid "Hidden" msgstr "Схаваны" #: Source/monstdat.cpp:50 msgctxt "monster" msgid "Stalker" msgstr "Пераследнік" #: Source/monstdat.cpp:51 msgctxt "monster" msgid "Unseen" msgstr "Нябачны" #: Source/monstdat.cpp:52 msgctxt "monster" msgid "Illusion Weaver" msgstr "Ткач ілюзій" #: Source/monstdat.cpp:53 msgctxt "monster" msgid "Satyr Lord" msgstr "Валадар сатыраў" #: Source/monstdat.cpp:54 Source/monstdat.cpp:62 msgctxt "monster" msgid "Flesh Clan" msgstr "Клан плоці" #: Source/monstdat.cpp:55 Source/monstdat.cpp:63 msgctxt "monster" msgid "Stone Clan" msgstr "Клан каменя" #: Source/monstdat.cpp:56 Source/monstdat.cpp:64 msgctxt "monster" msgid "Fire Clan" msgstr "Клан полымя" #: Source/monstdat.cpp:57 Source/monstdat.cpp:65 msgctxt "monster" msgid "Night Clan" msgstr "Клан ночы" #: Source/monstdat.cpp:58 msgctxt "monster" msgid "Fiend" msgstr "Нячысты" #: Source/monstdat.cpp:59 msgctxt "monster" msgid "Blink" msgstr "Мірг" #: Source/monstdat.cpp:60 msgctxt "monster" msgid "Gloom" msgstr "Морак" #: Source/monstdat.cpp:61 msgctxt "monster" msgid "Familiar" msgstr "Фамільяр" #: Source/monstdat.cpp:66 msgctxt "monster" msgid "Acid Beast" msgstr "Звер кіслаты" #: Source/monstdat.cpp:67 msgctxt "monster" msgid "Poison Spitter" msgstr "Ядаплюй" #: Source/monstdat.cpp:68 msgctxt "monster" msgid "Pit Beast" msgstr "Звер з ямы" #: Source/monstdat.cpp:69 msgctxt "monster" msgid "Lava Maw" msgstr "Лававая ляпа" #: Source/monstdat.cpp:70 Source/monstdat.cpp:474 msgctxt "monster" msgid "Skeleton King" msgstr "Кароль шкілетаў" #: Source/monstdat.cpp:71 Source/monstdat.cpp:482 msgctxt "monster" msgid "The Butcher" msgstr "Мяснік" #: Source/monstdat.cpp:72 msgctxt "monster" msgid "Overlord" msgstr "Усеўладар" #: Source/monstdat.cpp:73 msgctxt "monster" msgid "Mud Man" msgstr "Брыда" #: Source/monstdat.cpp:74 msgctxt "monster" msgid "Toad Demon" msgstr "Чорт-рапуха" #: Source/monstdat.cpp:75 msgctxt "monster" msgid "Flayed One" msgstr "Залупцаваны" #: Source/monstdat.cpp:76 msgctxt "monster" msgid "Wyrm" msgstr "Змей" #: Source/monstdat.cpp:77 msgctxt "monster" msgid "Cave Slug" msgstr "Пячорны смоўж" #: Source/monstdat.cpp:78 msgctxt "monster" msgid "Devil Wyrm" msgstr "Чортаў Змей" #: Source/monstdat.cpp:79 msgctxt "monster" msgid "Devourer" msgstr "Паглынальнік" #: Source/monstdat.cpp:80 msgctxt "monster" msgid "Magma Demon" msgstr "Дэман магмы" #: Source/monstdat.cpp:81 msgctxt "monster" msgid "Blood Stone" msgstr "Камень крыві" #: Source/monstdat.cpp:82 msgctxt "monster" msgid "Hell Stone" msgstr "Камень пекла" #: Source/monstdat.cpp:83 msgctxt "monster" msgid "Lava Lord" msgstr "Валадар лавы" #: Source/monstdat.cpp:84 msgctxt "monster" msgid "Horned Demon" msgstr "Чорт рагаты" #: Source/monstdat.cpp:85 msgctxt "monster" msgid "Mud Runner" msgstr "Брудны бягун" #: Source/monstdat.cpp:86 msgctxt "monster" msgid "Frost Charger" msgstr "Ледзяны нападнік" #: Source/monstdat.cpp:87 msgctxt "monster" msgid "Obsidian Lord" msgstr "Абсідыянавы ўладар" #: Source/monstdat.cpp:88 msgctxt "monster" msgid "oldboned" msgstr "трухлявыя косці" #: Source/monstdat.cpp:89 msgctxt "monster" msgid "Red Death" msgstr "Чырвоная смерць" #: Source/monstdat.cpp:90 msgctxt "monster" msgid "Litch Demon" msgstr "Кашчэй" #: Source/monstdat.cpp:91 msgctxt "monster" msgid "Undead Balrog" msgstr "Мёртвы балраг" #: Source/monstdat.cpp:92 msgctxt "monster" msgid "Incinerator" msgstr "Спальвальнік" #: Source/monstdat.cpp:93 msgctxt "monster" msgid "Flame Lord" msgstr "Валадар полымя" #: Source/monstdat.cpp:94 msgctxt "monster" msgid "Doom Fire" msgstr "Агонь пагібелі" #: Source/monstdat.cpp:95 msgctxt "monster" msgid "Hell Burner" msgstr "Пякельны смальнік" #: Source/monstdat.cpp:96 msgctxt "monster" msgid "Red Storm" msgstr "Чырвоная бура" #: Source/monstdat.cpp:97 msgctxt "monster" msgid "Storm Rider" msgstr "Вершнік буры" #: Source/monstdat.cpp:98 msgctxt "monster" msgid "Storm Lord" msgstr "Валадар буры" #: Source/monstdat.cpp:99 msgctxt "monster" msgid "Maelstrom" msgstr "Вір" #: Source/monstdat.cpp:100 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Нячысты гіцаль" #: Source/monstdat.cpp:101 msgctxt "monster" msgid "Winged-Demon" msgstr "Крылаты чорт" #: Source/monstdat.cpp:102 msgctxt "monster" msgid "Gargoyle" msgstr "Гаргулля" #: Source/monstdat.cpp:103 msgctxt "monster" msgid "Blood Claw" msgstr "Крывавы кіпцюр" #: Source/monstdat.cpp:104 msgctxt "monster" msgid "Death Wing" msgstr "Крыло смерці" #: Source/monstdat.cpp:105 msgctxt "monster" msgid "Slayer" msgstr "Забойца" #: Source/monstdat.cpp:106 msgctxt "monster" msgid "Guardian" msgstr "Ахоўнік" #: Source/monstdat.cpp:107 msgctxt "monster" msgid "Vortex Lord" msgstr "Валадар віхру" #: Source/monstdat.cpp:108 msgctxt "monster" msgid "Balrog" msgstr "Балраг" #: Source/monstdat.cpp:109 msgctxt "monster" msgid "Cave Viper" msgstr "Пячорная гадзіна" #: Source/monstdat.cpp:110 msgctxt "monster" msgid "Fire Drake" msgstr "Вогненны смок" #: Source/monstdat.cpp:111 msgctxt "monster" msgid "Gold Viper" msgstr "Залатая гадзіна" #: Source/monstdat.cpp:112 msgctxt "monster" msgid "Azure Drake" msgstr "Блакітны смок" #: Source/monstdat.cpp:113 msgctxt "monster" msgid "Black Knight" msgstr "Чорны рыцар" #: Source/monstdat.cpp:114 msgctxt "monster" msgid "Doom Guard" msgstr "Вартавы лёсу" #: Source/monstdat.cpp:115 msgctxt "monster" msgid "Steel Lord" msgstr "Валадар сталі" #: Source/monstdat.cpp:116 msgctxt "monster" msgid "Blood Knight" msgstr "Рыцар крыві" #: Source/monstdat.cpp:117 msgctxt "monster" msgid "The Shredded" msgstr "Шаткаваны" #: Source/monstdat.cpp:118 msgctxt "monster" msgid "Hollow One" msgstr "Пусты" #: Source/monstdat.cpp:119 msgctxt "monster" msgid "Pain Master" msgstr "Гаспадар болю" #: Source/monstdat.cpp:120 msgctxt "monster" msgid "Reality Weaver" msgstr "Ткач запраўднасці" #: Source/monstdat.cpp:121 msgctxt "monster" msgid "Succubus" msgstr "Сукуб" #: Source/monstdat.cpp:122 msgctxt "monster" msgid "Snow Witch" msgstr "Снежная ведзьма" #: Source/monstdat.cpp:123 msgctxt "monster" msgid "Hell Spawn" msgstr "Вырадак пекла" #: Source/monstdat.cpp:124 msgctxt "monster" msgid "Soul Burner" msgstr "Спалідуша" #: Source/monstdat.cpp:125 msgctxt "monster" msgid "Counselor" msgstr "Дараднік" #: Source/monstdat.cpp:126 msgctxt "monster" msgid "Magistrate" msgstr "Суддзя" #: Source/monstdat.cpp:127 msgctxt "monster" msgid "Cabalist" msgstr "Махляр" #: Source/monstdat.cpp:128 msgctxt "monster" msgid "Advocate" msgstr "Заступнік" #: Source/monstdat.cpp:129 msgctxt "monster" msgid "Golem" msgstr "Голем" #: Source/monstdat.cpp:130 msgctxt "monster" msgid "The Dark Lord" msgstr "Цёмны валадар" #: Source/monstdat.cpp:131 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Архі-Ліч Малігнус" #: Source/monstdat.cpp:132 msgctxt "monster" msgid "Hellboar" msgstr "Пякельны дзік" #: Source/monstdat.cpp:133 msgctxt "monster" msgid "Stinger" msgstr "Джальнік" #: Source/monstdat.cpp:134 msgctxt "monster" msgid "Psychorb" msgstr "Псіхавока" #: Source/monstdat.cpp:135 msgctxt "monster" msgid "Arachnon" msgstr "Павучыска" #: Source/monstdat.cpp:136 msgctxt "monster" msgid "Felltwin" msgstr "Адцяты спарыш" #: Source/monstdat.cpp:137 msgctxt "monster" msgid "Hork Spawn" msgstr "Вырадак рыгатні" #: Source/monstdat.cpp:138 msgctxt "monster" msgid "Venomtail" msgstr "Атрутны хвост" #: Source/monstdat.cpp:139 msgctxt "monster" msgid "Necromorb" msgstr "Мёртвае вока" #: Source/monstdat.cpp:140 msgctxt "monster" msgid "Spider Lord" msgstr "Валадар павукоў" #: Source/monstdat.cpp:141 msgctxt "monster" msgid "Lashworm" msgstr "Рабак-пуга" #: Source/monstdat.cpp:142 msgctxt "monster" msgid "Torchant" msgstr "Успыхун" #: Source/monstdat.cpp:143 Source/monstdat.cpp:483 msgctxt "monster" msgid "Hork Demon" msgstr "Рыготны чорт" #: Source/monstdat.cpp:144 msgctxt "monster" msgid "Hell Bug" msgstr "Пеклаў жук" #: Source/monstdat.cpp:145 msgctxt "monster" msgid "Gravedigger" msgstr "Далакоп" #: Source/monstdat.cpp:146 msgctxt "monster" msgid "Tomb Rat" msgstr "Магільны пацук" #: Source/monstdat.cpp:147 msgctxt "monster" msgid "Firebat" msgstr "Агнявы кажан" #: Source/monstdat.cpp:148 msgctxt "monster" msgid "Skullwing" msgstr "Чарапун" #: Source/monstdat.cpp:149 msgctxt "monster" msgid "Lich" msgstr "Ліч" #: Source/monstdat.cpp:150 msgctxt "monster" msgid "Crypt Demon" msgstr "Склепавы чорт" #: Source/monstdat.cpp:151 msgctxt "monster" msgid "Hellbat" msgstr "Пякельны кажан" #: Source/monstdat.cpp:152 msgctxt "monster" msgid "Bone Demon" msgstr "Касцяны чорт" #: Source/monstdat.cpp:153 msgctxt "monster" msgid "Arch Lich" msgstr "Архіліч" #: Source/monstdat.cpp:154 msgctxt "monster" msgid "Biclops" msgstr "Лупаты" #: Source/monstdat.cpp:155 msgctxt "monster" msgid "Flesh Thing" msgstr "Плоцень" #: Source/monstdat.cpp:156 msgctxt "monster" msgid "Reaper" msgstr "Жнец" #. TRANSLATORS: Monster Block end #. MT_NAKRUL #: Source/monstdat.cpp:158 Source/monstdat.cpp:485 msgctxt "monster" msgid "Na-Krul" msgstr "На-Крул" #. TRANSLATORS: Unique Monster Block start #: Source/monstdat.cpp:473 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Кволы Гарбад" #: Source/monstdat.cpp:475 msgctxt "monster" msgid "Zhar the Mad" msgstr "Шалёны Зар" #: Source/monstdat.cpp:476 msgctxt "monster" msgid "Snotspill" msgstr "Смаркач" #: Source/monstdat.cpp:477 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Архібіскуп Лазар" #: Source/monstdat.cpp:478 msgctxt "monster" msgid "Red Vex" msgstr "Чырованая бяда" #: Source/monstdat.cpp:479 msgctxt "monster" msgid "Black Jade" msgstr "Чорная Жад" #: Source/monstdat.cpp:480 msgctxt "monster" msgid "Lachdanan" msgstr "Лакданан" #: Source/monstdat.cpp:481 msgctxt "monster" msgid "Warlord of Blood" msgstr "Крывавы ваявода" #: Source/monstdat.cpp:484 msgctxt "monster" msgid "The Defiler" msgstr "Паганнік" #: Source/monstdat.cpp:486 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Дурыла Вострая сякера" #: Source/monstdat.cpp:487 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Лязаскурац Засякун" #: Source/monstdat.cpp:488 msgctxt "monster" msgid "Soulpus" msgstr "Гнойная душа" #: Source/monstdat.cpp:489 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Ванітапац Нячысты" #: Source/monstdat.cpp:490 msgctxt "monster" msgid "Boneripper" msgstr "Вырвікосці" #: Source/monstdat.cpp:491 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Банкетагнілец Галодны" #: Source/monstdat.cpp:492 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Кішканожак Хуткі" #: Source/monstdat.cpp:493 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Бітылоб Шчытабой" #: Source/monstdat.cpp:494 msgctxt "monster" msgid "Bongo" msgstr "Банго" #: Source/monstdat.cpp:495 msgctxt "monster" msgid "Rotcarnage" msgstr "Гнілабой" #: Source/monstdat.cpp:496 msgctxt "monster" msgid "Shadowbite" msgstr "Укус з Ценю" #: Source/monstdat.cpp:497 msgctxt "monster" msgid "Deadeye" msgstr "Гнілы Яблычак" #: Source/monstdat.cpp:498 msgctxt "monster" msgid "Madeye the Dead" msgstr "Мёртвы Утрапенец" #: Source/monstdat.cpp:499 msgctxt "monster" msgid "El Chupacabras" msgstr "Чупакабра" #: Source/monstdat.cpp:500 msgctxt "monster" msgid "Skullfire" msgstr "Вогненны Чэрап" #: Source/monstdat.cpp:501 msgctxt "monster" msgid "Warpskull" msgstr "Крывы Чэрап" #: Source/monstdat.cpp:502 msgctxt "monster" msgid "Goretongue" msgstr "Крывавы Язык" #: Source/monstdat.cpp:503 msgctxt "monster" msgid "Pulsecrawler" msgstr "Пульс" #: Source/monstdat.cpp:504 msgctxt "monster" msgid "Moonbender" msgstr "Сагнімесяц" #: Source/monstdat.cpp:505 msgctxt "monster" msgid "Wrathraven" msgstr "Крумкач Шалу" #: Source/monstdat.cpp:506 msgctxt "monster" msgid "Spineeater" msgstr "Хрыбтаед" #: Source/monstdat.cpp:507 msgctxt "monster" msgid "Blackash the Burning" msgstr "Спалены Попел" #: Source/monstdat.cpp:508 msgctxt "monster" msgid "Shadowcrow" msgstr "Цёмная Варона" #: Source/monstdat.cpp:509 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Чумны Камень Слабы" #: Source/monstdat.cpp:510 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Гаспадар Ямы Пенажоўць" #: Source/monstdat.cpp:511 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Цёмны лук Крываскур" #: Source/monstdat.cpp:512 msgctxt "monster" msgid "Foulwing" msgstr "Брыдкакрыл" #: Source/monstdat.cpp:513 msgctxt "monster" msgid "Shadowdrinker" msgstr "Піток Ценю" #: Source/monstdat.cpp:514 msgctxt "monster" msgid "Hazeshifter" msgstr "Змянісмугу" #: Source/monstdat.cpp:515 msgctxt "monster" msgid "Deathspit" msgstr "Плявок Смерці" #: Source/monstdat.cpp:516 msgctxt "monster" msgid "Bloodgutter" msgstr "Крывасцёк" #: Source/monstdat.cpp:517 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Рвіскуры Цень смерці" #: Source/monstdat.cpp:518 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Шалёная Гніда" #: Source/monstdat.cpp:519 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Зубчасты Шклочэрап" #: Source/monstdat.cpp:520 msgctxt "monster" msgid "Blightfire" msgstr "Агонь халеры" #: Source/monstdat.cpp:521 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Нячулая Начніца" #: Source/monstdat.cpp:522 msgctxt "monster" msgid "Gorestone" msgstr "Крывакамень" #: Source/monstdat.cpp:523 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Бронзавы Кулак агнякаменю" #: Source/monstdat.cpp:524 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Пракляты Гнеў Полымя" #: Source/monstdat.cpp:525 msgctxt "monster" msgid "Firewound the Grim" msgstr "Змрочны Апёк" #: Source/monstdat.cpp:526 msgctxt "monster" msgid "Baron Sludge" msgstr "Барон Дрыгвень" #: Source/monstdat.cpp:527 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Чумны Рог Стальная Булава" #: Source/monstdat.cpp:528 msgctxt "monster" msgid "Chaoshowler" msgstr "Равун Хаосу" #: Source/monstdat.cpp:529 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Згнілы Вышчар" #: Source/monstdat.cpp:530 msgctxt "monster" msgid "Madburner" msgstr "Ліхі Падпальшчык" #: Source/monstdat.cpp:531 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Кашчэй Піла" #: Source/monstdat.cpp:532 msgctxt "monster" msgid "Breakspine" msgstr "Зламіхрыбет" #: Source/monstdat.cpp:533 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Востры Чэрап" #: Source/monstdat.cpp:534 msgctxt "monster" msgid "Brokenstorm" msgstr "Ветрабой" #: Source/monstdat.cpp:535 msgctxt "monster" msgid "Stormbane" msgstr "Ліхая Навала" #: Source/monstdat.cpp:536 msgctxt "monster" msgid "Oozedrool" msgstr "Цячысліна" #: Source/monstdat.cpp:537 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Злотачума Полымя" #: Source/monstdat.cpp:538 msgctxt "monster" msgid "Blackstorm" msgstr "Чорная Бура" #: Source/monstdat.cpp:539 msgctxt "monster" msgid "Plaguewrath" msgstr "Гнеў Чумы" #: Source/monstdat.cpp:540 msgctxt "monster" msgid "The Flayer" msgstr "Скуралуп" #: Source/monstdat.cpp:541 msgctxt "monster" msgid "Bluehorn" msgstr "Блакітны Рог" #: Source/monstdat.cpp:542 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Пеклаў Вырадак Крывагню" #: Source/monstdat.cpp:543 msgctxt "monster" msgid "Fangspeir" msgstr "Пазурак" #: Source/monstdat.cpp:544 msgctxt "monster" msgid "Festerskull" msgstr "Гнілы Чэрап" #: Source/monstdat.cpp:545 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Крывы Ільвачэрап" #: Source/monstdat.cpp:546 msgctxt "monster" msgid "Blacktongue" msgstr "Чорны Язык" #: Source/monstdat.cpp:547 msgctxt "monster" msgid "Viletouch" msgstr "Брыдкі дотык" #: Source/monstdat.cpp:548 msgctxt "monster" msgid "Viperflame" msgstr "Змеяполамень" #: Source/monstdat.cpp:549 msgctxt "monster" msgid "Fangskin" msgstr "Ікласкур" #: Source/monstdat.cpp:550 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Бязбожная Вогненная Ведзьма" #: Source/monstdat.cpp:551 msgctxt "monster" msgid "Blackskull" msgstr "Чорны чэрап" #: Source/monstdat.cpp:552 msgctxt "monster" msgid "Soulslash" msgstr "Душасек" #: Source/monstdat.cpp:553 msgctxt "monster" msgid "Windspawn" msgstr "Дзіця ветру" #: Source/monstdat.cpp:554 msgctxt "monster" msgid "Lord of the Pit" msgstr "Валадар ямы" #: Source/monstdat.cpp:555 msgctxt "monster" msgid "Rustweaver" msgstr "Іржаткач" #: Source/monstdat.cpp:556 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Цень Равучага Гневу" #: Source/monstdat.cpp:557 msgctxt "monster" msgid "Doomcloud" msgstr "Воблака Долі" #: Source/monstdat.cpp:558 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Агонь Душы з Крывавай Луны" #: Source/monstdat.cpp:559 msgctxt "monster" msgid "Witchmoon" msgstr "Ведзьма Месяцу" #: Source/monstdat.cpp:560 msgctxt "monster" msgid "Gorefeast" msgstr "Крывавы Банкет" #: Source/monstdat.cpp:561 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Шэравойна Людабойца" #: Source/monstdat.cpp:562 msgctxt "monster" msgid "Dreadjudge" msgstr "Жуда-Суддзя" #: Source/monstdat.cpp:563 msgctxt "monster" msgid "Stareye the Witch" msgstr "Ведзьма Зоркавока" #: Source/monstdat.cpp:564 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Стальны чэрап Паляўнічы" #: Source/monstdat.cpp:565 msgctxt "monster" msgid "Sir Gorash" msgstr "Сэр Гораш" #: Source/monstdat.cpp:566 msgctxt "monster" msgid "The Vizier" msgstr "Візір" #: Source/monstdat.cpp:567 msgctxt "monster" msgid "Zamphir" msgstr "Замфір" #: Source/monstdat.cpp:568 msgctxt "monster" msgid "Bloodlust" msgstr "Крывавая Прага" #: Source/monstdat.cpp:569 msgctxt "monster" msgid "Webwidow" msgstr "Удава Павуціння" #: Source/monstdat.cpp:570 msgctxt "monster" msgid "Fleshdancer" msgstr "Скураны Танцор" #: Source/monstdat.cpp:571 msgctxt "monster" msgid "Grimspike" msgstr "Змрокашып" #. TRANSLATORS: Unique Monster Block end #: Source/monstdat.cpp:573 msgctxt "monster" msgid "Doomlock" msgstr "Замок Долі" #: Source/monster.cpp:3384 msgid "Animal" msgstr "Звер" #: Source/monster.cpp:3386 msgid "Demon" msgstr "Чорт" #: Source/monster.cpp:3388 msgid "Undead" msgstr "Навец" #: Source/monster.cpp:4643 msgid "Type: {:s} Kills: {:d}" msgstr "Тып: {:s} Забітых: {:d}" #: Source/monster.cpp:4645 msgid "Total kills: {:d}" msgstr "Усяго забітых: {:d}" #: Source/monster.cpp:4677 msgid "Hit Points: {:d}-{:d}" msgstr "Ачкоў здароўя: {:d}-{:d}" #: Source/monster.cpp:4682 msgid "No magic resistance" msgstr "Ані супраціву магіі" #: Source/monster.cpp:4685 msgid "Resists:" msgstr "Супраціў:" #: Source/monster.cpp:4687 Source/monster.cpp:4697 msgid " Magic" msgstr " Магія" #: Source/monster.cpp:4689 Source/monster.cpp:4699 msgid " Fire" msgstr " Агонь" #: Source/monster.cpp:4691 Source/monster.cpp:4701 msgid " Lightning" msgstr " Маланка" #: Source/monster.cpp:4695 msgid "Immune:" msgstr "Імунітэт:" #: Source/monster.cpp:4712 msgid "Type: {:s}" msgstr "Тып: {:s}" #: Source/monster.cpp:4717 Source/monster.cpp:4723 msgid "No resistances" msgstr "Ані супраціваў" #: Source/monster.cpp:4718 Source/monster.cpp:4727 msgid "No Immunities" msgstr "Ані імунітэтаў" #: Source/monster.cpp:4721 msgid "Some Magic Resistances" msgstr "Трохі супраціву магіі" #: Source/monster.cpp:4725 msgid "Some Magic Immunities" msgstr "Трохі імунітэту магіі" #: Source/msg.cpp:486 msgid "Trying to drop a floor item?" msgstr "Хочаце кінуць рэч на зямлю?" #: Source/msg.cpp:989 Source/msg.cpp:1024 Source/msg.cpp:1055 #: Source/msg.cpp:1182 Source/msg.cpp:1214 Source/msg.cpp:1246 #: Source/msg.cpp:1276 msgid "{:s} has cast an illegal spell." msgstr "{:s} наклаў незаконную чару." #: Source/msg.cpp:1684 Source/multi.cpp:738 Source/multi.cpp:787 msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Гулец '{:s}' (узроўню {:d}) далучыўся да гульні" #: Source/msg.cpp:1994 msgid "The game ended" msgstr "Гульня скончана" #: Source/msg.cpp:2000 msgid "Unable to get level data" msgstr "Немагчыма дазнацца аб узроўні" #: Source/multi.cpp:198 msgid "Player '{:s}' just left the game" msgstr "Гулец '{:s}' выйшаў з гульні" #: Source/multi.cpp:201 msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Гулец '{:s}' забіў Д'яблу і выйшаў з гульні!" #: Source/multi.cpp:205 msgid "Player '{:s}' dropped due to timeout" msgstr "Гулец '{:s}' выляцеў з-за таймаута" #: Source/multi.cpp:789 msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Гулец '{:s}' (узроўню {:d}) ужо ў гульні" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:106 msgid "Mysterious" msgstr "Таямнічы" #: Source/objects.cpp:107 msgid "Hidden" msgstr "Схаваны" #: Source/objects.cpp:108 msgid "Gloomy" msgstr "Змрочны" #: Source/objects.cpp:110 Source/objects.cpp:117 msgid "Magical" msgstr "Магічны" #: Source/objects.cpp:111 msgid "Stone" msgstr "Каменны" #: Source/objects.cpp:112 msgid "Religious" msgstr "Набожны" #: Source/objects.cpp:113 msgid "Enchanted" msgstr "Зачараваны" #: Source/objects.cpp:114 msgid "Thaumaturgic" msgstr "Цудадзейны" #: Source/objects.cpp:115 msgid "Fascinating" msgstr "Чароўны" #: Source/objects.cpp:116 msgid "Cryptic" msgstr "Утоены" #: Source/objects.cpp:118 msgid "Eldritch" msgstr "Жудасны" #: Source/objects.cpp:119 msgid "Eerie" msgstr "Вусцішны" #: Source/objects.cpp:120 msgid "Divine" msgstr "Боскі" #: Source/objects.cpp:122 msgid "Sacred" msgstr "Святы" #: Source/objects.cpp:123 msgid "Spiritual" msgstr "Духоўны" #: Source/objects.cpp:124 msgid "Spooky" msgstr "Страшны" #: Source/objects.cpp:125 msgid "Abandoned" msgstr "Кінуты" #: Source/objects.cpp:126 msgid "Creepy" msgstr "Жахлівы" #: Source/objects.cpp:127 msgid "Quiet" msgstr "Ціхі" #: Source/objects.cpp:128 msgid "Secluded" msgstr "Самотны" #: Source/objects.cpp:129 msgid "Ornate" msgstr "Аздоблены" #: Source/objects.cpp:130 msgid "Glimmering" msgstr "Зіхатлівы" #: Source/objects.cpp:131 msgid "Tainted" msgstr "Запэцканы" #: Source/objects.cpp:132 msgid "Oily" msgstr "Маслены" #: Source/objects.cpp:133 msgid "Glowing" msgstr "Святлівы" #: Source/objects.cpp:134 msgid "Mendicant's" msgstr "Жабрацкі" #: Source/objects.cpp:135 msgid "Sparkling" msgstr "Бліскучы" #: Source/objects.cpp:137 msgid "Shimmering" msgstr "Мігатлівы" #: Source/objects.cpp:138 msgid "Solar" msgstr "Сонечны" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:140 msgid "Murphy's" msgstr "Мёрфева" #. TRANSLATORS: Book Title #: Source/objects.cpp:270 msgid "The Great Conflict" msgstr "Вялікае Змаганне" #. TRANSLATORS: Book Title #: Source/objects.cpp:271 msgid "The Wages of Sin are War" msgstr "Плата за грэх — вайна" #. TRANSLATORS: Book Title #: Source/objects.cpp:272 msgid "The Tale of the Horadrim" msgstr "Аповесць Харадрым" #. TRANSLATORS: Book Title #: Source/objects.cpp:273 msgid "The Dark Exile" msgstr "Цёмнае выгнанне" #. TRANSLATORS: Book Title #: Source/objects.cpp:274 msgid "The Sin War" msgstr "Вайна граху" #. TRANSLATORS: Book Title #: Source/objects.cpp:275 msgid "The Binding of the Three" msgstr "Палон Трох" #. TRANSLATORS: Book Title #: Source/objects.cpp:276 msgid "The Realms Beyond" msgstr "Пазамежжа" #. TRANSLATORS: Book Title #: Source/objects.cpp:277 msgid "Tale of the Three" msgstr "Аповесць аб Трох" #. TRANSLATORS: Book Title #: Source/objects.cpp:278 msgid "The Black King" msgstr "Чорны Кароль" #. TRANSLATORS: Book Title #: Source/objects.cpp:279 msgid "Journal: The Ensorcellment" msgstr "Дзённік: Зачараванасць" #. TRANSLATORS: Book Title #: Source/objects.cpp:280 msgid "Journal: The Meeting" msgstr "Дзённік: Сустрэча" #. TRANSLATORS: Book Title #: Source/objects.cpp:281 msgid "Journal: The Tirade" msgstr "Дзённік: Тырада" #. TRANSLATORS: Book Title #: Source/objects.cpp:282 msgid "Journal: His Power Grows" msgstr "Дзённік: Яго моц расце" #. TRANSLATORS: Book Title #: Source/objects.cpp:283 msgid "Journal: NA-KRUL" msgstr "Дзённік: НА-КРУЛ" #. TRANSLATORS: Book Title #: Source/objects.cpp:284 msgid "Journal: The End" msgstr "Дзённік: Канец" #. TRANSLATORS: Book Title #: Source/objects.cpp:285 msgid "A Spellbook" msgstr "Кніга Чараў" #: Source/objects.cpp:5383 msgid "Crucified Skeleton" msgstr "Распяты шкілет" #: Source/objects.cpp:5387 msgid "Lever" msgstr "Рычаг" #: Source/objects.cpp:5396 msgid "Open Door" msgstr "Адчыненыя дзверы" #: Source/objects.cpp:5398 msgid "Closed Door" msgstr "Зачыненыя дзверы" #: Source/objects.cpp:5400 msgid "Blocked Door" msgstr "Заваленыя дзверы" #: Source/objects.cpp:5405 msgid "Ancient Tome" msgstr "Старажытны фаліянт" #: Source/objects.cpp:5407 msgid "Book of Vileness" msgstr "Кніга Подласці" #: Source/objects.cpp:5412 msgid "Skull Lever" msgstr "Чэрапаў рычаг" #: Source/objects.cpp:5415 msgid "Mythical Book" msgstr "Міфічная кніга" #: Source/objects.cpp:5419 msgid "Small Chest" msgstr "Куфэрак" #: Source/objects.cpp:5423 msgid "Chest" msgstr "Скрыня" #: Source/objects.cpp:5428 msgid "Large Chest" msgstr "Куфар" #: Source/objects.cpp:5431 msgid "Sarcophagus" msgstr "Саркафаг" #: Source/objects.cpp:5434 msgid "Bookshelf" msgstr "Кніжная паліца" #: Source/objects.cpp:5438 msgid "Bookcase" msgstr "Кніжная шафа" #: Source/objects.cpp:5443 msgid "Pod" msgstr "Кокан" #: Source/objects.cpp:5445 msgid "Urn" msgstr "Урна" #: Source/objects.cpp:5447 msgid "Barrel" msgstr "Бочка" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:5451 msgid "{:s} Shrine" msgstr "{:s} Алтар" #: Source/objects.cpp:5454 msgid "Skeleton Tome" msgstr "Фаліянт шкілета" #: Source/objects.cpp:5457 msgid "Library Book" msgstr "Бібліятэчная кніга" #: Source/objects.cpp:5460 msgid "Blood Fountain" msgstr "Фантан крыві" #: Source/objects.cpp:5463 msgid "Decapitated Body" msgstr "Абезгалоўлены труп" #: Source/objects.cpp:5466 msgid "Book of the Blind" msgstr "Кніга Сляпых" #: Source/objects.cpp:5469 msgid "Book of Blood" msgstr "Кніга крыві" #: Source/objects.cpp:5472 msgid "Purifying Spring" msgstr "Ачышчальная крыніца" #: Source/objects.cpp:5479 Source/objects.cpp:5503 msgid "Weapon Rack" msgstr "Паліца са зброяю" #: Source/objects.cpp:5482 msgid "Goat Shrine" msgstr "Казліны алтар" #: Source/objects.cpp:5485 msgid "Cauldron" msgstr "Кацёл" #: Source/objects.cpp:5488 msgid "Murky Pool" msgstr "Мутны басейн" #: Source/objects.cpp:5491 msgid "Fountain of Tears" msgstr "Фантан слёз" #: Source/objects.cpp:5494 msgid "Steel Tome" msgstr "Стальны фаліянт" #: Source/objects.cpp:5497 msgid "Pedestal of Blood" msgstr "Крывавы п'едэстал" #: Source/objects.cpp:5506 msgid "Mushroom Patch" msgstr "Грыбны лапік" #: Source/objects.cpp:5509 msgid "Vile Stand" msgstr "Агідная стойка" #: Source/objects.cpp:5512 msgid "Slain Hero" msgstr "Загінулы герой" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:5519 msgid "Trapped {:s}" msgstr "{:s} з пасткай" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls leaver #: Source/objects.cpp:5524 msgid "{:s} (disabled)" msgstr "{:s} (немагчыма)" #: Source/options.cpp:420 Source/options.cpp:551 Source/options.cpp:557 msgid "ON" msgstr "УКЛ" #: Source/options.cpp:420 Source/options.cpp:549 Source/options.cpp:555 msgid "OFF" msgstr "ВЫКЛ" #: Source/options.cpp:539 msgid "Start Up" msgstr "Запуск" #: Source/options.cpp:539 msgid "Start Up Settings" msgstr "Запуск Наладаў" #: Source/options.cpp:540 msgid "Game Mode" msgstr "Рэжым Гульні" #: Source/options.cpp:540 msgid "Play Diablo or Hellfire." msgstr "Гуляць у Diablo ці Hellfire." #: Source/options.cpp:546 msgid "Restrict to Shareware" msgstr "Абмежаваць дэма-рэжым" #: Source/options.cpp:546 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Робіць гульню спалучанаю з дэма. Дазваляе мультыплэер з сябрамі, якія не " "маюць поўную копію Д'яблы." #: Source/options.cpp:547 Source/options.cpp:553 msgid "Intro" msgstr "Інтра" #: Source/options.cpp:547 Source/options.cpp:553 msgid "Shown Intro cinematic." msgstr "Уступны сінематык паказаны." #: Source/options.cpp:559 msgid "Splash" msgstr "Вітальны экран" #: Source/options.cpp:559 msgid "Shown splash screen." msgstr "Паказаны вітальны экран." #: Source/options.cpp:561 msgid "Logo and Title Screen" msgstr "Лагатып і тытульны экран" #: Source/options.cpp:562 msgid "Title Screen" msgstr "Тытульны экран" #: Source/options.cpp:581 msgid "Diablo specific Settings" msgstr "Спецыяльныя налады Diablo" #: Source/options.cpp:595 msgid "Hellfire specific Settings" msgstr "Спецыяльныя налады Hellfire" #: Source/options.cpp:609 msgid "Audio" msgstr "Аўдыя" #: Source/options.cpp:609 msgid "Audio Settings" msgstr "Налады аўдыя" #: Source/options.cpp:612 msgid "Walking Sound" msgstr "Гук хадзьбы" #: Source/options.cpp:612 msgid "Player emits sound when walking." msgstr "Гулец утварае гукі ходзячы." #: Source/options.cpp:613 msgid "Auto Equip Sound" msgstr "Гук аўтаэквіпа" #: Source/options.cpp:613 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "Аўтаматычны эквіп рэчаў утварае гук." #: Source/options.cpp:614 msgid "Item Pickup Sound" msgstr "Гук падбірання" #: Source/options.cpp:614 msgid "Picking up items emits the items pickup sound." msgstr "Падбіранне рэчаў утварае гук." #: Source/options.cpp:615 msgid "Sample Rate" msgstr "Частата Дыскрэтызацыі" #: Source/options.cpp:615 msgid "Output sample rate (Hz)." msgstr "Частата дыскрэтызацыі вывада гуку (Гц)." #: Source/options.cpp:616 msgid "Channels" msgstr "Каналы" #: Source/options.cpp:616 msgid "Number of output channels." msgstr "Колькасць каналаў вывада." #: Source/options.cpp:617 msgid "Buffer Size" msgstr "Памер буфера" #: Source/options.cpp:617 msgid "Buffer size (number of frames per channel)." msgstr "Памер буфера (колькасць кадраў на канал)" #: Source/options.cpp:618 msgid "Resampling Quality" msgstr "Якасць перадыскрэтызацыі" #: Source/options.cpp:618 msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgstr "Якасць перадыскрэтызатару, ад 0 (найменшы) да 10 (найбольшы)" #: Source/options.cpp:641 msgid "Resolution" msgstr "Раздзяляльнасць" #: Source/options.cpp:641 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Памяняць унутраную раздзяляльнасць гульні і вызначыць ваш бачны абсяг. " "Заўвага: Можа быць розніца з раздзяляльнасцю экрана, калі карыстуецца " "Павелічэнне маштабу, Цэлалікавае маштабаванне ці Падагнаць пад экран." #: Source/options.cpp:737 msgid "Graphics" msgstr "Графіка" #: Source/options.cpp:737 msgid "Graphics Settings" msgstr "Налады графікі" #: Source/options.cpp:738 msgid "Fullscreen" msgstr "Поўны экран" #: Source/options.cpp:738 msgid "Display the game in windowed or fullscreen mode." msgstr "Паказаць гульню ва акне ці ў поўным экране." #: Source/options.cpp:740 msgid "Fit to Screen" msgstr "Падагнаць пад экран" #: Source/options.cpp:740 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Аўтаматычна наладжваць акно з гульнёю да вашых суадносінаў бакоў і " "раздзяляльнасці." #: Source/options.cpp:743 msgid "Upscale" msgstr "Павелічэнне маштабу" #: Source/options.cpp:743 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Дазваляе маштабаванне ад раздзяляльнасці гульні да раздзяляльнасці вашага " "манітора. Забараняе змяняць раздзяляльнасць манітора і дазваляе змяняць " "памер вокнаў." #: Source/options.cpp:744 msgid "Scaling Quality" msgstr "Якасць маштабавання" #: Source/options.cpp:744 msgid "Enables optional filters to the output image when upscaling." msgstr "" "Уключае дадатковыя фільтры да выходнага відарыса пры павелічэнні маштабу." #: Source/options.cpp:746 msgid "Nearest Pixel" msgstr "Найбліжэйшы Піксель" #: Source/options.cpp:747 msgid "Bilinear" msgstr "Білінейны" #: Source/options.cpp:748 msgid "Anisotropic" msgstr "Анізатропны" #: Source/options.cpp:750 msgid "Integer Scaling" msgstr "Цэлалікавае маштабаванне" #: Source/options.cpp:750 msgid "Scales the image using whole number pixel ratio." msgstr "Маштабуе відарыс, карыстуючы суадносіны пікселей цэлага ліку." #: Source/options.cpp:751 msgid "Vertical Sync" msgstr "Вертыкальная Сінхранізацыя" #: Source/options.cpp:751 msgid "" "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " "frame. Disabling it can help with mouse lag on some systems." msgstr "" "Прымушае пачакаць Вертыкальную Сінхранізацыю. Забараняе эфект разрыву калі " "кадр вымалёўваецца. Калі яго выключыць у некаторых сістэмах ладзіць лаг " "мышкі." #: Source/options.cpp:754 msgid "Color Cycling" msgstr "Зацыкліванне колеру" #: Source/options.cpp:754 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" "Эфект зацыклівання колеру карыстуецца для анімацыі вады, лавы, і кіслаты." #: Source/options.cpp:755 msgid "Alternate nest art" msgstr "Альтэрнатыўны малюнак гнязда" #: Source/options.cpp:755 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" "У гульні выкарыстуецца альтэрнатыўная палітра для набору пліт гнязда для " "Hellfire." #: Source/options.cpp:757 msgid "Hardware Cursor" msgstr "Курсор апаратнага забеспячэння" #: Source/options.cpp:757 msgid "Use a hardware cursor" msgstr "Карыстаць курсор апаратнага забеспячэння" #: Source/options.cpp:758 msgid "Hardware Cursor For Items" msgstr "Курсор апаратнага забеспячэння для рэчаў" #: Source/options.cpp:758 msgid "Use a hardware cursor for items." msgstr "Карыстаць курсор апаратнага забеспячэння для рэчаў." #: Source/options.cpp:759 msgid "Hardware Cursor Maximum Size" msgstr "Максімальны размер курсора апаратнага забеспячэння" #: Source/options.cpp:759 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "Максімальная шырыня / даўжыня для курсора апаратнага забеспячэння." #: Source/options.cpp:761 msgid "FPS Limiter" msgstr "Абмежавальнік FPS" #: Source/options.cpp:761 msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." msgstr "" "FPS абмежаваны, каб пазбегнуць высокай нагрузкі на працэсар. Ліміт улічвае " "частату абнаўлення." #: Source/options.cpp:762 msgid "Show FPS" msgstr "Паказваць FPS" #: Source/options.cpp:762 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Адлюстроўвае FPS у левым верхнім куце экрана." #: Source/options.cpp:763 msgid "Show health values" msgstr "Паказаць велічыню здароўя" #: Source/options.cpp:763 msgid "Displays current / max health value on health globe." msgstr "" "Адлюстроўвае цяперашняе / максімальнае значэнне здароўя на шары здароўя." #: Source/options.cpp:764 msgid "Show mana values" msgstr "Паказаць велічыню маны" #: Source/options.cpp:764 msgid "Displays current / max mana value on mana globe." msgstr "Паказаць цяперашнюю / максімальную велічыню маны на шары маны." #: Source/options.cpp:813 msgid "Gameplay" msgstr "Гульня" #: Source/options.cpp:813 msgid "Gameplay Settings" msgstr "Налады гульні" #: Source/options.cpp:815 msgid "Run in Town" msgstr "Бегчы ў Горадзе" #: Source/options.cpp:815 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Дазволіць бег/хуткую хаду ў горадзе для Diablo і Hellfire. Гэтая опцыя была " "ўведзена ў пашырэнні." #: Source/options.cpp:816 msgid "Grab Input" msgstr "Захапіць мыш" #: Source/options.cpp:816 msgid "When enabled mouse is locked to the game window." msgstr "Калі ўключана, мыш фіксуецца ў акне гульні." #: Source/options.cpp:817 msgid "Theo Quest" msgstr "Заданне Тэа" #: Source/options.cpp:817 msgid "Enable Little Girl quest." msgstr "Уключыць квэст \"Маленькая дзяўчынка\"." #: Source/options.cpp:818 msgid "Cow Quest" msgstr "Заданне Каровы" #: Source/options.cpp:818 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "Уключыць квэст Джэрсі. Фермер Лестэр замяняецца." #: Source/options.cpp:819 msgid "Friendly Fire" msgstr "Агонь па сваіх" #: Source/options.cpp:819 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Дазволіць наносіць шкоду стрэламі/чарамі паміж гульцамі ў " "шматкарыстальніцкім рэжыме, нават калі ўключаны сяброўскі рэжым." #: Source/options.cpp:820 msgid "Test Bard" msgstr "Проба Барда" #: Source/options.cpp:820 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Прымусіць тып персанажа \"Бард\" з'явіцца ў меню выбара героя." #: Source/options.cpp:821 msgid "Test Barbarian" msgstr "Проба Варвара" #: Source/options.cpp:821 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "Прымусіць тып персанажа \"Бард\" з'явіцца ў меню выбара героя." #: Source/options.cpp:822 msgid "Experience Bar" msgstr "Палоска досведу" #: Source/options.cpp:822 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "У карыстальніцкі інтэрфэйс унізе экрана дадаецца палоска досведу." #: Source/options.cpp:823 msgid "Enemy Health Bar" msgstr "Палоска здароўя ворагаў" #: Source/options.cpp:823 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "Наверсе экрана паказваецца палоска здароўя ворага." #: Source/options.cpp:824 msgid "Auto Gold Pickup" msgstr "Аўта-падняцце золата" #: Source/options.cpp:824 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Золота аўтаматычна збіраецца, калі блізка да гульца." #: Source/options.cpp:825 msgid "Auto Elixir Pickup" msgstr "Аўта-падняцце эліксіраў" #: Source/options.cpp:825 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "Эліксіры падымаюцца аўтаматычна, калі блізка да гульца." #: Source/options.cpp:826 msgid "Auto Pickup in Town" msgstr "Аўта-падняцце ў Горадзе" #: Source/options.cpp:826 msgid "Automatically pickup items in town." msgstr "Аўтаматычна падымаць рэчы ў горадзе." #: Source/options.cpp:827 msgid "Adria Refills Mana" msgstr "Эйдрыя Папаўняе Ману" #: Source/options.cpp:827 msgid "Adria will refill your mana when you visit her shop." msgstr "Эйдрыя будзе папаўняць вашу ману, пры наведванні яе крамы." #: Source/options.cpp:828 msgid "Auto Equip Weapons" msgstr "Аўта-эквіп зброі" #: Source/options.cpp:828 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" "Зброя будзе аўтаматычна экіпіравана пры атрыманні або куплі, калі ўключана." #: Source/options.cpp:829 msgid "Auto Equip Armor" msgstr "Аўта-эквіп брані" #: Source/options.cpp:829 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" "Браня будзе аўтаматычна экіпіравана пры атрыманні або куплі, калі ўключана." #: Source/options.cpp:830 msgid "Auto Equip Helms" msgstr "Аўта-эквіп шаломаў" #: Source/options.cpp:830 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" "Шаломы будуць аўтаматычна экіпіраваны пры атрыманні або куплі, калі ўключана." #: Source/options.cpp:831 msgid "Auto Equip Shields" msgstr "Аўта-эквіп шчытоў" #: Source/options.cpp:831 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" "Шчыты будуць аўтаматычна экіпіраваны пры атрыманні або куплі, калі ўключана." #: Source/options.cpp:832 msgid "Auto Equip Jewelry" msgstr "Аўта-эквіп каштоўнасцяў" #: Source/options.cpp:832 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" "Каштоўнасці будуць аўтаматычна экіпіраваны пры атрыманні або куплі, калі " "гэтая функцыя ўключана." #: Source/options.cpp:833 msgid "Randomize Quests" msgstr "Выпадковы парадак заданняў" #: Source/options.cpp:833 msgid "Randomly selecting available quests for new games." msgstr "Выпадковы выбар даступных заданняў для новых гульняў." #: Source/options.cpp:834 msgid "Show Monster Type" msgstr "Паказаць тып монстра" #: Source/options.cpp:834 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Пры навядзенні курсора на монстра яго тып будзе адлюстроўвацца ў полі " "апісання ў карыстальніцкім інтэрфейсе." #: Source/options.cpp:835 msgid "Auto Refill Belt" msgstr "Аўтазапаўненне ячэйкі пояса" #: Source/options.cpp:835 msgid "Refill belt from inventory when belt item is consumed." msgstr "Укладваць з інвентара ў пояс, калі рэч на поясе спажываецца." #: Source/options.cpp:836 msgid "Disable Crippling Shrines" msgstr "Адключыць шкодныя Алтары" #: Source/options.cpp:836 msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines " "and Sacred Shrines are not able to be clicked on and labeled as disabled." msgstr "" "Калі ўключана, нельга націснуць на Катлы, Чароўныя Алтары, Казліныя Алтары, " "Аздобленыя Алтары, Святыя Алтары, і яны паказаныя як нерабочыя." #: Source/options.cpp:837 msgid "Quick Cast" msgstr "Хуткія чары" #: Source/options.cpp:837 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "Гарачыя клавішы Чар імгненна накладваюць чары." #: Source/options.cpp:838 msgid "Heal Potion Pickup" msgstr "Падбіранне зелляў лячэння" #: Source/options.cpp:838 msgid "Number of Healing potions to pick up automatically." msgstr "Колькасць зелляў лячэння, што аўтаматычна падбяруцца." #: Source/options.cpp:839 msgid "Full Heal Potion Pickup" msgstr "Падбіранне зелляў поўнага лячэння" #: Source/options.cpp:839 msgid "Number of Full Healing potions to pick up automatically." msgstr "Колькасць зелляў поўнага лячэння, што аўтаматычна падбяруцца." #: Source/options.cpp:840 msgid "Mana Potion Pickup" msgstr "Падбіранне зелляў маны" #: Source/options.cpp:840 msgid "Number of Mana potions to pick up automatically." msgstr "Колькасць зелляў маны, што аўтаматычна падбяруцца." #: Source/options.cpp:841 msgid "Full Mana Potion Pickup" msgstr "Падбіранне зелляў поўнай маны" #: Source/options.cpp:841 msgid "Number of Full Mana potions to pick up automatically." msgstr "Колькасць зелляў поўнай маны, што аўтаматычна падбяруцца." #: Source/options.cpp:842 msgid "Rejuvenation Potion Pickup" msgstr "Падбіранне зелляў аднаўлення" #: Source/options.cpp:842 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "Колькасць зелляў аднаўлення, што аўтаматычна падбяруцца." #: Source/options.cpp:843 msgid "Full Rejuvenation Potion Pickup" msgstr "Падбіранне зелляў поўнага аднаўлення" #: Source/options.cpp:843 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "Колькасць зелляў поўнага аднаўлення, што аўтаматычна падбяруцца." #: Source/options.cpp:886 msgid "Controller" msgstr "Кантролер" #: Source/options.cpp:886 msgid "Controller Settings" msgstr "Налады кантролера" #: Source/options.cpp:895 msgid "Network" msgstr "Сетка" #: Source/options.cpp:895 msgid "Network Settings" msgstr "Налады Сеткі" #: Source/options.cpp:907 msgid "Chat" msgstr "Чат" #: Source/options.cpp:907 msgid "Chat Settings" msgstr "Налады чата" #: Source/options.cpp:916 Source/options.cpp:1032 msgid "Language" msgstr "Мова" #: Source/options.cpp:916 msgid "Define what language to use in game." msgstr "Выбраць мову для гульні." #: Source/options.cpp:1032 msgid "Language Settings" msgstr "Налады Мовы" #: Source/options.cpp:1044 msgid "Keymapping" msgstr "Раскладка" #: Source/options.cpp:1044 msgid "Keymapping Settings" msgstr "Налады раскладкі" #: Source/panels/charpanel.cpp:130 msgid "Level" msgstr "Узровень" #: Source/panels/charpanel.cpp:132 msgid "Experience" msgstr "Досвед" #: Source/panels/charpanel.cpp:134 msgid "Next level" msgstr "Наступны ўзровень" #: Source/panels/charpanel.cpp:142 msgid "Base" msgstr "Базавы" #: Source/panels/charpanel.cpp:143 msgid "Now" msgstr "Цяперашні" #: Source/panels/charpanel.cpp:144 msgid "Strength" msgstr "Моц" #: Source/panels/charpanel.cpp:148 msgid "Magic" msgstr "Магія" #: Source/panels/charpanel.cpp:152 msgid "Dexterity" msgstr "Спрыт" #: Source/panels/charpanel.cpp:155 msgid "Vitality" msgstr "Жывучасць" #: Source/panels/charpanel.cpp:158 msgid "Points to distribute" msgstr "Ачкі на размеркаванне" #: Source/panels/charpanel.cpp:168 msgid "Armor class" msgstr "Клас даспехаў" #: Source/panels/charpanel.cpp:170 msgid "To hit" msgstr "Ударыць" #: Source/panels/charpanel.cpp:172 msgid "Damage" msgstr "Шкода" #: Source/panels/charpanel.cpp:179 msgid "Life" msgstr "Жыццё" #: Source/panels/charpanel.cpp:183 msgid "Mana" msgstr "Мана" #: Source/panels/charpanel.cpp:188 msgid "Resist magic" msgstr "Супраціў магіі" #: Source/panels/charpanel.cpp:190 msgid "Resist fire" msgstr "Супраціў агню" #: Source/panels/charpanel.cpp:192 msgid "Resist lightning" msgstr "Супраціў маланцы" #: Source/panels/mainpanel.cpp:62 Source/panels/mainpanel.cpp:106 #: Source/panels/mainpanel.cpp:108 msgid "voice" msgstr "голас" #: Source/panels/mainpanel.cpp:84 msgid "char" msgstr "перс" #: Source/panels/mainpanel.cpp:85 msgid "quests" msgstr "квэсты" #: Source/panels/mainpanel.cpp:86 msgid "map" msgstr "мапа" #: Source/panels/mainpanel.cpp:87 msgid "menu" msgstr "меню" #: Source/panels/mainpanel.cpp:88 msgid "inv" msgstr "інв" #: Source/panels/mainpanel.cpp:89 msgid "spells" msgstr "чары" #: Source/panels/mainpanel.cpp:101 Source/panels/mainpanel.cpp:103 #: Source/panels/mainpanel.cpp:105 msgid "mute" msgstr "мут" #: Source/panels/spell_book.cpp:155 Source/panels/spell_list.cpp:162 msgid "Skill" msgstr "Уменне" #: Source/panels/spell_book.cpp:159 msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Посах ({:d} зарад)" msgstr[1] "Посах ({:d} зарады)" msgstr[2] "Посах ({:d} зарадаў)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:164 msgctxt "spellbook" msgid "Level {:d}" msgstr "Узровень {:d}" #: Source/panels/spell_book.cpp:166 msgid "Unusable" msgstr "Нягодны" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:174 msgid "Heals: {:d} - {:d}" msgstr "Лечыць: {:d} – {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:176 msgid "Damage: {:d} - {:d}" msgstr "Шкода: {:d} – {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:180 msgid "Dmg: 1/3 target hp" msgstr "Шкд: 1/3 хп цэлі" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:182 msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Мана: {:d}" #: Source/panels/spell_list.cpp:169 msgid "Spell" msgstr "Чара" #: Source/panels/spell_list.cpp:172 msgid "Damages undead only" msgstr "Шкодзіць толькі наўцам" #: Source/panels/spell_list.cpp:183 msgid "Scroll" msgstr "Скрутак" #: Source/panels/spell_list.cpp:204 msgid "Spell Hotkey {:s}" msgstr "Гарачая клавіша Чары {:s}" #: Source/pfile.cpp:266 msgid "Failed to open player archive for writing." msgstr "Не ўдалося адкрыць архіў гульца для запісу." #: Source/pfile.cpp:308 msgid "Failed to open stash archive for writing." msgstr "Не ўдалося адкрыць схаваны архіў для запісу." #: Source/pfile.cpp:419 msgid "Unable to open archive" msgstr "Немагчыма адкрыць архіў" #: Source/pfile.cpp:421 msgid "Unable to load character" msgstr "Немагчыма загрузіць персанажа" #: Source/pfile.cpp:445 Source/pfile.cpp:465 msgid "Unable to read to save file archive" msgstr "Немагчыма прачытаць, каб захаваць архіў файла" #: Source/pfile.cpp:484 msgid "Unable to write to save file archive" msgstr "Немагчыма запісаць, каб захаваць архіў файла" #: Source/plrmsg.cpp:83 Source/qol/chatlog.cpp:126 msgid "{:s} (lvl {:d}): " msgstr "{:s} (узр {:d}): " #: Source/qol/chatlog.cpp:154 msgid "Chat History (Messages: {:d})" msgstr "Гісторыя чата (Паведамленняў: {:d})" #: Source/qol/itemlabels.cpp:72 msgid "{:s} gold" msgstr "{:s} золата" #: Source/qol/stash.cpp:619 msgid "How many gold pieces do you want to withdraw?" msgstr "Колькі манет хочаце пакінуць?" #. TRANSLATORS: Thousands separator #: Source/qol/xpbar.cpp:61 msgid "," msgstr "," #: Source/qol/xpbar.cpp:146 msgid "Level {:d}" msgstr "Узровень {:d}" #: Source/qol/xpbar.cpp:152 Source/qol/xpbar.cpp:160 msgid "Experience: {:s}" msgstr "Досвед {:s}" #: Source/qol/xpbar.cpp:153 msgid "Maximum Level" msgstr "Максімальны ўзровень" #: Source/qol/xpbar.cpp:161 msgid "Next Level: {:s}" msgstr "Наступны ўзровень {:s}" #: Source/qol/xpbar.cpp:162 msgid "{:s} to Level {:d}" msgstr "{:s} да ўзроўню {:d}" #. TRANSLATORS: Quest Name Block #: Source/quests.cpp:44 msgid "The Magic Rock" msgstr "Магічны камень" #: Source/quests.cpp:46 msgid "Gharbad The Weak" msgstr "Кволы Гарбад" #: Source/quests.cpp:47 msgid "Zhar the Mad" msgstr "Шалёны Зар" #: Source/quests.cpp:48 msgid "Lachdanan" msgstr "Лакданан" #: Source/quests.cpp:50 msgid "The Butcher" msgstr "Мяснік" #: Source/quests.cpp:51 msgid "Ogden's Sign" msgstr "Знак Огдэна" #: Source/quests.cpp:52 msgid "Halls of the Blind" msgstr "Залы Сляпых" #: Source/quests.cpp:53 msgid "Valor" msgstr "Адвага" #: Source/quests.cpp:55 msgid "Warlord of Blood" msgstr "Крывавы ваявода" #: Source/quests.cpp:56 msgid "The Curse of King Leoric" msgstr "Праклён караля Леорыка" #: Source/quests.cpp:57 Source/setmaps.cpp:27 msgid "Poisoned Water Supply" msgstr "Атручаны запас вады" #. TRANSLATORS: Quest Map #: Source/quests.cpp:58 Source/quests.cpp:94 msgid "The Chamber of Bone" msgstr "Пакой Касцей" #: Source/quests.cpp:59 msgid "Archbishop Lazarus" msgstr "Архібіскуп Лазар" #: Source/quests.cpp:60 msgid "Grave Matters" msgstr "Магільныя справы" #: Source/quests.cpp:61 msgid "Farmer's Orchard" msgstr "Садок фермера" #: Source/quests.cpp:62 msgid "Little Girl" msgstr "Дзяўчынка" #: Source/quests.cpp:63 msgid "Wandering Trader" msgstr "Вандроўны гандляр" #: Source/quests.cpp:64 msgid "The Defiler" msgstr "Паганнік" #: Source/quests.cpp:65 msgid "Na-Krul" msgstr "На-Крул" #: Source/quests.cpp:66 Source/trigs.cpp:449 msgid "Cornerstone of the World" msgstr "Краевугольны камень свету" #. TRANSLATORS: Quest Name Block end #: Source/quests.cpp:67 msgid "The Jersey's Jersey" msgstr "Джэрсі Джэрсі" #. TRANSLATORS: Quest Map #: Source/quests.cpp:93 msgid "King Leoric's Tomb" msgstr "Грабніца караля Леорыка" #. TRANSLATORS: Quest Map #: Source/quests.cpp:95 Source/setmaps.cpp:26 msgid "Maze" msgstr "Лабірынт" #. TRANSLATORS: Quest Map #: Source/quests.cpp:96 msgid "A Dark Passage" msgstr "Цёмны Пераход" #. TRANSLATORS: Quest Map #: Source/quests.cpp:97 msgid "Unholy Altar" msgstr "Паганы алтар" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:451 msgid "To {:s}" msgstr "Для {:s}" #: Source/setmaps.cpp:24 msgid "Skeleton King's Lair" msgstr "Логава Караля шкілетаў" #: Source/setmaps.cpp:25 msgid "Chamber of Bone" msgstr "Пакой Касцей" #: Source/setmaps.cpp:28 msgid "Archbishop Lazarus' Lair" msgstr "Логава архібіскупа Лазара" #: Source/spelldat.cpp:16 msgctxt "spell" msgid "Firebolt" msgstr "Вогненная бліскавіца" #: Source/spelldat.cpp:17 msgctxt "spell" msgid "Healing" msgstr "Лячэнне" #: Source/spelldat.cpp:18 msgctxt "spell" msgid "Lightning" msgstr "Маланка" #: Source/spelldat.cpp:19 msgctxt "spell" msgid "Flash" msgstr "Бліск" #: Source/spelldat.cpp:20 msgctxt "spell" msgid "Identify" msgstr "Выявіць" #: Source/spelldat.cpp:21 msgctxt "spell" msgid "Fire Wall" msgstr "Сцяна агню" #: Source/spelldat.cpp:22 msgctxt "spell" msgid "Town Portal" msgstr "Партал у Горад" #: Source/spelldat.cpp:23 msgctxt "spell" msgid "Stone Curse" msgstr "Каменны праклён" #: Source/spelldat.cpp:24 msgctxt "spell" msgid "Infravision" msgstr "Скрозьбачанне" #: Source/spelldat.cpp:25 msgctxt "spell" msgid "Phasing" msgstr "Перамяшчэнне" #: Source/spelldat.cpp:26 msgctxt "spell" msgid "Mana Shield" msgstr "Шчыт маны" #: Source/spelldat.cpp:27 msgctxt "spell" msgid "Fireball" msgstr "Вогненны шар" #: Source/spelldat.cpp:28 msgctxt "spell" msgid "Guardian" msgstr "Стораж" #: Source/spelldat.cpp:29 msgctxt "spell" msgid "Chain Lightning" msgstr "Маланкавы ланцуг" #: Source/spelldat.cpp:30 msgctxt "spell" msgid "Flame Wave" msgstr "Хваля полымя" #: Source/spelldat.cpp:31 msgctxt "spell" msgid "Doom Serpents" msgstr "Змеі пагібелі" #: Source/spelldat.cpp:32 msgctxt "spell" msgid "Blood Ritual" msgstr "Крывавы рытуал" #: Source/spelldat.cpp:33 msgctxt "spell" msgid "Nova" msgstr "Новая Зорка" #: Source/spelldat.cpp:34 msgctxt "spell" msgid "Invisibility" msgstr "Нябачнасць" #: Source/spelldat.cpp:35 msgctxt "spell" msgid "Inferno" msgstr "Пекла" #: Source/spelldat.cpp:36 msgctxt "spell" msgid "Golem" msgstr "Голем" #: Source/spelldat.cpp:37 msgctxt "spell" msgid "Rage" msgstr "Гнеў" #: Source/spelldat.cpp:38 msgctxt "spell" msgid "Teleport" msgstr "Тэлепорт" #: Source/spelldat.cpp:39 msgctxt "spell" msgid "Apocalypse" msgstr "Апакаліпсіс" #: Source/spelldat.cpp:40 msgctxt "spell" msgid "Etherealize" msgstr "Этэрызаваць" #: Source/spelldat.cpp:41 msgctxt "spell" msgid "Item Repair" msgstr "Наладка рэчы" #: Source/spelldat.cpp:42 msgctxt "spell" msgid "Staff Recharge" msgstr "Перазарадка посаха" #: Source/spelldat.cpp:43 msgctxt "spell" msgid "Trap Disarm" msgstr "Абяскоджванне пасткі" #: Source/spelldat.cpp:44 msgctxt "spell" msgid "Elemental" msgstr "Элементаль" #: Source/spelldat.cpp:45 msgctxt "spell" msgid "Charged Bolt" msgstr "Зараджаная бліскавіца" #: Source/spelldat.cpp:46 msgctxt "spell" msgid "Holy Bolt" msgstr "Святая бліскавіца" #: Source/spelldat.cpp:47 msgctxt "spell" msgid "Resurrect" msgstr "Уваскрасіць" #: Source/spelldat.cpp:48 msgctxt "spell" msgid "Telekinesis" msgstr "Тэлекінэз" #: Source/spelldat.cpp:49 msgctxt "spell" msgid "Heal Other" msgstr "Лячы Блізкага" #: Source/spelldat.cpp:50 msgctxt "spell" msgid "Blood Star" msgstr "Крывавая зорка" #: Source/spelldat.cpp:51 msgctxt "spell" msgid "Bone Spirit" msgstr "Дух косці" #: Source/spelldat.cpp:52 msgctxt "spell" msgid "Mana" msgstr "Мана" #: Source/spelldat.cpp:53 msgctxt "spell" msgid "the Magi" msgstr "Мудрацы" #: Source/spelldat.cpp:54 msgctxt "spell" msgid "the Jester" msgstr "Блазан" #: Source/spelldat.cpp:55 msgctxt "spell" msgid "Lightning Wall" msgstr "Сцяна маланкі" #: Source/spelldat.cpp:56 msgctxt "spell" msgid "Immolation" msgstr "Спаленне" #: Source/spelldat.cpp:57 msgctxt "spell" msgid "Warp" msgstr "Скажэнне" #: Source/spelldat.cpp:58 msgctxt "spell" msgid "Reflect" msgstr "Адбіццё" #: Source/spelldat.cpp:59 msgctxt "spell" msgid "Berserk" msgstr "Берсерк" #: Source/spelldat.cpp:60 msgctxt "spell" msgid "Ring of Fire" msgstr "Пярсцёнак агню" #: Source/spelldat.cpp:61 msgctxt "spell" msgid "Search" msgstr "Пошук" #: Source/spelldat.cpp:62 msgctxt "spell" msgid "Rune of Fire" msgstr "Руна агню" #: Source/spelldat.cpp:63 msgctxt "spell" msgid "Rune of Light" msgstr "Руна святла" #: Source/spelldat.cpp:64 msgctxt "spell" msgid "Rune of Nova" msgstr "Руна новай зоркі" #: Source/spelldat.cpp:65 msgctxt "spell" msgid "Rune of Immolation" msgstr "Руна спалення" #: Source/spelldat.cpp:66 msgctxt "spell" msgid "Rune of Stone" msgstr "Руна каменя" #: Source/stores.cpp:93 msgid "Griswold" msgstr "Грызвальд" #: Source/stores.cpp:94 msgid "Pepin" msgstr "Піпін" #: Source/stores.cpp:96 msgid "Ogden" msgstr "Огдэн" #: Source/stores.cpp:97 msgid "Cain" msgstr "Каін" #: Source/stores.cpp:98 msgid "Farnham" msgstr "Фарнам" #: Source/stores.cpp:99 msgid "Adria" msgstr "Эйдрыя" #: Source/stores.cpp:100 Source/stores.cpp:1313 msgid "Gillian" msgstr "Джыльен" #: Source/stores.cpp:101 msgid "Wirt" msgstr "Вірт" #: Source/stores.cpp:220 Source/stores.cpp:227 msgid "Back" msgstr "Назад" #: Source/stores.cpp:249 Source/stores.cpp:255 msgid ", " msgstr ", " #: Source/stores.cpp:266 msgid "Damage: {:d}-{:d} " msgstr "Шкода {:d}-{:d} " #: Source/stores.cpp:268 msgid "Armor: {:d} " msgstr "Браня: {:d} " #: Source/stores.cpp:270 msgid "Dur: {:d}/{:d}, " msgstr "Мац: {:d}/{:d}, " #: Source/stores.cpp:272 msgid "Indestructible, " msgstr "Незнішчальны " #: Source/stores.cpp:280 msgid "No required attributes" msgstr "Няма патрэбных атрыбутаў" #: Source/stores.cpp:312 Source/stores.cpp:1064 Source/stores.cpp:1300 msgid "Welcome to the" msgstr "Вітаем у" #: Source/stores.cpp:313 msgid "Blacksmith's shop" msgstr "Кавальская майстэрня" #: Source/stores.cpp:314 Source/stores.cpp:668 Source/stores.cpp:1066 #: Source/stores.cpp:1124 Source/stores.cpp:1302 Source/stores.cpp:1314 #: Source/stores.cpp:1327 msgid "Would you like to:" msgstr "Ці хацелі б вы:" #: Source/stores.cpp:315 msgid "Talk to Griswold" msgstr "Пагаварыць з Грызвальдам" #: Source/stores.cpp:316 msgid "Buy basic items" msgstr "Купіць базавыя рэчы" #: Source/stores.cpp:317 msgid "Buy premium items" msgstr "Купіць асаблівыя рэчы" #: Source/stores.cpp:318 Source/stores.cpp:671 msgid "Sell items" msgstr "Прадаць" #: Source/stores.cpp:319 msgid "Repair items" msgstr "Наладзіць" #: Source/stores.cpp:320 msgid "Leave the shop" msgstr "Сысці з крамы" #: Source/stores.cpp:363 Source/stores.cpp:728 Source/stores.cpp:1101 msgid "I have these items for sale:" msgstr "Маю тут на продаж:" #: Source/stores.cpp:428 msgid "I have these premium items for sale:" msgstr "На продаж маю асаблівыя рэчы:" #: Source/stores.cpp:547 Source/stores.cpp:821 msgid "You have nothing I want." msgstr "Не маеш, што мне трэба." #: Source/stores.cpp:558 Source/stores.cpp:833 msgid "Which item is for sale?" msgstr "Што прадаеш?" #: Source/stores.cpp:629 msgid "You have nothing to repair." msgstr "Няма чаго ладзіць." #: Source/stores.cpp:640 msgid "Repair which item?" msgstr "Якую рэч наладзіць?" #: Source/stores.cpp:667 msgid "Witch's shack" msgstr "Ведзьмін будан" #: Source/stores.cpp:669 msgid "Talk to Adria" msgstr "Пагаварыць з Эйдрыяй" #: Source/stores.cpp:670 Source/stores.cpp:1068 msgid "Buy items" msgstr "Купіць" #: Source/stores.cpp:672 msgid "Recharge staves" msgstr "Перазарадзіць посахі" #: Source/stores.cpp:673 msgid "Leave the shack" msgstr "Сысці з будана" #: Source/stores.cpp:895 msgid "You have nothing to recharge." msgstr "Няма чаго перазараджаць." #: Source/stores.cpp:906 msgid "Recharge which item?" msgstr "Якую рэч перазарадзіць?" #: Source/stores.cpp:919 msgid "You do not have enough gold" msgstr "Не хапае золата" #: Source/stores.cpp:927 msgid "You do not have enough room in inventory" msgstr "Не хапае месца ў інтвентары" #: Source/stores.cpp:966 msgid "Do we have a deal?" msgstr "Ну, згода?" #: Source/stores.cpp:969 msgid "Are you sure you want to identify this item?" msgstr "Праўда хочаце выявіць гэту рэч?" #: Source/stores.cpp:975 msgid "Are you sure you want to buy this item?" msgstr "Праўда хочаце гэта купіць?" #: Source/stores.cpp:978 msgid "Are you sure you want to recharge this item?" msgstr "Праўда хочаце перазарадзіць гэту рэч?" #: Source/stores.cpp:982 msgid "Are you sure you want to sell this item?" msgstr "Праўда хочаце гэта прадаць?" #: Source/stores.cpp:985 msgid "Are you sure you want to repair this item?" msgstr "Праўда хочаце наладзіць гэту рэч?" #: Source/stores.cpp:999 Source/towners.cpp:150 msgid "Wirt the Peg-legged boy" msgstr "Аднаногі хлопчык Вірт" #: Source/stores.cpp:1002 Source/stores.cpp:1009 msgid "Talk to Wirt" msgstr "Пагаварыць з Віртам" #: Source/stores.cpp:1003 msgid "I have something for sale," msgstr "Маю тут на продаж," #: Source/stores.cpp:1004 msgid "but it will cost 50 gold" msgstr "але проста каб зірнуць" #: Source/stores.cpp:1005 msgid "just to take a look. " msgstr "50 золата. " #: Source/stores.cpp:1006 msgid "What have you got?" msgstr "Што маеш?" #: Source/stores.cpp:1007 Source/stores.cpp:1010 Source/stores.cpp:1127 #: Source/stores.cpp:1317 msgid "Say goodbye" msgstr "Развітацца" #: Source/stores.cpp:1020 msgid "I have this item for sale:" msgstr "Прадаю вось гэту рэч:" #: Source/stores.cpp:1042 msgid "Leave" msgstr "Сысці" #: Source/stores.cpp:1065 msgid "Healer's home" msgstr "Дом лекара" #: Source/stores.cpp:1067 msgid "Talk to Pepin" msgstr "Пагаварыць з Піпінам" #: Source/stores.cpp:1069 msgid "Leave Healer's home" msgstr "Сысці з дома лекара" #: Source/stores.cpp:1123 msgid "The Town Elder" msgstr "Гарадскі старэйшына" #: Source/stores.cpp:1125 msgid "Talk to Cain" msgstr "Пагаварыць з Кейнам" #: Source/stores.cpp:1126 msgid "Identify an item" msgstr "Выявіць рэч" #: Source/stores.cpp:1219 msgid "You have nothing to identify." msgstr "Няма чаго выяўляць." #: Source/stores.cpp:1230 msgid "Identify which item?" msgstr "Якую рэч выявіць?" #: Source/stores.cpp:1247 msgid "This item is:" msgstr "Гэта рэч:" #: Source/stores.cpp:1250 msgid "Done" msgstr "Гатова" #: Source/stores.cpp:1259 msgid "Talk to {:s}" msgstr "Пагаварыць з {:s}" #: Source/stores.cpp:1262 msgid "Talking to {:s}" msgstr "Гаворыць з {:s}" #: Source/stores.cpp:1263 msgid "is not available" msgstr "недаступны" #: Source/stores.cpp:1264 msgid "in the shareware" msgstr "ва ўмоўна-бясплатнай версіі" #: Source/stores.cpp:1265 msgid "version" msgstr "версія" #: Source/stores.cpp:1292 msgid "Gossip" msgstr "Пляткарыць" #: Source/stores.cpp:1301 msgid "Rising Sun" msgstr "Узыходнае Сонца" #: Source/stores.cpp:1303 msgid "Talk to Ogden" msgstr "Пагаварыць з Огдэнам" #: Source/stores.cpp:1304 msgid "Leave the tavern" msgstr "Сысці з карчмы" #: Source/stores.cpp:1315 msgid "Talk to Gillian" msgstr "Пагаварыць з Джыліен" #: Source/stores.cpp:1316 msgid "Access Storage" msgstr "Адкрыць сховішча" #: Source/stores.cpp:1326 Source/towners.cpp:205 msgid "Farnham the Drunk" msgstr "П'яніца Фарнам" #: Source/stores.cpp:1328 msgid "Talk to Farnham" msgstr "Пагаварыць з Фарнамам" #: Source/stores.cpp:1329 msgid "Say Goodbye" msgstr "Развітацца" #: Source/stores.cpp:2458 msgid "Your gold: {:s}" msgstr "Ваша золата: {:s}" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:15 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" " А-а, цікавішся нашым Каралём, гм? Трагічнае падзенне Леорыка было страшным " "ударам для гэтай зямлі. Людзі заўсёды любілі Караля, зараз жа яны да смерці " "страшацца яго. Я ўсё пытаю сябе, як ён так аддаліўся ад Святла, бо Леорык " "заўсёды быў з найсвяцейшых між людзьмі. Толькі самыя паганыя сілы Пекла " "маглі так вынішчыць чалавека знутры..." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:17 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "Вёсцы патрэбна твая дапамога, добры чалавек! Колькі месяцаў таму Прынца " "Альбрэхта, Караля Леорыкава сына, скралі. Кароль раз'юшыўся і пачаў " "абшнарваць тут усюль, шукаючы дзіця. З кожным днём Леорык усё больш шалеў ад " "гора. Ён абвінаваціў ва ўсім нявінных гараджан і жорстка пакараў многіх " "смерцю. Менш паловы з нас вырабілася ад яго вар'яцтва...\n" "Рыцары Караля і Святары стараліся яго супакоіць, але ён і на іх узбурыўся, " "і, на жаль, ім давялося забіць яго. На самым сконе ён страшна пракляў сваіх " "былых прыхільнікаў. Ён прысягнуў, што яны будуць служыць яму ў цемры " "вечна...\n" "І тут пачалося такое, я ўжо думаў горш не можа быць! Былы Кароль прачнуўся " "ад вечнага сну, і цяпер у Лабірынце ўзначаліў легіён мярцоў. Яго цела " "пахавалі ў грабніцы на трэцім ўзроўні пад Саборам. Калі ласка, добры " "чалавек, сунімі яго душу, зніжчы яго праклятае аблічча..." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:19 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Як я і гаварыў, добры чалавек, Караля пахавалі на тры паверхі ўніз. Ён там, " "у агіднай цемры, толькі і чакае магчымасці знішчыць гэты край..." #. TRANSLATORS: Quest dialog spoken by Ogden (Quest End) #: Source/textdat.cpp:21 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "Праклён нашага Караля прайшоў, але баюся, што вінавата тут большае зло. " "Аднак мы яшчэ можам выратавацца ад цемры, што ахінае наш край: твая перамога " "– добры знак. Няхай Святло вядзе цябе, добры чалавек." #. TRANSLATORS: Quest dialog spoken by Pepin #: Source/textdat.cpp:23 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" "Згуба сына давяла Караля. Я зрабіў усё, што мог, каб палегчыць яго " "шаленства, але нарэшце яно адолела яго. Чорны праклён навіс над каралеўствам " "з таго дня, але, можа, калі б яго дух вызвалілі ад яго зямной турмы, праклён " "бы адрабіўся..." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:25 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" "Не хачу думаць пра смерць Караля. Мне падабаецца ўспамінаць яго добрым і " "справядлівым, бо такім ён і быў. Яго смерць была такой сумнай, і " "няправільнай, чамусьці." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:27 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" "Я выкуў шмат зброі і большую частку даспехаў, якія Кароль Леорык даваў сваім " "рыцарам. Для яго я нават вымайстраваў вялізны двуручны меч з " "найдасканалейшаго мітрыля, як і баявую карону ў том жа духу. Дасюль не магу " "паверыць, што ён умёр, але гэто дакладно нейкая паганая воля звяла яго з " "розуму!" #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:29 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Ды начхаць мне на гэта. Слухай, ніякі шкілет не будзе МАІМ каралём. Леорык – " "кароль. Кароль, чуеш? СЛАВА КАРАЛЮ!" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:31 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" "Мёртвыя, што ходзяць меж жывых, служаць праклятаму Каралю. Ён валодае сілаю, " "каб падняць у войска наўцоў яшчэ больш новых ваяроў. Калі не супыніш яго " "панаванне, ён дакладна пройдзе маршам па гэтай зямлі, забіваючы ўсіх, хто " "яшчэ тут жывы." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:33 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" "Слухай, я тут справу вяду. Інфармацыяй я не гандлюю, і мне ўсё роўна на " "нейкага там Караля, каторы мёртвы дольш, чым я жывы. Калі ж табе трэба " "нешта, каб біцца з ім ці з мярцамі яго, от тут я табе памочнік.." #. TRANSLATORS: Quest dialog spoken by The Skeleton King (Hostile) #: Source/textdat.cpp:35 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" "Цяпло жыцця ўвайшло ў мой склеп. Рыхтуйся, смертны, служыць майму Гаспадару " "век вечны!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:37 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" "Бачу, і цябе бянтэжаць гэтыя дзіўныя паводзіны. Магу толькі здагадвацца, але " "раз многія дэманы баяцца сонечнага святла і вераць, што яно мае вялікую " "сілу, магчыма, яны палічылі, што ўзыходнае сонца на знаку таксама стрымлівае " "нейкую таемную сілу. Гм-м, мабыць, не такія яны і разумныя, як мы баяліся..." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:39 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" "Добры чалавек, я тут хацеў падзяліцца адным дзіўным досведам. Знаю, ты шмат " "ведаеш пра пачвар, што насяляюць лабірынт, і я тут ніяк не магу зразумець " "адну рэч... Я прачнуўся сярод ночы ад нейкага скрыгатання ля шынка. Я " "выглянуў са спальні, аж убачыў на дварэ карчмы абрысы нейкіх чарцят. " "Неўзабаве яны збеглі, але адно пасля таго, як скралі шыльду. Я не разумею, " "нашто дэманы ўкралі знак, але маю сям'ю не чапілі... дзіўна, праўда?" #. TRANSLATORS: Quest dialog spoken by Ogden (Quest End) #: Source/textdat.cpp:41 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" "Вой, табе не абавязкова было вяртаць знак, хаця так цяпер не трэба новы " "рабіць, ці купляць. Ну, дай пагляну, чым ж я магу з табою разлічыцца? Гм-м-" "м, што тут у нас... Ага! Вось, гэту шапку пакінуў адзін чараўнік у нашых " "пакоях, ён нядаўна тут спыняўся. Можа табе будзе неяк карысна." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:43 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" "Бацюхны, дэманы бегаюць па вёсцы ўночы, рабуюць нашы дамы – хіба не " "засталося нічога святога? Спадзяюся, Огдэн з Гардаю ў парадку. Яны б, " "здаецца, прыйшлі, каб іх паранілі..." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:45 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" "Авохці! Дык вось куды знак прапаў? Мы з бабуляю відаць усё праспалі. Дзякуй " "Святлу гэтыя пачвары не напалі на карчму." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:47 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "Кажаш, дэманы скралі Огдэнаў знак? Непадобно гэто да іхных лютасцяў, пра " "якія я чуў. Ці якія бачыў сам. \n" " \n" "Дэманам бы сэрца табе вырваць, а не знак." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:49 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" "Ведаеш, што я мяркую? Нехта сцягнуў той знак, і цяпер запросіць за яго кучу " "грашыскаў. Быў бы я Огдэнам... а я не Огдэн, але каб я быў... Я б проста " "купіў новы, з якім малюнкам прыгожым. Мо з чарачкаю элю, або кавалачкам " "сыру..." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:51 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Ніводзін смертны не можа дасканала ахапіць розум дэмана. \n" " \n" "Ніколі не давай іх цьмяным учынкам збіць цябе з тропу, бо гэта можа быць " "частка іх плана." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:53 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" "Што... ён кажа, гэта я ўзяў? Відаць, і Грызвальд на яго баку.\n" "\n" "Слухай, я ўжо некалькі месяцаў крадзяжом шыльдаў не займаюся. На кавалку " "дрэва не разжывешся." #. TRANSLATORS: Quest dialog spoken by Snotspill (Hostile) #: Source/textdat.cpp:55 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" "Гэй, гэта ты тут усіх б'еш! Нясі мне Магічны Сцяг, бо нападзём! З жыццём не " "пойдзеш! Забі вялікіх гадасцяў і вярні Магічнага. Прайдзі ля вугла і " "дзвярэй, знайдзі там гадасцяў. Аддасі, і хадзі!" #. TRANSLATORS: Quest dialog spoken by Snotspill (Hostile) #: Source/textdat.cpp:57 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "Гадасцяў забі, сцяг забяры. Прынясеш мне, а не то..." #. TRANSLATORS: Quest dialog spoken by Snotspill (Hostile) #: Source/textdat.cpp:59 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "Дай! Так, добра! Ідзі, мы сільныя. Мы з вялікім Магічным усіх забіць!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:61 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" "Нічога добрага гэта не вяшчуе – усё пацвярджае мае найзмрочнейшыя страхі. " "Хаця я адмаўляўся верыць старасвецкім легендам, цяпер я не магу. Прыйшла " "пара адкрыць, хто я такі.\n" "\n" "Маё спраўдная імя Дэкард Кейн Старэйшы, і я апошні нашчадак старасвецкага " "Брацтва, якое прысвяціла сябе ахове таямніц вечнага Зла. Зла, якое, цяпер " "ужо відавочна, вызвалілі.\n" "\n" "Архібіскуп Лазар, калісьці дарадчык Караля Леорыка, якому той давяраў як " "нікому, завёў натоўп простых гараджан у Лабірынт на пошукі сына Караля, " "Альбрэхта. Мінула шмат часу, калі яны вярнуліся, і жывымі выратавалася зусім " "мала.\n" "\n" "Бадай мяне, дурня! Я мусіў быў западозрыць скрыты падман. Гэта сам Лазар, " "мусіць, і скраў Альбрэхта, і хаваў яго з тае пары ў Лабірынце. Але я не " "разумею, чаму Архібіскуп звярнуўся да Цемры, ці якая яму карысць у дзіцяці. " "Хіба толькі дзеля таго, каб ахвяраваць яго Цёмным Валадарам!\n" "\n" "Вось у чым была яго задума! Ацалелыя з таго “гурту ратавальнікаў” казалі, " "што ў апошні раз, калі Лазара бачылі, ён збягаў у найглыбейшыя нетры " "Лабірынта. Ты мусіш спяшацца, выратуй Прынца ад ахвярнага нажа гэтага " "шалёнага ліхадзея!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:63 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Спяшайся! Альбрэхта трэба выратаваць з рук Лазара. Прынц і люд каралеўства " "разлічваюць на цябе!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:65 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" "Змрочная гісторыя твая, дружа мой. Лазар дакладна будзе гарэць у Пекле за " "свой жудасны ўчынак. З твайго апісання гэта быў не наш прынц, але я думаю, " "Альбрэхт яшчэ можа быць у бядзе. Сімвал сілы, які ты апісваеш, мусіць, " "партал у самае сэрца лабірынта.\n" "Ведай жа, дружа мой: зло, на якое ты ідзеш, – Цёмны Пан Жаху. Смяротным " "людзям ён вядомы як Д’ябла. Гэта яго зняволілі у лабірынце многа вякоў таму, " "і я страшуся, што ён зноў жадае пасеяць хаос на зямлі чалавечай. Ты мусіш " "прайсці праз партал і знішчыць Д’ябла, покуль не позна!" #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:67 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" "Лазар – гэта той Архібіскуп, каторы тады павёў многіх гараджан у лабірынт. У " "той дзень я страціў многа сяброў, а Лазар так і не вярнуўся. Мяркую, яго " "забілі, як і шмат каго яшчэ. Калі вам гэта не цяжар, прашу, зрабіце мне " "ласку, не гаварыце з Фарнамам пра той дзень." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:71 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Планы гараджан, калі я іх пачуў тае ночы, проста ашаламілі мяне. Я думаў, " "што ў Лазара-то будзе больш развагі. Ён быў Архібіскупам, і заўсёды " "падавалася, што ён вельмі любіў жыхароў Трыстрама. Столькі параненых, я не " "змог выратаваць усіх..." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:73 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Я помню Лазара вельмі добрым і велікадушным чалавекам. Ён гаварыў на " "пахаванні маёй мацеры, і заўсёды падтрымліваў маю бабулю і мяне у цяжкія " "часы. Я малюся кожны вечар, што ён неяк і дасюль жывы і здаровы." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:75 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Я быў там, калі Лазар завёў нас у лабірынт. Гаварыў пра святое пакаранне, а " "як пачалася бойка з параджэннямі пекла, ён нат булавы свае не падняў на іх. " "Просто бег уніз, у цемрадзь, у бясконцыя пакоі лабірынта, поўныя служак " "цемры!" #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:77 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" "Поруць, кусаюцца, ажно яны ўсе вакол цябе. Хлус! ХЛУС! Яны ўсе мёртвыя! " "Мёртвыя! Чуеш? Яны ўсё падаюць ды падаюць… кроў, іхняя кроў па ўсёй падлозе… " "усё яго віна…" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:79 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" "Я не ведала таго Лазара, пра якога ты гаворыш, але я адчуваю вялікі разлад у " "яго сутнасці. Ён уяўляе сабою страшную небяспеку, і ні перад чым не спыніцца " "ў службе сілам цемры, што назвалі яго сваім." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:81 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" "Ага, праведны Лазар, які ну та-а-ак ужо нам усім дапамог тады ўнізе. Назе " "маёй адно не дапамог, от бяда. Слухай, дам за так табе параду. Спытай " "Фарнама, ён быў там." #. TRANSLATORS: Quest dialog spoken by Lazarus (Hostile) #: Source/textdat.cpp:83 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" "Кінь сваё дурное пачынанне. Усё, што цябе чакае, толькі гнеў майго Валадара! " "Ужо запозна ратаваць дзіця. Скора ты далучышся да яго ў Пекле!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:86 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" "Гм-м, не ведаю, што я мог бы расказаць табе, што магло б табе дамагчы. Вада " "ў нашым калодзежы плыве з падземнай крыніцы. Я чуў пра тунель, які вядзе да " "нейкага вяліка возера, магчыма, гэта адно яна і ёсць. На жаль, мне невядома, " "з якой прычыны наша вада забрудзілася." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:88 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Я заўсёды стараўся трымаць у склепе вялізны запас ежы і пітва, але без " "крыніцы свежай вады ва ўсім горадзе, наша сховішча скора зусім спустошыцца.\n" "Калі ласка, зрабіце, што зможаце, інакш я не ведаю, што нам рабіць." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:90 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "Рады я заспеў цябе ў пару! У нас у калодзезе застаялася вада, уся " "саленаватая стала, нехта з гарадскіх нават захварэў, выпіўшы яе. Нашы запасы " "свежай вады хутка вычэрпваюцца. Здаецца, ёсць праход, які вядзе да крыніц, " "што жывяць наш горад. Адшукай, якая прычына гэтаму бедству, калі ласка, " "інакш мы ўсе дакладна згінем." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:92 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Прашу, спяшайся. З кожнай гадзінаю мы маем усё менш вады для піцця.\n" "Без тваёй дапамогі мы доўга не пратрываем." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:94 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "Што кажаш? Сама прысутнасць дэманаў — прычына забруджвання вады? Вой, пад " "нашым горадам сапраўды таіцца вялікае зло, але твая стойкасць і храбрасць " "даюць нам надзею. Калі ласка, вазьмі гэты пярсцёнак — магчыма, ён дапаможа " "табе ў знішчэнні гэтых пачвар." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:96 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Маёй бабулі вельмі няможацца, а Гарда сказала, што нам няможна піць " "калодзезя. Можаш неяк нам дапамагчы, калі ласка?" #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:98 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" "Піпін праўду сказаў. Нам сільно будзе патрэбна свежая вада, дый скоро. " "Спрабаваў ачысціць адзін з калодзезяў, меншых, але там смуродзіць гэтай " "затхлай гадасцю. Мусіць на крыніцы забрудзілося." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:100 msgid "You drink water?" msgstr "Ты што, п'еш ваду?" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:101 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "Жыхары Трыстрама памруць, калі не аднавіць падачу свежай вады ў іх " "калодзезі.\n" "\n" "Ведай жа: за гэтаю справаю стаяць дэманы, але ім не вядома, што яны " "спарадзілі." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:103 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Гэты раз я за цябе. Дзела стаіць, як той кажа, калі няма каму прадаваць. " "Лепш дазнайся, што там робіцца, і хутчэй!" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:105 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" "Кніга, у якой гаворыцца пра пакой з чалавечых касцей? Што ж, у некаторых " "старадаўніх тэкстах, якія я даследаваў у Бібліятэцы Усходу, згадваецца Пакой " "Касцей. У тых фаліянтах паведамлялася, што, калі Валадары Падзямелля " "зажадалі ахаваць свае вялікія скарбы, яны стваралі ўладанні, дзе тыя, хто " "паміраў, спрабуючы скрасці скарбы, назаўсёды звязваўся б з гэтым месцам і " "бараніў яго. Жахлівы канец, але дзіўна адпаведны?" #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:107 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" "Баюся, што пра гэта я нічога не ведаю, добры чалавек. Кейн мае шмат кніг, " "можа яны дапамогуць." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:109 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" "Гучыць як вельмі небяспечнае месца. Калі накіруецеся туды, прашу, будзьце " "вельмі асцярожны." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:111 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Баюся, нічога пра гэта я не чула. Можа Расказчык Кейн мог бы дапамагчы." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:113 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" "Я пра гэто нічагуткі не ведаю, але мо Кейна спытай. Ён пра шмат чаго " "гамоніць, і мяне б не здзівіло, калі ён ведае адказ на тваё пытанне." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:115 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Так, слухай. Ёсць такі пакой дрэва, бач. А жонка ягоная, ну ведаеш – гэтая " "самая – кажа дрэву... не, ты пачакай. А я, кажу, яно яму бокам вылезе, але " "калі думаеш, што я буду ПЛАЦІЦЬ... ты... гэта... так." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:117 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Ты станеш вечным слугою цёмных уладароў, калі загінеш у тым праклятым " "месцы.\n" "На свой страх і рызыку ўваходзь у Пакой Касцей." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:119 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "Вялізны, таямнічы скарб, кажаш? Мо мне і было б цікава заграбці з цябе рэч " "другую... О, а шчэ лепш, табе не трэба якіх рэдкіх і дарагіх прыпасаў, каб " "прайсці гэткае выпрабаванне?" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:121 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" "Здаецца так, што архібіскуп Лазар прымусіў шмат мясцовых спусціцца ў " "Лабірынт, каб адшукаць зніклага сына Караля. Ён зыграў на іх страхах, і " "загнаў іх напрасткі ў шалёны натоўп. Ніхто не быў гатовы да таго, што ляжыць " "пад халоднай зямлёю… Лазар пакінуў іх там, унізе – у кіпцюрах невымоўных " "жахаў – на смерць." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:123 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" "Ну, Фарнам нешта мармытаў пра нейкае грамаднае стварэнне, якое арудавала " "ліхой зброяй. Здаецца, ён назваў яго мясніком." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:125 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "О, Святло! Я ведаю аб гэтым агідным дэмане. Сярод тых нешматлікіх ацалелых з " "паствы Лазара, хто выпаўз з-пад Храма, багата людзей мела сляды яго гневу на " "сабе. Не ведаю, чым ён калечыў сваіх ахвяр, але тое дакладна не з гэтага " "свету. Раны гнілі хваробаю, і нават мне падавалася, што загаіць іх " "немагчыма. Шануйся, калі думаеш біцца з гэтай бэстыяй…" #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:127 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Калі Фарнам нешта казаў пра мясніка, які забівае людзей, я адразу не " "паверыла. Але раз і ты гэта ўзгадваеш, мабыць, гэта і праўда." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:129 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" "Я бачыў, што Фарнам назваў Мясніком, калі яно касіло сабе дарогу праз цяла " "маіх сяброў. Ён размахваў секачом з сакеру, сцінаючы рукі-ногі, столькі " "смельчакоў забіваючы на месцы. Мяне аддзяліло ад бітвы зборышчо крыклівых " "д’ябалят, і я нек выйшаў на сходы наверх. Больш я таго брыдкаго пачварня не " "бачыў, але ягоная крывавая морда і па гэты дзень у мяне перад вачыма." #. TRANSLATORS: Quest dialog spoken by Farnham (*sad face*) #: Source/textdat.cpp:131 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "Вялізны! Вялізны сякач – усіх маіх сяброў пазабіваў. Не даў рады спыніць, " "трэ было ўцякаць, не змог іх выратаваць. Прымкнула ў пакою, а там столькі " "целаў… столькі сяброў… НЕЕЕЕЕЕЕЕЕЕ!" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:133 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "Мяснік – садысцкая істота, якая цешыцца болем і катаваннем людзей. Яго " "работу бачна ў п’яніцы Фарнаме. Знішчэнне яго дакладна забяспечыла б " "захаванасць гэтай вёскі." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:135 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" "Я аб гэтым страшэнным чорце ведаю больш, чым ты думаеш. Ягоныя дружакі мяне " "злавілі і нагу маю адабралі, да таго як Грызвальд выцягнуў мяне з той " "дзіры. \n" "\n" "Скажу проста – забі яго, і бі першым, а то ён дадасць твой труп у сваю " "калекцыю." #. TRANSLATORS: Quest dialog spoken by Wounded Townsman (Dying) #: Source/textdat.cpp:137 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" "Прашу, выслухай мяне. Архібіскуп Лазар, ён нас завёў сюды каб адшукаць " "прынца зніклага. Сволач завёў нас у пастку! Цяпер усіх забіла… забіў іх " "дэман, ён яго назваў Мяснік. Адпомсці за нас! Знайдзі гэтага Мясніка ды забі " "яго, каб нашы душы знайшлі спакой…" #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:140 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" "Цікавы верш ты цытуеш, стыль мне нешта нагадвае. Дай падумаць – як жа там " "было?\n" " \n" "…Цьма ахінае Схаванае. Вочы нябачныя свецяцца, чуваць адно, як коратка " "скрабуцца брытвы-кіпцюры, мучаючы бедныя душы аслепленых на век вечны. " "Цямніца для гэтых Праклятых завецца Залы Сляпых…" #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:142 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Заўсёды быў нейкі абыякавы да паэзіі. Іншы раз даводзілася наймаць " "менестрэляў, калі справы ў карчме ішлі добра, але гэта быццам было так " "даўно.\n" "\n" "Што? А, так... гм, ну, думаю, можаш астатніх спытаць, што яны ведаюць." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:144 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" "Гучыць праўда знаёма, чамусьці. Здаецца, прыпамінаю, што чытаў штосьці " "вельмі падобнае, калі вывучаў гісторыю дэманічных насланняў. Там казалася " "пра месца вялікага зла... пастой – ты ж не збіраешся ісці туды, праўда?" #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:146 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" "Калі ў цябе пытанні пра слепату, пагавары з Піпінам. Я ведаю, што ён даў " "маёй бабулі зелле, што праясніла ёй зрок, можа ён і табе дапаможа." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:148 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" "Баюся я ні відам не відаў, ні слыхам не чуваў пра месцо, каб падыходзіло пад " "гэдакае жывое апісанне, дружа. Расказчык Кейн мог бы спамагчы." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:150 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "Ну глядзі... праўда смешна, га? Зразумела? Зала сляпых– глядзі?" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:152 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Тое месца вялікіх пакут і жахаў, таму яно і служыць добра сваім гаспадарам.\n" "\n" "Ступай асцярожна, або і ты затрымаешся там дольш, чым спадзяваешся." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:154 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" "Так, паглядзім, я табе нешта прадаю? Не. Ты мне даеш грошы, каб я пра гэта " "нешта расказаў? Не. Ты ідзеш адсюль і размаўляеш з расказчыкам, які і жыве " "для такога? Так." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:156 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" "Гаворыш, Лакданан размаўляў з табою? Ён быў вялікім героем пры жыцці. " "Шляхетны і справядлівы чалавек, які шмат год служыў Каралю вераю і праўдаю. " "Але ты, канечне, ужо гэта ведаеш. \n" "\n" "З усіх, каго апанаваў праклён Караля, Лакданан як ніхто іншы не здаваўся б " "Цемры без бою, таму, мяркую, што твае словы – праўда. Будзь я на тваім " "месцы, дружа, я бы паспрабаваў знайсці спосаб адратаваць яго ад такой пакуты." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:158 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "Ты пра храбрага воіна, які даўно памёр! Я не жадаю размоў пра згінулыя душы " "на маім двары, дзякуй вам вялікі." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:160 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Кажаш, залаты элікір. Ніколі не варыў зелляў такога колеру, таму не ведаю, " "які яно мела б эфект, выпі ты яго. Як твой лекар, вельмі раю зрабіць, як " "прасіў Лакданан, калі знойдзеш такі эліксір, і НЕ ПРАБУЙ яго піць." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:162 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Ніколі не чула пра Лакданана. Выбачай, але я не думаю, што магу табе неяк " "дапамагчы." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:164 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" "Калі гэта сапраўды быў Лакданан, я бы параіў табе дапамагчы яму. Я меў з ім " "справу некалькі разоў, і палічыў яго па прыродзе шчырым і верным чалавекам. " "Праклён, што ахінуў паслядоўнікаў Караля Леорыка, паў быў на яго асабліва " "цяжка." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:166 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" " Лакданан памёр. Гэта ўсе ведаюць, і ты мяне не абдурыш, што не памёр. З " "мёртвымі не пагаворыш. Я-то ведаю!" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:168 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Ты можаш сустрэць у Лабірынце тых, хто захрас там у пастцы, як Лакданан.\n" "\n" "Я адчуваю ў ім годнасць і вялікае пачуццё віны. Дапаможаш яму, і ты " "дапаможаш усяму Трыстраму." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:170 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" "Пастой, дай згану. Пад Кейнам разявілася агромная рышчыліна і яго туды " "зацягнула. Яго спапяліла ў шары пякельнага агню і ён больш не можа адказваць " "на твае пытанні. Як, не было такога? Ну, тады ты або купляеш нешта, або " "ідзеш сваёй дарогай." #. TRANSLATORS: Quest dialog spoken by Lachdanan (in despair) #: Source/textdat.cpp:172 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" "Прашу, не забівай, выслухай мяне! Я быў Капітанам Рыцараў Караля Леорыка, " "падтрымліваў законы гэтай зямлі з чэсцю і справядлівасцю. Але потым яго " "цёмны Праклён паў на нас, за ролю, што мы сыгрылі ў яго трагічнай смерці. " "Пакуль мае сабраты скарыліся свайму страшнаму лёсу, я збег з усыпальні " "Караля, шукаючы, як збавіцца ад Праклёну. Аднак я не даў рады...\n" "\n" "Я чуў пра Залаты Эліксір, што мог бы адрабіць Праклён і дараваць маёй душы " "спакой, але не змог знайсці яго. Мая сіла цяпер згасае, і разам з ёю астаткі " "маёй чалавечнасці. Прашу, дапамажы мне, знайдзі Эліксір. Я адплачу табе за " "намаганні — клянуся сваёю чэсцю." #. TRANSLATORS: Quest dialog spoken by Lachdanan (in despair) #: Source/textdat.cpp:174 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "Залаты Эліксір не знайшоўся. Страшуся, я на векі вякоў пракляты. Прашу, " "пашукай яшчэ..." #. TRANSLATORS: Quest dialog spoken by Lachdanan (Quest End) #: Source/textdat.cpp:176 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" "Табою выратавана мая душа ад пракляцця, і за гэта я табе абавязаны. Калі " "ёсць спосаб разлічыцца з табою з таго свету, я знайду яго, а пакуль — вазьмі " "мой шалом. У падарожжы, у якое я адпраўляюся, мне ён не будзе надта карысны. " "Няхай ён ахоўвае цябе ад цёмных сіл унізе. Ідзі са Святлом, друг мой..." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:178 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" "Грызвальд гаворыць пра Кавадла Ярасці, легендарны артэфакт, які доўга " "шукалі, але так не знайшлі. Яно выраблена з касцей дэманаў з Лязовай Ямы, і " "выплаўлялі яго побач чэрапаў пяцярых наймацнейшых магаў падземнага свету. На " "ім высячаны руны сілы і хаосу, а любая зброя ці браня, выкутая на гэтым " "Кавадле, паглынецца царствам Хаосу, якое ўкараніць у рэчы магічныя " "ўласцівасці. Казалі, праз непрадказальную прыроду хаосу цяжка ведаць " "дакладна, які будзе вынік такой коўкі…" #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:180 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" "Не думаеш, што было б лепш спытаць пра гэта Грызвальда? Ён да таго зручны, " "знаеш." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:182 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" "Калі б табе былі патрэбны звесткі пра Таўкачык Гаення ці пра Срэбны Келіх " "Ачышчэння, я мог бы дапамагчы табе, друг мой. Аднак, у гэтай справе было б " "карысней пагаварыць з Грызвальдам ці з Кейнам." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:184 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" "Грызвальдаў бацька бывала расказваў нам малым пра вялізнае кавадла, на якім " "рабілі магутную зброю. Ён казаў, што калі молат біў па тым кавадле, зямля аж " "трэслася шалёна. Калі трасецца зямля, я заўсёды гэта ўспамінаю." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:186 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "Вітаю! Заўсёды рады бачыць аднаго са сваіх найлепшых пакупнікоў! Я ведаю, ты " "часто ходзіш глыбоко ў Лабірынт, а мне якраз нешто расказалі, што можа табе " "паказацца вартым часу...\n" "\n" "Адзін чалавек, які вярнуўся з Лабірынта, гаварыў, што, калі ўцякаў, знайшоў " "нейкае таямнічае кавадло. Яго апісанне нагадало мне легенды, якія я чуў шчэ " "юнаком, пра Пякельную Кузню, дзе куюць чарадзейную, моцную зброю. Па " "легендзе ў глыбіні Пякельнай Кузні стаяло Кавадло Ярасці! Гэтае Кавадло " "змяшчало ў сабе самую існасць падземнаго свету...\n" "\n" "Кажуць, любая зброя выкутая на пякельным Кавадле насычана вялікай сілаю. " "Калі гэто дапраўды Кавадло Ярасці, я змагу выкаваць такую зброю, што і " "самаго найцямнейшаго ўладара Пекла змагла б паразіць!\n" "\n" "Знайдзі Кавадло, і я вазьмуся за працу!" #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:188 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" "Покуль нічого, так? Ну, шукай. Зброя, выкутая на гэтым Кавадле была б " "найлепшай надзеяю, і я пэўны я змог бы вырабіць для цябе нешто сапраўды " "вялікае." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:190 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" "Не магу паверыць! Кавадло Ярасці — малайчына, сябра. Цяпер-то мы ім пакажам, " "падлюгам, што няма ў Пякле зброі смяротнейшай, ніж той, што зрабіў чалавек! " "Трымай, і няхай Святло бароніць цябе." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:192 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Грызвальд не прадасць свайго кавадла. Што ён тады будзе рабіць? І я б " "таксама злаваў, каб нехта скраў маё кавадла!" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:194 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" "Унутры Лабірынта ёсць шмат артэфактаў, што маюць сілу па-за межамі разумення " "смертных. Некаторыя з іх маюць сілу, якую могуць карыстаць як Святло, так і " "Цемра. Дастаць адтуль Кавадла было б кідком на шалю Святла ў Вайне Гневу." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:196 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" "Калі знойдзеш Грызвальду гэты артэфакт, гэта моцна ўдарыць па паім дзеле. " "Ай, ты яго ніколі не знойдзеш." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:198 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" "Вароты Крыві і Залі Агню — месцы таямнічага паходжання. Дзе б табе не " "трапілася гэтая кніга, месца, дзе яна захоўваецца, дакладна вялікай сілы.\n" "\n" "Легенды гавораць аб п'едэстале, высечаным з абсідыянавага каменя, а наверсе " "яго паверхні, убранай касцьмі, знаходзіцца басейн кіпучай крыві. Таксама " "намякаецца аб Камянях Крыві, якія адкрыюць дзверы, што ахоўваюць нейкі " "старасвецкі скарб...\n" "\n" "Прырода таго скарба ахутана загадкамі, дружа мой, але сказана, што " "старадаўні герой Аркейн пакінуў святыя даспехі Адвагі ў патаемным скляпенні. " "Аркейн быў першы смертны, што змяніў ход вайны Граху, прагнаўшы легіёны " "цемры аж да самых Агністых Пекел.\n" "\n" "Акурат перад яго смерцю, даспехі Аркейна схавалі ў патаемным скляпенні. " "Сказана, што калі святыя даспехі зноў будуць у патрэбе, паўстане іншы герой, " "які апране Адвагу. Магчыма, ты і ёсць той герой..." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:200 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" "Усякае дзіця ведае пра воіна Аркейна і яго таямнічыя даспехі, якія завуцца " "Адвага. Калі знойдзеш, дзе яно захоўваецца, атрымаеш добрую абарону ад зла ў " "Лабірынце." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:202 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Гм-м... гучыць як нешта, што я б ведаў, але я так доўга вывучаў новыя лекі " "ды паляпшаў эліксіры, што я, мусіць, забыў. Выбачай..." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:204 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" "Раней я часта чула пра чароўныя даспехі, якія завуць Адвагай, пра такое " "раней гаманілі хлопцы. Лепш мужыкоў папытай па вёсцы." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:206 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" "Даспехі пад назвай Адвага могуць схіліць шалі на твой бок. Скажу, што многія " "іх шукалі — у тым ліку і я. Аркейн добра іх схаваў, сябру, і аднае ўдачы тут " "не хопіць, каб раскрыць сакрэты, якія хавалі іх, ух, столькі многа гадоў." #. TRANSLATORS: Quest dialog "spoken" by Farnham #: Source/textdat.cpp:208 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Хр-р-р-р-р-р-р-р-р-р-р-р-р-р-р-р..." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:209 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" "Знойдзеш гэтыя Камяні Крыві, абыходзься з імі асцярожна.\n" "\n" "Шлях таіць у сябе небяспеку, і ёсць толькі адна надзея — у табе." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:211 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "Думаеш знайсці даспехі пад назвай Адвага?\n" "\n" "Ніхто так і не адшукаў, дзе там Аркейн іх схаваў, і раз мае сувязі не далі " "рады, нешта я сумняваюся, што ты калі-небудзь дасі." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:213 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" "Я ведаю толькі адну легенду, якая апавядае пра такога воіна, якога ты " "апісваеш. Яго гісторыя сустракаецца ў старадаўніх хроніках вайны Граху...\n" "\n" "Заплямлены тысяччу год вайны, крыві і смерці, Крывавы ваявода стаў на гары " "сваіх падраных ахвяр.\n" " \n" "Таксама пісалася, што ён быў калісьці смертны, і біўся супраць Легіёна Цемры " "ў Вайне Граху, але ён страціў сваёй чалавечнасць з-за няўтольнай прагі да " "крыві." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:215 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Баюся, нічога пра такога ліхога воіна я не чуў, добры чалавек. Я " "спадзяваюся, табе не спатрэбіцца біцца з ім, бо здаецца ён вельмі небяспечны." #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:217 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" "Кейн мог бы расказаць табе пра такое значна больш, чым я б калі-небудзь " "хацеў ведаць." #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:219 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" "Калі табе прыйдзецца біцца з такім лютым ворагам, няхай Святло будзе табе " "спадарожнікам і ахоўцам. Я буду думаць пра цябе." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:221 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" "Цёмныя і страшныя легенды ахінаюць Крывавага Ваяводу таго. Добра " "падрыхтуйся, сябру, бо ён не пакажа ані літасці." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:223 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" "Вечна ты пра Кроў. А як жа кветкі там, сонейка, або красуня тая, што выпіўку " "носіць. Слухай, друг, ты проста апантаны, табе не гаварылі?" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:225 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" "Яго ўмельства са зброяю жахлівае, і ён пражыў цэлыя тысячы год, ведаючы адно " "вайну. Прабач мне... Я не бачу, ці пераможаш ты яго." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:227 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Ніколі не меў спраў з гэтым Ваяводам, але відаць ён пераводзіць шмат мячоў. " "Забяспечваць яго было б ідэяй..." #. TRANSLATORS: Quest dialog spoken by Warlord of Blood (Hostile) #: Source/textdat.cpp:229 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" "Мой клінок пяе тваёй крыві, смертны, і клянуся сваімі гаспадарамі, яго не " "спыняць." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:231 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" "Грызвальд гаворыць пра Нябёсны Камень. Яго меліся адвезці на Усход вучоным, " "каб лепш даследаваць. Камень ззяў Энергіяю, якая нейкім чынам абдорвала " "зрокам, які звычайнаму чалавеку недаступны. Не ведаю, якія сакрэты ён тоіць, " "дружа мой, але знайсці гэты камень дорага б каштавала." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:233 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" "Той караван тут спыняўся купіць прыпасаў на дарогу на Усход. Я прадаў ім " "багата фруктаў і прыўдалага мяса, якое Гарда толькі напякла. Шкада, што ўсё " "так адбылося…" #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:235 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" "Не ведаю, што яны там хацелі ўбачыць з гэтым каменем, але я вось што скажу. " "Калі з неба падаюць камяні, лепш быць асцярожным!" #. TRANSLATORS: Quest dialog spoken by Gillian #: Source/textdat.cpp:237 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Ну, тут праўда спыняўся караван з нейкімі надта важнымі людзьмі, але гэта " "ўжо даволі даўно было. Вымова ў іх была дзіўная. Калі нічога не блытаю, яны " "пачыналі доўгае падарожжа.\n" "\n" "Не ўяўляю, як ты зможаш знайсці хоць штосьці з паклажы." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:239 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" "А пастаі хвілінку, маю тут адну рэч, мо цябе зацікавіць. Тут нядаўна караван " "праязджаў, які кіраваўся на ўсходнія каралеўствы. Везлі там нібыто кавалак " "неба, што ўпаў на зямлю! Караван трапіў у засаду нейкіх вершнікаў у плашчах, " "на поўнач па дарозе адсюль. Я абшукаў там абломкі, але каменя і след " "прастыў. Калі ён табе знойдзецца, думаю я б змог нешто карыснае з яго " "зладзіць." #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:241 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Я ўсё яшчэ чакаю, ці прынясеш ты мне таго каменя з нябёсаў. Я ведаю, што " "змагу зладзіць з яго нешто моцнае." #. TRANSLATORS: Quest dialog spoken by Griswold(Quest End) #: Source/textdat.cpp:243 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" "Дай глянуць — але... але, так я і думаў. Дай хвілінку...\n" "\n" "Ну вось, трымай. Я ўладзіў кавалкі каменя ў сярэбраным пярсцёнку, які мне " "дай мой бацько. Буду спадзявацца, яно стане табе ў прыгодзе." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:245 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" "Быў у мяне некалі пярсцёнак. Даражэзны, з блакітным на ім, і зялёным, і " "чырвоным, і серабром. Не памятаю, што з ім сталася, праўда. Моцна па ім " "сумую..." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:247 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "Нябёсны камень магутны, і калі б цябе прасіў яго знайсці хто-небудзь, апроч " "Грызвальда, я бы не дазволіла таго. Ён пакорыць яго сілу, і камень будзе " "ўжыты на карысць усім нам." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:249 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" "Калі нехта можа нешта зрабіць з таго каменя, дык гэта Грызвальд. Ён знае, " "што робіць, хоць я і сцягваю ў яго пакупнікоў, якасць яго працы я паважаю." #. TRANSLATORS: Quest dialog spoken by Cain #: Source/textdat.cpp:251 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" "Ведзьма Эйдрыя шукае чорны грыб? Пра такое я магу сказаць не больш, чым пра " "селядзец. Мабыць, Піпін ведае больш, але гэта нешта такое, чаго ў маіх " "кнігах і байках не знойдзеш." #. TRANSLATORS: Quest dialog spoken by Ogden #: Source/textdat.cpp:253 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" "Вось што я табе скажу. Ні Гарда, ні я, ніколі, ніколі б не падалі сваім " "шаноўным гасцям чорных грыбоў. Калі Эйдрыі хочацца сабе ў поліўку грыбоў, яе " "дзела, але я табе тут не памочнік. Чорныя грыбы… тфу!" #. TRANSLATORS: Quest dialog spoken by Pipin #: Source/textdat.cpp:255 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" "Ведзьма казала, ты шукаеш мозг дэмана, каб дамагчы мне зварыць эліксір. Ён " "будзе вельмі каштоўны для ўсіх, каго паранілі гэтыя вычварні, мне бы толькі " "разгадаць сакрэт, што тоіць яго састаў. Дастанеш мозг з забітага дэмана, я " "буду вельмі ўдзячны, калі прынясеш яго мне." #. TRANSLATORS: Quest dialog spoken by Pepin #: Source/textdat.cpp:257 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" "Выдатна, гэта я і ўяўляў. Я змог даварыць эліксір і без гэтага, але мець " "такое для доследу лішнім не будзе. Калі ласка, аднясеш вось гэта ведзьме? " "Думаю, яна якраз чакае." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:259 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "Думаю, Огдэн захаваў крыху грыбоў у склепенні. Можа яго спытай?" #. TRANSLATORS: Quest dialog spoken by Griswold #: Source/textdat.cpp:261 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Калі і Эйдрыя не мае такіх, мусіць, і праўда рэдкая рэч. Наўрад ці магу яшчэ " "як спамагчы, але са слоў тваіх гэто… ну, здаровы такі, велізарны, уздуты, " "азызлы грыб! Ну, добраго палявання, мусіць." #. TRANSLATORS: Quest dialog spoken by Farnham #: Source/textdat.cpp:263 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Огдэн гоніць ФАЙНЫ “чорны грыб”, хаця мяне ірве калі вып’ю такога. Слухай, " "слухай… Вось у чым сакрэт – ТРЭБА ВЕДАЦЬ МЕРУ!" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:265 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "Што гэта тут у нас? Цікава. З віду гэта Кніга Рэагентаў. Усюды пільна шукай " "Чорны Грыб. Ён даволі вялікі, не памылішся. Калі знойдзеш, ці не прынясеш " "мне, гм?" #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:267 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" "Мне патрэбны вялікі чорны грыб. Давай, бяжы, прынясеш мне такі, і я дадам " "яго ў асаблівае зелле, з якім зараз работаю." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:269 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" "Так. Дасканала для майго адвару будзе. Дарэчы, лекар шукаў мозг якогасьці " "дэмана, каб лячыць людзей, якіх заразілі атрутным ядам. Думаю, ён маецца " "зрабіць эліксір. Калі дапаможаш яму знайсці, што яму трэба, калі ласка, " "прынясі мне трохі гэтага эліксіру на пробу." #. TRANSLATORS: Quest dialog spoken by Adria #: Source/textdat.cpp:271 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "Навошта ты яго сюды нясеш? Гэты раз мне дэманаў мозг без патрэбы. Хаця мне " "патрэбна трохі эліксіра, які лекар цяпер варыць. Лепш аднясі гэты гратэскны " "орган яму, а мне потым прынясеш эліксіру. Праўда проста, калі трохі " "падумаць, гм?" #. TRANSLATORS: Quest dialog spoken by Adria (Quest End) #: Source/textdat.cpp:273 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "Чаго? Цяпер-то ты мне нясеш эліксір лекара? Я ўжо скончыла адвар і без яго. " "Саба пакінь..." #. TRANSLATORS: Quest dialog spoken by Wirt #: Source/textdat.cpp:275 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" "Я не прадаю грыбы, любых колераў і памераў. Можа табе лепш чаго больш " "карыснага?" #. TRANSLATORS: Quest dialog spoken by Cain (currently unused) #: Source/textdat.cpp:277 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" "Значыць, легедная ад Мапе праўдзівая. Нават я ніколі да канца не верыў! " "Здаецца, мне пара расказаць табе, хто я сапраўды такі, дружа мой. Бач, я не " "зусім той, кім падаюся...\n" " \n" "Маё сапраўднае імя Дэкард Кейн Старэйшы, і я апошні нашчадак старасвецкага " "Брацтва, якое прысвяціла сябе ахове і нагляду таямніц вечнага Зла. Зла, " "якое, цяпер ужо відавочна, вызвалілі...\n" "\n" "Зло, супраць якога ты рушыш, — цёмны Пах Жаху, смертным людзям вядомы як " "Д'ябла. Гэта яго паланілі ў Лабірынце многія стагоддзі таму. Мапа ў тваіх " "руках была створана даўным даўно, каб адзначыць час, калі Д'ябла зноў " "паўстане са свайго палону. Калі дзве зоркі з гэтай мапы зыдуцца, Д'ябла " "сягне вышыні сваёй сілы. Ён стане амаль неадольны!" #. TRANSLATORS: Quest dialog spoken by Cain (currently unused) #: Source/textdat.cpp:279 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "Наш час канчаецца! Я адчуваю, як яго цёмная сіла збіраецца, і толькі ты " "можаш не даць яму дасягнуць усяе яго магутнасці." #. TRANSLATORS: Quest dialog spoken by Cain (currently unused) #: Source/textdat.cpp:281 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Я ўпэўнены, большага ў цябе нельга прасіць, але я страшуся нават тваёй сілы " "будзе не дастаткова. Д'ябла цяпер дасягнуў вышыні сваёй сілы, і табе " "спатрэбіцца ўся твая смеласьці і моц, каб перамагчы яго. Няхай Святло будзе " "тваім правадніком і абаронцам, дружа мой. Я дапамагу чым толькі змагу." #. TRANSLATORS: Quest dialog spoken by Ogden (currently unused) #: Source/textdat.cpp:283 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" "Раз ведзьма не дапамагла і кажа ісці да Кейна, чаго ты думаеш, што я нешта " "ведаю? Відаць вельмі важная справа. Спяшацца бы табе да расказчыка, як " "Эйдрыя і гаворыць." #. TRANSLATORS: Quest dialog spoken by Pipin (currently unused) #: Source/textdat.cpp:285 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" "Я не надта разумею напісанае, але можа Эйдрыя ці Кейн могуць дапамагчы " "расшыфраваць, пра што тут.\n" " \n" "Я бачу, што гэта мапа зорнага неба, але больш за тое ўжо па-за мамі " "талентамі." #. TRANSLATORS: Quest dialog spoken by Gillian (currently unused) #: Source/textdat.cpp:287 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "Найлепш спытаць аб гэтым нашага Расказчыка. \n" " \n" "Кейн вельмі добра знаецца на старажытным пісьме, а гэта лёгка можа быць " "найстарэшай паперкай, якую я бачыла." #. TRANSLATORS: Quest dialog spoken by Griswold (currently unused) #: Source/textdat.cpp:289 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "Ніколі такой мапы не бачыў. Скуль яна? Хоць і не ведаю, як яе разумець, Кейн " "або Эйдрыя можа і дадуць табе патрэбныя адказы." #. TRANSLATORS: Quest dialog spoken by Farnham (currently unused) #: Source/textdat.cpp:291 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Слухай сюды, бліжэй стань. Не ведаю, ці ты ведаеш, што я ведаю, але тут у " "цябе сапраўды штосьці. Гэта мапа." #. TRANSLATORS: Quest dialog spoken by Adria (currently unused) #: Source/textdat.cpp:293 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" "Авохці, нічога добрага гэта не вяшчуе. Мапа зорнага неба, яна прадракае " "вялікую бяду, але яе сакрэты не мае. Таму прыйшла пара табе вельмі сур'ёзна " "пагаварыць з Расказчыкам." #. TRANSLATORS: Quest dialog spoken by Wirt (currently unused) #: Source/textdat.cpp:295 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" "Мапу я шукаў, але дакладна не такую. Эйдрыі пакажы. Напэўна аб'ясніць, што " "гэта. Скажу адно што з віду старая, а старая — значыць дарагая." #. TRANSLATORS: Quest dialog spoken by Gharbad the Weak #: Source/textdat.cpp:297 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" "Калі лааааска, не бі. Не забі. Пакіні ў жывых, наступны раз нясці табе " "харошае." #. TRANSLATORS: Quest dialog spoken by Gharbad the Weak #: Source/textdat.cpp:299 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Нешта раблю для цябе. Не забівай Гарбада, калі ласка. Жыць і даць харошае.\n" "\n" "На, доказ, што праўду гавару..." #. TRANSLATORS: Quest dialog spoken by Gharbad the Weak #: Source/textdat.cpp:301 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "Яшчэ нічога! Амаль гатова.\n" "\n" "Вельмі моцнае, вельмі сільнае. Жыць! Жыць!\n" "\n" "Болю не будзе і абяцананне выканаю!" #. TRANSLATORS: Quest dialog spoken by Gharbad the Weak (Hostile) #: Source/textdat.cpp:303 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Для цябе занадта харошае. Вельмі моцнае! Ты хацець — ты забіраць!" #. TRANSLATORS: Quest dialog spoken by Zhar the Mad (annoyed / Hostile) #: Source/textdat.cpp:305 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "Што?! Ты тут чаго? Так і здурнець можна калі столькі перыпыняюць. На, бяры і " "дай мне працаваць. Не дакучай мне больш!" #. TRANSLATORS: Quest dialog spoken by Zhar the Mad (Hostile) #: Source/textdat.cpp:307 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "А-а-ай! Твая цікаўнасць стане табе смерцю!!!" #. TRANSLATORS: Neutral dialog spoken by Cain #: Source/textdat.cpp:308 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Прывет, дружа мой. Пастой крыху ды паслухай..." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:309 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "На сваім шляху ўглыб Лабірынта ты можаш знайсці кнігі, якія таяць вялікія " "веды. \n" "\n" "Уважліва іх чытай, бо яны табе раскажуць такое, чаго і я нават не змог бы." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:311 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Пакуль ты вандруеш па Лабірынце, ў цябе могуць паўстаць пытанні, адказы на " "якія могуць таіць міфы і легенды якія я ведаю. Калі сутыкнешся з нейкім " "такім пытаннем і зацікавішся ім, адшукай мяне, ды я раскажу, пра што змагу." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:313 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" "Грызвальд — чалавек вельмі руплівы і смелы. Я ўпэўнены, ён ніколі не " "расказваў табе, як выратаваў Вірта з Лабірынта, так жа? Ён добра ведае, якая " "вялікая небяспека тоіцца там, як і ты. Ён умелы майстар, таму калі ён " "сцвярджае, што ва ўсім табе можа дапамагчы, можаш не сумнявацца ў яго " "сумленнасці ды, ўмельстве." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:315 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Огдэн ужо чатыры гады карчмарыць ва Узыходным Сонцы. Ён выкупіў яго ўсяго за " "пару месяцаў да таго, як тут усё пакацілася к чорту. Яму і жонцы яго Гардзе " "не хапае грошай, каб з'ехаць адсюль, бо ўсё патрацілі, каб тут асесці. Ён " "добры чалавек, з моцным пачуццём адказнасці." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:317 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" "Небарака Фарнам. Неспакойны напамін аб загубленай суполцы, што ступіла ў " "Храм за Лазарам тым змрочным днём. Ён збег адтуль жывым, але храбрасць сваю, " "як і частку здаровага розуму, пакінуў у якойсьці чорнай яме. Цяпер ён " "знаходзіць суцяшэнне толькі на дне свайго куфля, але ў плыні яго бясконцай " "балбатні схаваны крупінкі праўды." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:319 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" "Тая ведзьма, Эйдрыя, — як анамалія тут у Трыстраме. Яна прыбыла якраз калі " "апаганілі Храм, пакуль усе вакол з’язджалі адсюль. Яна збудавала хату на " "ўскраіне горада, відаць, ноччу, і мае доступ да многіх дзіўных і таемных " "артэфактаў і кніг з ведамі, якія нават я ніколі не бачыў." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:321 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" "Гісторыя Вірта жахлівая і трагічная. Гадкія дэманяты з коп’ямі вырвалі яго з " "рук яго мацеры і сцягнулі ў лабірынт. Многа дзяцей забралі ў той дзень, " "уключаючы сына Караля. Рыцары палаца спускаліся туды, але ніхто не вярнуўся. " "Наш каваль знайшоў хлопчыка, але ўжо пасля таго, як яны пачалі яго катаваць " "дзеля сваёй садысцкай асалоды." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:323 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "А, Піпін. Я лічу яго сапраўдным сябром, магчыма, бліжэйшага і не маю. Ён " "часам трохі збянтэжаны, але не нарадзілася яшчэ душа клапатлівейшая ці " "чуллівейшая. Мала з кім можна параўнаць яго веды і ўменне, а дзверы яго " "заўсёды адчыненыя." #. TRANSLATORS: Neutral dialog spoken by Cain (Gossip) #: Source/textdat.cpp:325 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" "Джыліен добрая жанчына. Яе любяць за вясёлы нораў і смяшлівасць, і яна " "занімае асобнае месца ў маім сэрцы. Яна засталася ў карчме, каб даглядаць " "сваю пажылую бабулю, хвароба не дае ёй ехаць. Падчас я баюся за яе, але " "ведаю, што ўсякі ў вёсцы хутчэй бы сам памёр, чым даў бы яе ў крыўду." #. TRANSLATORS: Neutral dialog spoken by Ogden #: Source/textdat.cpp:327 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "Дабрыдзень, добры чалавек. Вітаю ў карчме Узыходнага Сонца!" #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:329 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" "Шмат прайдзісветаў удастоілі сталы маёй карчмы, а аповесцяў за імі расказалі " "ў дзесяць разоў больш пад столькі ж эля. Адзіная рэч, у якой яны ўсе " "сыходзіліся, была адна старая аксіёма. Можа яна табе дапаможа. Плоць можна " "разрэзаць, але мусіш разбіць косць." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:331 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" "Каваль Грызвальд надзвычай дасведчаны ў зброі і брані. Калі табе як-небудзь " "спатрэбіцца апрацаваць свой рыштунак, табе дакладна да яго." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:333 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Фарнам тут зашмат часу бавіць, топіць сваё гора ў танным элю. Я б праганяў " "яго, але ж тады ў Лабірынце ён і праўда многа выпакутаваў." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:335 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Эйдрыя мудрая не па гадах, але мушу прызнацца — яна мяне трохі пужае.\n" "\n" "Ну і няважна. Калі табе спатрэбяцца якія чараўнічыя рэчы, яе хата за ракою, " "з вельмі дзіўна багатым выбарам тавараў." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:337 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Калі хочаш больш знаць пра гісторыю нашай вёсці, расказчык Кейн ведае даволі " "многа пра мінульшчыну." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:339 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Вірт — маленькі жулік і нягоднік. Ён заўсёды трапляў у бяду, няма дзіва, што " "з ім такое здарылася.\n" "\n" "Відаць залез падурэць куды нельга было. Мне шкада яго, праўда, але тых, з " "кім ён водзіцца, я не трываю." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:341 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Піпін — добры чалавек, і дакладна самы велікадушны ў вёсцы. Ён заўсёды дбае " "пра патрэбы другіх, але нейкая бяда быццам і праўда следам за ім ідзе, куды " "б ён ні ішоў..." #. TRANSLATORS: Neutral dialog spoken by Ogden (Gossip) #: Source/textdat.cpp:343 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" "Джыліен, барменка мая? Каб не пачуццё абавязку перад сваёй бабуляй, яна б " "даўно адсюль уцякла.\n" "\n" "Неба — сведка, маліў яе з'ехаць адсюль, казаў, маўляў, пагляджу старую, але " "яна занадта ўжо добрая і клапатлівая, каб так зрабіць." #. TRANSLATORS: Neutral dialog spoken by Pipin #: Source/textdat.cpp:345 msgid "What ails you, my friend?" msgstr "Што вам далягае, дружа мой?" #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:346 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" "Я вынайшаў адну надта цікавую рэч. У адрозненні ад нас, істоты ў Лабірынце " "могуць гаіць свае раны без чужога ўмяшання ці магіі. Калі раніш пачвару, " "упэўніся, што яна мёртая, іначай яна лёгка адновіцца." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:348 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Да таго як Храм захапілі гэтыя, ну, хто там цяпер тоіцца, гэта было выдатнае " "месца для адукацыі. Там можна знайсці багата кніг. Усё што знойдзеш, чытай, " "бо з некаторых з іх можаш дазнацца аб тым, як устроены Лабірынт." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:350 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" "Грызвальд знаецца на ваенным дзеле так жа, як я на дзеле лячэбным. " "Праніклівы ён гандляр, але ж работа яго нікому не ўступае. Ой, мабыць гэта " "таму, што ён адзіны каваль, які тут застаўся." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:352 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" "Кейн – сапраўдны сябар і вялікі мудрэц. Ён трымае вялізную бібліятэку і ад " "прыроды здольны распазнаць існую натуру многіх рэчаў. Калі будзеш мець якое-" "небудзь пытанне, да яго дакладна звярніся." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:354 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Нават са сваімі ўменнямі я не змог да канца вылячыць Фарнама. Эх, целам-то " "ён ачуняў, але розум яго і дух па-за ўсімі маімі здольнасцямі." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:356 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" "Хоць я і карыстаюся магіяю, у абмежаваных яе формах, каб у запас ствараць " "зеллі ды эліксіры, вось Эйдрыя – сапраўдная чараўніца. Яна быццам ніколі не " "спіць, і заўсёды мае доступ да многіх таямнічых артэфактаў і фаліянтаў. " "Думаецца мне, хата яе — не проста халупа, якой яна падаецца, але туды ніколі " "не атрымліваецца зайсці." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:358 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Бедны Вірт. Я зрабіў ўсё, што мог, для малыша, але я ведаю, ён ненавідзіць " "тую кавялу, якую прыйшлося прымацаваць к яго назе. Раны былі жудасныя. Ды " "ніхто – асабліва такое малое дзіця – не мусіць пакутаваць так, як давялося " "яму." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:360 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" "Я праўда не разумею, чаму Огдэн дасюль застаецца ў Трыстраме. У яго трохі " "разышліся нервы, але ён дасціпны і працавіты чалавек, дзе б ні быў, ён бы " "прыстасаваўся. Мяркую, гэта з-за страху забойстваў, што чыняцца ў ваколіцы, " "або гэта жаданне жонкі ягонай, што ён з сям'ёй нікуды не з'язджае." #. TRANSLATORS: Neutral dialog spoken by Pipin (Gossip) #: Source/textdat.cpp:362 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "Огдэнава барменка мілая дзяўчына. Бабуля яе хварэе, мучыцца ад нейкага " "мораку.\n" "\n" "Кажа, яна бачыць відзежы, але доказаў таго у мяне няма так ці інакш." #. TRANSLATORS: Neutral dialog spoken by Gillian #: Source/textdat.cpp:364 msgid "Good day! How may I serve you?" msgstr "Добры дзень! Чым магу служыць?" #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:365 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Мая бабуля мне сказала, што ты прыйдзеш пагаварыць са мною. А яна бачыць " "відзежы, можа глядзець у будучыню." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:367 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "Жанчына з ускраіны горада — ведзьма! Ну, яна досыць добрая, і імя ў яе, " "Эйдрыя, прыемнае вуху, але я вельмі яе баюся.\n" "\n" "Толькі нехта досыць смелы, як ты, мог бы глянуць, чым яна там займаецца." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:369 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" "Наш каваль — прычына гордасці трыстрамцаў. Не толькі ў яго рукі залатыя, і " "ён многа спаборніцтваў у сваёй гільдыі выйграў, але яго сам кароль Леорык " "хваліў, няхай ён спіць спакойна. Грызвальд і вялікі герой, Кейна спытай." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:371 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Кейн быў расказчыкам Трыстрама колькі сябе помню. Ён столькі ўсяго ведае, " "можа расказаць пра што хочаш бадай усё што можна хацець." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:373 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" "Фарнам – п’яніца. Сабе ў глотку эль ліе, а іншым у вушы трызніць.\n" "\n" "Я ведаю, што Піпін і Огдэн спачуваюць яму, але мяне так засмучае бачыць, як " "ён штоноч скочваецца ў п’яны ступар ўсё глыбей і глыбей." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:375 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" "Піпін выратаваў жыццё маёй бабулі, і я ведаю, што ніколі не адгаджу яму за " "гэта. Яго здольнасць вылячыць любую хваробу сільнейшая за наймагутнейшы меч, " "і больш таямнічая за любыя чары, якія ведаеш. Калі спатрэбіцца лячэнне, " "Піпін можа дапамагчы." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:377 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" "Я вырасла з маці Вірта, Кэнэс. Хаця яе нясільна ранілі, калі тыя вычварні " "скралі яго, яна так і не ачулася. Думаю, яна памярла, бо ёй скрышыла сэрца. " "Вірт стаў зламысным басяком, усё шукае як нажыцца з чужой працы. Я ведаю, як " "ён нацярпеўся, а якіх жахаў нагледзеўся, не магу ўявіць, але тая цемра ўсё-" "такі яшчэ вісіць над ім." #. TRANSLATORS: Neutral dialog spoken by Gillian (Gossip) #: Source/textdat.cpp:379 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Огдэн і жонка яго ўзялі мяне і маю бабулю к сабе, нават даюць мне зарабляць " "сабе на хлеб у сваёй карчме. Я ім столькім абавязана, спадзяваюся аднойчы " "з’ехаць адсюль з імі і дапамагчы ім адкрыць на ўсходзе вялікую гасцініцу." #. TRANSLATORS: Neutral dialog spoken by Griswold #: Source/textdat.cpp:381 msgid "Well, what can I do for ya?" msgstr "Ну, чым магу спамагчы?" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:382 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" "Шукаеш сабе якой добрай зброі, дай вось што пакажу. Узяць хоць звычайную " "тупую зброю, напрыклад паліцу. Супраць тых неўміручых страхаў проста цуды " "робіць, дый няма лепшай штукі, каб такой шкілеціка раскрышыць!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:384 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" "Сякера? Але, добра зброя, на любого ворага збалансавана. Палянь як яна " "паветро расцінае, просто ўяві харошанькаго такого гаматного дэмана на яе " "шляху. Адно ж не забывай, што ёю размахвацца цяжко — але які пасля будзе " "моцны ўдар!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:386 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Толькі лянь на вастрыё, якая раўнавага. У правільных руках, меч, дый супраць " "правільнаго ворага, гаспадар над усёй зброяю. Востраму лязу яго амаль няма " "чаго сячы ці пратыкаць на мярцах, але жывому, што шчэ дыхае, меч добра плоць " "пасячэ!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:388 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "На тваёй зброі ды брані будзе відаць сляды барацьбы з Цемраю. Прынясеш мне, " "распалім горан, трохі папрацацуймо, і вернем ім лепшы баявы стан." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:390 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" "Покі я, лічы, шмуглюю, каб струманты там ды метал прыдбаць з караванаў, што " "ходзяць часам за нашым клятым горадам, ведзьма тая, Эйдрыя, нібыто ўсё што " "трэба дастаць можа. Знайся я хоць крыху як яна на чарадзействе, я б вырабляў " "сапраўды файныя рэчы." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:392 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Джыліен слаўная дзеўчына. Шкада, што бабулі ейнай так далягае, а то б я " "ўладзіў, каб які гандлёвы караван вывез іх адсюль." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:394 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" "Іншым разам задумваюся, маўляў Кейн замнога гаворыць, але відаць такое яго " "прызванне. Ездзі я па жалезе так, як ён табе па вушах, я б зрабіў ужо такі " "раскошны гарнітур, Імператару б пасаваў!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:396 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Я быў з Фарнамам тае ночы, калі Лазар завёў нас у Лабірынт. Больш я " "Архібіскупа не бачыў, і я б сам не вырабіўся, каб не Фарнам. Баюся, тая " "атака скалечыла яму душу, ды так, як, ну, як мне маю нагу. Зараз у ягонай " "бітве я не магу біцца за яго, але каб прымеў, біўся б." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:398 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Добры той чалавек што ставіць патрэбы іншых вышэй за свае. Не знойдзеш " "нікого ў Трыстраме — і дзе яшчэ, раз на тое пайшло, — хто б казаў пра лекара " "нешто дрэннае." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:400 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" "І трапіць жа той малец раз у бяду... ну, мушу сказаць, зноў трапіць. Я хацеў " "быў яго зацікавіць работай, каб вучыўся тут чэснай працы, але яму больш " "даспадобы вялікая выгада з гандлю таварам невядома адкуль узятым. Не магу " "яго за тое вініць, пасля ўсяго што з ім было, але я б вельмі хацеў, каб ён " "хоць асцярожны быў." #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) #: Source/textdat.cpp:402 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" "Справы ў карчмара ідуць не надто, ён амаль не мае выгады са свайго дзела. " "Канцы з канцамі зводзіць, забяспечваючы ежай і начлегам тых, хто падчас едзе " "праз вёску, але яны хутчэй змыюцца ўночы, ніж яму заплацяць. Каб ён не " "назапасіў у сваіх скляпах збожжа і сушанага мяса, авохці, з голаду б памерла " "большасць з нас у першы ж год, калі ўсю ваколіцу запаланілі дэманы." #. TRANSLATORS: Neutral dialog spoken by Farnham #: Source/textdat.cpp:404 msgid "Can't a fella drink in peace?" msgstr "Ужо і выпіць нельга спакойна?" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:405 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "Дзеўка, што выпіўку падае? Ой, ну так, такая прыгажуня. Яшчэ і добрая." #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:407 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "Чаго тая карга хоць для разнастайнасці нешта не зробіць. Вядома, вядома ж, " "яна там нечым займаецца, але ты мяне паслухай... яна не нармальная. Я ніколі " "не бачыў, каб яна ела ці піла, а нельга давяраць таму, хто хоць крыху не п'е." #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:409 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Кейн не той, за каго се выдае. Вядома, вядома ж, зубы загаворвае ён умела... " "а часам вельмі і страшна і смешна... але думаецца мне, ён ведае больш, чым " "сам ведае, што ведае." #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:411 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Грызвальд-то? Стары добры Грызвальд. Ды я яго як брата люблю! Мы тады разам " "біліся, ну знаеш, калі гэта... мы... Лазар... Лазар... Лазар!!!" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:413 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" "Хехехе, мне да спадобы Піпін. Ён сапраўды стараецца, знаеш так. Слухай, ты з " "ім дакладна пазнаёмся. Добры таварыш сярод люду, якім вечна трэба помач. Ну, " "амаль нехта як ты, а, герой? І я быў героем…" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:415 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "У Вірта праблем нат больш ніж у мяне, а я-то пра праблемы знаю. Паслухай – " "дзела-то ў хлапчука спорыцца, аж ён быў там, знаеш? Нагу страціў! Клыпаць " "цяпер на кавалку дзерава. Якое гора, якое гора…" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:417 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Огдэн – найлепшы мужык у горадзе. Не думаю, што жонка ягоная мяне надта " "падабае, але пакуль яна з бочак разлівае, буду і я яе падабаць. З Огдэнам я " "баўлю нібыта больш часу, ніж яшчэ з кім, але ён такі добры…" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:419 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" "Нешта я табе скажу, бо ж гэта, я пра гэта ўсё ведаю. То мой канёк. От, ля, " "гэты проста найлепшы… нааааайлепшы! Той другі эль зусім не харошы, усё з-за " "тых тупых сабак…" #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:421 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" "Ніхто мяне нік… ніколі ні слухае. Недзе — не пэўны дзе — недзе пад цэркваю ё " "цэлая куча золата. Блішчыць, зіхаціць ды адно чакае, каб яго нехта прыбраў." #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:423 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" "Знаю, ў цябе там сваё наўме, дый знаю, што не паверыш, але зброя твая — ну " "зусім не дзела проці той нечысці! Ай, ды пляваць мне што там кажа Грызвальд, " "цяперака не робяць як у даўнейшыя часы..." #. TRANSLATORS: Neutral dialog spoken by Farnham (Gossip) #: Source/textdat.cpp:425 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" "Будзь я табой... хаця я не ты... але будзь я, я б прадаў усё, што маеш, і " "даў бы драпака адсюлека. Унь хлопец гэны... У яго заўсёды нешта файнае, але " "яму трэ золата адсыпаць, а то ён нат не пакажа, што там у яго." #. TRANSLATORS: Neutral dialog spoken by Adria #: Source/textdat.cpp:427 msgid "I sense a soul in search of answers..." msgstr "Душу ў пошуках адказаў адчуваю..." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:428 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "Мудрасць здабываецца, а не даецца. Адшукаўшы фаліянт вед, паглыні яго словы. " "Калі высветліцца, што ты ўжо валодаеш запаветнымі тайнамі, вылажанымі ў " "кнізе, помні – гэты ўзровень майстэрства заўжды можна перасягнуць." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:430 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" "Найвялікшая сіла часцей і жыве карацей ад усіх. Ты можаш знайсці словы " "старасвецкай сілы на скрутках пергаменту. Моц такіх скруткаў заключана ва " "уласцівасці іх карыстання: аднолькавы поспех чакае і простага вучня, і " "адэпта. Слабасць жа іх у тым, што спачатку павінна прачытаць іх услых, і іх " "нельга захаваць сабе ў памяці. Ведай і тое, што выкарыстаць такі скрутак " "можна толькі аднойчы, таму выкарыстоўвай іх абачліва." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:432 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" "Хаця гарачыню сонца нельга змераць, полымя ўсяго аднае свечкі " "небяспечнейшае. Няма энергіі, якою можна было б карыстацца без належнага " "фокусу. Для многіх чар гэта працуе так, што праз скрутак магчыма зарадзіць " "посах іх магічнаю сілаю, і многа раз. Я магу аднавіць іх сілу, але ведай, " "што нічога не робіцца дарма." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:434 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "Сума нашых вед у суме людзей, што імі валодаюць. Калі знойдзеш кнігу ці " "скрутак, які не можаш расшыфраваць, не вагайся звярнуцца да мяне. Калі я " "ўбачу ў тым сэнс, я падзялюся, чым змагу." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:436 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "Для чалавека, каторы ведае толькі жалеза, няма большай магіі за Сталь. " "Каваль Грызвальд большы чараўнік, чым ён здагадваецца. У гэтым краі ў " "здольнасці зліваць агонь і метал яму няма роўных." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:438 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "Псота валодае сілай падману, але нявіннасць змяшчае сілу чысціні. Юная " "кабета Джыліен мае чыстае сэрца, і ставіць беды сваёй роданачальніцы вышэй " "за свае. Яна страшыцца мяне, але толькі таму, што не разумее мяне." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:440 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" "Куфар, калі адкрыты ў цемры, не змяшчацьме большы скарб, чым калі адкрыты ў " "святле. Расказчык Кейн – таямніца, але толькі для тых, хто не ўглядаецца. " "Яго веды аб тым, што знаходзіцца пад саборам, значна большыя, чым ён нават " "дазваляе сабе ўявіць." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:442 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" "Чым вышэй ты ставіш веру ў аднаго чалавека, тым вышэй ёй потым упасці. " "Фарнам страціў душу, але не на карысць дэманам. Яна страцілася, калі ён " "бачыў, як яго землякам здрадзіў Архібіскуп Лазар. Ён валодае ведамі, якія " "варта збіраць па каліве, але мусіш адрозніваць факт ад фантазіі." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:444 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" "Рука, сэрца і розум твораць цуды, калі яны ў суладнасці. Лекар Піпін зазірае " "ў цела так, як не магу нават я. Яго здольнасць аднаўляць хворых і параненых " "памножана яго разуменнем стварання эліксіраў і зелляў. Лепшага паплечніка ў " "Трыстраме табе не знайсці." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:446 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" "Мы многа не ведаем аб будучыні, але калі яна надыходзіць, то трапляе ў рукі " "дзяцей. Хлопчык Вірт мае чарнату на душы сваёй, але ён не кідае ценю пагрозы " "ні на горад, ні на яго людзей. Вынік яго патаемных спраў з абадранцамі і " "скрытнымі гільдыямі вакольных гарадоў у яго доступе да многіх прылад, якія " "не так проста знайсці ў Трыстраме. Яго спосабы можна дакараць, але Вірт можа " "паспрыяць табе ў тваёй бітве з Цемраю, якая не адступае." #. TRANSLATORS: Neutral dialog spoken by Adria (Gossip) #: Source/textdat.cpp:448 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" "То не сцены ды страха дом ствараюць. У гэтым мястэчку карчмар Огдэн служыць " "большай мэце, чым многія могуць падумаць. Ён дае прытулак Джыліен і яе " "роданачальніцы, падтрымлівае жыццё Фарнама, якое той скінуў на яго, і " "служыць усім, хто застаўся ў горадзе, якарам колішняга Трыстрама. Яго карчма " "і простыя радасці, што яшчэ можна знайсці там, даюць мелькам зірнуць на " "жыццё, якое людзі тут яшчэ помняць. Гэта ж памяць і жывіць іх надзею на твой " "поспех." #. TRANSLATORS: Neutral dialog spoken by Wirt #: Source/textdat.cpp:450 msgid "Pssst... over here..." msgstr "Пс... ходзь сюды..." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:451 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "Не ўсім у Трыстраме ёсць карысць – ці інтарэс – да ўсяго, што ты знаходзіш у " "лабірынце. Нават мне, як ні цяжка гэтаму паверыць.\n" "\n" "Іншым разам толькі табе ўдасца знайсці прымяненне некаторым рэчам." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:453 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "Не давай веры ўсяму, што кажа апівоша. Столькі многа элю патупіла яму і вочы " "і цвярозы розум." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:455 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" "Калі не добра відаць, я нічога не купляю з Трыстрама. Я імпарцёр якаснага " "тавару. Калі хочаш смецце каму загнаць, ідзі да Грызвальда там, Піпіна, або " "да ведзьмы той, Эйдрыі. Упэўнены, з рукамі адарвуць, што ні прынясеш…" #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:457 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Здаецца я кавалю жыццём абавязаны, вярней тым што ад яго засталося. Вядома, " "Грызвальд мяне ў вучні зваў на кузню, і ён даволі добры мужык, але там я " "ніколі не накаплю на… ну, скажам так, ёсць у мяне адна задума, для якой мне " "трэба вялікая куча золата." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:459 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Будзь я трохі старшэй, хадзіла б яна ў золаце, якое толькі б пажадала, і не " "сумнявайся, я магу дастаць сапраўды харошыя рэчы. Джыліен – добрая дзяўчына, " "трэба ёй цякаць з Трыстрама, пакуль мажліва. Гм-м-м… мо ўзяць яе з сабою, " "калі пайду…" #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:461 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" "Кейн ведае занадта многа. Як ён мяне гэтым страшыць, нават мацней за тую, " "што за ракой жыве. Ён усё кажа, маўляў, як мне павязло што я жывы, і што мая " "гісторыя прадказана ў легендзе. Мне здаецца ён зусім з глуздоў з'ехаў." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:463 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Фарнам — вось у каго сур'ёзныя праблемы, а я-то знаю, якімі сур'ёзнымі " "могуць быць праблемы. Даверыўся шчырасці аднаго чалавека засільна, Лазар яго " "і завёў у самую пашчу смерці. А, дый я знаю, як там, нават не пачынай пра " "свае планы знішчыць зло, што асялілася ў Лабірынце. Ногі беражы, вось што " "скажу..." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:465 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Калі толькі табе не трэба нешта прымацаваць, лепш старога Піпіна тут няма. \n" "\n" "Былі б у мяне тады яго зеллі, можа і з нагою бы застаўся..." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:467 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Вось Эйдрыя мяне праўда непакоіць. Кейн канешне страшны, як ён табе пра тваё " "мінулае можа расказаць, аж ведзьма тая можа ў тваё мінулае зазірнуць. І ёй " "вечна ўдаецца дастаць усё, што ёй зажадаецца. Яна столькі тавару прыдабывае, " "больш чым я бачыў цераз браму на каралеўскім базары ў Вялікі Фэст." #. TRANSLATORS: Neutral dialog spoken by Wirt (Gossip) #: Source/textdat.cpp:469 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" "Дурань Огдэн, што тут застаецца. Я бы яго адсюль вывез ды па вельмі разумнай " "цане, але ён упёрся, хоча з гэтай дурной карчмы разбагацець. Ну, хоць " "Джыліен ёсць дзе рабіць, і жонка ягоная, Гарда, проста цудоўную запяканку " "гатуе..." #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:471 Source/textdat.cpp:479 Source/textdat.cpp:487 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" "За заламі герояў стаіць Пакой Касцей. Вечная смерць чакае ўсякага хто " "паквапіцца на скарбы ахаваныя ў тым месцы. Так кажа Пан Жаху, так і напісана." #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:473 Source/textdat.cpp:481 Source/textdat.cpp:489 #: Source/textdat.cpp:527 Source/textdat.cpp:535 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" "І вось, замкнёная за варотамі крыві, цераз залу агнёў, Адвага чакае, калі " "герой святла абудзіцца…" #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:475 Source/textdat.cpp:483 Source/textdat.cpp:491 #: Source/textdat.cpp:529 Source/textdat.cpp:537 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Віджу, што табе не відна,\n" "Зрок тупее, вочы ў гнілі.\n" "Іх няма, як абярнешся, \n" "Шэпчуць толькі тайну песню.\n" "Тут убачыш, што не ўбачыць.\n" "Дзе святло? Там цені танчуць.\n" "З глудзу, з цьмы ты паляціш.\n" "Проста ўніз, у Зал Сляпых." #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:477 Source/textdat.cpp:485 Source/textdat.cpp:493 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Збраёўні Пекла — дом Крывавага ваяводы. За ім ляжаць знявечаныя целы тысяч і " "тысяч. Забіты і анёлы і людзі, усё ў ахвяру Цёмным, што крычаць аб адным — " "крыві." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:505 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" "Зважайце ж і будзьце сведкамі ісціны, выказанае тут, бо то ёсць апошняе са " "спадчыны Харадрым. Нават цяпер лютуе вайна, па-за межамі, вядомымі нам – меж " "Бездакорных каралеўстваў Вярхоўных Нябёс і Бязладнай Прорвы Агністых Пекел. " "Імя тае вайны – Вялікае Змаганне, і бушуе яны ды гарыць ужо даўжэй за ўсякую " "зорку на небе. Ніводзін бок не бярэ верх надоўга, покуль сілы Святла і Цемры " "бесперастанку спаборнічаюць за ўладу над усім існым." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:507 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" "Зважайце ж і будзьце сведкамі ісціны, выказанае тут, бо то ёсць апошняе са " "спадчыны Харадрым. Калі адвечнае змаганне меж Вярхоўнымі Нябёсамі і " "Агністымі Пекламі выпадае на смертных грунт, то і ёсць Вайна Граху. Анёлы і " "дэманы скрытыя ходзяць сярод людзей, таемна змагаючыся, чым далей ад " "цікаўных чалавечых вачэй. Некаторыя са смертных, магутныя і смелыя, нават " "сталі ў хаўрус з адным з двух бакоў, дапамагаючы весці ход вайны." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:509 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Зважайце ж і будзьце сведкамі ісціны, выказанае тут, бо то ёсць апошняе са " "спадчыны Харадрым. Амаль трыста год таму, стала вядома аб таямнічым прыходзе " "тройцы Першага Зла з Агністых Пекел у наш свет. Трое братоў спусташалі землі " "Усходу дзесяцігоддзямі, покуль чалавецтва дрыжала пад імі. Нашае брацтва — " "Харадрым — заснавалася скрытнаю суполкаю Мудрацоў, дзеля таго, каб высачыць " "і паланіць Трое Зол раз і назаўжды.\n" "\n" "Першыя Харадрым двух з Трох захапілі ў моцныя артэфакты, вядомыя як Камяні " "Душ, і пахавалі іх глыбока пад усходнімі пустынямі. Трэцяе з Зол пазбегла " "палону і ўцякло ад вялікай пагоні Харадрым на Захад. Яго, вядомага як " "Д'ябла, Пан Жаху, урэшце паланілі, і існасць яго змясцілі ў Камень Душы і " "пахавалі ў Лабірынце.\n" "\n" "Сцеражыцеся ж, бо нельга таму стацца, каб Камень Душы знайшлі тыя, хто не ад " "Веры. Калі яны вызваляць Д'ябла, ён шукацьме цела, лёгкае для ўладання, бо " "ён будзе вельмі слабы. Магчыма, цела як старога альбо дзіцяці." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:511 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "І сталася так, што ў Агністых Пеклах быў мяцеж, празваны Цёмным Выгнаннем. " "Меншае Зло зрынула тройцу Першага Зла, і прагнала дух іх у царства смертных. " "Дэманы ж Бэліял, валадар ілжы, і Азмадан, валадар граху, за адсутнасцю Трох " "Братоў распачалі барацьбу за стырно ўлады. Усё Пекла падзялілася на два " "лагеры ў супрацьстаянні Бэліяла і Азмадана, покуль сілы Вярхоўных Нябёс " "бесперастанна таранілі ў самую браму Пякельную." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:513 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Многія дэманы адправіліся ў царства смертных, шукаючы Трох Братоў. А за тымі " "дэманамі рушылі следам анёлы, і палявалі на іх па неабсяжных гарадах Усходу. " "Анёлы аб'ядналіся з брацтвам смертных Мудрацоў, што звалася Харадрым, якія ў " "скорым часе сталі дасведчанымі паляўнічым на дэманаў. А ў падземных светах " "знайшлі яны сабе многа цёмных ворагаў." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:515 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" "І сталася так, што духі Тройцы Першага Зла былі выгнаны ў царства смертных, " "а пасля таго, як яны сеялі хаос па ўсім Усходзе дзсяцігоддзямі, праклятае " "брацтва Харадрым высачыла іх. Харадрым выкарысталі артэфакты, называныя " "Камяні Душ, каб стрымаць у іх існасць Мефіста, Пана Нянавісці, і брата яго " "Баала, Пана Знішчэння. Малодшы брат, Д'ябла, Пан Жаху, збег на Захад.\n" "\n" "Урэшце, Харадрым зняволілі ў Каменю Душы і Д'ябла, і пахавалі яго пад " "старадаўнім, забытым Саборам. Там Пан Жаху і спіць, чакаючы гадзіну свайго " "адраджэння. Ведайце ж, што ён шукацьме цела юнае і моцнае, каб ухапіцца ў " "яго, такое, што бязвінным ёсць і для ўладання лёгкім. Тады паўстане ён, каб " "збавіць братоў сваіх і зноў раздзьмуць полымя вайны Граху..." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:517 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" "Аднаго Д’ябла ўсхваляем, пана Жаху, ацалелага Цёмнага Выгнання. Калі ён " "прачнуўся ад свайго доўгага сну, пан мой і гаспадар загаварыў са мною аб " "тайнах, якія немногім смертным вядомыя. Сказаў ён, што Вярхоўныя Нябёсы і " "Прорвы Агністых Пекел вядуць вечную вайну. І адкрыў ён мне сілы, што ўнеслі " "разлад у царствы Чалавекаў. І назваў пан і гаспадар мой бітву за свет гэты і " "ўсіх у ім Вайною Граху." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:519 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" "Уся хвала і слава Д'яблу, пану жаху і галаве над Трыма. Пан мой гаварыў са " "мною аб дваіх братах сваіх, Мефіста і Баале, выгнаных ў свет гэты даўным-" "даўно. Пан мой жадае вычакаць зручны момант і сабраць сваю жахлівую сілу, " "каб вызваліць сваіх паланёных братоў з грабніц пад усходнімі пяскамі. Калі " "мой пан збавіць братоў сваіх, Вайна Граху ізноў спазнае лютасць Трох." #. TRANSLATORS: Book read aloud #: Source/textdat.cpp:521 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" "Слава і ахвяра Д'яблу, Пану Жаху і Душагубу. Калі абудзіў я Гаспадара свайго " "ада сну яго, ён намогся ўхапіцца ў смертную абалонку. Д'ябла абраў цела " "Караля Леорыка, але Гаспадар мой быў заслабым праз сваё зняволенне. Пан мой " "меў патрэбу ў адным простым і бязвінным якары ў свет гэты, і так палічыў " "хлопчыка Альбрэхта найлепшым для той справы. Пакуль слаўны Кароль Леорык " "застаўся звар'яцелым пасля няўдалага апанавання Д'яблам, я скраў сына яго, " "Альбрэхта, і прывёў яго к Пану свайму. Цяпер чакаю поклічу Д'яблы, і малюся, " "што буду ўзнагароджаны, калі нарэшце ён паўстане як Пан гэтага свету." #. TRANSLATORS: Neutral Text spoken by Ogden #: Source/textdat.cpp:523 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "Слава небу вы тут!\n" "Шмат змянілася з часу, калы вы тут жылі, дружа мой. Усё было спакойна, але " "потым прыехалі цёмныя вершнікі і знішчылі нашу вёску. Многіх забілі на месцы " "ж, а хто адбіваўся, тых або забілі, або зацягнулі ў рабства, ці нават горш. " "Царкву на ўскраіне апаганілі, і цяпер там чыняцца цёмныя рытуалы. То не " "чалавечыя крыкі адтуль адгукаюцца ноччу, але магчыма, што нехта з гараджан і " "выжыў. Ідзіце дарогай між маёй карчмой і кузняй і знайдзіце царкву, " "выратуйце, каго зможаце.\n" "\n" "Можа другі раз змагу расказаць больш. Удачы." #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:525 Source/textdat.cpp:533 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits " "any who would seek to steal the treasures secured within this room. So " "speaks the Lord of Terror, and so it is written." msgstr "" "За заламі герояў стаіць Пакой Касцей. Вечная смерць чакае ўсякага хто " "паквапіцца на скарбы ахаваныя ў тым месцы. Так кажа Пан Жаху, так і напісана." #. TRANSLATORS: Quest text spoken aloud from a book by player #: Source/textdat.cpp:531 Source/textdat.cpp:539 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and man alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Збраёўні Пекла — дом Крывавага Ваяводы. За ім ляжаць знявечаныя целы тысяч і " "тысяч. Забіты і анёлы і людзі, усё ў ахвяру Цёмным, што крычаць аб адным — " "крыві." #. TRANSLATORS: Quest text spoken by Adria #: Source/textdat.cpp:541 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" "Не спыняй свае пошукі. Знайсці згублены скарб складана. Знайсці схаваны " "скарб — ужо менш. Гэтага табе пакуль хопіць. Не дай пяскам часу збянтэжыць " "цябе ў пошуках." #. TRANSLATORS: Quest text spoken by Griswold #: Source/textdat.cpp:543 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" "Каго?! Глупство гэто. Няма пад Трыстрамам ніякіх скарбаў. Дай гляну!! А, " "лянь, малюнкі з памылкамі. Зусім на наш горад не падобно. Я б думаў пра тое, " "што пад саборам, а не што пад верхнім пластом глебы." #. TRANSLATORS: Quest text spoken by Pipin #: Source/textdat.cpp:545 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" "Па праўдзе, не маю часу, каб абмяркоўваць нейкую мапу. У мяне тут шмат " "людзей, якім мне трэба дапамагчы, як і табе таксама." #. TRANSLATORS: Quest text spoken by Adria #: Source/textdat.cpp:547 Source/textdat.cpp:559 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" "Некалі горды Ізуал апынуўся ў пастцы пад паверхняй гэтага свету. Яго гонар " "адняты, а твар зменены. Ён у пастцы вечных пакут. Абавязаны скрываць тую " "адзіную рэч, што магла б яго вызваліць." #. TRANSLATORS: Quest text spoken by Ogden #: Source/textdat.cpp:549 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" "Стаўлю, што Вірт проста нешта выдумаў, калі цябе ўбачыў, каб потым " "пасмяяцца, калі пачнеш бегаць па вёсцы ў гразным носам. Я б не звяртаў на " "яго ўвагі." #. TRANSLATORS: Quest text spoken by Cain #: Source/textdat.cpp:551 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" "Быў час, калі ў падарожных з усіх старон гэты горад быў частым прыпынкам. " "Шмат чаго перамянілася. Але схаваныя пячоры ды пахаваныя скарбы, усё гэта " "звычайныя фантазіі ўсякага дзіцяці. Вірт рэдка дазваляе сабе пазабаўляцца. " "Магчыма, гэта проста яго ўяўленне." #. TRANSLATORS: Quest text spoken by Farnham #: Source/textdat.cpp:553 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Слухай сюды. Падыдзі. Я не ведаю, ці ведаеш ты, што я ведаю, але ў цябе тут " "нешта праўда важнае. Гэта мапа." #. TRANSLATORS: Quest text spoken by Gillian #: Source/textdat.cpp:555 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetary, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Бабуля мне часта расказвае пра дзіўныя сілы, што насяляюць могілкі каля " "царквы. Можа, адна такая гісторыя зацікавіць цябе. Яна казала, што калі " "пакінуць на цвінтары належную ахвяру, зайсці ў сабор і памаліцца за мёртвых, " "а потым вярнуцца, ахвяра неяк дзіўна зменіцца. Не ведаю, ці гэта проста " "словы хворай старой, але гэтыя дні быццам усё можа быць." #. TRANSLATORS: Quest text spoken by Wirt #: Source/textdat.cpp:557 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" "Гммм. Вялізны і таямнічы скарб, кажаш? Мммм. Мо я і зацікаўлюся, каб " "прыкупіць у цябе чагось. А мо шчэ лепей, не трэба табе нейкіх рэдкіх ды " "дарагіх прыпасаў, каб падолець у такім выпрабаванні?" #. TRANSLATORS: Neutral text spoken by Farmer (Gossip) #: Source/textdat.cpp:561 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "Дык гэта пра цябе героя тут усе балакаюць. Мо дапаможаш простаму беднаму " "фермеру ў жудаснай бядзе? На краі майго саду, рыхтык на поўдзень адсюль, " "нешта ўспухла проста з зямлі! Няма як падступіцца да майго жніва ды стагоў, " "кароўкі мае бедныя будуць галадаць. Тая ведзьма дала мне вось, сказала гэта " "ўзарве тую гадасць з майго поля. Калі знішчыш тое, век табе буду ўдзячны. Я " "б і сам гэта зрабіў, але некаму трэба пільнаваць кароў..." #. TRANSLATORS: Neutral text spoken by Farmer (Gossip) #: Source/textdat.cpp:563 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Так і думаў, што ўсё не так проста, як са слоў ведзьмы. Сумны гэта свет, дзе " "не можаш давяраць нават сваім суседзям." #. TRANSLATORS: Neutral text spoken by Farmer (Gossip) #: Source/textdat.cpp:565 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "Усё? Згінула яно назад у цёмныя ямы Аду што парадзіў яго? Чаго? Ой, не " "гавары што руна прапала! Яны ж не танныя, ну. Знайдзі яе, і ўзарві той " "страх, каб духу яго тут не было." #. TRANSLATORS: Neutral text spoken by Farmer (Gossip) #: Source/textdat.cpp:567 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" "Я адсюль чуў выбух! Вялікі дзякуй табе, добры незнаёмец. І што за напасць, " "усякае з зямлі лезе, пачвары царкву захапілі, і гэтак далей, цяжкая гэта " "часіна. Я проста бедны фермер, але вось, прымі гэта з маёй вялікай падзякай." #. TRANSLATORS: Neutral text spoken by Farmer (Gossip) #: Source/textdat.cpp:569 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" "Ох, якая бяда мая... можа... Не, не буду навязвацца, столькі бедаў і так. " "Можа калі крыху ачысціш царкву ад гэтай нечысці, вернешся... і прысвеціш " "колькі часу, каб дапамагчы беднаму фермеру?" #. TRANSLATORS: Quest text spoken by Little Girl #: Source/textdat.cpp:571 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Уэээ! (шморг) Уэээ! (шморг)" #. TRANSLATORS: Quest text spoken by Little Girl #: Source/textdat.cpp:572 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" "Я Тэа згубіла! Майго лепшага сябра! Мы гулялі за ракой, а Тэа сказаў што " "хоча паглядзець на тую зялёную штукенцыю. Я сказала што нам няможна, але мы " "залезлі туды, і тут вылез той ЖУК! Мы ходу, а Тэа ўпаў і жук той яго ХОП і " "забраў яго!" #. TRANSLATORS: Quest text spoken by Little Girl #: Source/textdat.cpp:574 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" "Вы знайшлі яго? Знайдзіце, калі ласка! Ён такі маленькі. Ён сябе не можа " "даглядаць! Калі ласка!" #. TRANSLATORS: Quest text spoken by Little Girl (Quest End) #: Source/textdat.cpp:576 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "Знайшлі! Знайшлі! Дзякуй вам! Ох, Тэа, цябе напужалі тыя гадкія жукі? Эй! " "Фэ! У цябе нешта ў поўсці прыліпла! Фу! Хадзем, Тэа, дамоў! Яшчэ раз дзякуй " "вам, герой!" #. TRANSLATORS: Quest text spoken by Defiler (Hostile) #: Source/textdat.cpp:578 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Доўга ляжалі мы ў дрымоце, надышла пара нам прабудзіцца. Па нашым доўгім " "сне, нас поўніць вялікі голад. Скора, цяпер жа, мы пакормімся..." #. TRANSLATORS: Quest text spoken by Defiler (Hostile) #: Source/textdat.cpp:580 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "Весялішся тут, малое млекакормячае? Якая нікчэмнасць. Ваш маленькі свет не " "стане ані перашкодаю." #. TRANSLATORS: Quest text spoken by Defiler (Hostile) #: Source/textdat.cpp:582 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Гэтыя землі будуць апаганены, і наш вывадак ахопіць палі, якія людзі клічуць " "домам. Нашы атожылкі ахутаюць гэты свет, і мы нажыруемся плоццю яго жыхароў. " "Чалавек будзе нам раб і харч." #. TRANSLATORS: Quest text spoken by Defiler (Hostile) #: Source/textdat.cpp:584 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Ах, чую твой пах... ты блізка! Блізка! Сссс... пах крыві і страху... як " "прывабна..." #. TRANSLATORS: Quest text spoken by Narrator #: Source/textdat.cpp:592 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "І ў год жа Залатога Святла было загадана, каб паўстаў Сабор. Краевогульны " "камень было павінна выразаць з напаўпразрыстага каменя Антыраэля, названага " "ў гонар Анёла, што дзяліўся сілаю сваёю з Харадрым.\n" "\n" "У Год Набліжэння Ценяў затрэслася зямля, і абрушыўся Сабор той. Калі " "будаўніцтва катакомб і замкаў пачалося, а чалавек паўстаў супраць " "разбурэнняў вайны Граху, разваліны тыя абабраны былі з-за камення свайго " "каштоўнага. І тым чынам згінуў з вачэй чалавечых краевугольны камень.\n" "\n" "Камень той быў ад гэтага свету — і ад усіх светаў — як Святло разам і ўнутры " "кожнай рэчы і па-за ёю. Святло і адзінства ёсць вынік гэтае святое асновы, " "адзінства цэлі і адзінства валодання." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:594 msgid "Moo." msgstr "Му-у." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:595 msgid "I said, Moo." msgstr "Му-у, кажу." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:596 msgid "Look I'm just a cow, OK?" msgstr "Слухай, я проста карова, добра?" #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:597 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" "Добра, добра. Я не зусім карова. Я так звычайна не хаджу, але ж, ну, сядзеў " "я дома, займаўся там сваім, аж раптам павылазілі жукі, лозы, лямпы і ўсякае " "такое прама з падлогі… Жах які! Каб я меў што нармальнае надзець, ды не было " "б усё так кепска. Слухай! А можа сходзіш да маёй хаты і возьмеш мне мой " "касцюм? Толькі карычневы, не шэры, яго я на вечар надзяю. Я б і сам схадзіў, " "але не хачу, каб мяне бачылі такога. Вось, на, можа прыгадзіцца… Каб прыбіць " "усё, чым там зарасло. Маю хату не прапусціш. Проста на поўдзень адтуль, дзе " "рака разгаліноўваецца… Ну… Дзе там зарослы агарод." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:599 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "Навошта марнуеш час? Ідзі і прынясі мне мой касцюм! І хутчэй! Вунь бычара " "адзін мне ўжо падміргвае!" #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:601 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Прывет, гэта касцюм мой у цябе? Хутчэй, давай сюды! Гэтыя вушы так свярбяць, " "не паверыш!" #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:603 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" "Не, не, не, не! Гэта мой ШЭРЫ касцюм! Ён на вечар! Для афіцыйных выпадкаў! Я " "не магу ГЭТА надзець. Ты што, дзівак нейкі? Мне патрэбен Карычневы касцюм." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:605 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" "Аа, вось гэта значна лепш будзе. Фух! Нарэшце, хоць якая годнасць! У мяне " "рогі роўна сталі? Добра. Слухай, дзякуй табе вялікі за дапамогу. Вось, " "вазьмі ў падарак, ведаеш... маленькая парада па модзе... можаш трохі... " "навейшае.... ну разумееш пра што я? Увесь гэты матыў шукальніка прыгод " "такі... рэтравы. Проста парада, добра? Чао." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:607 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Слухай. Я карова. А ты, ты прынада на пачвар. Назапасься досведу! Тады " "пагаворым." #. TRANSLATORS: Quest text spoken by Farmer #: Source/textdat.cpp:610 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Мусіць, і праўда страшную задачу я табе паставіў. Каб быў хоць нейкі " "спосаб... а табе не дапамог бы збанок свежага малачка?" #. TRANSLATORS: Quest text spoken by Farmer #: Source/textdat.cpp:612 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "О, мне б прыдалася твая дапамога, але можа пасля таго, як выратуеш катакомбы " "ад тых пачвар-паганнікаў." #. TRANSLATORS: Quest text spoken by Farmer #: Source/textdat.cpp:614 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Мне тут трэба помач, але не магу ж я навязвацца зусім незнаёмаму чалавеку. " "Можа, як вы пабудзеце тут крыху, вось тады мне будзе лягчэй папрасіць аб " "паслузе." #. TRANSLATORS: Quest text spoken by Farmer #: Source/textdat.cpp:616 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "Я бачу твой патэнцыял вялікасці. Можа якім разам прыйдзеш, пакуль выконваеш " "свой лёс, і зробіш мне адну паслугу?" #. TRANSLATORS: Quest text spoken by Farmer #: Source/textdat.cpp:618 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" "Думаю, мажліва вы маглі б мне дапамагчы, але можа калі станеце зусім крыху " "мацнейшымі. Я б не хацеў шкодзіць адзінай надзеі вёсцы, што можа знішчыць " "пагрозу ў царкве!" #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:620 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Я, я сваімі сіламі стаў каровай. Зрабі з сябе што-небудзь... тады і " "пагаворым." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:622 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "І зусім не трэба мне вытлумачвацца перад кожным прахожым турыстам! Ці табе " "не трэба было там пачвар біць? Можа потым пагаворым. Калі дажывеш..." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:624 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" "Годзе мяне турбаваць. Я шукаю нешта сапраўды геройскае. І гэта не ты. Як мне " "табе давяраць, цябе могуць зжэрці монстры хоць сёння... Мне патрэбен " "дасведчаны герой." #. TRANSLATORS: Quest text spoken by Complete Nut #: Source/textdat.cpp:626 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" "Добра, годзе заліваць. Не хацеў збіць цябе з тропу. Сяджу я дома, на душы " "смууутна, аж раптам усё стала няў-стоойліва, з падлогі вылез цэлы статак " "пачвар! Я аж замыкаў са страху. І так ужо сталася, што я быў апрануты ў гэты " "Джэрсі, калі ўцякаў за дзверы, і цяпер выглядаю зусім недарэчна. Была б у " "мяне якая іншая вопратка, не было б так дрэнна. Слухай! А можа сходзіш да " "маёй хаты і возьмеш мне мой касцюм? Толькі карычневы, не шэры, яго я на " "вечар надзяю. Я б і сам схадзіў, але не хачу, каб мяне бачылі такога. Вось, " "на, можа прыгадзіцца… Каб прыбіць усё, чым там зарасло. Маю хату не " "прапусціш. Проста на поўдзень адтуль, дзе рака разгаліноўваецца… Ну… Дзе там " "зарослы агарод." #. TRANSLATORS: Quest text spoken by Unknown, Maybe Farmer #: Source/textdat.cpp:628 msgid "" "Cloudy and cooler today. Casting the nets of necromancy across the void " "landed two new subspecies of flying horror; a good day's work. Must " "remember to order some more bat guano and black candles from Adria; I'm " "running a bit low." msgstr "" "Сёння халадней, і хмар больш. Закінутыя ў пустату сеткі некрамансіі злавілі " "два новыя падвіды лятучага жаху, слаўна сёння паработаў. Трэба не забыць " "заказаць яшчэ кажанова калу і чорных свечак у Эйдрыі. Патраціліся мае запасы." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:630 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "Чары, пагрозы, адрачэнне, торг, усё спрабаваў – марна. Мае метады " "паняволення меншых дэманаў не дзейнічаюць на гэтую жудасную пачвару, зусім." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:632 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" "Мой дом павольна глуміцца поганню яго непажаданага вязня. Крыпты поўняцца " "ценямі, што рушаць па-за вугламі майго зроку. На краі майго слыху ціха " "скрабуцца ў танцы кіпцюры. Яны шукаюць, думаецца мне, гэты дзённік." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:634 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" "У рове стварэння прамільгнула яго імя — На-Крул. Я сіліўся даследаваць гэтае " "імя, але меншыя дэманы нейкім чынам знішчылі маю бібліятэку. На-Крул... " "Гэтае імя ўсяляе ва мне халодную жудасць. Палічу за лепшае думаць аб ім " "толькі як аб Стварэнні, не разважаючы пра яго сапраўднае імя." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:636 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" "Шалёнае выццё палоннага не дае мне спаць, як моцна бы я ні хацеў. Яно лютуе " "супраць таго, хто паслаў яго ў Пустату, і выкрыквае на мяне агідныя праклёны " "за сваё зняволенне. Яго словы поўняць маё сэрца жахам, але ж заглушыць яго " "голас я не магу." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:638 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" "Час хутка скончваецца. Я мушу запісаць спосаб, як аслабіць дэмана, і потым " "схаваць гэты тэкст, каб яго прыслужнікі не скарысталі мае веды, каб " "вызваліць свайго валадара. Спадзяваюся, што той, хто знойдзе гэты дзённік, " "адшукае тыя веды." #. TRANSLATORS: Quest text read aloud from book #: Source/textdat.cpp:640 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Таму, хто знойдзе гэты скрутак, даручаецца спыніць дэманічнае стварэнне, што " "мяшкае ў гэтых сценах. Мой час мінуў. Нават цяпер, яго пякельныя прыслужнікі " "скрабуцца ў крохкія дзверы, за якімі я схаваўся.\n" "\n" "Я затрымаў дэмана таемнаю магіяю і замкнуў ў вялікіх сценах, але я страшуся, " "што гэтага не стане.\n" "\n" "Чары, якія знаходзяцця ў трох маіх грымуарах, дазволяць бяспечна ўвайсці ў " "яго ўладанні, але толькі калі выкарыстаны ў правільным парадку. Рычагі пры " "ўваходзе здымуць бар'еры і вызваляць дэмана. Не руш іх! Карыстайся толькі " "тымі чарамі, каб трапіць унутр, альбо яго сіла будзе неадольнаю." #. TRANSLATORS: Quest text read aloud from book by player #: Source/textdat.cpp:642 Source/textdat.cpp:645 Source/textdat.cpp:648 #: Source/textdat.cpp:651 Source/textdat.cpp:654 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #. TRANSLATORS: Quest text read aloud from book by player #: Source/textdat.cpp:643 Source/textdat.cpp:646 Source/textdat.cpp:649 #: Source/textdat.cpp:652 Source/textdat.cpp:655 msgid "Praedictum Otium." msgstr "Praedictum Otium." #. TRANSLATORS: Quest text read aloud from book by player #: Source/textdat.cpp:644 Source/textdat.cpp:647 Source/textdat.cpp:650 #: Source/textdat.cpp:653 Source/textdat.cpp:656 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/towners.cpp:79 msgid "Griswold the Blacksmith" msgstr "Каваль Грызвальд" #: Source/towners.cpp:101 msgid "Ogden the Tavern owner" msgstr "Карчмар Огдэн" #: Source/towners.cpp:110 msgid "Wounded Townsman" msgstr "Паранены Гараджанін" #: Source/towners.cpp:132 msgid "Adria the Witch" msgstr "Ведзьма Эйдрыя" #: Source/towners.cpp:141 msgid "Gillian the Barmaid" msgstr "Барменка Джыліен" #: Source/towners.cpp:172 msgid "Pepin the Healer" msgstr "Лекар Піпін" #: Source/towners.cpp:189 msgid "Cain the Elder" msgstr "Старэйшына Кейн" #: Source/towners.cpp:218 msgid "Cow" msgstr "Карова" #: Source/towners.cpp:242 msgid "Lester the farmer" msgstr "Фермер Лэстэр" #: Source/towners.cpp:255 msgid "Complete Nut" msgstr "Ляснуты" #: Source/towners.cpp:264 msgid "Celia" msgstr "Сілія" #: Source/towners.cpp:277 msgid "Slain Townsman" msgstr "Забіты Гараджанін" #: Source/trigs.cpp:343 msgid "Down to dungeon" msgstr "Уніз у падзямелле" #: Source/trigs.cpp:354 msgid "Down to catacombs" msgstr "Уніз у катакомбы" #: Source/trigs.cpp:364 msgid "Down to caves" msgstr "Уніз у пячоры" #: Source/trigs.cpp:374 msgid "Down to hell" msgstr "Уніз у пекла" #: Source/trigs.cpp:386 msgid "Down to Hive" msgstr "Уніз у Вулей" #: Source/trigs.cpp:398 msgid "Down to Crypt" msgstr "Уніз у Склеп" #: Source/trigs.cpp:414 Source/trigs.cpp:494 Source/trigs.cpp:541 #: Source/trigs.cpp:636 msgid "Up to level {:d}" msgstr "Наверх на ўзровень {:d}" #: Source/trigs.cpp:416 Source/trigs.cpp:471 Source/trigs.cpp:523 #: Source/trigs.cpp:602 Source/trigs.cpp:619 Source/trigs.cpp:666 msgid "Up to town" msgstr "Наверх у горад" #: Source/trigs.cpp:427 Source/trigs.cpp:505 Source/trigs.cpp:558 #: Source/trigs.cpp:583 Source/trigs.cpp:648 msgid "Down to level {:d}" msgstr "Уніз на ўзровень {:d}" #: Source/trigs.cpp:439 msgid "Up to Crypt level {:d}" msgstr "Наверх на ўзровень Склепа {:d}" #: Source/trigs.cpp:454 msgid "Down to Crypt level {:d}" msgstr "Уніз на ўзровень Склепа {:d}" #: Source/trigs.cpp:570 msgid "Up to Nest level {:d}" msgstr "Наверх на ўзровень Гнязда {:d}" #: Source/trigs.cpp:679 msgid "Down to Diablo" msgstr "Уніз да Д'ябла" #: Source/trigs.cpp:712 Source/trigs.cpp:726 Source/trigs.cpp:740 msgid "Back to Level {:d}" msgstr "Назад да ўзроўню {:d}" ================================================ FILE: Translations/bg.po ================================================ # Translation of DevilutionX to Bulgarian # Nikolay Popov , 2021. # @Krezzin, 2021. # msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:19+0200\n" "PO-Revision-Date: \n" "Last-Translator: Nikolay Popov \n" "Language-Team: Nikolay Popov ; Zakxaev68 / " "Krezzin\n" "Language: bg\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Идея и дизайн на играта" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Старши дизайнери" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Допълнителен дизайн" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Главен програмист" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Старши програмисти" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Програмиране" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Специални гост-програмисти" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Програмиране на Battle.net" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Програмиране на сериини комуникации" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Програмиране на инсталатор" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Художествени директори" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Художествено оформление" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Техническо художествено оформление" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Кинематографични Художествени директори" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D кинематографично художествено оформление" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Кинематографично техническо оформление" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Изпълнителен продуцент" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Продуцент" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Асистент продуцент" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Strike Team" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Музика" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Звуков дизайн" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Кинематографична музика и звук" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Гласова продукция, режисура и кастинг" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Сценарий и история" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Редактиране на диалог" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Гласове" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Инженер по звукозапис" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Дизайн и оформление на ръководство" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Художествено оформление на ръководство" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Временен директор по качеството (водещ изпитател)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "Екип по осигуряване на качеството (изпитатели)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "Екип за специални операции на QA (изпитатели за съвместимост)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "QA Артилерийска поддръжка (допълнителни изпитатели) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "QA Контраразузнаване" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Орден на мрежови информационни услуги" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Поддръжка на клиенти" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Продажби" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Dunsel" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Бек вокалисти на г-н Дабири" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Връзки с обществеността" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Маркетинг" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Международни продажби" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "Продажби в САЩ" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Производство" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Правни и бизнес въпроси" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Специални благодарности на" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Благодарение на" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "В памет на" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Специални благодарности на" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Генерален мениджър" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Софтуерно инженерство" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Художествен директор" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Художници" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Дизайн" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Звуков дизайн, специални ефекти и аудио инженеринг" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Ръководител по осигуряване на качеството" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Изпитатели" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Ръководство" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\tДопълнителна работа" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Оформяне на текстове за приключенията" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Благодарности на" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tСпециални благодарности на Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Мениджър по осигуряване на качеството" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Водещ изпитател за осигуряване на качеството" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Основни изпитатели" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Допълнителни изпитатели" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Мениджър продуктов маркетинг" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Мениджър връзки с обществеността" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Асистент продуктов мениджър" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Пръстенът на хилядата" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tПри създаването на тази игра не е продадена нито една душа." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "Добре" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Изберете клас" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Отказ" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Нов герой за Групова игра" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Нов герой за Единична игра" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Запис вече съществува" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Зареди игра" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Нова игра" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Герои за Единична игра" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Разбойникът и Магьосникът са достъпни само в пълната версия на играта. " "Посетете https://www.gog.com/game/diablo, за да я закупите." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Въведете име" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Неправилно име. Името не може да съдържа интервал, специални символи или " "непозволени думи.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Не е възможно създаването на герой." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Ниво:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Сила:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Магия:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Сръчност:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Жизненост:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Номер запис:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Изберете герой" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Нов герой" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Изтрий" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Герои за Групова игра" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Изтрий Герой за Групова игра" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Изтрий Герой за Единична игра" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Наистина ли искате да изтриете героя \"{:s}\"?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Единична игра" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Групова игра" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Настройки" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Поддръжка" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Виж създателите" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Изход от Hellfire" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Изход от Diablo" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Пробна версия" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Клиент-сървър (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 #, fuzzy msgid "Offline" msgstr "Обратна връзка" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Групова игра" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Изисквания:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "" "не е необходим\n" "регион" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Изберете връзка" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Промяна на регион" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Всички компютри трябва да са свързани с TCP-съвместима мрежа." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Всички компютри трябва да са свързани с интернет." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Играй в единична игра, без достъп до интернет." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Брой играчи: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Diablo пробна версия" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Hellfire пробна версия" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "Домакинът играе различна игра, в сравнение с Вас." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "Домакинът играе различен игрови режим ({:s}), в сравнение с Вас." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "Вашата версия {:s} не съвпада с тази на домакина {:d}.{:d}.{:d}." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Описание:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Изберете действие" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Нова игра" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Нова публична игра" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Влез в игра" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Публична игра" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Зареждане..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "Няма" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "ОТКАЗ" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Създайте игра с трудност по Ваш избор." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "Създайте публична игра с трудност по Ваш избор." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "Въведете Game ID за да се присъединете към игра." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "Въведете IP или hostname и се присъединете към текущата игра." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "" "Присъединете към текущата публична игра.\n" "." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Нормалнa" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Кошмарнa" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Адска" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Трудност: {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Скорост: Нормална" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Скорост: Бърза" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Скорост: По-бърза" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Скорост: Най-бърза" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Играчи: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Изберете трудност" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Присъедини се към {:s} игри" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Въведете Game ID" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Въведете адрес" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Нормална трудност\n" "Тук начинаещия герой започва своето приключение да срази Диабло." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Кошмарна трудност\n" "Обитателите на Лабиринта са по-силни и ще бъдат по-голямо предизвикателство. " "Препоръчително за герои които имат опит." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Адска трудност\n" "Най-могъщите адски същества се таят пред портите на Преизподнята. Само най-" "опитните герой могат да се осмелят да поемат този риск." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Вашият герой трябва да достигне ниво 20, преди да можете да се присъедините " "към игра от Кошмарна трудност." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Вашият герой трябва да достигне ниво 30, преди да можете да се присъедините " "към игра от Адска трудност." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Изберете скорост" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Бърза" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "По-бърза" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Най-бърза" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Нормална скорост\n" "Тук начинаещия герой започва своето приключение да срази Диабло." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Бърза скорост\n" "Обитателите на Лабиринта са ускорени и ще бъдат по-голямо предизвикателство. " "Препоръчително за герои които имат опит." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "По-бърза скорост\n" "Повечето чудовища от подземието ще Ви издирят по-бързо от когато и да било. " "Само опитни герои могат да изпробват късмета си на тази скорост." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Най-бърза скорост\n" "Слуги на подземното царство ще нападат изненадващо и без задръжки. Само " "някой със светкавични рефлекси би се справил с това темпо." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Въведи парола" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Режим Hellfire" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Режим Diablo" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Да" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Не" #: Source/DiabloUI/settingsmenu.cpp:162 #, fuzzy #| msgid "Press any key to change." msgid "Press gamepad buttons to change." msgstr "Зададете нов клавиш." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Зададен клавиш:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Зададете нов клавиш." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Освободете клавиш" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Предишно меню" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Поддържаме чат сървър на Discord.gg/devilutionx Следвайте връзката за да се " "присъедините към нашата общност където дискутираме всичко свързано с Diablo " "и допълнението Hellfire." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX е поддържан от Diasurgical, сведения за проблеми и бъгове могат " "да бъдат докладвани на: https://github.com/diasurgical/devilutionX За да " "предложим по-добро качество на продукта, моля, посочете номер на версията, " "операционна система и естеството на проблема." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Отказ от отговорност:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX не е обвързан и/или поддържан от Blizzard Entertainment, нито " "от GOG.com. Blizzard Entertainment или GOG.com не са изпитали или " "сертифицирали качеството и съвместимостта на DevilutionX. Всички Въпроси за " "DevilutionX трябва да бъдат насочени към Diasurgical, и не към Blizzard " "Entertainment или GOG.com." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tТози порт използва шрифтовете Charis SIL, New Athena Unicode, Unifont, и " "Noto лицензирани под SIL Open Font License, както и Twitmoji лицензирани под " "CC-BY 4.0. Този порт използва също SDL, лицензиран под zlib-license. Виж " "ReadMe (ПрочетиМе) за повече информация." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Запазени права © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Грешка" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Изглежда има грешка в {:s} при ред {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Грешка при прочитане на данни от файл" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Неуспешно прочитане на водещ файлов архив ({:s}).\n" "\n" "Уверете се, че се съответният се намира в папката на играта." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Грешка при отваряне на папка с зададен достъп само за четене" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Неуспешно записване в директория:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Игра: " #: Source/automap.cpp:1424 #, fuzzy msgid "Offline Game" msgstr "Обратна връзка" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Парола: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Публична игра" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Ниво: Свърталище {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Ниво: Гробница {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Град" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Ниво: {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Tab" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Enter" #: Source/control.cpp:206 msgid "Character Information" msgstr "Информация за персонажа" #: Source/control.cpp:207 msgid "Quests log" msgstr "Приключенски дневник" #: Source/control.cpp:208 msgid "Automap" msgstr "Автоматична карта" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Главно меню" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Инвентар" #: Source/control.cpp:211 msgid "Spell book" msgstr "Книга за Заклинания" #: Source/control.cpp:212 msgid "Send Message" msgstr "Изпрати съобщение" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr "" #: Source/control.cpp:633 Source/control.cpp:634 #, fuzzy #| msgid "Description:" msgid "Description: " msgstr "Описание:" #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" #: Source/control.cpp:634 #, fuzzy #| msgid "Players: " msgid "" "\n" "Parameters: " msgstr "Играчи: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "" #: Source/control.cpp:653 #, fuzzy #| msgid "Are you sure you want to buy this item?" msgid "What arena do you want to visit?" msgstr "Наистина ли искате да купите този предмет?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "" #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "" #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "" #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "" #: Source/control.cpp:800 msgid "[command]" msgstr "" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "" #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "" #: Source/control.cpp:802 #, fuzzy #| msgid "Amber" msgid "" msgstr "Кехлибарени" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "" #: Source/control.cpp:803 #, fuzzy #| msgctxt "monster" #| msgid "Flayed One" msgid "" msgstr "Кожодер" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "" #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Приятелска игра" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Враждебна игра" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Бърз клавиш: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Бутон за избор на текущо заклинание" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Бърз Клавиш: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "{:s} Умение" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "{:s} Заклинание" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Заклинание Ниво 0 - Неизползваемо" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Заклинание Ниво {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "Свитък на {:s}" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Свитък" msgstr[1] "{:d} Свитъци" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "Жезъл на {:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Заряд" msgstr[1] "{:d} Заряди" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} златна монета" msgstr[1] "{:s} златни монети" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Изискванията не са покрити" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Ниво: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Точки живот {:d} от {:d}" #: Source/control.cpp:1525 #, fuzzy #| msgid "Right-click to use" msgid "Right click to inspect" msgstr "Десен клик за използване" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Ново ниво" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Имате {:s} златна монета. Колко искате да отделите?" msgstr[1] "Имате {:s} златни монети. Колко искате да отделите?" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Градски портал" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "от {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Портал до" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "Нечестивия олтар" #: Source/cursor.cpp:636 msgid "level 15" msgstr "Ниво 15 " #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, fuzzy, c++-format #| msgid "Unable to load character" msgid "Unable to load data from file {0}" msgstr "Неуспешно зареждане на герой" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Показване на това съобщение и изход" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Показване на версията и изход" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Посочете местонахождението на diabdat.mpq" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Посочете папката с записните файлове" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Посочете папката с файл diablo.ini" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Пропускане на началните видеа" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Показване на индикатор за кадри в секунда" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "Използване на подробно регистриране на команди" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Записване на демо файл" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Репродуциране на демо файл" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Забраняване на всички лимити на кадри при репродукция" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Избор на игра:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Принудителен режим Пробна версия" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Принудителен режим Diablo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Принудителен режим Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Настройки на Hellfire:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "" "Докладвайте несъответствия на https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Неуспешно зареждане на ресурси за интерфейса.\n" "\n" "Уверете се, че файлът devilutionx.mpq е в папката на играта и е актуален." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Мрежово прекъсване --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Изчакване на играчи --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "Не е намерен помощен файл" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "докато сте в магазин" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Предмет от колана {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Използване на предмет от колана." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Бързо заклинание {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Клавиш за умение или заклинание." #: Source/diablo.cpp:1809 #, fuzzy #| msgid "Previous Menu" msgid "Previous quick spell" msgstr "Предишно меню" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 #, fuzzy #| msgid "Quick spell {}" msgid "Next quick spell" msgstr "Бързо заклинание {}" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "" #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "" #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Бързи Заклинания" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Отваряне на листа с бърз достъп до заклинанния." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Бърз Запис" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Записва текуща игра." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Бързо Зареждане" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Зарежда запис." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Изход от играта" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Затваря играта." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Спри герой" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Спира героя и прекратява всички предстоящи действия." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Показване на предмети" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Подчертава/скрива предмети на пода." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Постоянно показване на предмети" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Подчертаването/скриването предмети на пода е за постоянно." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Включи авто-карта" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Включва показването на авто-карта." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Отваряне на инвентар." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Герой" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Отваряне на прозорец с информация за персонажа." #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Задачи" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Q: Отваряне на Приключенски дневник." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Книга за Заклинания" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Отваряне на книга с заклинания." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Бързо съобщение {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Изпрати бързо съобщение в чата." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Скриване Инфо Екрани" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Скриване на всички информационни екрани." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Мащабиране" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Мащабиране на екрана." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Пауза" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Паузира играта." #: Source/diablo.cpp:2004 #, fuzzy #| msgid "Pause Game" msgid "Pause Game (Alternate)" msgstr "Пауза" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy #| msgid "Increase screen brightness." msgid "Decrease Brightness" msgstr "Увеличаване яркостта на екрана." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Намаляване яркоста на екрана." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy #| msgid "Increase screen brightness." msgid "Increase Brightness" msgstr "Увеличаване яркостта на екрана." #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Увеличаване яркостта на екрана." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Помощ" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Помощен диалог." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Снимка Екран" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Прави снимка на текущия екран на играта." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Игрова информация" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Показва информация на играта." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Чат регистър" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Показва регистър на чат съобщенията." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 #, fuzzy #| msgid "Inventory" msgid "Sort Inventory" msgstr "Инвентар" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" #: Source/diablo.cpp:2144 #, fuzzy #| msgid "Select Action" msgid "Secondary action" msgstr "Изберете действие" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "" #: Source/diablo.cpp:2159 #, fuzzy #| msgid "Select Action" msgid "Spell action" msgstr "Изберете действие" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "" #: Source/diablo.cpp:2174 #, fuzzy #| msgid "Cancel" msgid "Cancel action" msgstr "Отказ" #: Source/diablo.cpp:2175 #, fuzzy #| msgid "Closes the game." msgid "Close menus." msgstr "Затваря играта." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "" #: Source/diablo.cpp:2201 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character up." msgstr "Герои за Групова игра" #: Source/diablo.cpp:2206 msgid "Move down" msgstr "" #: Source/diablo.cpp:2207 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character down." msgstr "Герои за Групова игра" #: Source/diablo.cpp:2212 #, fuzzy #| msgid "$Movement:" msgid "Move left" msgstr "$Движение:" #: Source/diablo.cpp:2213 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character left." msgstr "Герои за Групова игра" #: Source/diablo.cpp:2218 #, fuzzy #| msgid "the night" msgid "Move right" msgstr "Нощта" #: Source/diablo.cpp:2219 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character right." msgstr "Герои за Групова игра" #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "" #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "" #: Source/diablo.cpp:2310 #, fuzzy #| msgid "Automap" msgid "Automap Move Up" msgstr "Автоматична карта" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 msgid "Automap Move Down" msgstr "" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 msgid "Automap Move Left" msgstr "" #: Source/diablo.cpp:2323 msgid "Moves the automap left when active." msgstr "" #: Source/diablo.cpp:2328 msgid "Automap Move Right" msgstr "" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "" #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "" #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "" #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "" #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "" #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "" #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "" #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "" #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 #, fuzzy #| msgid "The game ended" msgid "Toggle game menu" msgstr "Играта приключи" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 #, fuzzy #| msgid "Saves the game." msgid "Opens the game menu." msgstr "Записва текуща игра." #: Source/diablo_msg.cpp:63 #, fuzzy #| msgctxt "spell" #| msgid "Flame Wave" msgid "Game saved" msgstr "Огнена вълна" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "Мрежовата функция не е достъпна в демонстрационната версия" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Грешка при създаване на Direct Sound" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "Не е налично в пробната версия" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "Няма достатъчно място за направата на запис" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Прекъсването на играта не е позволено в града" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Препоръчително е копирането върху хард-диск" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Проблем с мрежовото синхронизиране" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Пауза не е позволена в групова игра" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Запазване..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Някои линеят, докато едно расте силно" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Нова сила е сътворена чрез разруха" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Който брани, рядко атакува" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "Мечът на справедливостта е бърз и остър" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Тялото заяква, докато духът е бдителен" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Мощта на пренасочената мана я подновява" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "Времето не може да отслаби мощта на стоманата" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "Магията не винаги е това което изглежда" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Каквото е било отворено, сега е затворено" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "Усилието идва на цената на проникновението" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "Тайнствената сила носи разруха" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Онова, което не може да се държи, не може да се повреди" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Ален и Лазурен се превръщат като слънцето" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Знание и мъдрост на цената на живеца" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Пий и изпитай бодростта" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Където отидеш, там оставаш" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "Енергията идва на цената на мъдростта" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Богатство идва когато най-малко го очакваш" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "" "Там, където скъперничеството се проваля, търпението носи възнаграждения" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "Благословен от доброжелателен спътник!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Човешките дела могат да бъдат направлявани от съдбата" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "Силата е подсилена с небесна воля" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "Естеството на живота идва от вътре" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "Пътят се вижда по-ясно, бидейки гледан от високо" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "Избавлението идва с цената на мъдростта" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Загадките разбулва светлината на логиката" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Изостаналите все пак могат и да са първи" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "Щедростта носи своите ползи" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Трябва да сте поне ниво 8, за да използвате това." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Трябва да сте поне ниво 13, за да използвате това." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Трябва да сте поне ниво 17, за да използвате това." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "Усвоено бе тайнствено знание!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "Това, което не те убива..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "Знанието е сила." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Дай и ще получиш." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Малко опит се усвоява чрез допир." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "Няма друго място като у дома." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Духовната сила е възстановена." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Чувстваш се по-сръчен." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Чувстваш се по-силен." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Чувстваш се по-мъдър." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Чувстваш се по-здрав." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "Чупливото ще се счупи." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Катедрала" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Катакомби" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Пещери" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Свърталище" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Крипта" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Нв {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "{} трудност" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "В меню" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "Обратна връзка" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "Свързването e невъзможно" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "грешка: прочетени 0 байта от сървъра" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq или spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Някой MPQ файловете от Hellfire липсват." #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Не всички Hellfire MPQ файлове бяха намерени.\n" "Моля, поставете всички hf*.mpq файлове." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Разделителна способност" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Бърз ход в града" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "Задача Търсенето на Тео" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "Задача Кравешка мисия" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Авто взимане на злато" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Авто взимане на Еликсир" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 #, fuzzy #| msgid "Auto Gold Pickup" msgid "Auto Oil Pickup" msgstr "Авто взимане на злато" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Авто взимане на предмети в града" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Адрия пълни Мана" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Авто-екипиране на оръжия" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Авто-екипиране на брони" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Авто-екипиране на шлемове" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Авто-екипиране на щитове" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Авто-екипиране на накити" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Произволни Задачи" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 #, fuzzy #| msgid "Show mana values" msgid "Show Item Labels" msgstr "Покажи стойности мана" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Авто-пълнене на колан" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Забрани Увреждащи светилища" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Авто-взимане лечение" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Авто-взимане пълно лечение" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Авто-взимане мана" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Авто-взимане пълна мана" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Авто-взимане възобновление" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Авто-взимане пълно възобновление" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Настройки" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Запази игра" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy #| msgid "Main Menu" msgid "Exit to Main Menu" msgstr "Главно меню" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Изход от играта" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Гама" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Скорост" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Изключена музика" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Звук" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Изключен Звук" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Пауза" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Бързи клавиши:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Помощен диалог" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Главно меню" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Tab: Авто-карта" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Space: Скриване на всички информационни екрани" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Отваряне на книга с магии за бърз достъп" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Отваряне на книга с заклинания" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Отваряне на инвентар" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Отваряне на прозорец с информация за персонажа" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "Q: Отваряне на Приключенски дневник" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Намаляване яркоста на екрана" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "G: Увеличаване яркостта на екрана" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Мащабиране на екрана" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Мащабиране на картата" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Използване на предмет от колана" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Задаване на бърз клавиш за умение или заклинание" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Left Mouse Button: Замахване в неподвижно състояние" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Left Mouse Button (в прозореца на персонажа): Усвояване на всички " "точки за усъвършенстване" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Left Mouse Button (в инвентара): Преместване на предмет в колана или " "въоръжаване/разоръжаване на предмет" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "" "Shift + Left Mouse Button (в колана): Преместване на предмет в инвентара" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Движение:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" "Ако задържите бутона на мишката, вашият герой ще се придвижи в съответната " "посока." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Битка:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Задръжте клавиша Shift и натиснете ляв бутон на мишката, за да замахвате, " "докато сте неподвижни." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Авто-карта:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "За да включите картата,натиснете бутона 'КАРТА' на информационната ивица или " "натиснете клавиша 'TAB' на клавиатурата. Мащабирането на картата е чрез " "клавишите + и -.Отместването е чрез клавишите за посока." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Взимане на Предмети:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Използваемите предмети, които са малки по размер, като например отвари и " "свитъци, автоматично преминават в 'колана', намиращ се в горната ивица на " "интерфейса. А когато предмет бъде поставен в колана, върху иконката му " "излиза цифра Предметите могат да бъдат ползвани чрез десен бутон мишката или " "съответната цифра на клавиатурата." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Злато:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Може да изберете колко злато искате да оставите, като натиснете десен бутон " "върху купчината златни монети в инвентара." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Умения & Заклинания:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "Можете да получите достъп до списъка си с умения и заклинания, като кликнете " "с левия бутон на мишката върху бутона 'ЗАКЛ' върху интерфейса. Тук са " "изброени научените заклинания и тези, които са достъпни чрез жезли. " "Натискането на левия бутон на мишката върху заклинанието, което желаете да " "използвате, ще го подготви. Подготвеното заклинание може да бъде изречено " "чрез кликване с десния бутон на мишката в игралната зона." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Използване на Бърз списък за Заклинания:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Натискане с левия бутон на мишката върху бутона 'Подготвено заклинание' ще " "отвори 'Бърз лист', която ви позволява да изберете умение или заклинание за " "незабавна употреба. За да използвате Подготвеното умение или заклинание, " "просто натиснете с десния бутон на мишката в основната игрална зона." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Бутон Shift + Ляв бутон на мишката върху бутона 'Подготвено заклинание' ще " "изчисти подготвеното заклинание. " #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Задаване бързи клавиши за заклинания" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Може да зададете до четири бързи клавиша за умения, заклинания или свитъци. " "Започнете с отварянето на 'бърз лист' както е обяснено в горния абзац. " "Натиснете клавиши F5, F6, F7 или F8 след като посочите заклинанието което " "искате да зададете." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Книги със Заклинания" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Четенето на повече от една книга за едно и също заклинание, прави същото по-" "ефикасно." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Помощ за пробна версия на Hellfire" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Помощ за Hellfire" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Помощ за пробна версия на Diablo" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Помощ за Diablo" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "" "Наиснете ESC, за да прекъснете или клавишните стрелки за да превъртите." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "Невъзможно създаването на основен прозорец" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Масло на Точността" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Масло на Майсторството" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Масло на Остротата" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Масло на Смъртта" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Масло на Умението" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Ковашко масло" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Масло на Крепкостта" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Масло на Неизменността" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Масло на Втвърдяването" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Масло на Непробиваемостта" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} на {1}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{0} {1} на {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{0} {1} на {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} на {1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "увеличава" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "шанса за попадение на оръжието" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "значително увеличава шанса" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "за попадение на оръжието" #: Source/items.cpp:1652 msgid "damage potential" msgstr "потенциал за щети" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "значително увеличава" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "потенциала за щети - изкл. лъкове" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "намалява нужните атрибути" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "използва се в/у брони и оръжия" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "възстановява 20% от" #: Source/items.cpp:1664 msgid "item's durability" msgstr "здравината на предмета" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "увеличава" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "текуща и макс здравина на предмета" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "прави предмет неразрушим" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "увеличава клас на бронята" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "на брони и щитове" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "значително увеличава клас на бронята" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "на брони и щитове" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "залага огнена клопка" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "залага мълниена клопка" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "залага вкаменяваща клопка" #: Source/items.cpp:1695 msgid "restore all life" msgstr "напълно възстановява живот" #: Source/items.cpp:1698 msgid "restore some life" msgstr "частично възстановява живот" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "частично възстановява мана" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "напълно възстановява мана" #: Source/items.cpp:1707 msgid "increase strength" msgstr "увеличава сила" #: Source/items.cpp:1710 msgid "increase magic" msgstr "увеличава магия" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "увеличава сръчност" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "увеличава жизненост" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "частично възстановява живот и мана" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "напълно възстановява живот и мана" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Десен клик за поглед" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Десен клик за използване" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Десен клик за прочитане,\n" "следван от ляв клик върху цел" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Десен клик за прочитане" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Активиране за поглед" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Отворете инвентар за да използвате" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Активиране за използване" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Изберете от книгата със заклинания,\n" "след това изричане на заклинанието за прочит" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Активиране за прочит" #: Source/items.cpp:1800 #, fuzzy, c++-format #| msgid "Activate to view" msgid "{} to view" msgstr "Активиране за поглед" #: Source/items.cpp:1806 #, fuzzy, c++-format #| msgid "{:+d} to strength" msgid "{} to use" msgstr "{:+d} към сила" #: Source/items.cpp:1809 #, fuzzy, c++-format #| msgid "" #| "Select from spell book, then\n" #| "cast spell to read" msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Изберете от книгата със заклинания,\n" "след това изричане на заклинанието за прочит" #: Source/items.cpp:1811 #, fuzzy, c++-format #| msgid "Activate to read" msgid "{} to read" msgstr "Активиране за прочит" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Ниво: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Удвоява лимит на златото" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Необходими:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Сила" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Маг" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Сръч" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "Книга на {:s}" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "Ухо на {:s}" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "шанс за удар: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% щети" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "шанс удар: {:+d}%, {:+d}% щети" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% броня" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "клас броня: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Устойчивост Огън: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Устойчивост Огън: {:+d}%" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Устойчивост Мълния: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Устойчивост Мълния: {:+d}%" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Устойчивост Магия: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Устойчивост Магия: {:+d}%" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Устойчивост Всичко: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Устойчивост Всичко: {:+d}%" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "заклинанията са увеличени с {:d} ниво" msgstr[1] "заклинанията са увеличени с {:d} нива" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "заклинанията са намалени с {:d} ниво" msgstr[1] "заклинанията са намалени с {:d} нива" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "нива на заклинания непроменени (?)" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Допълнителни заряди" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} заряд" msgstr[1] "{:d} {:s} заряди" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Щети Огнен Удар: {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Щети Огнен Удар: {:d}-{:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Щети Мълниен Удар: {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Щети Мълниен Удар: {:d}-{:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} към сила" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} към магия" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} към сръчност" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} към жизненост" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} към всички атрибути" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} щети от врагове" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Живот: {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Мана: {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "висока издържливост" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "понижена издържливост" #: Source/items.cpp:3960 msgid "indestructible" msgstr "неразрушими" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% светлинен радиус" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% светлинен радиус" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "множество стрели при изстрел" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "щети огнени стрели: {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "щети огнени стрели: {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "щети мълнийни стрели {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "щети мълнийни стрели {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "щети огненo кълбо {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "щети огнено кълбо {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "атакуващият получава 1-3 щети" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "играчът губи всичката мана" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "поглъща половината щети от капани" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "избутва целта назад" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% щети срещу демони" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Всички устойчивости са 0" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "удар отнема 3% мана" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "удар отнема 5% мана" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "удар отнема 3% жизненост" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "удар отнема 5% жизненост" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "пробива бронята на врага" #: Source/items.cpp:4010 msgid "quick attack" msgstr "ускорен замах" #: Source/items.cpp:4012 msgid "fast attack" msgstr "бърз замах" #: Source/items.cpp:4014 msgid "faster attack" msgstr "по-бърз замах" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "най-бърз замах" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Друга способност (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "бързо възстановяване от удар" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "по-бързо възстановяване от удар" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "най-бързо възстановяване от удар" #: Source/items.cpp:4027 msgid "fast block" msgstr "бързо блокиране" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "добавя {:d} точка към щети" msgstr[1] "добавя {:d} точки към щети" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "изстрелва стрели със случайна скорост" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "необичайни щети за тип предмет" #: Source/items.cpp:4035 msgid "altered durability" msgstr "променена издръжливост" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "меч за една ръка" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "постоянно губене на живот" #: Source/items.cpp:4041 msgid "life stealing" msgstr "изсмукване на живот" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "няма изисквания за сила" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "мълнийни щети: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "мълнийни щети: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "светкавични заряди при удар" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "случайни тройни щети" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "разпадащи се {:+d}% щети" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x щети за чуд., 1x за играч" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Произволни 0 - 600% щети" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "ниска изд., {:+d}% щети" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "допълнително КБ с/у демони" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "допълнително КБ с/у немъртви" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% Мана пренесена в Живот" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% Живот пренесен в Мана" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "щети: {:d} Неразрушими" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "щети: {:d} Изд: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "щети: {:d}-{:d} Неразрушим" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "щети: {:d}-{:d} Изд: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "броня: {:d} Неразрушим" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "броня: {:d} Изд: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Заряди: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "уникат" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Неразпознат" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Леговището на Краля на скелетите" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Камера на Костите" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Лабиринт" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Отровения извор" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Убежището на Архиепископ Лазар" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "" #: Source/levels/setmaps.cpp:33 #, fuzzy #| msgid "Hellfire" msgid "Hell Arena" msgstr "Hellfire" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Надолу към Подземието" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Надолу към Катакомбите" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Надолу към Пещерите" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Надолу към Ада" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Надолу към Свърталището" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Надолу към Криптата" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Нагоре към ниво {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Нагоре към Града" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Надолу към ниво {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Надолу към Диабло" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Нагоре към Свърталище ниво {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Нагоре към Крипта ниво {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Основоположният камък на Света" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Надолу към Крипта ниво {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Нагоре към Ниво {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Невъзможно отваряне на файл за запис" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Невалиден файлов запис" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "Играчът е на ниво само за Hellfire" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Невалидно игрово състояние" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "Невъзможно да се покаже главно меню" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "Животно" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Демон" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Немъртъв" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Тип: {:s} Убити: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Всичко убити: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Точки живот: {:d}-{:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Няма магическа устойчивост" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Устойчивост:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Магия" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Огън" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Мълния" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Имунитет:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Тип: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Няма устойчивости" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Няма имунитети" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Някакви Магически Устойчивости" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Някакви Магически Имунитети" #: Source/mpq/mpq_writer.cpp:174 #, fuzzy #| msgid "Failed to open stash archive for writing." msgid "Failed to open archive for writing." msgstr "Неуспешно отваряне на файла на склада за запис." #: Source/msg.cpp:1701 #, fuzzy, c++-format #| msgid "{:s} has cast an illegal spell." msgid "{:s} has cast an invalid spell." msgstr "{:s} е направил непозволено заклинание." #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "{:s} е направил непозволено заклинание." #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Играч '{:s}' (ниво {:d}) се присъедини към играта" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "Играта приключи" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "Невъзможно да получи данни за ниво" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "Играч '{:s}' напусна играта" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Играч '{:s}' уби Диабло и напусна играта!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "Играч '{:s}' отпадна заради прекъсване" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Играч '{:s}' (ниво {:d}) в вече в играта" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Мистериозно" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Прикрит" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Мрачно" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "Особени" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Магическо" #: Source/objects.cpp:132 msgid "Stone" msgstr "Каменено" #: Source/objects.cpp:133 msgid "Religious" msgstr "Религиозно" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Омагьосано" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Чудно" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Очарователно" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Загадъчно" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Ужасно" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Свръхестествено" #: Source/objects.cpp:141 msgid "Divine" msgstr "Божествено" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "Свещени" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Неприкосновено" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Духовно" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Призрачно" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Изоставено" #: Source/objects.cpp:147 msgid "Creepy" msgstr "Зловещо" #: Source/objects.cpp:148 msgid "Quiet" msgstr "Тихо" #: Source/objects.cpp:149 msgid "Secluded" msgstr "Усамотено" #: Source/objects.cpp:150 msgid "Ornate" msgstr "Инкрустирано" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "Мъждеещо" #: Source/objects.cpp:152 msgid "Tainted" msgstr "Опозорено" #: Source/objects.cpp:153 msgid "Oily" msgstr "Мазно" #: Source/objects.cpp:154 msgid "Glowing" msgstr "Ярко" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "Просещо" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "Искрящо" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "Блещукащо" #: Source/objects.cpp:159 msgid "Solar" msgstr "Слънчево" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "Мърфово" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "Великия Сблъсък" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "Възмездието на Греха е Война" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "Историята на Хорадрим" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "Тъмното Изгнание" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "Войната на Греха" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "Запечатването на Тримата" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "Царствата Отвъд" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "Историята на Тримата" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "Черния Крал" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "Дневник: Омагьосването" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "Дневник: Срещата" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "Дневник: Гневния вой" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "Дневник: Мощта му расте" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "Дневник: На-Крул" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "Дневник: Краят" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "Книга за заклинания" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "Разпнат Скелет" #: Source/objects.cpp:4799 msgid "Lever" msgstr "Лост" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "Отворена Врата" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "Затворена Врата" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "Блокирана Врата" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "Древен Том" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "Книга на Безчестието" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "Черепен Лост" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "Митична Книга" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "Ковчеже" #: Source/objects.cpp:4833 msgid "Chest" msgstr "Сандък" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "Ракла" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "Саркофаг" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "Лавица за книги" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "Библиотека" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "Бъчва" #: Source/objects.cpp:4851 msgid "Pod" msgstr "Зародиш" #: Source/objects.cpp:4854 msgid "Urn" msgstr "Урна" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "{:s} Светилище" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "Скелетен Том" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "Библиотечен Том" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "Кръвен Фонтан" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "Обезглавено Тяло" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "Книга на Слепите" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "Книга на Кръвта" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "Пречистващ извор" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "Броня" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "Оръжейна стойка" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "Козьо Светилище" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "Котел" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "Мътен вир" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "Фонтан на сълзите" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "Стоманен Том" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "Педестал от Кръв" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "Гъбен мицел" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "Ужасяваща стойка" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Повален герой" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "{:s} клопка" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (изключено)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "Вкл." #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "Изкл." #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Режим на игра" #: Source/options.cpp:422 #, fuzzy #| msgid "Gameplay Settings" msgid "Game Mode Settings" msgstr "Игрови настройки" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Играй Diablo или Hellfire." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Лимитирай до Пробна версия" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Прави играта съвместима с Пробната версия. Позволява игра в мрежа с " "потребители които нямат копие на пълната версия на играта." #: Source/options.cpp:442 msgid "Start Up" msgstr "Начало" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Начални настройки" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Интро" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Покажи Интро видео" #: Source/options.cpp:455 msgid "Splash" msgstr "Лого" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Показва логото на създателите" #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Лого и Заглавен екран" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Заглавен екран" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Специфични настройки за Diablo" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Специфични настройки за Hellfire" #: Source/options.cpp:501 msgid "Audio" msgstr "Звук" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Звукови настройки" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Звук при ход" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Героят издава звук от стъпки при движение." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Звук при Авто-екипиране" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "Автоматичното екипиране на снаряжение издава звук." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Звук при взимане на предемет" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Взимането на предмети от пода издава звук." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Честота на сигнала" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Честота на сигнала при изход (Hz)." #: Source/options.cpp:508 msgid "Channels" msgstr "Канали" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Брой на канали при възпроизвеждане." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Размер на буфера" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Размер на буфера (номер на кадри за канал)" #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Качество мащабиране звук" #: Source/options.cpp:510 #, fuzzy #| msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "" "Качество на мащабиращия алгоритъм за звук, от 0 (най-ниско) to 10 (най-" "високо)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Тази настройка се отнася базовата разделителна способност на играта и " "определя границите на поглед. Забележка: Това се различава от разделителната " "способност на монитора при Мащабиране, цялостно мащабиране, приспособяване " "към екрана." #: Source/options.cpp:574 msgid "Resampler" msgstr "" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "" #: Source/options.cpp:631 msgid "Device" msgstr "" #: Source/options.cpp:631 #, fuzzy #| msgid "Audio Settings" msgid "Audio device" msgstr "Звукови настройки" #: Source/options.cpp:688 msgid "Graphics" msgstr "Графика" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Графични настройки" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Пълен екран" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Възпроизвежда играта в режим Прозорец или Пълен екран" #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "Приспособяване към екран" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Автоматично наглася прозореца на играта да използва вашите текущи настройки " "на монитора." #: Source/options.cpp:700 msgid "Upscale" msgstr "Мащабиране" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Включва режим на мащабиране от игровата разделителна способност до тази на " "вашия монитор. Запазва разделителна способност на екрана и позволява смяна " "на размер на прозореца." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Филтър мащабиране" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "Включва филтри за подобряване на изображението при мащабиране." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Най-близък пиксел" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Билинеарен" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Анизотропен" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Цялостно мащабиране" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Смяна размера на картината използвайки цели числа за всеки пиксел." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Вертикална синхронизация" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "" #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Мълния" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Цветен цикъл" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" "Включва ефект на цветови цикъл на палетизирани текстури, използван за " "анимация на вода, лава и киселина." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Алтернативна палитра за Свърталище" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" "Играта ще използва алтернативна палитра за Hellfire нивата от Свърталището." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Хардуерен курсор" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Включване на хардуерен курсор" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Хардуерен курсор за предмети" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Използване на хардуерен курсор при графика на предмети." #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Макс. размер хардуерен курсор" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Максимална ширина / височина за хардуерния курсор. Големите размери включват " "обратно софтуерен режим." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Покажи Кадри в секунда (FPS)" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Показва кадрите в секунда в горен ляв ъгъл на екрана." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Игра" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Игрови настройки" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Включи бягане/бързо ходене в града за Diablo и Hellfire. Тази опция се " "появява за първи път в допълнението на играта." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Захват на мишката" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "При включване мишката е заключена в прозореца на играта." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Включи мисията на Малкото момиче " #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" "Включи мисията Говежди одежди. Фермера Лестър е заместен от Пълния " "Откачалник." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Приятелски огън" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Позволява щети от стрели/заклинания между играчи в групова игра дори когато " "приятелски режим е включен." #: Source/options.cpp:790 #, fuzzy #| msgid "No pause in multiplayer" msgid "Full quests in Multiplayer" msgstr "Пауза не е позволена в групова игра" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "" #: Source/options.cpp:791 msgid "Test Bard" msgstr "Тествай Бард" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Принуждава героинята Бард да се появи в менюто избор на герой." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Тествай Варварин" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "Принуждава героят Варварин да се появи в менюто избор на герой." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Скала Опит" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "" "Скала показваща текущо ниво на опит е изобразена в долната част на екрана." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" #: Source/options.cpp:795 msgid "Show health values" msgstr "Покажи стойности живот" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "" "Показва текущи / максимални стойности на живот върху кълбото на живота." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Покажи стойности мана" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "" "Показва текущи / максимални стойности на живот върху кълбото на маната." #: Source/options.cpp:797 #, fuzzy #| msgid "Character Information" msgid "Show Party Information" msgstr "Информация за персонажа" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Скала за Живот на врага" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "" "Скала показваща текущо ниво на живот на врага е изобразена в горната част на " "екрана." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Включва автоматично взимане на злато от земята при близост с играча." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "" "Включва автоматично взимане на Еликсири от земята при близост с играча." #: Source/options.cpp:802 #, fuzzy #| msgid "" #| "Elixirs are automatically collected when in close proximity to the player." msgid "Oils are automatically collected when in close proximity to the player." msgstr "" "Включва автоматично взимане на Еликсири от земята при близост с играча." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Автоматичното взимане на предмети от пода когато героя е в града." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Адрия пълни Вашата мана когато разговаряте с нея." #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "Оръжията ще бъдат автоматично екипирани при взимане или покупка." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "Броните ще бъдат автоматично екипирани при взимане или покупка." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "Шлемовете ще бъдат автоматично екипирани при взимане или покупка." #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "Щитовете ще бъдат автоматично екипирани при взимане или покупка." #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "Накитите ще бъдат автоматично екипирани при взимане или покупка." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "Избира случаен набор от всички възможни задачи при нова игра." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Покажи тип на чудовището" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Посочването на чудовище с курсора на мишката показва типът му в " "информационния блок." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "" #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "Запълва автоматично колана когато е използван консуматив." #: Source/options.cpp:814 #, fuzzy #| msgid "" #| "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines " #| "and Sacred Shrines are not able to be clicked on and labeled as disabled." msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" "При Вкл. Котли (Cauldron), Кози светилища (Goat shrine), Очарователни " "(Fascinating) Неприкосновени (Sacred), Инкрустирани (Ornate) светилища " "(shrines) не могат да бъдат ползвани и са означени като изключени." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Бързо Заклинание" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Бързите клавиши директно използват заклинанието вместо да сменят подготвено " "заклинание." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Максимален брой на отвари за лечение за автоматично взимане от пода." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "" "Максимален брой отвари за пълно лечение за автоматично взимане от пода." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Максимален брой на отвари за мана за автоматично взимане от пода." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "" "Максимален брой на отвари за пълна мана за автоматично взимане от пода." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "" "Максимален брой на отвари за възобновление за автоматично взимане от пода." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "" "Максимален брой на отвари за пълно възобновление за автоматично взимане от " "пода." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" #: Source/options.cpp:824 #, fuzzy msgid "Off" msgstr "Обратна връзка" #: Source/options.cpp:825 #, fuzzy #| msgid "Randomize Quests" msgid "Random Angles" msgstr "Произволни Задачи" #: Source/options.cpp:826 #, fuzzy #| msgid "Vertical Sync" msgid "Vertical Only" msgstr "Вертикална синхронизация" #: Source/options.cpp:880 msgid "Controller" msgstr "Управление" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Настройки за управление" #: Source/options.cpp:889 msgid "Network" msgstr "Мрежа" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Мрежови настройки" #: Source/options.cpp:901 msgid "Chat" msgstr "Чат" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Настройки за Чат" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Език" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Избери какъв език да използва играта." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Езикови настройки" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Настойки клавиши" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Настойки клавиши" #: Source/options.cpp:1260 #, fuzzy #| msgid "Keymapping" msgid "Padmapping" msgstr "Настойки клавиши" #: Source/options.cpp:1260 #, fuzzy #| msgid "Keymapping Settings" msgid "Padmapping Settings" msgstr "Настойки клавиши" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 #, fuzzy #| msgid "Settings" msgid "Mod Settings" msgstr "Настройки" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Ниво" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Опит" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Следващо ниво" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "База" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Сега" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Сила" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Магия" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Сръчност" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Жизненост" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Точки за подобрение" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Злато" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Клас броня" #: Source/panels/charpanel.cpp:176 #, fuzzy #| msgid "chance to hit" msgid "Chance To Hit" msgstr "шанса за попадение на оръжието" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Щети" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Живот" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Мана" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "" " \n" "Устойчивости:\n" "Магия\n" "\n" " " #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Огън" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Мълния" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "герой" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "задачи" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "карта" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "mеню" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "инв" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "закл" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "глас" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "ням" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Неизползваемо" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Щети: 1/3 ж.т.цел" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Лекува: {:d}/{:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Щети: {:d}-{:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Умение" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Жезъл ({:d} заряд)" msgstr[1] "Жезъл ({:d} заряди)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Ниво {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Мана: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Закл." #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Поразява само немъртви" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Свитък" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Жезъл" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Бърз клавиш за заклинания {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "Неуспешно отваряне на архив" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "Неуспешно зареждане на герой" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (ниво {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "История на Чата (Съобщения: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} злато" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "Колко златни монети искате да изтеглите?" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Ниво {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Опит: {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Максимално ниво" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Следващо ниво: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} до Ниво {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "Гробницата на Крал Леорик" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "Залата на Костите" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Мрачен проход" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Неосветения олтар" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "Към {:s}" #: Source/quick_messages.cpp:10 #, fuzzy #| msgid "I need help! Come Here!" msgid "I need help! Come here!" msgstr "Ела! Нуждая се от помощ!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Следвай ме." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Ето нещо за теб." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "Сега ще УМРЕШ!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 #, fuzzy #| msgid "Thanks To" msgid "Thanks." msgstr "Благодарение на" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 msgid "Griswold" msgstr "Грисволд" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Пепин" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Огдън" #: Source/stores.cpp:135 msgid "Cain" msgstr "Кайн" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Фарнам" #: Source/stores.cpp:137 msgid "Adria" msgstr "Адрия" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Джилиън" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Върт" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Назад" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Щети: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Броня: {:d} " #: Source/stores.cpp:315 #, fuzzy, c++-format #| msgid "Dur: {:d}/{:d}, " msgid "Dur: {:d}/{:d}" msgstr "Издр.: {:d}/{:d}, " #: Source/stores.cpp:317 #, fuzzy #| msgid "indestructible" msgid "Indestructible" msgstr "неразрушими" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Добре дошли в" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Работилницата на Ковача" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Искате ли да:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Говорите с Грисволд" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Купите обикновени предмети" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Купите качествени предмети" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Продадете предмети" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Поправите предмети" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Напуснете ателието" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Продавам тези предмети." #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Продавам тези изключителни предмети." #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "Нямате нищо, което ми трябва." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "Кой предмет е за продажба?" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "Нямате предмет за поправка." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "Кой предмет е за поправка?" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Колибата на Вещицата" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Говорите с Адрия" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Купите магически предмети" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Презаредите жезли" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Напуснете Колибата" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "Нямате предмет за презареждане." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "Кой предмет е за презареждане?" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "Нямате достатъчно злато" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "Нямате достатъчно място в инвентара" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "Договорихме ли се?" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "Наистина ли искате да разпознаете този предмет?" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "Наистина ли искате да купите този предмет?" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "Наистина ли искате да презаредите този предмет?" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "Наистина ли искате да продадете този предмет?" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "Наистина ли искате да поправите този предмет?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Върт, момчето с дървения крак" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Говорите с Върт" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Имам нещо за продан," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "но това ще ти струва 50 злато" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "само да погледнеш. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "Какво имаш?" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Кажете довиждане" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Имам този предмет за продан:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Напуснете" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Домът на Лечителя" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Говорите с Пепин" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Напуснете дома на Лечителя" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "Градският старейшина" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Говорите с Кайн" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Разпознаване на предмет" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "Нямате нищо за разпознаване." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "Кой предмет е за разпознаване?" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "Този предмет е:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Готово" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Говорете с {:s}" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Говори с {:s}" #: Source/stores.cpp:1217 msgid "is not available" msgstr "не е налично" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "в пробната" #: Source/stores.cpp:1219 msgid "version" msgstr "версия" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Общи приказки" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Изгряващото слънце" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Говорите с Огдън" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Напуснете гостилницата" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Говорите с Джилиън" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Влезите в Склада" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Пияницата Фарнам" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Говорите с Фарнам" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Кажете довиждане" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Вашето злато: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Поваления жител" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Ковача Грисволд" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Лечителя Пепин" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Ранения жител" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Гостилничаря Огдън" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Старейшината Кайн" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Вещицата Адрия" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Кръчмарката Джилиън" #: Source/towners.cpp:786 msgid "Cow" msgstr "Крава" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Фермера Лестър" #: Source/towners.cpp:788 msgid "Celia" msgstr "Селия" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Пълния Откачалник" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Войн" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Разбойник" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Магьосник" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Монах" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Бард" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Варварин" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Зомби" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Таласъм" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Гниещ Труп" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Черна Смърт" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "Изпаднал" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Дълбач" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Дяволска Челяд" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "Очернен" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Скелет" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Корпус Брадвен" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Горящ Мъртвец" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Кошмар" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Лешояд" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Чумояд" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Звяр от Сенките" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Костодрач" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Корпус Лъков" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Скелет Капитан" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Корпус Капитан" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Горящ Мъртвец Капитан" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Кошмар Капитан" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Невидим Повелител" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Прикрит" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Следящ от Сенките" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Незабелязан" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Предач на Илюзии" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Сатир Господар" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Плътски Клан" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Каменен Клан" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Огнен Клан" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Нощен Клан" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Злосторник" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Моменталник" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Мрачник" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Познайник" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Киселинен Звяр" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Отровохрачещ" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Ямен Звяр" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Магмена Паст" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Кралят Скелет" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "Касапина" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Надзирател" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Кален човек" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Крастав демон" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "Кожодер" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Змей" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Пещерен плужек" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Дяволски змей" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Поглъщащ" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Магмен Демон" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Кръвен Kамък" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Адски Камък" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Господар на Лавата" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Рогат Демон" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Кален Бегач" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Връхлитащ Мраз" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Обсидианов Повелител" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "Старокостен" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Червена Смърт" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Лич Демон" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Немъртъв Барлог" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Изгарящ" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Огнен Господар" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Обречен Пламък" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Адски Горяч" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Червена Буря" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Буреносен Водач" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Господар на Бурите" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Маелстрьом" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Дяволска Челяд Здравеняк" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Крилат Демон" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Гаргойл" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Кървав нокът" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Смъртоносно Крило" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Унищожител" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Пазител" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Господар на Вихъра" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Балрог" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Пещерна Усойница" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Огнена Ламя" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Златна Усойница" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Лазурена Ламя" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Черен Рицар" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Пазител на Обречените" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Стоманен Повелител" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Кървав Рицар" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "Раздробените" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "Празнодушен" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Господар на Болката" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Предач на Rеалности" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Сукуби" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Ледена Вещица" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Адска Отродия" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Душеизпепеляваща" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Съветник" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Магистрат" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Кабалист" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Адвокат" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Голем" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "Диабло" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Архи-немъртвия Малигнус" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Хилавия Гарбад" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Лудият Жар" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Сополивия" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Архиепископ Лазар" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Червената Векс" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Черната Джейд" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Лахданан" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Кървавият Пълководец" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Демона Хорк" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "Осквернителят" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "На-Крул" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Кокалоглав Секирострастен" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Срязващия Острокож" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Душегной" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Нечистливия Бълвоплъх" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Костотрошача" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Гладния Гнилопир" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Пъргавия Чреволеш" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Чупеглав Хлопащит" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Бонго" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Кланегнил" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Сенкохапливия" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Мъртвоко" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Мъртвия Лудоок" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "El Chupacabras" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Черепогън" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Кривочереп" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Кръвоезик" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Пулсолазещия" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Луногърчещ" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Гнявогарван" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Шипояд" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Горящия Чернопепел" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Сянкогарван" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Слабия Болнокамен" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Жлъчепян Началника на Ямата" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Кръвокож Тъмнолък" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "Долнокрил" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Сянкопиеца" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Омараменящия" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Смъртоплюй" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Кръвопорещия" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Смъртосян Плъточук" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Лудият Войночервей" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Нащърбения Стъклочереп" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Болестогън" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Студения Нощокрил" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Съсирекамък" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Бронзюмрук Огнекамък" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Гнявоплам Обречения" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Огнерана Безжалостния" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Барон Шлам" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Отровошип Стоманобух" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Хаосовиещия" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Орисхил Гниещия" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Лудогорящия" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Лича Косторез" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Чупигръбнак" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Дявочереп Острококал" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Разбибуря" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Бурегибел" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Течелига" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Заразлато от Пламъка" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Чернобур" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Чумоярост" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "Бичуващия" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Синерог" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Кривогън Пъклочадие" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Зъбекопие" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Гноечереп" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Лъвочереп Привития" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Черноезик" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Гнуседопир" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Усойнопламък" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Зъбекож" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Вещогън Нечестивия" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Черночереп" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Душерез" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Ветрочеляд" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Господаря на Ямата" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Ръждоплет" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Гнявой Сянката" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Обреченоблак" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Кърволуна Душеогнена" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Вещолуна" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Съсирепир" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Сивойна Палача" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Страхосъдия" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Звездноока Вещицата" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Стоманочереп Ловецът" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Сър Гораш" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "Визирът" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Сапфирени" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Кръвожажда" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Оплетовдовица" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Плътотанц" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "Мрачешип" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Гибелобречен" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Къс меч" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Кръгъл щит" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Сопа" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Къс лък" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Къс жезъл за мана" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Сатър" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "Корона на немъртвите" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Небесна гривна" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Магически камък" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Оптичен амулет" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Пръстен на Истината" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Табела от страноприемница" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Арлекинов герб" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Стоманено було" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Златен еликсир" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Наковалня на Яростта" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Черна гъба" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Мозък" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Гъбен алманах" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Спецтрален еликсир" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Кръвен камък" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Карта на катедралата" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Отвара за лечение" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Отвара за мана" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Свитък за разпознаване" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Свитък за Градски портал" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Доблестта на Аркайн" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Отвара за Пълно лечение" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Отвара за Пълна мана" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Острието на Грисволд" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Говежда броня" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Жезълът на Лазар" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Свитък за Възкресение" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Къс жезъл" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Меч" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Кама" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Рунична бомба" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Теодор" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Блестящ амулет" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Разкъсана бележка 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Разкъсана бележка 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Разкъсана бележка 3" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Възстановено Завещание" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Кафяв костюм" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Сив костюм" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Каска" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Наскулна каска" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Шлем" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Пълен шлем" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Корона" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Велик шлем" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Пелерина" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Дрипи" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Наметало" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Роба" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Ватирана броня" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Кожена броня" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Твърда кожена броня" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Ошипена кожена броня" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Пръстенова ризница" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Ризница" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Плетена ризница" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Плочна ризница" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Нагръдни доспехи" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Доспехи" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Шинирана ризница" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Плочни Доспехи" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Полеви Доспехи" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Готически Доспехи" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Пълни плочни доспехи" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Щит" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Малък щит" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Голям щит" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Гвардейски щит" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Крепостен щит" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Готически щит" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Отвара за Възобновление" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Отвара за Пълно възобновление" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Масло" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Еликсир за Сила" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Еликсир за Магия" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Еликсир за Сръчност" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Еликсир за Жизненост" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Свитък за Лечение" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Свитък за Издирване" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Свитък за Мълния" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Свитък за Огнена стена" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Свитък за Инферно" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Свитък за Светкавица" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Свитък за Инфразрение" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Свитък за Произволно положение" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Свитък за Щит от мана" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Свитък за Огнена вълна" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Свитък за Огнено кълбо" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Свитък за Каменно проклятие" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Свитък за Верижна мълния" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Свитък за Пазител" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Свитък за Нова" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Свитък за Голем" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Свитък за Телепортация" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Свитък за Апокалипсис" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Фалхион" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Ятаган" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Двуострен меч" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Острие" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Сабя" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Дълъг меч" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Широк меч" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Батардов меч" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Двурък меч" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Велик меч" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Секира" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Брадва" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Голяма брадва" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Широка брадва" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Бойна брадва" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Велика брадва" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Боздуган" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Утринна звезда" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Боен чук" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Чук" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Ошипена палка" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Млатило" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Млат" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Лък" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Ловен лък" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Дълъг лък" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Комбиниран лък" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Къс боен лък" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Дълъг Боен лък" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Къс военен лък" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Дълъг военен лък" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Дълъг жезъл" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Комбиниран жезъл" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Четвъртичен жезъл" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Военен жезъл" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Пръстен" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Амулет" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Руна за Огън" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Руна" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Руна за Мълния" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Мощна Руна за огън" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Мощна Руна за мълния" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Руна за Вкаменяване" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Къс жезъл за Светкавична искра" #: Source/translation_dummy.cpp:368 #, fuzzy #| msgid "Mana Potion Pickup" msgid "Arena Potion" msgstr "Авто-взимане мана" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "Сатъра на Касапина" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Lightsabre" msgid "Lightforge" msgstr "Светосабя" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "Разривен лък" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "Игленикът" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "Небесния лък" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Смъртоносен Ловец" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Лък на мъртвите" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "Чернодъбовият Лък" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Огнено острило" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Плътожилец" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Вятърмощ" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Орелорог" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Камата на Гонагал" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "Закрилникът" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Грифонски нокът" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Черен бръснач" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Непълна луна" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Леден Къс" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "Острието на Палача" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "Трион за кости" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Сенкоястреб" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Чародейски шип" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Светосабя" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "Соколски нокът" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Инферно" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "Гибелоносец" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "Гризлито" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "Старецът" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "Разкъсвача" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Остър Клюн" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Кървав унищожител" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "Звездната Брадва" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "Нечестива Брадва" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "Каменен секач" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "Агинарова секира" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "Адски секач" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "Месершмидски Опустошител" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "Цеперъжд" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "Чукът на Жолм" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "Бухалката на Сиверб" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "Небесна звезда" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "Звездата на Баранар" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "Възлест корен" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "Черепен трошач" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "Чукът на Шаефър" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "Събуждач" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "Жезъл от Сенки" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "Подпалвач" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "Бурешпил" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "Искрапесен" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "Гръмовит" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "Защитника" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "Загаждача на Наж" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "Умостенание" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "Тоягата на Онан" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "Шлем от Духове" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "Мислеща Шапка" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "Надзирателски шлем" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "Емблема на Шута" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "Залезът на боговете" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "Кралска диадема" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "Откъсната плът от души" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "Неволята на Гладиатора" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "Пъстроцветната пелерина" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "Кожа от Аут" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "Прегръдка на Мъдростта" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "Искряща ризница" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "Мършоядска коруба" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "Тъмоствол" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "Леките доспехи на Наж" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "Демоншипско палто" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "Отразителят" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "Сцепен черепен щит" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "Драконов пробив" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "Чернодъбов щит" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "Свещен пазител" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "Бурещит" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "Бодливенец" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "Пръстен на Рега" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "Кървящият" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "Сътворяващ пръстен" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "Венчален Пръстен" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "Оловни" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "Месингови" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "Бронзови" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "Железни" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "Стоманени" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "Сребърни" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "Платинени" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "Митрилови" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "Метеоритни" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "Страненни" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "Безполезни" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "Криви" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "Слаби" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "Назъбени" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "Смъртоносни" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "Тежки" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "Коварни" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "Безмилостни" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "Мощни" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "Свирепи" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "Жестоки" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "Зверски" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "Непохватни" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "Тъпи" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "Остри" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "Изящни" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "Войнски" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "Войнишки" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "Господарски" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "Рицарски" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "Повелителски" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "Юнашки" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "Кралски" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "Уязвими" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "Ръждиви" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "Могъщи" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "Внушителни" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "Доблестни" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "Славни" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "Благословени" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "Блажени" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "Невероятни" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "Божествени" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "Червени" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "Пурпурни" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "Гранатени" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "Рубинени" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "Сини" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "Лазурни" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "Лаписови" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "Кобалтови" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "Сапфирени" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "Бели" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "Перлени" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "Кварцови" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "Кристални" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "Диамантени" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "Топазени" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "Кехлибарени" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "Нефритени" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "Обсидиани" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "Изумрудни" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "Хиенови" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "Жабешки" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "Паешки" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "Гарвански" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "Змийски" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "Гущерски" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "Ламски" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "Драконови" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "Змейски" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "Хидрови" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "Ангелски" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "Архангелски" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "Щедри" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "Обилни" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "Пламени" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "Светкавични" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "Качеството" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "Осакатяване" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "Посичането" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "Кръвопролитието" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "Касапницата" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "Клането" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "Болката" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "Сълзите" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "Здравето" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "Закрилата" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "Поглъщането" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "Отклонението" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "Осмозата" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "Хилавостта" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "Слабостта" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "Силата" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "Мощта" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "Могъществото" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "Великаните" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "Титаните" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "Парализата" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "Атрофията" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "Сръчността" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "Умението" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "Точността" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "Прецизността" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "Съвършенството" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "Глупака" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "Дислексията" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "Магията" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "Мисълта" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "Блясъка" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "Магьосничеството" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "Чародейството" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "Болестта" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "Заразата" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "Бодростта" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "Жарта" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "Живостта" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "Крепкостта" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "Живота" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "Бедата" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "Ямата" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "Небето" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "Луната" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "Звездите" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "Висините" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "Зодиака" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "Лешоядът" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "Чакала" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "Лисицата" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "Ягуара" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "Орела" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "Вълка" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "Тигъра" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "Лъвът" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "Мамута" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "Китът" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "Крехкостта" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "Чупливостта" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "Здравината" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "Изработката" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "Структурата" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "Епохите" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "Тъмнината" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "Нощта" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "Светлината" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "Сиянието" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "Пламъка" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "Огъня" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "Горенето" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "Сътресението" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "Мълнията" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "Гръмотевицата" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "Множеството" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "Изобилието" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "Шиповете" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "Покварата" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "Крадците" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "Мечката" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "Прилепа" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "Вампирите" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "Пиявицата" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "Кръвта" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "Пронизването" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "Пробождането" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "Блъскането" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "Готовността" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "Пъргавината" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "Скоростта" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "Бързината" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "Баланса" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "Стабилността" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "Хармонията" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "Блокирането" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "Магическият камък" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "Гарбад Хилавия" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "Лудият Жар" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "Лахданан" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "Касапина" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "Табелата на Огдън" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "Залата на Слепите" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "Доблест" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "Кървавият Пълководец" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "Проклятието на Крал Леорик" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "Архиепископ Лазар" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "Задгробни Въпроси" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "Овошките на Фермера" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "Малкото момиче" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "Пътуващият търговец" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "Осквернителят" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "На-Крул" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "Говежди Одежди" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "Огнен болид" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "Лекуване" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "Мълния" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "Светкавица" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "Разпознаване" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "Огнена стена" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "Градски портал" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "Каменно прооклятие" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "Инфразрение" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "Произволно положение" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "Щит от мана" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "Огнена топка" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "Пазител" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "Верижна мълния" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "Огнена вълна" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "Гибелни Змии" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "Кървав ритуал" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "Нова" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "Невидимост" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "Инферно" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "Голем" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "Ярост" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "Телепортиране" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "Апокалипсис" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "Ефирализация" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "Поправяне на предмет" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "Презареждане на жезъл" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "Обезвреждане на клопка" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "Стихия" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "Мълниен заряд" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "Свещено кълбо" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "Възкресяване" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "Телекинеза" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "Лекувай приятел" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "Кървава звезда" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "Костен дух" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" " Ааа, за историята на Краля ли? Трагичния крах на Леорик дойде като гръм от " "ясно небе за тези земи. Хората обичаха Краля, а сега живеят в дълбок страх " "от него. Въпросът, който продължавам да си задавам, е, как е могъл да падне " "толкова далеч от Светилната, Леорик винаги е бил най-светлият от всички. " "Единствено най-покварените сили на Ада са способни да унищожат някого " "отвътре..." #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "Градът се нуждае от Вашата помощ, драги господарю! Преди няколко месеца, " "Принц Албрихт, синът на Крал Леорик, бе отвлечен. Побеснял, Кралят обърна " "всеки камък, за да намери своята рожба. Но с всеки изминал ден, Леорик сякаш " "изпадаше във все по-дълбока лудост. Обвиняваше невинни жители за изчезването " "на сина си, като ги осъждаше на несправедлива екзекуция. По-малко от " "половината от нас успяха да се спасят от неговото безумие.\n" "\n" "Рицарите му и придворните свещеници се опитаха да го усмирят, но той се " "обърна срещу тях и за жалост, те бяха принудени на цареубийство. С последния " "си дъх, Кралят отправи жестоко проклятие към бившите си слуги, кълнейки, се " "че те ще му служат вечно в мрака.\n" "\n" "Точно тогава, събитията придобиха още по-мрачен изглед, какъвто не съм и " "предполагал! Някогашният ни Крал се надигна от своя вечен сън и в момента " "ръководи легион от немъртви чудовища в Лабиринта. Тялото му лежи заровено в " "гробница, на три нива под Катедралата. Умолявам те, драги господарю, върни " "душата му в покой, като унищожиш тази му прокълната същност..." #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Както вече споменах, драги господарю, Кралят е загробен на три нива " "дълбочина. Той е там долу, в зловещата тъмнина, чакайки момента да съсипе " "тази земя." #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "Проклятието на Краля отмина, но се опасявам, че това е само частица от " "дебнещо по-велико зло. Въпреки всичко, може би има надежда да бъдем спасени " "от тъмнината, стелещо се над нашата земя, защото твоята победа е само добро " "знамение. Нека светлина озари твоя път, добри господарю." #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" "Крал Леорик не можа да превъзмогне загубата на сина си. Положих всякакви " "усилия, за да облекча лудостта му, но все пак тя го пребори. От този ден " "нататък черно проклятие се стели над това кралство, но може би ако освободиш " "духа му от неговите земни окови, проклятието ще бъде повдигнато..." #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" "Не искам да си спомням как се спомина Краля. Искам да си го спомням като " "добър и справедлив владетел, какъвто беше. Неговата смърт донесе много скръб " "за всички и бе някак не намясто." #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" "Изковал съм повечето от оръжията и броните, с които Крал Леорик екипираше " "своите рицари. Дори направих огромен меч от най-добрия митрил за негово " "величество, както и корона, за да си подхождат. Все още не мога да повярвам " "как си отиде, но определено трябва да е била някаква наистина злокобна сила, " "която да го доведе до пълна лудост!" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Това не ме интересува. Слушай ме, никакъв скелет няма да бъде МОЙ крал. " "Единственият крал е Леорик. Крал, разбираш ли? ДА ЖИВЕЕ КРАЛЯТ!" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" "Мъртвите, бродещи сред живите, следват прокълнатия Крал. Той притежава " "силата да призовава още повече войни, умножавайки своята вечно растяща армия " "от немъртви. Ако не сложиш край на неговото господство, той със сигурност не " "ще се поколебае да съсече всеки, който живее по тези земи." #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" "Виж, имам си достатъчно работа. Не продавам информация и не ме увлича " "някакъв си Крал, особено ако е мъртъв от години. Ако ти трябва нещо, с което " "да се изправиш срещу този Господар на Немъртвите, може би мога да ти " "помогна..." #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" "Топлината на живота е пристъпила в гробницата ми. Приготви се, смъртнико, да " "служиш вовеки на моя Господар!" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" "Забелязвам, това неясно държание озадачава и теб. Подозирам, че немалко " "демони се страхуват от светлината, вярвайки че тя има велика сила. Може би " "изгряващото слънце върху табелата им е заприличало на символ, носещ някаква " "тайнствена сила. Хммм, възможно е да не са чак толкова умни колкото " "подозираме..." #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" "Господарю, имам да споделя едно странно преживяване. Знам, че имаш дълбоки " "познания за тези чудовищни създания, които обитават лабиринта, а това е " "нещо, което не мога да проумея за нищо на света... През нощта бях събуден от " "стържещ звук точно пред моята кръчма. Когато погледнах от спалнята си, видях " "в двора на кръчмата силуетите на малки демоноподобни същества. Миг по-късно " "те се разбягаха, но не и преди да откраднат табелата на кръчмата ми. Не знам " "защо демоните откраднаха табелата ми, а оставиха семейството ми на мира... " "странно е, нали?" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" "Оооо, не беше нужно да ми връщаш табелата, но предполагам, че това ще ми " "спести разходите по изработката на нова. Добре, нека да видим с какво мога " "да ти се отплатя за намирането ѝ? Хммм, какво имаме тук... а, да! Тази шапка " "бе забравена в една от стаите от един вълшебник, който остана тук преди " "време. Може би ще ти бъде от полза." #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" "Боже мой, демони обикалят града нощем и плячкосват домовете ни? Нима няма " "нищо свято останало? Надявам се, че Огдън и Гарда са добре. Предполагам, че " "щяха да дойдат да ме видят, ако бяха ранени..." #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" "О, Боже! Така ли е изчезнала табелата? Баба ми и аз сигурно сме преспали " "цялата случка. Благодаря на Светлината, че тези чудовища не са нападнали " "гостилницата." #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "Казваш, че демони са откраднали табелата на Огдън? Това не звучи много като " "зверствата, за които съм чувал или виждал.\n" "\n" "Демоните са загрижени как да изтръгнат сърцето ти, а не от дървената ти " "табела." #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" "Знаеш ли какво си мисля? Някой е взел тази табела и ще иска една камара пари " "за нея. Ако бях Огдън... не, че съм, но ако бях... щях просто да си купя " "нова табела с някаква хубава рисунка на него. Може би една хубава халба пиво " "или парче сирене..." #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Никой смъртен не може да разбере ума на демона.\n" " \n" "Никога не позволявай на блуждаещите те им постъпки да те объркват, тъй като " "това също може да е тяхната цел." #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" "Какво? Той мисли, че Аз съм го взел? Предполагам, че и Грисволд е на негова " "страна.\n" " \n" "Слушай, спрях да крада табели преди месеци. Не можеш да изкараш много от " "парче дърво." #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" "Хей, ти този, който убива всички! Донеси ми Магическо знаме или нападаме! " "Няма да тръгнеш с живот! Ти убиваш големи грозници и връщаш Магия. Върви " "покрай ъгъл и врата, намираш грозници. Даваш, минаваш!" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "Убиваш грозници, взимаш знаме. Ти носиш, иначе..." #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" "Ти даваш! Да, добре! Върви сега, ние силни. Ние убиваме всички с голяма " "магия!" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" "Това не вещае нищо добро и потвърждава най-мрачните ми страхове. Макар да не " "си позволявах да вярвам на древните предания, сега не мога да ги отрека. " "Настъпи момента да разкрия кой всъщност съм аз.\n" "\n" "Истинското ми име е Декард Кайн - Старейшината и съм последният потомък на " "древно Братство, посветено на опазването на тайните на безвременно зло. Зло, " "което очевидно вече е на свобода.\n" "\n" "Архиепископ Лазар, някогашният най-доверен съветник на крал Леорик, поведе " "група от обикновени жители на града в Лабиринта, за да намерят Албрихт, " "изчезналия син на краля. Мина доста време, преди малцината оцелели завърнат, " "избягали на косъм от смъртта .\n" "\n" "Проклета да бъде глупостта ми! Трябваше да предположа скритото му " "предателство още тогава. Сигурно самият Лазар е отвлякъл Албрихт и о го е " "отвел в Лабиринта. Не разбирам защо архиепископът се е преминал към мрака и " "какъв е интересът му към детето... Освен ако не смята да го принесе в жертва " "на тъмните си господари!\n" "\n" "Трябва да е замислил точно това! Оцелелите от неговата 'спасителна дружина' " "разказват, че за последен път са видели Лазар да бяга в най-дълбоките дебри " "на лабиринта. Трябва да побързаш и да спасиш принца от жертвеното острие на " "този обезумял злодей!" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Трябва да побързаш и да спасиш Албрихт от ръцете на Лазар. Принцът и народът " "на това кралство разчитат на теб!" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" "Разказа ти е доста зловещ, приятелю. Лазар със сигурност ще гори в ада за " "ужасното си деяние. Момчето, което описваш, не е нашият принц, но вярвам, че " "Албрихт все още може да е в опасност. Символът от тъмна сила, за който " "говориш, трябва да е портал в самото сърце на лабиринта.\n" "\n" "Знай това, приятелю - злото, срещу което се изправяш, е тъмният Господар на " "ужаса. Той е познат сред смъртните като Диабло. Именно той е бил затворен " "преди много векове в Лабиринта и се опасявам, че се стреми отново да сее " "хаос в царството на хората. Трябва да преминеш през портала и да унищожиш " "Диабло, преди да е станало твърде късно!" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" "Лазар е архиепископът, който поведе много от жителите на града към " "лабиринта. В онзи ден загубих много добри приятели, а Лазар така и не се " "завърна. Предполагам, че е бил убит заедно с повечето от останалите. Ако " "можеш да ми направиш услуга, добри господарю - моля те, не говори на Фарнам " "за онзи ден." #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Бях поразен, когато чух за какво се готвеха жителите на града онази нощ. " "Мислех си, че от всички хора Лазар би имал повече разум от това. Той беше " "архиепископ и винаги изглеждаше, че го е много грижа за жителите на " "Тристрам. Толкова много бяха ранените, нямаше как да ги спася всичките..." #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Спомням си Лазар като много добър и щедър човек. Той говори на погребението " "на майка ми и подкрепи баба ми и мен, в един много труден момент. Всяка " "вечер се моля да е жив и в безопасност." #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Бях там, когато Лазар ни поведе в лабиринта. Говореше за свято възмездие, но " "когато започнахме да се бием с тези адски гадове, той дори не повдигна " "боздугана си срещу тях. Той просто побягна навътре в мрачните, безкрайни " "коридори, които бяха пълни със слугите на мрака!" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" "Пронизват, после хапят, а после са навсякъде около теб. Лъжец! ЛЪЖЕЦ! Всички " "те са мъртви! Мъртви! Чуваш ли ме? Те продължават да падат и падат... кръвта " "им се разлива по земята... всичко е по негова вина..." #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" "Не познавам този Лазар, за когото говориш, но усещам голям конфликт в " "същността му. Той представлява голяма опасност и няма да се спре пред нищо, " "за да служи на силите на мрака, които са го приели за свой." #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" "Да, праведника Лазар, който беше толкова деен срещу онези чудовища в " "подземията. Но така и не ми помогна да спася крака си, нали? Един безплатен " "съвет... Говори с Фарнам, той беше там..." #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" "Откажи се от глупавото ти търсене. Очаква те само гневът на моя господар! " "Твърде късно е за да спасиш детето. Скоро ще се присъединиш към него в Ада!" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" "Хммм, не знам какво точно мога да ти кажа по този въпрос, за да съм полезен. " "Водата, която тече в нашите кладенци, идва от подземен извор. Чувал съм за " "тунел, който води до голямо езеро - може би те са свързани. За съжаление не " "знам какво би могло да доведе до замърсяване на водата ни." #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Винаги съм се опитвал да поддържам големи запаси от храна и напитки в избата " "ни, но тъй като целият град е останал без прясна вода, скоро и нашите запаси " "ще се изчерпят. \n" "\n" "Моля, направи каквото е по силите ти, иначе не знам какво ще стане с нас." #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "Радвам се, че те хванах навреме! Кладенците ни са мътни и застояли и някои " "от жителите на града се разболяха, пиейки от тях. Запасите ни от прясна вода " "бързо се изчерпват. Смятам, че има проход, който води до изворите, от които " "града ни черпи вода. Моля те, потърси какво ни е причинило това нещастие, " "иначе всички ние със сигурност ще изтлеем от жажда." #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Моля те, побързай. С всеки изминал час запасите ни от питейна вода " "намаляват.\n" "\n" "Няма да оцелеем дълго без твоята помощ." #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "Какво говориш?! Самото присъствие на демони около извора е замърсило водата? " "О, наистина голямо зло се крие под нашия град, но твоята упоритост и смелост " "ни дават надежда. Моля те, вземи този пръстен, може би той ще ти помогне в " "унищожаването на тези противни създания." #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Баба ми се чувства много слаба, а Гарда казва, че не можем да пием вода от " "кладенците. Моля те, не можеш ли да направиш нещо, за да ни помогнеш?" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" "Пепин е прав. Наистина се нуждаем от прясна вода, и то скоро. Опитах се да " "изчистя един от по-малките кладенци, но той вони на застояла мръсотия. " "Сигурно е запушил при източника." #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "Ти пиеш вода?" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "Жителите на Тристрам ще загинат, ако не успееш да върнеш прясната вода в " "кладенците им.\n" "\n" "Знай, че демони са в основата на това дело, но самите те не знаят какво " "всъщност са породили." #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Този път, съм с теб. Търговията ми пресъхва - така да се каже - ако нямам " "пазар, на който да продавам. По-добре разбери какво е в основата всичко, и " "то бързо!" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" "Книга, в която се говори за Камера с човешки кости? Да, Камера на Костите се " "споменава в определени архаични писания, които изучавах в библиотеките на " "Изтока. В тези съчинения се загатва, че когато Владетелите на подземния свят " "искали да защитят големи богатства, те създавали територии, в които онези, " "загинали в опит да откраднат съкровището, били завинаги осъдени да го " "защитават като немъртви. Извратен, но странно подходящ край?" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" "Боя се, че не знам нищо за това, добри господарю. Кайн има много книги, " "които могат да ти помогнат." #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" "Това звучи като много опасно място. Ако ще ходиш натам, моля те, то бъди " "много внимателен." #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Опасявам се, че не съм чувал нищо такова. Може би Кайн Разказвачът би могъл " "да ти помогне." #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" "Нищо не съм чувал за подобно място, но може да попиташ Кайн. Той говори за " "много неща и няма да се изненадам, ако може да отговори на въпроса ти." #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Добре, слушай сега. Има тази камера от дърво, сещаш се. И жена му, нали " "знаеш - нейната - казва на дървото... защото трябва да почакаш. Тогава аз " "рекох, че това може и да проработи срещу него, но ако мислиш, че аз ще " "ПЛАЩАМ за това... ти... ъъъ... да." #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Ако загинеш в това прокълнато място, ще станеш вечен слуга на тъмните му " "владетели.\n" "\n" "Пристъпи в Камерата на Костите на собствен риск." #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "Казваш, че това е огромно и тайнствено съкровище? Може би бих се " "заинтересувал да взема няколко неща от теб... или още по-добре, не ти ли " "трябват рядко и скъпо снаряжение, което да ти помогне да преминеш през това " "изпитание?" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" "Изглежда, че архиепископ Лазар е подтикнал много от жителите на града да се " "впуснат в Лабиринта, за да намерят изчезналия син на краля. Възползвайки се " "от страховете им, той ги превръща в бясна тълпа. Никой от тях не е бил " "подготвен за това, което ги е очаквало в смразяващите подземия... Лазар ги " "изоставя там - в лапите на неописуеми ужаси - да умрат." #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" "Да, Фарнам промърмори нещо за грамаден звяр, който използвал жестоко оръжие. " "Струва ми се, че го нарече Касапина." #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "В името на Светлината, знам за този свиреп демон. Много носеха белезите на " "яростта му по телата си, когато малцината оцелели от свитата, водена от " "Лазар, изпълзяха от катедралата. Не знам какво е използвал, за да разфасова " "жертвите си, но не може да е било от този свят. Рани, гноясали от зараза, " "дори за мен, те бяха почти непосилни за лечение. Пази се, ако възнамеряваш " "да се изправиш пред този злодей..." #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Когато Фарнам заговори нещо за касапин, който убива хора, не му обърнах " "внимание. Но сега след като ти го споменаваш, може би е вярно." #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" "Видях онова, което Фарнам нарича Касапина, докато си проправяше път през " "телата на моите приятели. Той размахваше сатър, голям колкото брадва, " "откъсвайки крайници и съсичайки храбрите мъже на място. Бях отделен от " "схватката от множество малки пищящи демони и някак намерих пътя до " "стълбището, което водеше навън. Никога повече не видях този отвратителен " "звяр, но мисълта за окървавеният му лик ме преследва и до днес." #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "Огромен! Огромен сатър, съсича всичките ми приятели. Не можех да го спра, " "трябваше да избягам, не можех да ги спася. Хванати в капан, стая с толкова " "много тела... толкова много другари... НЕЕЕЕЕЕЕЕЕЕЕЕ!" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "Касапина е садистично същество, което изпитва удоволствие от мъченията и " "болката на другите. Виждал си делата му в лицето на пияницата Фарнам. " "Унищожаването му ще допринесе много за безопасността на този град." #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" "Знам повече, отколкото можеш да си предтавиш, за този ужасен злодей. Малките " "му приятели ме хванаха и успяха отмъкнат крака ми, преди Грисволд да ме " "издърпа от онази дупка.\n" "\n" "Ще го кажа направо - убий го пръв, преди да те е съсякъл и да е добавил " "трупът ти към неговата колекцията си." #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" "Моля те, чуй ме... Архиепископ Лазар, той ни поведе тук долу, за да търсим " "изчезналия принц. Мръсникът ни вкара в капан! Сега всички са мъртви... убити " "от демон, когото той нарече Касапина. Отмъсти за нас! Намери този Касапин и " "го убий, за да могат душите ни най-накрая да си отидат спокойни..." #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" "Рецитираш интересна рима, написана в стил, който ми напомня за други " "произведения. Нека сега помисля - как точно беше?\n" "\n" "...Тъмнината забулва Скритите. Очи блестят незабелязани, и само звуците от " "стържене на остри нокти измъчват онези бедни души, които са останали незрящи " "за цяла вечност. Затворът за тези прокълнати е наречен Залите на слепите..." #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Никога не съм изпитвал особен интерес към поезията. От време на време наемах " "странстващи музиканти, когато странноприемницата вървеше добре, но това вече " "изглежда много отдавна.\n" "\n" "Какво? А, да... ами, предполагам, че можеш да попиташ някой друг." #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" "Това изглежда някак познато. Спомням си, че четох нещо много подобно на този " "стих, докато проучвах историята на демоничните страдания. В него се говореше " "за място на голямо зло, което... Чакай, няма да ходиш там, нали?" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" "Ако имаш въпроси що се отнася до слепотата, посъветвай се с Пепин. Той " "приготви за баба ми отвара, която й помогна да изчисти зрението си, така че " "може и на теб да ти е от помощ." #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" "Опасявам се, че нито съм чувал, нито виждал място, което да отговаря на " "твоето описание, приятелю. Може би разказвачът Кейн би могъл да ти помогне." #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "" "Гледай тук... Това е доста смешно, а? Схващаш ли? Слепият вика - виж това?" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Това е място на голямо страдание и ужас, служещо добре на своя господар.\n" "\n" "Спохождай внимателно, защото може да се окаже, че ще останеш много по-дълго, " "отколкото си очаквал." #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" "Нека се разберем, аз продавам ли ти нещо? Не. Даваш ли ми пари, за да ти го " "разяснявам? Не. Сега, тръгваш ли си и заминавайки да разговаряш с " "разказвача, който живее само за такива неща? Да." #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" "Твърдиш, че си разговарял с Лахданан? Приживе той бе велик герой. Почтен и " "справедлив човек, който години наред е служил вярно на своя крал. Но, " "разбира се, ти вече знаеш това.\n" "\n" "От онези, които са попаднали в хватката на проклятието на Краля, Лахданан е " "най-малко склонен да се подчини на мрака без борба, така че предполагам, че " "историята ти може и да е вярна. Ако бях на твое място, приятелю, щях да " "намеря начин да го освободя от терзанията му." #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "Говориш за храбър воин, който е мъртъв от много време! Няма да допусна " "такива разговори за напусна ли ни отдавна души в двора на странноприемницата " "ми, не благодаря." #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Златен еликсир, така ли? Никога досега не съм приготвял отвара с такъв " "оттенък, така че не мога да ти кажа как ще ти подейства, ако се опиташ да я " "изпиеш. Като твой лечител горещо те съветвам, ако намериш такъв еликсир, да " "постъпиш, както те моли Лахданан, и да НЕ се опитваш да го използваш." #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Никога преди не съм чувала за Лахданан. Съжалявам, но не мисля, че мога да " "ти бъда от голяма полза." #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" "Ако действително си срещнал Лахданан, те посъветвам да му помогнеш. Търгувал " "съм с него няколко пъти и съм го смятал за честен и лоялен по природа. " "Проклятието, което се стовари върху служещите на крал Леорик, ще се стовари " "особено тежко върху него." #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" " Лахданан е мъртъв. Всички знаят това и не можеш да ме заблудиш, да мисля по " "друг начин. Не можеш да говориш с мъртвите. Това го знам!" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Възможно е да срещнеш хора, като Лахданан, които са затворени в капана на " "Лабиринта. Усещам в него благородство и голяма вина.\n" "\n" "Помогни му и ще помогнеш на цял Тристрам." #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" "Чакай малко, нека да предположа. Кайн бе погълнат от гигантска пукнатина, " "която се е отворила под него. Бил е изпепелен в кълбо от адски огън и вече " "не може да отговаря на въпросите ти. Оооо, нищо подобно не му се е случило? " "Тогава предполагам, си тук да пазаруваш или ще хващаш пътя." #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" "Моля те, не ме убивай, просто ме изслушай. Някога аз бях капитан на рицарите " "на крал Леорик и с чест и справедливост браних правдата по тези земи. Тогава " "върху нас падна неговото мрачно проклятие заради ролята, която изиграхме в " "трагичната му смърт. Докато моите другари рицари се поддаваха на извратената " "си съдба, аз избягах от погребалната камера на краля, търсейки начин да се " "освободя от неговото Проклятие. Но аз се провалих...\n" "\n" "Чувал съм че съществува Златен Еликсир, който би могъл да премахне " "проклятието и да даде покой на душата ми, но не успях да го намеря. Силите " "ми вече чезнат, а с тях и малкото ми останала човечност. Моля те, помогни ми " "и намери Еликсира. Ще се отплатя за усилията ти, кълна се в честта си." #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "Не си намерил златния еликсир... Страхувам се, че съм обречен навеки. Моля " "те, не спирай да търсиш..." #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" "Ти спаси душата ми от проклятието, и за това ще съм ти навеки задължен. Ако " "има начин да ти се отплатя от отвъдното, ще го намеря, но засега вземи шлема " "ми. По пътя, което ми предстои, той няма да ми е нужен. Нека да те пази от " "мрачните сили там долу. Върви със Светлината, приятелю..." #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" "Грисволд говори за Наковалнята на Яростта - легендарен артефакт, който дълго " "е бил търсен, но никога не е бил намерен. Изработена от металните кости на " "демоните от Ямата на Остриетата, Наковалнята на Яростта е излята между " "черепите на петте най-могъщи магове на подземния свят. Гравирана с руни на " "силата и хаоса, всяко оръжие или броня, изковани на тази наковалня, ще бъдат " "потопени в Царството на отвъдното, което ще им придаде магически свойства. " "Твърди се, че непредсказуемостта на Хаоса прави трудно да се разбере какъв " "ще бъде резултатът от изковаването..." #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" "Не мислиш ли, че Грисволд би бил по-подходящия човек, когото да попиташ за " "това? Той е на близо, нали знаеш." #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" "Ако търсеше информация за Чукалото за Изцелението или за Сребърния бокал за " "Пречистването, щях да съм ти от помощ, приятелю. Но в този случай обаче, би " "било по-добре да поговориш с Грисволд или Кайн." #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" "Когато бяхме деца, Бащата на Грисволд ни разказваше история, за гигантска " "наковалня, на която се изработвали могъщи оръжия. Говореше ни, че когато чук " "удари върху тази наковалня, земята се разтърсва от голяма ярост. Винаги, " "когато има земетресение, си спомням тази история." #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "Привет! Винаги е удоволствие да видя един от най-добрите си клиенти! Знам, " "че си навлязъл дълбоко в Лабиринта и имам една история, която ми разказаха и " "която може би си струва да чуеш...\n" "\n" "Един от мъжете, завърнали се от Лабиринта, ми разказа за тайнствена " "наковалня, на която се натъкнал по време на бягството си. Описанието му ми " "напомни за легендите, които бях чувал в младостта си, за Горящите Адски " "Огнища, където се изработват могъщи магически оръжия. Легендата разказваше, " "че дълбоко в Адските Огнища се намирала Наковалнята на Яростта! Тази " "наковалня съдържала в себе си самата същност на демоничния подземен свят...\n" "\n" "Говори се, че всяко оръжие, изработено върху горящата наковалня, е наситено " "с голяма сила. Ако тази наковалня наистина е Наковалнята на Яростта, може би " "ще успея да ти направя оръжие, способно да победи дори най-мрачния господар " "на Ада!\n" "\n" "Намери ми наковалнята и аз ще се заема с работата!" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" "Все още нищо, а? Добре, но продължавай да търсиш. Оръжие, изковано върху " "наковалнята, може да бъде най-добрата ти надежда и аз съм сигурен, че мога " "да ти изработя такова от легендарни пропорции." #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" "Не мога да повярвам на очите си! Това е Наковалнята на Яростта! Добра " "работа, приятелю. Сега ще покажем на тези уроди, че в Ада няма по-" "смъртоносни оръжия от тези, които са направени от хора! Вземи това и нека " "Светлината те закриля." #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Грисволд не може да продаде наковалнята си. Какво ще прави тогава? И аз също " "бих се ядосал, ако някой ми вземе наковалнята!" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" "В Лабиринта има много артефакти, които притежават сили, отвъд разбиранията " "на смъртните. Някои от тях притежават неизмерима сила, която може да се " "използва както от Мрака, така и от Светлината. Завземането на Наковалнята от " "дълбините на подземието може да промени хода на Войната на Греха в полза на " "Светлината." #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" "Ако откриеш тази наковалня за Грисволд, това може сериозно да накърни " "бизнеса ми тук. Аааааа, едва ли някога ще я намериш." #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" "Портата на Кръвта и Залите на Огъня са ориентири с мистичен произход. Където " "и да се намира тази книга, от която четеш, тя със сигурност е място с голяма " "сила.\n" " \n" "Легендите разказват за пиедестал, издялан от обсидианов камък, на който има " "съд с вряща кръв, върху повърхност с гравирани кости. Има предания за камъни " "от кръв, които ще отворят врата, пазеща древно съкровище...\n" "\n" "Същността на това съкровище е забулена в спекулации, приятелю, но се говори, " "че древният герой Аркайн е поставил свещената броня Доблест в тайно " "хранилище. Аркайн бил първият смъртен, който променил хода на Войната на " "Греха и прогонил легионите на мрака обратно в Горящия Ад.\n" "\n" "Точно преди да умре, бронята на Аркайн е била скрита в тайно хранилище. " "Казват, че когато свещените доспехи отново бъдат необходими, ще се появи " "герой, който отново ще облече Доблестта. Може и ти да си този герой..." #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" "Всяко дете знае историята на героя Аркайн и неговата мистична броня, " "известна като Доблест. Намериш ли мястото, където тя е пазена, ще бъдеш " "добре защитен от злото, дебнещо в Лабиринта." #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Хмм, Звучи като нещо, което трябва да си спомня, но напоследък съм толкова " "зает с изучаването на нови лекарства и създаването на по-добри еликсири, че " "сигурно съм забравил. Съжалявам..." #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" "Историята за вълшебната броня, наречена Доблест, е нещо, за което често " "чувах момчетата да говорят. По-добре попитай някой от мъжете в града." #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" "Бронята Доблест, може да наклони везните в твоя полза. Едно ще ти кажа, че " "мнозина са я търсили - включително и аз. Аркайн я е скрил добре, приятелю, и " "ще ти трябва повече от късмет, за да разкриеш тайните, които са я държали " "потулена толкова години." #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Хърррррррррррррррррррррррррр..." #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" "Ако намериш тези Камъни на кръвта, използвайте ги внимателно.\n" "\n" "Пътят е осян с опасности и единствената ти надежда е увереността в самия теб." #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "Смяташ да намериш бронята, известна като Доблест?\n" "\n" "Никой не знае къде Аркайн я е скрил, а щом моите връзки не са способни да я " "намерят, силно се съмнявам, че и ти някога ще успееш." #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" "Познавам само една легенда, в която се говори за такъв войн, какъвто " "описваш. Неговата история се намира в древните хроники на Войната на " "Греха...\n" "\n" "Очернен от хиляда години война, кръв и смърт, Кървавият Пълководец стои " "върху планина от своите разкъсани жертви. Мрачното му острие крещи черно " "проклятие за живите; измъчена покана към всеки, изправил се пред този " "Екзекутор на Ада.\n" "\n" "Пише също, че макар някога да е бил смъртен и да се е сражавал в редиците на " "Легиона на Мрака по време на Войната на Греха, той е загубил човешката си " "същност заради ненаситната си жажда за кръв." #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Опасявам се, че не съм чувал нищо за такъв жесток войн, добри господарю. " "Надявам се, че няма да ти се наложи да се сражаваш с него, защото всичко " "това звучи изключително опасно." #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" "Кайн би могъл да ти разкаже много повече за нещо подобно, отколкото аз бих " "искал да знам, когато и да било." #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" "Ако ти предстои да се сражаваш с такъв свиреп противник, нека Светлината " "бъде твой водач и защитник. Ще те пазя в мислите си." #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" "Тъмни и злокобни легенди обграждат единствения Кървав Пълководец. Подготви " "се добре , приятелю, защото той не проявява нито капка милост." #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" "Постоянно ли трябва да говориш за кръв? А, какво ще кажеш за цветята, " "слънцето и красивото момиче, което носи напитките. Слушай, приятелю - ти си " "вманиачен, наясно ли си с това?" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" "Умението му да борави с острието е несравнимо, а освен това е живял хиляди " "години в нестихваща битка. Съжалявам... Не мога да видя дали ще го победиш." #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Не съм чувал за този военачалник, за когото говориш, но звучи така, сякаш " "през неговите ръце минават много мечове. Нямам нищо против да снабдявам " "армията му..." #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" "Острието ми стене за твоята кръв, смъртнико... и в името на моите тъмни " "господари, не ще му бъде отказано." #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" "Грисволд говори за Небесния камък, който бе предназначен за анклава, " "разположен на изток. Беше на път за там, където го очакваше по-нататъшно " "проучване. Този камък излъчваше особена енергия, която по някакъв начин ти " "даваше поглед отвъд това, което обикновеният човек можеше да си позволи да " "види. Не знам какви тайни крие той, приятелю, но намирането му със сигурност " "ще донесе много ползи." #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" "Керванът спря тук, за да набави някои припаси за пътуването си на изток. " "Продадох им доста пресни плодове и малко от невероятните сладкиши, които " "Гарда току-що беше изпекла. Жалко е какво ги сполетя..." #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" "Не знам какво са си въобразявали, че ще видят в този камък, но ще ти кажа " "само едно. Ако от небето завалят камъни, по-добре внимавай!" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Да, оттук мина керван с много важни хора, но това беше доста отдавна. " "Доколкото си спомням, говореха някак странно и се приготвяха за дълъг път.\n" "\n" "Не виждам как би могъл да се надяваш да намериш нещо, което те биха носили." #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" "Почакай малко, имам една история, която може да ти се стори интересна. Преди " "известно време оттук мина керван, който се отправяше към източните кралства. " "Предполагаше се, че носи парче от небесата, което е паднало на земята! " "Керванът бе нападнат от маскирани ездачи северно оттук, следвайки този път. " "Претърсих останките за този небесен камък, но не можах да го намеря никъде. " "Ако го намериш, вярвам, че мога да изработя нещо полезно от него." #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Още чакам да ми донесеш онзи камък от небесата. Сигурен съм, че мога да " "направя нещо могъщо от него." #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" "Нека да видя - да... да, това е, както и предполагах. Дай ми малко време...\n" "\n" "Ааа, ето те. Подредих парчетата от камъка в сребърен пръстен, който ми " "завеща баща ми. Надявам се, че ще ти служи добре." #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" "Някога имах хубав пръстен, изключително ценен, със синьо, зелено, червено и " "сребърно. Не си спомням, обаче, какво стана с него. Много ми липсва този " "пръстен..." #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "Небесният камък притежава несъразмерима мощ и ако някой друг, освен " "Грисволд, ти предлагаше да го намериш, щях да го възпра. Той ще овладее тази " "сила и ще създаде нещо добро от нея в името на всички ни." #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" "Ако някой може да създаде нещо от този камък, то това е Грисволд. Той знае " "какво прави и колкото и да се опитвам да открадна клиентите му, уважавам " "неговия труд." #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" "Вещицата Адрия търси черна гъба? Знам толкова за черни гъби , колкото и за " "зелени коне. Може би лечителят Пепин ще ти е по-полезен, но това е нещо, " "което не може да се намери в нито една от моите истории или книги." #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" "Нека ти кажа нещо. Двамата с Гарда никога, НИКОГА не бихме поднесли черни " "гъби на нашите почетни гости. Ако Адрия иска гъби в яхнията си, това си е " "нейна работа, но аз не знам откъде може да ги набавиш. Черни гъби... " "отвратително!" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" "Разбрах от Вещицата, че търсиш мозък на демон, който би могъл да ми помогне " "да създам моят еликсир. Той ще бъде от голяма полза за мнозина, ранени от " "тези отвратителни зверове, само ако успея да разгадая тайните му, които " "подозирам, че крие неговата алхимия. Ако можеш да извадиш мозъка на демон, " "след като го убиеш и ми го донесеш, ще съм ти много благодарен." #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" "Чудесно, именно това имах предвид. Успях да приготвя еликсира и без него, но " "няма да навреди да го имам за изучаване. Би ли занесъл мострата на вещицата? " "Мисля, че тя я очаква." #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "Мисля, че Огдън може да има някакви гъби в мазето. Защо не го попиташ?" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Ако Адрия няма една от тези, бъди сигурен, че това е настина рядко срещано " "нещо. Не мога да ти кажа нищо повече, но на мен ми звучи като... огромна, " "великанска, дебела, разплута, гъба! Е, в такъв случай, наслука." #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Огдън смесва страховита черна гъба, но ми става лошо, ако я пия. Слушай, " "слушай... Знам аз тайната - умереното пиене е ключът!" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "Я да видим какво имаме тук? Интересно, изглежда е книга за реактиви. " "Оглеждай се за черна гъба. Трябва да е доста голяма и лесна за разпознаване. " "Ако я видиш някъде, ще ми я донесеш, нали?" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" "Нуждая се от голяма, черна гъба. Побързай и ми я донеси, за да мога да " "завърша специалната отвара, върху която работя." #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" "Да, това е достатъчно добро за отварата, което забърквам. Между другото, " "лечителят търси мозък от някакъв демон, за да може да цери онези, поразени " "от ужасната им отрова. Подозирам, че възнамерява да изготви еликсир от него. " "Ако му помогнеш да намери това, което му трябва, моля те, виж дали можеш да " "ми донесеш мостра от еликсира." #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "Защо го носиш на мен? Нямам нужда от този мозък в момента. По-скоро ми " "трябва еликсира, който лечителят приготвя. Дай му отвратителния орган, който " "държиш, и се върни с еликсира. Нищо сложно, като се замислиш, нали?" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "Какво? Чак сега ли ми носиш този еликсир от лечителя? Успях да довърша " "отварата и без него. Защо просто не го задържиш..." #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" "Не разполагам с гъби от какъвто и да е размер или цвят за продажба. Какво ще " "кажеш за нещо по-полезно?" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" "Значи легендата за Картата е истина. Дори аз никога не съм вярвал дотолкова " "в това! Предполагам, че е време да ти разкрия истината кой съм всъщност аз, " "приятелю. Тъй като не съм това, което изглеждам на пръв поглед...\n" "\n" "Истинското ми име е Декард Кайн Старейшината и съм последният потомък на " "древно Братство, което се е посветило на опазването и съхраняването на " "тайните на едно вечно зло. Зло, което очевидно вече е освободено... \n" " \n" "Тъмнината срещу което се изправяш, е мрачния Господар на Ужаса, познат на " "смъртните като Диабло. Именно той е бил затворен в Лабиринта преди много " "векове. Картата, която държиш сега, е създадена преди стотици години, за да " "отбележи времето, когато Диабло ще се издигне отново от затвора си. Когато " "двете звезди на картата съвпаднат, Диабло ще бъде на върха на своята мощ. " "Той ще бъде почти непобедим...\n" " \n" "Сега си в надпревара с времето, приятелю! Намери Диабло и го унищожи, преди " "звездите да съвпаднат, защото може би никога повече няма да имаме възможност " "да отървем света от злото му!" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "Времето ни изтича! Чувствам как тъмната му сила нараства и само ти можеш да " "го спреш, преди да възвърне напълно могъществото си." #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Сигурен съм, че си дал всичко от себе си, но се опасявам, че дори твоята " "сила и воля могат да се окажат недостатъчни. Сега Диабло е на върха на " "земната си мощ и ще се нуждаеш от всичката си смелост и сила, за да го " "победиш. Нека Светлината те закриля и води, приятелю мой. Ще ти помогна с " "каквото мога." #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" "Ако вещицата не може да ти помогне и ти предлага да отидеш при Кайн, защо си " "мислиш, че аз ще знам нещо? Изглежда, че това е много сериозен въпрос. " "Трябва да побързаш и да говориш с разказвача, както предлага Адрия." #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" "Не мога да разбера написаното на тази карта, но може би Адрия или Кайн ще ти " "помогнат да разбереш за какво се отнася. \n" " \n" "Виждам, че това е карта на звездите в нашето небе, но останалото е извън " "моите способности." #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "Най-добрият човек, когото можеш да попиташ за тези неща, е нашия разказвач. " "Кайн е много добре запознат с древните писания, а това е най-старинното " "парче хартия, което някога съм виждала." #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "Никога досега не съм виждал подобна карта. Откъде я взе? Въпреки че нямам " "представа как да я разчета, Кайн или Адрия може би ще успеят да дадат " "отговорите, които търсиш." #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Изслушай ме, приближи се. Не знам дали знаеш това, което аз знам, но ти " "наистина имаш нещо тук. Това е карта." #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" "Опасявам се, че това не вещае нищо добро. Тази карта на звездите предвещава " "голямо бедствие, но тайните ѝ не са мои, за да ги споделям. Дошло е времето " "да проведеш много сериозен разговор с Разказвача..." #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" "Търся една карта, но не е тази. Трябва да я покажеш на Адрия - тя вероятно " "ще ти обясни какво е това. Ще ти кажа едно: изглежда стара, а стара " "обикновено означава ценна." #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" "Моооооля, не наранява. Не убива. Пази живот си и следващ път носи добро на " "теб." #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Аз прави нещо за теб. Отново, не убива Гарбад. Живее и дава добро.\n" " \n" "Ти вземе това за уверение, че държи на дума..." #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "Още нищо! Почти готово. \n" " \n" "Много мощен, много силен. Жив! Жив! \n" " \n" "Не болка и аз обещание спазва!" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Това твърде добро за теб. Много мощен! Ти иска - ти получава!" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "Какво?! Защо правиш? Всички тези досадници могат за да докарат човек до " "лудост. Хайде, вземи това и ме остави да си върша работата. Не ме безпокой " "повече!" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "Ааааррх! Любопитството ти ще те вкара в гроба!!!" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Здравей, приятелю. Остани за малко и слушай..." #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "Навлизайки по-дълбоко в Лабиринта, ще откриеш скрити книги съдържащи велико " "познание. \n" " \n" "Прочети ги внимателно, защото те могат да ти разкрият неща, които дори аз не " "мога." #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Знам много митове и легенди, които могат да съдържат отговори на въпроси, " "възникнали по време на странстването ти в Лабиринта. Ако се сблъскаш с " "предизвикателства и въпроси, за които търсиш знания, потърси ме и аз ще ти " "кажа всичко, каквото мога." #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" "Грисволд - човек на големи дела и голяма смелост. Обзалагам се, че никога не " "ти е разказвал за времето, когато влезе в Лабиринта, за да спаси Върт, нали? " "Той е видял своя дял от опасностите, които го дебнат там, но от друга страна " "- ти също. Той е опитен занаятчия и ако твърди, че може да ви помогне по " "някакъв начин, можете да разчиташ на неговата честност и умения." #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Огдън е собственик и управител на странноприемницата и гостилница " "'Изгряващото слънце' вече почти четири години. Той я купи само няколко " "месеца преди всичко тук да се превърне в Ад. Двамата със съпругата му Гарда " "нямат пари, за да си тръгнат, тъй като са вложили всичко, което са имали, за " "да си създадат живот тук. Той е добър човек с дълбоко чувство за отговорност." #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" "Бедният Фарнам. Той е обезпокоителен спомен за обречената трупа, която се " "отправи в катедралата заедно с Лазар, в онзи мрачен ден. Спаси се на косъм " "от смъртта, но смелостта му и голяма част от разсъдъка му, останаха в някоя " "тъмна яма. Днес той намира утеха само на дъното на бъклицата си, но в " "постоянното му бърборене понякога се крие частица истина." #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" "Вещицата Адрия е аномалия в Тристрам. Тя пристигна малко след като " "катедралата бе осквернена, докато всички останали са бягаха. Построи си една " "малка колиба в края на града, сякаш от нощ за ден и има достъп до много " "странни и загадъчни артефакти и томове със знания, които дори аз не съм " "виждал преди." #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" "Историята на Вирт е ужасяваща и трагична. Той бе изтръгнат от ръцете на " "майка си и завлечен в лабиринта от малки, противни демони, носещи порочни " "копия. В онзи ден бяха отвлечени и много други деца, включително и синът на " "крал Леорик. Придворните рицарите се впуснаха надолу, но никога не се " "завърнаха. Ковачът намери момчето, но само след като мръсните зверове били " "започнали да го изтезават за свое садистично удоволствие." #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "Ах, Пепин. Считам го за истински приятел, може би най-близкият, който имам " "тук. На моменти е малко отвеян, но никога не е съществувала по-грижовна и " "внимателна душа. Малцина могат да се похвалят с неговите знания и умения, а " "вратата му е винаги отворена." #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" "Джилиан е прекрасна девойка. Тачена много заради доброто си настроение и " "бързия си смях, с което заема специално място в сърцето ми. Тя остана в " "гостилницата, за да се грижи за възрастната си баба, която е твърде болнава, " "за да пътува. Понякога се страхувам за нейната безопасност, но знам, че " "всеки мъж в селото би предпочел да умре, отколкото да я види наранена." #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "" "Привет, добри господарю. Добре дошъл в Гостилницата на Изгряващото Слънце!" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" "Много приключенци са седели на масите в моята таверна и десет пъти повече " "истории са били разказани на чаша пиво. Единственото нещо, за което някога " "съм ги чувал да се съгласяват, е тази стара поговорка. Може би тя ще ви " "помогне. Можеш да срежеш плътта, но трябва да строшиш костта." #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" "Ковачът Грисволд е изключително компетентен по отношение на оръжията и " "доспехите. Ако някога имаш нужда от работа по снаряжението ти, определено " "трябва да се обърнеш към него." #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Фарнам прекарва твърде много време тук, давейки мъката си в евтино пиво. Бих " "го накарал да си тръгне, но той е страдал неимоверно много докато е бил в " "Лабиринта." #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Адрия е изключително мъдра за годините си, и трябва да призная, че това " "малко ме плаши. \n" " \n" "Е, няма значение. Ако някога ти се наложи да търгуваш с магически предмети, " "тя държи една необичайно добре снабдена колиба точно от другата страна на " "реката." #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Ако искаш да научиш повече за историята на нашия град, разказвачът Кайн знае " "доста истории от миналото." #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Върт е нехранимайко и малък мошеник. Винаги се забърква в неприятности и не " "е изненада какво му се случили. \n" " \n" "Вероятно е ходил да се шляе някъде, където не е трябвало да бъде. Съжалявам " "за момчето, но не понасям кръгът от приятели, които държи." #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Пепин е добър човек - и със сигурност най-щедрият в града ни. Винаги се " "грижи за нуждите на другите, но където и да отиде, го преследват " "неприятности от един или друг вид..." #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" "Джилиан, моята ханджийка? Ако не беше чувството ѝ за дълг към баба ѝ , тя " "отдавна да бе избягала оттук. \n" " \n" "Бог знае, че я помолих да си тръгне, като й казах, че ще се грижа за " "старицата, но тя е твърде мила и грижовна, за да го направи." #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "Какво те мъчи, приятелю?" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" "Достигнах до много интересно откритие. За разлика от нас, съществата в " "Лабиринта могат да се лекуват без помощта на отвари или магии. Ако нараниш " "някое от чудовищата, увери се, че то е мъртво, защото в противен случай може " "да се възстанови." #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Преди да бъде завзета от, онова, което дебне долу, Катедралата бе дом на " "велико познание. Там лежат изгубени множество книги. Откриеш ли ги, трябва " "да ги прочетеш всичките, без значение на коя попаднеш, защото някои от тях " "може би пазят тайните на Лабиринта." #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" "Грисволд знае толкова за изкуството на войната, колкото аз за изкуството на " "лечението. Той е опитен търговец, но работата му като ковач, е ненадмината. " "Предполагам, това се дължи на факта, че е единственият занаятчия, който " "остана." #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" "Кайн - истински приятел и велик мъдрец. Притежава огромна библиотека и има " "вродената способност да разчита естеството на много неща. Ако имаш някакви " "питания, той е точният човек, към когото да се обърнеш." #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Дори моите умения не са достатъчни да излекуват напълно Фарнам. Да, поне " "успях да възстановя физическото тяло, но разумът и духът са отвъд " "възможностите ми." #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" "Макар да използвам незначителни форми на магия, за да правя отварите и " "еликсирите, които продавам тук, Адрия е истинска магьосница. Никога не спи и " "винаги разполага с най-различни тайнствени четива и артефакти. Определено " "колибата, в която живее, е много повече от това, което се вижда, но никога " "не мога да се добера вътре." #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Горкият Върт. Направих каквото можах за това дете, но независимо от това, " "знам, че ненавижда дървеният кол, който се наложи да прикрепя към крака му. " "Имаше страшни рани. Никой, особено младеж, не трябва да преживява това, на " "каквото той е бил подложен." #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" "Наистина не мога да си го обясня, защо Огдън решил да остане тук, в " "Тристрам. Въпреки, че страда от леки нервни разстройства, той е умен и " "трудолюбив човек, който би преуспял навсякъде. Предполагам, многото убийства " "в околностите или, най-вероятно са причината, за желанието на съпругата му " "държи него и семейството му тук." #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "Помощницата на Огдън, е хубаво момиче. Баба й е болна и страда от деменция.\n" "\n" "Тя твърди, че имала провидения - няма как да знам дали това е вярно или не." #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "Добър ден! С какво мога да бъда полезна?" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Баба ми сънува, че двамата с теб ще се срещнем и ще говорим. Тя има видения " "и разчита бъдещето, ако не знаеш." #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "Жената в края на града, е вещица! Изглежда добронамерена и името ѝ, Адрия, " "звучи доста звънко, но тя много ме плаши.\n" "\n" "Може би трябва някой смелчага, като теб, да отиде да види какви ги върши." #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" "Нашият ковач е гордост за жителите на Тристрам. Не само майстор-занаятчия, " "който има участия в множество конкурси в своята гилдия, но и носител на " "благословията на Крал Леорик - мир на праха му. Грисволд също е велик герой, " "само попитай Кайн..." #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Кайн е разказвачът на истории на Тристрам откакто се помня. Знае толкова " "много, може да ти каже за всеки и за всичко." #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" "Фарнам е пияница, пълни своят търбух с пиво, а ушите на останалите с " "небивалиците.\n" "\n" "Знам, че Пепин и Огдън го подкрепят, но това да го гледам как изпада и " "изпада във вцепенение, всяка нощ, ме обезсърчава." #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" "Пепин спаси живота на баба ми, никога няма да мога да му се отблагодаря " "достатъчно за това. Дарбата му да изцерява всяка болест е много по-могъща от " "най-силния меч и много по-мистериозна от което и да е заклинание, за което " "се сетиш. Ако някога си в нужда от лечител, Пепин ще ти помогне." #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" "Израснах заедно с майката на Върт, Канис. Макар да не беше сериозно ранена, " "когато тези безбожни чудовища го отвлякоха, тя никога успя да се възстанови. " "Умря съкрушена от случилото се. Върт отрасна като негодник, възползвайки се " "от труда на останалите. Знам, че е изстрадал много и е видял такива ужаси, " "каквито дори не мога да си представя, обаче част от това тъмно минало все " "още го преследва." #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Огдън и жена му бяха така добри да ни приемат с баба ми в дома си, дори ми " "позволиха да припечелвам някой грош в странноприемницата. Длъжница съм им и " "искрено се надявам някой ден, когато си тръгна оттук, да им помогна да " "отворят голям хотел на изток." #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "Е, с какво мога да помогна?" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" "Ако си търсиш сгодно оръжие, нека ти предложа нещо. Да речем, най-обикновено " "тъпо оръжие, като например боздуган. Върши чудесна работа срещу повечето " "страшилища, които ще ти се изпречат на пътя. Какво по-добро от това да " "строшиш грозни малки скелетчета на парчета!" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" "Брадвата? Да, добро оръжие, подходящо срещу всякакви противници. Погледни " "как разсича въздуха, сега си представи как острието минава през дебелата " "глава на демона. Имай в предвид, че е замахът е бавен, но за разлика от " "това, последващият ефект е поразителен!" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Погледни острието, какъв баланс. Меч в правилните ръце и срещу правилния " "враг, е господар на всички оръжия. Острият връх няма какво толкова да " "разсече или прониже в немъртвите, но пък срещу живия, дишащ противник, мечът " "ще го разфасова!" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "Оръжията и доспехите ти ще търпят щети в борбата ти срещу Мрака. Донеси ми " "ги и с чука и малко работа над огнището, мога да ги направя като нови." #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" "Докато практически нямам друг избор, освен да си набавям материали и " "инструменти чрез керваните, прекосяващи нашият проклет град от длъж и шир, " "тази вещица, Адрия, винаги получава всичко, от което има нуждае. Ако знаех " "дори и малко за това, как да използвам магията, както тя го прави, щях да " "правя наистина невероятни неща." #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Джилиън е мила девойка. Жалко, че нейното бабе е с лошо здраве, иначе досега " "да съм уредил да ги измъкнат от тук с един от търговските кервани." #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" "Понякога ми се струва, че Кайн говори твърде много. Може би защото това е " "неговото призвание. Ако можех да гъна стомана така, както той може да гъне " "думите, бих изковал ризница, като за Император!" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Бях редом с Фарнам онази нощ, когато Лазар ни поведе към Лабиринта. Никога " "повече не видях Архиепископа, а и нямаше да съм жив сега, ако Фарнам не беше " "до мен. Лошото е, че след случилото се, душата му остана осакатена завинаги, " "така както и кракът ми. Не мога да водя тази битка заради него сега, а ми се " "иска да можех." #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Добър човек, който слага нуждите на другите пред своите. Няма да намериш " "нито в Тристрам, нито където и да е другаде, някой да каже лоша дума за " "лечителя." #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" "Този калпазанин ще се забърка в сериозни неприятности... отново, ако трябва " "да съм точен. Опитах да привлека вниманието му, да го накарам да работи тук " "и да вземе нещо от мен за честния търговски занаят, той обаче предпочете " "парите от търговията на стока със неизвестен произход. Не мога да го " "обвинявам в нищо, след това, което го сполетя и поне се надявам, да внимава." #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" "Търговията на Кръчмарят не върви, и не може да спечели достатъчно от нея. " "Успява да свърже двата края, като предлага храна и подслон на онези, които " "от време на време се появяват в градчето, но много от тях се измъкват през " "нощта и често без да не му платят. Нямаше ли ги запасите от зърно и сушените " "меса, които пази в мазето си, болшинството от нас щяхме да гладуваме през " "първата година, когато цялата околия беше превзета от демоните." #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "Не може ли човек да пие на спокойствие?" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" "Девойчето, което сервира напитките? А, да, каква красавица. И е много мила, " "също." #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "Защо тази дърта кукумявка не се заеме да прави нещо за разнообразие. Да, да, " "ясно ми е, че има всичко, но, слушай какво ще ти кажа... има нещо " "необикновено в нея. Никога не съм я виждал да яде или да пие, а не можеш да " "се има вяра на някой, който не близва дори капка." #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Кайн не е този, който твърди, че е. Добър разказвач е, безспорно... някои от " "историите са страшни, други смешни.... Мен ако питаш, знае много повече от " "това, което твърди, че знае." #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Грисволд? Добрият стар Грисволд. Обичам го като свой брат! Бихме се рамо до " "рамо навремето, когато.... двамата.... Лазаре... Лазаре... Лазаре!!!" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" "Хи-хи-хи, харесвам Пепин. Той наистина се старае. Слушай, трябва да го " "опознаеш по-отблизо. Добър човечец, който винаги е при онези, които се " "нуждаят от помощ. Ей, мисля си, че е като теб, герой, а? И аз някога бях " "герой..." #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "Върт е хлапе с повече проблеми, дори и от мен, и аз знам доста за това. " "Слушай, на това момче сега му върви добре, но то беше там, нали разбираш? " "Загуби си крака! Сега ходи на дървен кол. Колко жалко, колко жалко.." #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Огдън - най-свестният мъж в града. Не мисля, че жена му ме харесва особено " "много, но докато тя отваря бъчвите, нямам нищо против това. Напоследък " "прекарвам повече време с Огдън, отколкото с останалите, но поне той ме " "оценява...." #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" "Искам да ти кажа нешщо, 'щото аз ги зная тия работи. То ми е в кръвта. Т'ва " "тук е най-доброто.... нааааааай-доброто! Другото пиво вече не струва откак " "тия нехранимайковци...." #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" "Никой вече не ме слу... (хлъц) не ме слуша. Нейде, и аз не знам точно къде, " "но там долу, под църквата, лежи голямо имане. Блещука, сияе, само чака някой " "да го намери." #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" "Знам, имаш свои виждания, и още по-добре знам, че няма да повярваш, но това " "оръжие, твоето - няма да те спаси от тези огромни гадове! Не ме интересува " "какво твърди Грисволд, вече не правят нищо така, както беше навремето." #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" "Ако бях теб.... а не съм.... но ако бях, щях да продам всичко и да се махна " "оттук. Онова момче там... то винаги има добро предложение, но за да ти го " "покаже, трябва първо да му платиш." #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "Усещам душа, в търсене на отговори..." #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "Мъдростта се придобива, не се дава. Ако откриеш книга със знанията, погълни " "думите й. Ако вече имаш знанията за мистериозните тайни, описани в книга, " "запомни - обсегът на тази материя винаги може да се задълбочи." #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" "Най-великата сила често е най-краткотрайната. Може да откриеш древни думи на " "могъществото, запечатани в свитъци от пергамент. Силата на тези свитъци се " "крие във възможността всеки начинаещ или напреднал, да ги използва с еднакъв " "резултат. Докато слабостта им е там, че трябва да бъдат прочитани на висок " "глас и никога наизустявани. Също така, знай, че могат да бъдат прочитани " "само веднъж, използвай ги разумно." #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" "Макар слънчевата топлина да е неизмерима, пламъци на една свещ представляват " "много по-голямата опасност. Няма енергия, независимо колкото велика да е тя, " "която може да бъде ползвана, без подходящото съсредоточение. За много от " "заклинанията, омагьосан Жезъл може многократно да бъде наситен със заряди от " "магическа енергия. Имам способността да възстановя тяхната сила, но нищо не " "идва без своята цена." #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "Сборът от нашите знания е сборът от хората които го имат. Намериш ли книга " "или свитък, който е непознат за теб, не се притеснявай да го донесеш на мен. " "Успея ли да го разчета, ще споделя каквото знам." #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "За човекът, който познава единствено Желязото, няма по-голяма магия от " "Стоманата. Ковачът Грисволд е магьосник, повече отколкото той може да " "осъзнае. Уменията му да топи огън и метал нямат равни по тези земи." #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "Покварата борави със силата на измамата, а невинността надделява със силата " "на непорочността. Младата девойка, Джилиън, има непокорно сърце, което " "поставя на преден план нуждите на баба ѝ пред нейните собствени. Тя се бои " "от мен, но това е така, защото не ме разбира." #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" "Отворен в мрака сандък не ще даде по-голямо съкровище, ако бъде отворен на " "светло. Разказвачът Кайн е загадка, но само за онези, които не внимават. " "Знанията му относно онова, което се намира под катедралата, са много по-" "големи от това, което той си позволява да осъзнае." #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" "На колкото по-високо се намира вярата ти в един човек, от толкова по-високо " "ще и трябва, за да падне. Фарнам загуби душата си, но не от демон. Загуби я, " "когато видя как архиепископ Лазар предаде своите съграждани. Той разполага с " "знания, от които може да черпиш, но трябва да разделиш фактите от нереалното." #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" "Ръката, сърцето и разумът могат да постигнат чудеса, когато се намират в " "пълна хармония. Лечителят Пепин вижда в тялото така, както аз не мога. Тази " "негова способност да вдига на крака болните и ранените се усилват от " "разбиранията му за елексирите и отварите. Той е толкова полезен съюзник в " "Тристрам, като никой друг." #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" "Има толкова много неща в бъдещето, които не можем да предвидим, но когато " "времето настане, децата са онези, които ще ги поемат. Момчето, Върт, има " "черно петно надвиснало над душата, но той не представлява опасност за града " "и неговите жители. Тайните му пазарлъци с хлапаците и неспоменавани гилдии " "от околните градове, му осигуряват достъп до много средства, които не могат " "да бъдат намерени лесно в Тристрам. Но макар неговите методи да са позорни, " "Върт може да ти помогне в битката срещу нахлуващия Мрак." #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" "Глинените стени и сламените навеси не градят дом. Кръчмарят, Огдън, " "допринася много повече в този град, отколкото мнозина осъзнават. Осигури " "подслон на Джилиън и баба ѝ, поддържа Фарнам на крака доколкото може и служи " "за крайъгълен камък напомнящ на всеки, останал в това градче, какво беше " "някога беше Тристрам. Неговата гостилница и обикновените забавления, които " "се предлагат там, връщат за момент глъчката от живота, който жителите " "помнят. Точно този спомен продължава да крепи надеждите им за твоя успех." #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "Пссст... насам..." #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "Не всеки в Тристрам има приложение или пазар за всичко, на което ще се " "натъкнеш в лабиринта. Нито аз, колкото и трудно да е за вярване.\n" "\n" "Понякога, единствено ти самия можеш да намериш предназначение за нещата, " "които откриеш." #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "Не вярвай на всичко, което идва от устата на пияницата. Прекалено много пиво " "е замъглило зрението и разума му." #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" "В случай, че не си забелязал, не купувам нищо от Тристрам. Внасям само " "високо-качествени стоки. Ако имаш боклуци за продан, иди при Грисволд, Пепин " "или оная вещерица, Адрия. Със сигурност ще вземат каквото и да им занесеш." #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Дължа живота си на ковача, колкото и да не ми се ще - или поне това, което е " "останало от него. Да, Грисволд е достатъчно свестен и ми предложи да бъда " "чирак в ковачницата. Но парите няма да ми стигат, за... нека кажем, че имам " "сериозни планове, които изискват доста злато." #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Ако бях малко по-възрастен, щях да я обсипя с всички богатства, които мога " "да събера и повярвай ми, мога да се сдобия с доста хубави неща. Джилиън е " "красавица, но трябва да се махне от Тристрам, възможно най-скоро. Хммм... " "може би... може би ще я взема със себе си, когато тръгна...." #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" "Кайн знае твърде много. Плаши ме до смърт, много повече от онази жена " "оттатък реката. Той продължава да ми натяква какъв късметлия съм, че съм жив " "и как историята ми е предсказана в легенда. Защо ли си мисля, че не е наред " "с главата." #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Фарнам - виж, това вече е човек със сериозни проблем, а на мен ми е пределно " "ясно колко сериозни могат да бъдат някои проблеми. Довери се твърде много на " "почтеността на един определен човек, а той го отведе право в пастта на змея. " "О, доста добре знам как е там долу, така че не ми идвай с тези приказки, че " "искаш да унищожиш злото, тегнещо над Лабиринта. Пази си краката..." #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Поне докато не се налага да съединяват крайници, старият Пепин ще ти свърши " "добра работа.\n" "\n" "Само да имах от онези отвари, които той приготвя, сега можеше и да ходя " "нормално." #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Адрия ме кара да настръхвам. Да, Кайн е странен, заради това, което може да " "ти наприказва за миналото, но Вещицата може да разчете твоето лично минало. " "Тя също винаги намира начини да се сдобие с онова, което и е необходимо. " "През ръцете й минава повече стока от всичкото, което съм виждал да минава " "през портите на Кралския пазар по времето на Големия панаир." #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" "Огдън е глупак, задето остана тук. Мога да го изкарам от града срещу скромна " "сума, но той продължава да опитва да печели от проклетата кръчма. Поне " "прибра Джилиън и ѝ даде работа, а жена му, Гарда, прави невероятен Овчарски " "пай..." #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" "Отвъд Залата на Героите се намира Камерата на Костите. Нетленна смърт грози " "всеки, който се осмели да открадне съкровищата, съхранени в това място. Така " "каза Господарят на Ужаса и така бе написано." #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" "...и тъй, заключена зад Портата на Кръвта и скрита отвъд Залата на Огъня, " "Доблестта очаква Героят на Светлината, за да бъде пробудена..." #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Аз виждам неща които ти убягват. \n" "Мътни образи гниещи очи замазват. \n" "Обърнеш ли се, вече чезнат. \n" "В неведение скрита песен шепнат. \n" "Изведнъж наблюдаваш ти поврат,\n" "Где бе светлина, тъдява сенки бродят.\n" "Далеч от разума, далеч в тъмите, \n" "Безнадежден в Залите на Слепите." #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Оръжейните на Ада са дом на Кървавия Пълководец. Зад него лежат разчленените " "трупове на хиляди. Ангели и хората са посечени, за да задоволят неговите " "нестихващи жертвоприношения за Мрачните, които стенат за едно нещо - кръв." #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" "Бъди бдителен и обследвай истините, които лежат тук, защото те са последното " "наследство на Хорадрим. Има война бушуваща дори в този час, отвъд земите " "които познаваме - между утопичните царства на Високите небеса и хаотичните " "бездни на Горящия ад. Тази война, известна като Великия сблъсък е вилняла и " "горяла по-дълго от която и да е от звездите в небето. Нито една от страните " "не взема надмощие, докато силите на Светлината и Мрака постоянно воюват за " "контрол над всичко живо." #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" "Бъди бдителен и обследвай истините, които лежат тук, защото те са последното " "наследство на Хорадрим. Когато Вечният конфликт между Високите небеса и " "Горящия ад, докосне земите на смъртните, се назовава Войната на Греха. " "Ангели и Демони странстват между хората в прикритие, сражавайки се потайно, " "далеч от зорките погледи на простосмъртните. Някой дръзки и могъщи смъртни, " "са влизали в съюз с една от двете страни, диктувайки хода на Войната на " "Греха." #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Бъди бдителен и обследвай истините, които лежат тук, защото те са последното " "наследство на Хорадрим. Преди приблизително три столетия, стана известно, че " "тайнствените Трите Върховни Злини на Горящия ад, са дошли в нашия свят. В " "продължение на десетилетия, Тримата братя опустошат земите на изток, докато " "човечеството е оставено да тъне в разруха. Нашият орден, Хорадрим, бе " "основан от група магьосници, с цел да открие и залови Трите Върховни Злини " "веднъж и завинаги.\n" "\n" "Първите Хорадрим запечатаха двама от тях в могъщи артефакти, наречени Камъни " "на душата и ги заровиха дълбоко под самотните източни пясъци. Третото зло " "успява да избяга от плен и поема на запад, преследван от множество Хорадрим. " "Трето и последно зло - наричано още Диабло, Господарят на Ужаса, в " "последствие бива заловен и запечатан в Камък на душата и заровен в този " "Лабиринт.\n" "\n" "Знай, че този Камък не трябва да бъде откриван от тези, които не са " "посветени на ордена. Бидейки освободен, Диабло ще опита да се всели в тяло, " "което лесно може да бъде контролирано, тъй като ще е много отслабен, " "например това на старец или на дете." #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "И така стана, че в Горящия ад започна велика революция - нарекоха я Тъмното " "изгнание. Малките злини свалиха от власт Трите Върховни Злини и запратиха " "техните душевни облици в Царството на смъртните. Демоните Белиал - " "Господарят на Измамата и Азмодан - Господарят на Греха, се сражаваха за " "власт над Ада, докато Тримата братя са заточени. Адът бива разделен между " "лагерите на Белиал и на Азмодан, в момент, в който Високите небеса " "продължително удряха върху Портите на Ада." #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Много демони се отправиха към Царството на смъртните, за да търсят Тримата " "братя, последвани от Ангелите в света на смъртните, които ги преследваха " "през обширните градове на Изток. Ангелите се съюзиха с потайния Орден на " "смъртните магьосници на име Хорадрим, които бързо станаха изкусни в лова на " "демони. Така те си създадоха множество врагове в подземните светове." #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" "И така стана, че душите на Трите Върховни злини бяха заточени в Царството на " "смъртните, всявайки хаос десетилетия наред сред земите на Изтока, те биват " "заловени от прокълнатия Орден на смъртните Хорадрим. Хорадрим използваха " "артефакти, наречени Камъни на Душата, за да съхранят същността на Мефисто - " "Господарят на Омразата и неговия брат, Баал - Господарят на Унищожението. " "Най-младият брат, Диабло, Господарят на Ужаса, бяга на запад.\n" "\n" "Рано или късно, Хорадрим залавят Диабло в Камък на душата. и го заравят под " "древната, забравена Катедрала. Там, Господарят на Ужаса спи и изчаква " "моментът на своето възраждане. Знай, че той ще потърси младо тяло носещо " "власт, такова, което лесно може да обладае - невинно и лесно контролируемо. " "Тогава той ще се надигне, за да освободи братята си и отново ще наклади " "огньовете на Войната на греховете." #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" "Все хвала за Диабло - Господар на Ужаса и Оцелял от Мрачното изгнание. " "Когато той се пробуди от дълбокия си сън, моят Господар и Учител, ми разкри " "тайни, които малцина смъртни знаят. Той ми описа как кралствата на Високите " "небеса и бездните на Горящия ад, водят вечна война. Показа ми силите, които " "са довели този раздор в света на човека. Моят Господар назова конфликта на " "този свят и всички останали, които съществуват, Войната на Греха." #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" "Слава и признание за Диабло - Господар на Ужаса и Водач на Тримата. Моят " "Господар ми разказа за неговите двама братя, Мефисто и Баал, които били " "запратени на този свят преди векове. Той желае да отдаде времето си да " "възстанови своята невероятната сила, за да освободи пленените си братя от " "техните гробници, дълбоко под пясъците на изток. И когато Господарят разбие " "оковите им, Войната на Греха отново ще усети яростта на Тримата." #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" "Приветствия и жертвувания за Диабло - Господар на Ужаса и Унищожител на " "Души. Когато пробудих моя Господар от съня, той се опита да приеме образа на " "смъртен. Диабло опита да завземе тялото на Крал Леорик, но моят Господар " "нямаше сили, задето беше държан в плен. Моят Повелител се нуждаеше от проста " "и невинна котва на този свят и той намери малкия Албрихт да бъде най-" "подходящият за задачата. Докато, в това време, добрият Крал Леорик изпадаше " "в лудост заради неуспешното вселяване на Диабло, аз отвлякох сина му, " "Албрихт и го отведох при моя Господар. Сега, очаквам Диабло да вземе решение " "и се надявам, че усилията ми ще бъдат възнаградени, когато той бъде " "провъзгласен за Владетел на този свят." #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "Слава богу, че се завърна!\n" "Доста неща се промениха откакто си замина, приятелю. Цареше мир, докато не " "минаха Мрачни ездачи и опустошиха градчето ни. Мнозина завариха смъртта си " "на място, а онези, осмелили се да вдигнат оръжие, бяха жестоко разсечени, " "взети за роби или ги застигна нещо още по-лошото. Църквата в края на града " "бе осквернена и ползвана за мрачни ритуали. Писъците, които се чуват през " "нощта оттам, са нечовешки, но някои от съжителите ни може и да е оцелял. " "Следвай пътеката, минаваща между моята гостилница и ковачницата, за да " "стигнеш до църквата и спасиш когото можеш.\n" "\n" "Може би ще можем да си поговорим по-обширно някой друг път. Успех." #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" "Не прекратявай твоето търсене. Да намериш изгубено съкровище, не е никак " "лесно. Но да намериш съкровище, което е скрито, е още по-трудно. Ще ти кажа " "следното. Не позволявай на пясъците на времето да попречат на твоето търсене." #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" "Какво?! Що за глупост!? Тук, в Тристрам, няма заровено съкровище. Нека да " "погледна! А, виж, тези очертания са неверни. Изобщо не съвпадат с тези " "нашето градче. Аз бих се съсредоточил над това, което се намира под " "катедралата, не върху това, което е под тревата." #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" "Нямам време да обсъждам някаква карта, която търсиш. Има доста болни хора, " "които се нуждаят от моята и от твоята помощ." #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" "Някогащният горд Исвол, сега е окован дълбоко под повърхността на този свят, " "лишен от честта си и с изменена същност. Затворен е в капана на безсмъртни " "мъчения, задължен да скрие единственото нещо, което може да го освободи." #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" "Готов съм да се обзаложа, че когато Върт те е видял да идваш, и ти е скроил " "някакъв номер, така че да може да ти се присмива когато бягаш из града с " "нос, заврян в калта. Не му обръщай внимание." #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" "Навремето, този град беше честа спирка за пътешествениците от всички краища " "на земята. Много неща се промениха оттогава. Но тайните пещери и заровените " "съкровища са винаги в главите на децата. Върт рядко взема участие в " "юношеските игри. Така че може просто да си въобразява." #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Слушай. Ела по близо. Не знам дали знаеш, каквото аз знам, но действително " "носиш нещо интересно. Това е карта." #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Моята баба често ми разказва истории за някаква странна сила, обитаваща " "пределите на гробището до църквата. Може би ще ти е интересно да чуеш една " "от тях. Тя ми прошепна, че ако оставиш подходящия дар в гробището, влезеш в " "катедралата, за да се помолиш за душите на мъртвите и се върнеш обратно, " "оставеният предмет щял да придобие друг вид. Не знам дали това е просто " "измислица в главата на една болна старица, но пък, всичко изглежда възможно " "тези дни." #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" "Хммммм. Огромно и тайнствено съкровище. М-м-м. Може и да се заинтересовам да " "купя някои неща от теб. Или всъщност, не ти ли трябват редки и ценни " "материали, които да те преведат през това изпитание?" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "А, значи ти си героят, за когото всички говорят. Навярно можеш да помогнеш " "на един беден, прост фермер, да се спаси от беда? На юг от тук, в края на " "овощната ми градина, има някакво ужасно подуване, излизащо от земята и " "препречва пътя. Не мога да стигна до моя посев или до балите сено, кравите " "ми нямат какво да ядат. Вещицата ми даде това и ми каза, че ще гръмне това " "нещо далеч от нивата. Ако можеш да го унищожиш, ще ти бъда благодарен до " "живот. Не искам да оставям кравите без надзор, иначе бих го направил сам." #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Предусещах, че няма да е толкова лесно, колкото вещицата го изкара, че ще е. " "Жалко място е светът, когато не можеш да се довериш на съседите си." #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "Няма го? Изпрати ли го обратно във владенията на Хадес, откъдето се е " "пръкнало? Е? Само не ми казвай, че го загуби! Тези неща струват скъпо, дано " "разбираш. Трябва да го намериш и да гръмнеш този ужас извън града ни." #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" "Чух тътена от тук. Много ти благодаря, драги ми страннико. Не знам какво " "става, нито откъде се взеха тези неща, излизащи от земята, чудовищата " "завладяващи църквата и всичко останало. Живеем в тежки времена. Аз съм беден " "фермер, но ето, това е за теб, моят начин да се отблагодаря." #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" "О, каква беда си навлякох... може би... Не, не мога да те забърквам в това, " "при всичките други проблеми. Може би, после, след като поразчистиш църквата " "от тези същества, можеш да се върнеш... и да отделиш малко време, за да " "помогнеш на един беден фермер?" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Аа-а-ааа! (подсмърча) Ааа-аа-а!(подсмърча)" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" "Загубих Тео! Загубих най-добрия си приятел! Играехме си при реката, Тео " "искаше да отиде да види какво е онова голямо зелено нещо. Казах му, че не " "трябва да го прави, но... отидохме до него и изведнъж тази голяма буболечка " "се показа! Побягнахме, но Тео се спъна и тя го отвлече!" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" "Намери ли го? Трябва да намериш Теодор, моля те! Той е толкова малък. Не " "знае да се грижи за себе си! Умолявам те!" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "Намерил си го! Намерил си го! Благодаря ти! О, Тео, тези гадни буболечки, " "изплашиха ли те? Ей! Ъъъ! Нещо се е лепнало за козината ти! Блах! Хайде, " "Тео, да се връщаме у дома! Благодаря ти отново, геройски персонаж!" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Ние бяхме в дълга летаргия и настана време да се събудим. А след толкова " "много сън, изпитваме велик глад. Скоро, ще се нахраним..." #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "Забавлявал ли си се досега, малки бозайнико? Колко жалък си. Твоят малък " "свят изобщо няма да бъде предизвикателство." #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Тези земи ще бъдат осквернени и нашето челяд ще завземе полетата, които " "хората наричат домове. Нашите пипала ще хванат в прегръдка този свят и ние " "ще се храним от плътта на неговите обитатели. Човекът ще бъде наш скот и " "препитание." #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Надушвам те... на близо си! Близо! Ссссс... мирисът на кръв и страх... колко " "съблазнително..." #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "И в годината на Златната Светлина, било речено да бъде издигната велика " "Катедрала. Основоположният камък на това свето място трябвало да бъде " "издялан от прозрачния камък Антираел, кръстен в чест на Ангела, споделил " "своята сила с Хорадрим.\n" "\n" "В годината на Застилащите сенки, земята се потресе и Катедралата се напука и " "рухна. Когато започнал строежът на катакомби и замъци започна наново и " "човека се изправи пред разрухата на Войната на Греха, руините бяха преровени " "за камъни. Така, крайъгълният камък изчезна от погледа на човека.\n" "\n" "Камъкът принадлежи на този свят и на всички други светове, също както " "Светлината, е вътре и отвъд всичко. Светлината и единството са плодовете от " "тази свята основа, единство за намерения и единство за притежание." #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "Муу." #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "Казах: муу." #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "Виж, аз съм крава, Ясно ли е?" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" "Добре де, добре де, Не съм всъщност крава. Обикновено не се разхождам така, " "но както си седя вкъщи вършейки моите си работи, изведнъж всички тези твари, " "пълзящи храсти, луковици и разни работи, започнаха да никнат от пода... " "страшно беше! Ако само имах нещо нормално, което да облека, нямаше да е чак " "толкова зле. Хей! Можеш ли да отидеш у дома и да ми донесеш костюма? " "Кафявият по-точно, не сивия - той е вечерно облекло. Бих го направил сам, " "но не искам никой да ме вижда в този вид. Ето, вземи това, може да ти " "потрябва... за да отстраниш тези неща, избуяли навсякъде. Няма как да " "пропуснеш дома ми, на юг при разклонението на реката... онази с избуялата " "зеленчукова градина..." #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "Какво чакаш още? Отиди да вземеш костюма ми! И побързай! Това Холщайнско " "говедо отсреща продължава да ми намигва!" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Е, взе ли костюма ми? Хайде, дай ми го! Ушите така ме сърбят така, че си " "нямаш и представа!" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" "Не-не-не! Това е СИВИЯТ костюм! Той е вечерното облекло! За специални " "случай! Не мога да облека ТОВА. Ти, да не би да имаш проблеми със слуха? " "Трябва ми КАФЯВИЯ." #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" "Е, това вече е много по-добре. Ааах! Най-накрая, нещо свястно! Я кажи, " "рогата прави ли са? Добре. Виж, много ти благодаря, че ми помогна. Така, " "това е подарък за теб и още нещо... един съвет за външния вид... можеш да " "използваш малко... и нови.... нали разбираш, какво имам предвид? Този " "приключенски стил... твърде старомоден е. Просто съвет, нали? Чао." #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Виж. Аз съм крава. А ти - стръв за зверовете. Първо натрупай опит! И ще " "говорим...." #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Определено трябва да съм ти поставил доста страховита задача. Ако имаше друг " "начин да... Дали бутилка с хубаво прясно мляко, ще ти свърши ли работа?" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "Ооо, Може да възползвам от помощта ти, но трябва да разчистиш катакомбите от " "напастта, която ги обитава." #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Имам една несвършена работа, но така и не мога да попадна на точния човек. " "Може би, ако си прекарал известно време в града, ще ми е по-удобно да те " "попитам." #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "У теб виждам потенциал за величие. Може би, някой път, докато вървиш по пътя " "определен от съдбата, можеш да се отбиеш и да ми направиш една услуга?" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" "Мисля, че можеш да ми помогнеш, но все още си твърде немощен. Не искам да " "пропилея единствения шанс селото да бъде избавено от напастта, върлуваща " "около църквата!" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Аз ? Аз,съм самоиздигната крава. Направи и ти нещо от себе си... тогава ще " "говорим." #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "Не съм длъжен да се представям на всеки турист, който минава оттук! Нямаш ли " "си работа? Може би ще говорим по-късно. Ако все още си жив..." #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" "Престани да ме разиграваш. Търся някой със способностите на герой. А ти не " "си това. Не мога да ти се доверя, имаш вид на някой, който ще го схрускат " "зверовете всеки момент. Трябва ми някой с опит." #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" "Добре, ще карам направо. Не исках да те вкарвам в заблуждения. Та, както си " "седя у дома, в приятно настроение, изведнъж нещата излязоха извън контрол - " "цели пълчища твари заизлизаха от пода! Избягах. Точно в този момент носех " "този пуловер, когато побягнах през вратата и сега, виж ме, изглеждам " "потресаващо. Де да имах нещо нормално, което да облека, нямаше да е лошо. " "Ей! Можеш ли да отидеш вкъщи и да вземеш костюма ми? Имам предвид кафявия, " "не сивия - той е вечерно облекло. Нямам нищо против да отида сам, но не " "искам да ме виждат така. Вземи това, може да ти потрябва.... да си проправиш " "път през избуялите навсякъде гадинки. Къщата не може да подминеш, тя се " "намира на юг от разклонението на реката... Онази с буйната зеленчукова " "градина." #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "Опитах с заклинания, заплахи, прочистване и дори преговори с това покварно " "създание - без успех. Методите ми за заробване на малки демони, не " "подействаха при този страховит звяр." #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" "Домът ми постепенно бива покварен от сквернотата на този нежелан затворник. " "В Криптите витаят множество силуети, на ръба на моя поглед. Плахото дращене " "от ноктите танцува на ръба на слуха ми. Те търсят дневника, мисля, този " "летопис." #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" "В яростта си, създанието издаде своето име -- На-Крул. Опитах да проуча " "името, но по-малките демони някак успяха да унищожат библиотеката ми. На-" "Крул.... Това име ме изпълва със студен страхопочитание. Предпочитам да го " "назовавам като Създанието, отколкото да промълвя истинското му име." #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" "Яростното виене на това оковано същество не ми дават покой. Изглежда, излива " "яростта си по онзи, който го е изпратил в Бездната и ме проклина, задето съм " "оковал тук. Думите му, вкаменяват сърцето ми, и не мога да ги спра да ги " "чувам." #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" "Времето ми бързо изтича. Трябва да запиша методите, чрез които мога да " "омаломощя демона и да скрия записките, в случай, че неговите слуги се опитат " "да използват знанията ми, за да освободят своя господар. Надявам се, че " "онзи, който открие този дневник, ще потърси знанието." #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Онзи, който открие този свитък, е отговорен да спре демоничното създание зад " "тези стени. Моето време свърши. Дори в този момент, неговите дяволски слуги, " "гневно дращят по крехката вратата, зад която се крия.\n" "\n" "Отнех силите на демона чрез мистериозно заклинание и го запечатах между " "велики стени, но се опасявам, че това няма да е достатъчно.\n" "\n" "Заклинанията, описани в моите три гримоарa, ще ти осигурят безопасен достъп " "до неговото владение, но само, ако ги прочетеш в правилния ред. Лостът при " "входа, ще вдигне барикадата и освободят демона - не го докосвай! Използвай " "единствено силата на заклинанията, за да преминеш, инак рискуваш да не можеш " "да го победиш." #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "Praedictum Otium." #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "Адски глиган" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "Бодливец" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "Психорба" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "Арахнон" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "Изпаднал близнак" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "Хоркски Отрок" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "Отровопашка" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "Некроморб" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "Паяк Властелин" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "Шибачервей" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "Факлоплюец" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "Адско насекомо" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "Гробар" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "Гробен плъх" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "Огнен прилеп" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "Черепокрил" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "Лич" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "Демон от Криптата" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "Адски прилеп" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "Костен демон" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "Архи-лич" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "Биклоп" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "Буцаплът" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "Жътвар" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "Великански Юмрук" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "Непредвидим Пръстен" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "Пръстен на Зорийн" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "Пръстен на Карик" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "Пръстен от Магма" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "Пръстен на Тайнствата" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "Пръстен на Гръмотевицата" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "Амулет на Бдението" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "Дразнещо жило" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "Пламокрасен" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "Мрачна Броня" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "Мълнистен" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "Гръмопер" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "Шиторачи" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "Поглъщач на Души" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "Диамантенострие" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "Костна ризница" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "Демонски доспехи" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "Псалтски амулет" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "Гладиаторски пръстен" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "Шегаджийски" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "Прозрачни" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "Двойнишки" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "Опустошението" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "Разпадането" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "Угрозата" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "Мана" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "на Магьосника" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "на Шута" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "Светкавична стена" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "Имолация" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "Пространствен скок" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "Отразяване" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "Обезумялост" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "Пръстен на Огъня" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "Издирване" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "Руна за Огън" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "Руна на Светлината" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "Руна на Нова" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "Руна на Изгарящ погром" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "Руна за Вкаменяване" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "," #~ msgid "Options:" #~ msgstr "Настройки:" #~ msgid "version {:s}" #~ msgstr "версия {:s}" #~ msgid "Decrease Gamma" #~ msgstr "Намаляване Гама" #~ msgid "Increase Gamma" #~ msgstr "Увеличаване Гама" #~ msgid "No automap available in town" #~ msgstr "Авто-картата не е достъпна в града" #~ msgid "Restart In Town" #~ msgstr "Презареждане в града" #~ msgid "Heart" #~ msgstr "Сърце" #~ msgid "recover life" #~ msgstr "възстановява живот" #~ msgid "deadly heal" #~ msgstr "смъртоносно изцеление" #~ msgid "decrease strength" #~ msgstr "намалява сила" #~ msgid "decrease dexterity" #~ msgstr "намалява сръчност" #~ msgid "decrease vitality" #~ msgstr "намалява жизненост" #~ msgid "you can't heal" #~ msgstr "не може да се лекувате" #~ msgid "hit monster doesn't heal" #~ msgstr "поразен враг не може да се лекува" #~ msgid "Faster attack swing" #~ msgstr "по-бърз удар с замах" #~ msgid "see with infravision" #~ msgstr "гледай с инфразрение" #~ msgid "Trying to drop a floor item?" #~ msgstr "Опит за хвърляне на предмет от пода?" #~ msgid "" #~ "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " #~ "frame. Disabling it can help with mouse lag on some systems." #~ msgstr "" #~ "Включва режим на Вертикална синхронизация. Предотвратява разкъсващ ефект " #~ "при изобразяване на всеки кадър. Изключването може да помогне с забавяне " #~ "на курсора на мишката при някои конфигурации." #~ msgid "FPS Limiter" #~ msgstr "Лимит на Кадри" #~ msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." #~ msgstr "" #~ "Броят на кадри е лимитиран за да намали използването на процесора. Лимита " #~ "е свързан с текущата честота на монитора." #~ msgid "To hit" #~ msgstr "" #~ "Шанс\n" #~ " за удар" #~ msgid "Failed to open player archive for writing." #~ msgstr "Неуспешно отваряне на файла на играча за запис." #~ msgid "Unable to read to save file archive" #~ msgstr "Неуспешно прочитане на архив за записен файл" #~ msgid "Unable to write to save file archive" #~ msgstr "Неуспешно записване в архив за записен файл" #~ msgid "Indestructible, " #~ msgstr "Неразрушими, " #~ msgid "No required attributes" #~ msgstr "Няма изисквани атрибути" #~ msgid "" #~ "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits " #~ "any who would seek to steal the treasures secured within this room. So " #~ "speaks the Lord of Terror, and so it is written." #~ msgstr "" #~ "Отвъд Залата на Героите се намира Камерата на Костите. Нетленна смърт " #~ "грози всеки, който се осмели да открадне съкровищата, съхранени в това " #~ "място. Така каза Господарят на Злото и така бе написано." #~ msgid "" #~ "The armories of Hell are home to the Warlord of Blood. In his wake lay " #~ "the mutilated bodies of thousands. Angels and man alike have been cut " #~ "down to fulfill his endless sacrifices to the Dark ones who scream for " #~ "one thing - blood." #~ msgstr "" #~ "Оръжейните на Ада са дом на Кървавия Пълководец. Зад него лежат " #~ "разчленените трупове на хиляди. Ангели и хората са посечени, за да " #~ "задоволят неговите нестихващи жертвоприношения за Мрачните, които стенат " #~ "за едно нещо - кръв." #~ msgid "" #~ "Cloudy and cooler today. Casting the nets of necromancy across the void " #~ "landed two new subspecies of flying horror; a good day's work. Must " #~ "remember to order some more bat guano and black candles from Adria; I'm " #~ "running a bit low." #~ msgstr "" #~ "Днес е облачно и хладно. Докато спусках мрежите на некромантството из " #~ "бездната, хванах два нови видя на летящ ужас. Денят мина добре. Дано не " #~ "забравя да заръчам още гуано и черни свещи на Адрия, защото ми остават " #~ "малко." ================================================ FILE: Translations/cs.po ================================================ # Translation of DevilutionX to Czech # Psojed , 2021. # @Srandista, 2021. # msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:19+0200\n" "PO-Revision-Date: \n" "Last-Translator: Psojed \n" "Language-Team: Psojed (realpsojed@gmail.com)\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Design Hry" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Starší Designéři" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Dodatečný Design" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Hlavní Programátor" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Starší Programátoři" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Programování" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Speciální Hostující Programátoři" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Battle.net Programování" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Programování Sériové Komunikace" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Programování Instalačního Programu" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Vedoucí Grafiky" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Grafika" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Technická Grafika" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Vedoucí filmové grafiky" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D Filmová Grafika" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Technická Filmová Grafika" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Výkonný Producent" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Producent" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Přidružený Producent" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Zásahový Tým" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Hudba" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Zvukový Design" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Filmová Hudba a Zvuk" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Hlasová Produkce, Režie a Casting" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Scénář a Příběh" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Úprava Hlasů" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Hlasy" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Inženýr Nahrávání" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Design a Rozvržení Manuálu" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Grafika Manuálu" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Prozatimní Vedoucí QA (Hlavní Tester)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "QA Útočný Tým (Testeři)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "QA Tým Speciálních Jednotek (Testeři Kompatibility)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "QA Palebná Podpora (Dodateční Testeři) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "QA Kontrarozvědka" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Řád Síťových Informačních Služeb" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Zákaznická Podpora" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Prodej" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Přebytečný" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Zpěváci v pozadí (od Pana Dabiriho)" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Styk s Veřejností" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Marketing" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Mezinárodní Prodej" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "U.S. Prodej" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Výroba" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Právní a Obchodní oddělení" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Speciální Poděkování" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Poděkování" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "K památce" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Velmi Speciální Poděkování" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Generální Manažer" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Softwarový Inženýr" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Vedoucí Grafiky" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Umělci" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Design" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Zvukový Design, SFX a Audio Inženýr" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Vedoucí Kontroly Kvality" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Testeři" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Manuál" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\tDodatečné Práce" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Psaní Textu Úkolů" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Poděkování" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tSpeciální poděkování pro Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Manažer Kontroly Kvality" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Hlavní Tester Kontroly Kvality" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Hlavní Testeři" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Dodateční Testeři" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Manažer Produktového Marketingu" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Manažer Styku s Veřejností" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Přidružený Produktový Manažer" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Kruh Jednoho Tisíce" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tŽádné duše nebyly zaprodány při tvorbě této hry." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "OK" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Vyber si Hrdinu" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Zrušit" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Nový Hrdina Hry pro Více Hráčů" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Nový Hrdina Hry pro Jednoho Hráče" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Existuje Uložená Hra" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Načíst Hru" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Začít Novou Hru" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Postavy Hry pro Jednoho Hráče" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Zlodějka a Čaroděj jsou dostupní pouze v plné verzi hry Diablo.Pro koupi hry " "navštiv https://www.gog.com/game/diablo ." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Zadej Jméno" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Neplatné jméno. Jméno nesmí obsahovat mezery, chráněné znaky a slova.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Nelze vytvořit postavu." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Úroveň:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Síla:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Magie:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Obratnost:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Vitalita:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Uložená hra:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Vyber Hrdinu" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Nový Hrdina" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Smazat" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Postavy Hry pro Více Hráčů" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Smazat Hrdinu Hry pro Více Hráčů" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Smazat Hrdinu Hry pro Jednoho Hráče" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Opravdu chceš smazat postavu \"{:s}\"?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Hra Jednoho Hráče" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Hra Více Hráčů" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Nastavení" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Podpora" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Ukaž Autory" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Ukonči Hellfire" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Ukonči Diablo" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Shareware" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Klient-Server (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 #, fuzzy msgid "Offline" msgstr "Smyčka" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Hra pro Více Hráčů" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Požadavky:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "brána není potřeba" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Vyber typ připojení" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Změň Bránu" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Všechny počítače musí být připojeny k TCP-kompatibilní síti." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Všechny počítače musí být připojeny k internetu." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Hraj sám, bez jakéhokoli připojení do sítě." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Možný Počet Hráčů: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Diablo Shareware" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Hellfire Shareware" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "Host má spuštěnou jinou hru než ty." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "Host má spuštěnou jinou verzi hry ({:s}) než ty." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "Tvoje verze {:s} neodpovídá verzi hosta {:d}.{:d}.{:d}." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Popis:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Vyber Akci" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Vytvoř Hru" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Vytvoř Veřejnou Hru" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Připoj se ke Hře" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Veřejné Hry" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Načítám..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "Žádný" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "ZRUŠIT" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Vytvoří novou hru s obtížností jakou si zvolíš." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "Vytvoří novou veřejnou hru s obtížností jakou si zvolíš." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "Zadej ID Hry a připoj se k probíhající hře." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "Zadej IP nebo název hry a připoj se k probíhající hře." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "Připoj se ke veřejné probíhající hře." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Normal" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Nightmare" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Hell" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Obtížnost: {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Rychlost: Normální" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Rychlost: Rychlá" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Rychlost: Rychlejší" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Rychlost: Nejrychlejší" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Hráči: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Vyber Obtížnost" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Připojit se k {:s} Hrám" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Zadej ID Hry" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Zadej adresu" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Obtížnost Normal\n" "Zde by měla začínající postava započít svoji výpravu k poražení Diabla." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Obtížnost Nightmare\n" "Obyvatelé Labyrintu byli posíleni a budou pro tebe opravdovou zkouškou. " "Doporučeno pouze pro zkušené postavy." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Obtížnost Hell\n" "Nejsilnější stvoření z podsvětí číhají u Pekelných bran. Pouze ty " "nejzkušenější postavy by měly vstoupit do tohoto světa." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Tvoje postava musí dosáhnout 20 úrovně než bude moci vstoupit do hry pro " "více hráčů na obtížnost Nightmare." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Tvoje postava musí dosáhnout 30 úrovně než bude moci vstoupit do hry pro " "více hráčů na obtížnost Hell." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Vyber Rychlost Hry" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Rychlá" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "Rychlejší" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Nejrychlejší" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Běžná\n" "Zde by měla začínající postava započít svoji výpravu k poražení Diabla." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Rychlá\n" "Obyvatelé Labyrintu byli zrychleni a budou pro tebe opravdovou zkouškou. " "Doporučeno pouze pro zkušené postavy." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "Rychlejší\n" "Většina potvor v Labyrintu tě najde rychleji než kdykoli předtím. Pouze " "zkušený šampion by měl zkusit štěstí na této rychlosti." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Nejrychlejší\n" "Pohůnci z podsvětí budou spěchat do útoku bez sebemenšího zaváhání. Pouze " "pravý rychlostní démon by měl hrát s tímto nastavením." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Zadej Heslo" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Otevři Hellfire" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Přepni na Diablo" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Ano" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Ne" #: Source/DiabloUI/settingsmenu.cpp:162 #, fuzzy #| msgid "Press any key to change." msgid "Press gamepad buttons to change." msgstr "Stiskni jakoukoli klávesu pro změnu." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Nastavená klávesa:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Stiskni jakoukoli klávesu pro změnu." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Odeber klávesu" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Předchozí Menu" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Udržujeme chat server na adrese Discord.gg/devilutionx Následuj odkaz a " "přidej se k naší komunitě, kde diskutujeme o věcech kolem Diabla a datadisku " "Hellfire." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX spravuje Diasurgical, problémy a chyby můžete hlásit na této " "adrese: https://github.com/diasurgical/devilutionX Abychom vám mohli lépe " "pomoci, prosím uveďte číslo verze hry, váš operační systém a popište váš " "problém." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Prohlášení:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX není podporován ani spravován společností Blizzard " "Entertainment, ani GOG.com. Blizzard Entertainment ani GOG.com netestovaly " "ani necertifikovaly kvalitu nebo kompatibilitu DevilutionX. Všechny dotazy " "ohledně DevilutionX by měly být směřovány k Diasurgical, nikoliv k Blizzard " "Entertainment nebo GOG.com." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tTento port využívá Charis SIL, New Athena Unicode, Unifont a Noto, které " "jsou licencované pod SIL Open Font License, a také Twitmoji licencované pod " "CC-BY 4.0. Port také využívá SDL, které jsou licencované pod zlib-license. " "Přečti si ReadMe pro více informací." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Copyright © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Chyba" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Chyba nastala v: {:s} řádek {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Chyba Datového Souboru" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Nelze otevřít hlavní datový archiv ({:s}).\n" "\n" "Ujistěte se, že je výše uvedený soubor ve složce se hrou." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Chyba - Adresář Pouze pro Čtení" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Nelze zapisovat do umístění:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Hra: " #: Source/automap.cpp:1424 #, fuzzy msgid "Offline Game" msgstr "Smyčka" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Heslo: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Veřejná Hra" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Patro: Hnízdo {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Patro: Krypta {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Městský" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Patro: {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Tab" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Zadej Jméno" #: Source/control.cpp:206 msgid "Character Information" msgstr "Informace o Postavě" #: Source/control.cpp:207 msgid "Quests log" msgstr "Deník úkolů" #: Source/control.cpp:208 msgid "Automap" msgstr "Automapa" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Hlavní Menu" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Inventář" #: Source/control.cpp:211 msgid "Spell book" msgstr "Kniha kouzel" #: Source/control.cpp:212 msgid "Send Message" msgstr "Odešli Zprávu" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr "" #: Source/control.cpp:633 Source/control.cpp:634 #, fuzzy #| msgid "Description:" msgid "Description: " msgstr "Popis:" #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" #: Source/control.cpp:634 #, fuzzy #| msgid "Players: " msgid "" "\n" "Parameters: " msgstr "Hráči: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "" #: Source/control.cpp:653 #, fuzzy #| msgid "Are you sure you want to buy this item?" msgid "What arena do you want to visit?" msgstr "Opravdu chceš koupit tento předmět?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "" #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "" #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "" #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "" #: Source/control.cpp:800 msgid "[command]" msgstr "" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "" #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "" #: Source/control.cpp:802 #, fuzzy #| msgid "Amber" msgid "" msgstr "Jantarový" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "" #: Source/control.cpp:803 #, fuzzy #| msgctxt "monster" #| msgid "Flayed One" msgid "" msgstr "Stažený z Kůže" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "" #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Neútočit na hráče" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Útočit na hráče" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Klávesová Zkratka: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Tlačítko pro výběr kouzla" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Klávesová Zkratka: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "Dovednost {:s}" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "Kouzlo {:s}" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Kouzlo Úrovně 0 - Nefunguje" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Kouzlo Úrovně {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "Svitek {:s}" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Svitek" msgstr[1] "{:d} Svitky" msgstr[2] "{:d} Svitků" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "Hůl - {:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Použití" msgstr[1] "{:d} Použití" msgstr[2] "{:d} Použití" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} zlaťák" msgstr[1] "{:s} zlaťáky" msgstr[2] "{:s} zlaťáků" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Nesplňuješ požadavky" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Úroveň: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Životy {:d} ze {:d}" #: Source/control.cpp:1525 #, fuzzy #| msgid "Right-click to use" msgid "Right click to inspect" msgstr "Použití - klikni Pravým" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Nová úroveň" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Máš {:s} zlaťák. Kolik jich chceš odebrat?" msgstr[1] "Máš {:s} zlaťáky. Kolik jich chceš odebrat?" msgstr[2] "Máš {:s} zlaťáků. Kolik jich chceš odebrat?" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Městský Portál" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "od {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Portál do" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "Bezbožný Oltář" #: Source/cursor.cpp:636 msgid "level 15" msgstr "15. patro" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, fuzzy, c++-format #| msgid "Unable to load character" msgid "Unable to load data from file {0}" msgstr "Nelze načíst postavu" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Zobraz tuto zprávu a ukonči" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Zobraz verzi a ukonči" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Vyber složku se souborem diabdat.mpq" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Vyber složku pro uložené hry" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Vyber složku pro soubor diablo.ini" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Přeskoč startovní videa" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Zobraz smínky za vteřinu" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "Zapni důkladné logování" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Nahrát demo soubor" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Přehrát demo soubor" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Vypnout omezení snímků během přehrávání dema" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Výběr hry:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Vynutit Shareware mód" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Vynutit Diablo mód" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Vynutit Hellfire mód" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Hellfire nastavení:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "Chyby hlašte na https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Nepodařilo se načíst UI zdroje.\n" "\n" "Ujistěte se, že devilutionx.mpq je ve složce se hrou a je aktuální." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Vypadlo spojení --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Čekám na hráče --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "Žádná nápověda" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "v obchodech" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Předmět v opasku {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Použije Předmět z opasku." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Rychlá volba kouzla {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Klávesová zkratka pro dovednost nebo kouzlo." #: Source/diablo.cpp:1809 #, fuzzy #| msgid "Previous Menu" msgid "Previous quick spell" msgstr "Předchozí Menu" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 #, fuzzy #| msgid "Quick spell {}" msgid "Next quick spell" msgstr "Rychlá volba kouzla {}" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "" #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "" #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Rychlá Volba Kouzel" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Otevře Rychlou Volbu Kouzel." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Rychlé uložení" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Uloží hru." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Rychlé načtení" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Načte hru." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Ukončit hru" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Ukončí hru." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Zastav hrdinu" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Zastaví chození a zruší naplánované akce." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Zvýrazňování předmětů" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Ukaž/schovej předměty na zemi." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Přepni zvýrazňování předmětů" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Trvale ukáže/schová předměty na zemi." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Přepni automapu" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Přepíná zda je automapa zobrazena." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Otevře obrazovku Inventáře." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Postava" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Otevře obrazovku Postavy." #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Deník úkolů" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Otevře Deník s úkoly." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Kniha Kouzel" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Otevře Knihu Kouzel." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Rychlé Zprávy {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Použije Rychlé Zprávy v chatu." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Schovat Info Obrazovky" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Zavře všechny informační obrazovky." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Přiblížení" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Přiblíží Herní Obrazovku." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Pauza Hry" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Zapauzuje hru." #: Source/diablo.cpp:2004 #, fuzzy #| msgid "Pause Game" msgid "Pause Game (Alternate)" msgstr "Pauza Hry" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy #| msgid "Increase screen brightness." msgid "Decrease Brightness" msgstr "Zvýší jas obrazovky." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Sníží jas obrazovky." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy #| msgid "Increase screen brightness." msgid "Increase Brightness" msgstr "Zvýší jas obrazovky." #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Zvýší jas obrazovky." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Nápověda" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Otevře Nápovědu." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Snímek obrazovky" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Udělá snímek obrazovky." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Informace o hře" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Zobrazí informace o hře." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Záznam Chatu" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Zobrazí záznam chatu." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 #, fuzzy #| msgid "Inventory" msgid "Sort Inventory" msgstr "Inventář" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" #: Source/diablo.cpp:2144 #, fuzzy #| msgid "Select Action" msgid "Secondary action" msgstr "Vyber Akci" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "" #: Source/diablo.cpp:2159 #, fuzzy #| msgid "Select Action" msgid "Spell action" msgstr "Vyber Akci" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "" #: Source/diablo.cpp:2174 #, fuzzy #| msgid "Cancel" msgid "Cancel action" msgstr "Zrušit" #: Source/diablo.cpp:2175 #, fuzzy #| msgid "Closes the game." msgid "Close menus." msgstr "Ukončí hru." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "" #: Source/diablo.cpp:2201 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character up." msgstr "Postavy Hry pro Více Hráčů" #: Source/diablo.cpp:2206 msgid "Move down" msgstr "" #: Source/diablo.cpp:2207 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character down." msgstr "Postavy Hry pro Více Hráčů" #: Source/diablo.cpp:2212 #, fuzzy #| msgid "$Movement:" msgid "Move left" msgstr "$Pohyb:" #: Source/diablo.cpp:2213 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character left." msgstr "Postavy Hry pro Více Hráčů" #: Source/diablo.cpp:2218 #, fuzzy #| msgid "the night" msgid "Move right" msgstr "noci" #: Source/diablo.cpp:2219 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character right." msgstr "Postavy Hry pro Více Hráčů" #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "" #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "" #: Source/diablo.cpp:2310 #, fuzzy #| msgid "Automap" msgid "Automap Move Up" msgstr "Automapa" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 msgid "Automap Move Down" msgstr "" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 msgid "Automap Move Left" msgstr "" #: Source/diablo.cpp:2323 msgid "Moves the automap left when active." msgstr "" #: Source/diablo.cpp:2328 msgid "Automap Move Right" msgstr "" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "" #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "" #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "" #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "" #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "" #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "" #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "" #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "" #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 #, fuzzy #| msgid "The game ended" msgid "Toggle game menu" msgstr "Hra byla ukončena" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 #, fuzzy #| msgid "Saves the game." msgid "Opens the game menu." msgstr "Uloží hru." #: Source/diablo_msg.cpp:63 #, fuzzy #| msgctxt "spell" #| msgid "Flame Wave" msgid "Game saved" msgstr "Ohnivá Vlna" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "Multiplayer není v demu dostupný" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Selhalo Vytvoření Direct Sound" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "Nedostupné v shareware verzi hry" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "Nedostatek místa pro uložení" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Nelze pauzovat ve městě" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Je doporučeno kopírovat na harddisk" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Problém synchronizace multiplayeru" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Nelze pauzovat v multiplayeru" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Ukládám..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Některé jsou oslabeny zatímco jeden zesílí" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Nová síla je ukována skrz ničení" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Ti jenž se brání málokdy útočí" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "Meč spravedlnosti je hbitý a ostrý" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Když je duch bdělý, tělo vzkvétá" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Zacílení zdroje many obnovuje" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "Čas nemůže zeslabit sílu oceli" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "Magie není vždy taková jak se zdá" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Co bylo jednou otevřené je nyní uzavřené" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "Intenzita přichází za cenu moudrosti" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "Tajemná síla přináší zničení" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Co nemůže být drženo nemůže být ani zraněno" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Rudá a Azurová stanou se jako slunce" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Znalosti a moudrost za cenu vlastního já" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Pijte a osvěžte se" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Kamkoli jdeš, už jsi tam" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "Energie přichází za cenu moudrosti" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Bohatství přichází když jej nejmíň čekáš" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "Kde lakomec nepochodí, trpělivost přináší růže" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "Byls požehnán laskavým společníkem!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Ruce lidí mohou být řízeny osudem" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "Síla je upevněna nebeskou vírou" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "Esence života vyvěrá z nitra" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "Cesta se vyjeví když se na ni díváš shora" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "Spása přichází za cenu moudrosti" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Tajemství jsou odhalena ve světle rozumu" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Ti co jsou poslední můžou ještě být první" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "Štědrost přináší své vlastní odměny" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Musíš být alespoň na 8. úrovni abys to mohl použít." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Musíš být alespoň na 13. úrovni abys to mohl použít." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Musíš být alespoň na 17. úrovni abys to mohl použít." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "Tajemná znalost získána!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "Co tě nezabije..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "Znalosti jsou síla." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Dej a taky dostaneš." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Některé zkušenosti získáš dotekem." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "Není žádné místo jako je domov." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Duchovní energie je obnovena." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Cítíš se hbitější." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Cítíš se silnější." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Cítíš se moudřejší." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Cítíš se svěží." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "Co se může rozbít se taky rozbije." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Katedrála" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Katakomby" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Jeskyně" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Hnízdo" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Krypta" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Lv {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "{} obtížnost" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "V Menu" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "zpětná vazba" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "Nelze se připojit" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "chyba: přečteno 0 bytů ze serveru" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq nebo spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Některé Hellfire MPQ soubory chybí" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Ne všechny Hellfire MPQ soubory nalezeny.\n" "Překopírujte všechny hf*.mpq soubory." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Rozlišení" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Běhání ve Městě" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "Theův Úkol" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "Kraví Úkol" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Auto Sbírání Zlaťáků" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Auto Sbírání Elixírů" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 #, fuzzy #| msgid "Auto Gold Pickup" msgid "Auto Oil Pickup" msgstr "Auto Sbírání Zlaťáků" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Auto Sbírání ve Městě" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Adria Doplňuje Manu" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Auto Přezbrojení Zbraně" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Auto Přezbrojení Zbroje" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Auto Přezbrojení Helmy" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Auto Přezbrojení Štítu" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Auto Přezbrojení Šperků" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Náhodné Úkoly" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 #, fuzzy #| msgid "Show mana values" msgid "Show Item Labels" msgstr "Zobraz hodnoty many" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Auto Plnění Opasku" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Deaktivovat Zmrzačující Oltáře" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Sbírání Léčívých Lektvarů" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Sbírání Plných Léčívých Lektvarů" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Sbírání Mana Lektvarů" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Sbírání Plných Mana Lektvarů" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Sbírání Lektvarů Omlazení" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Sbírání Plných Lektvarů Omlazení" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Nastavení" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Ulož Hru" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy #| msgid "Main Menu" msgid "Exit to Main Menu" msgstr "Hlavní Menu" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Ukonči Hru" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Jas" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Tempo" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Hudba Vypnuta" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Zvuky" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Zvuky Vypnuty" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Pauza" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Klávesové Zkratky:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Otevři Nápovědu" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Zobraz Hlavní Menu" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Tab: Zobraz Auto-mapu" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Mezerník: Schovej všechny obrazovky" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Otevři Rychlou Volbu Kouzel" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Otevři Knihu Kouzel" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Otevři obrazovku Inventáře" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Otevři obrazovku Postavy" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "Q: Otevři Deník s Úkoly" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Snížit Jas" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "G: Zvýšit Jas" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Přiblížení Herní Obrazovky" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Přiblížení Auto-mapy" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Použij Předmět z Opasku" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Nastav klávesovou zkratku pro Kouzlo" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Levé Tlačítko Myši: Útok bez pohybu" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Levé Tlačítko Myši (na obrazovce Postavy): Přiřaď všechny body " "statistik" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Levé Tlačítko Myši (v Inventáři): Přesuň předmět do Opasku nebo " "obleč/sundej předmět" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "Shift + Levé Tlačítko Myši (na Opasku): Přesuň předmět do Inventáře" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Pohyb:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "Držením tlačítka myši se postava bude pohybovat v daném směru." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Souboj:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Držením klávesy Shift a klikáním levým tlačítkem bude postava držet pozici a " "útočit." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Auto-mapa:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "Pro přístup k Auto-mapě, klikni na tlačítko 'MAPA' na informačním panelu " "nebo zmáčkni 'TAB' na klávesnici. Přiblížení a oddálení mapy lze provést " "pomocí kláves + a -. Pohybovat mapou lze pomocí šipek." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Sbírání předmětů:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Použitelné předměty malé velikosti, například lektvary a svitky, jsou " "automaticky umístěny do tvého 'opasku' v horní části informačního panelu. " "Když je předmět umístěn do opasku, objeví se malé číslo v dané kolonce. " "Předměty můžou být použity buď zmáčknutím odpovídajícího čísla na " "klávesnici, nebo kliknutím na předmět pomocí pravého tlačítka myši." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Zlaťáky:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Můžeš si zvolit konkrétní množství zlaťáků k odhození tak, že klikneš pravým " "tlačítkem myši na hromádku zlata ve tvém Inventáři." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Dovednosti & Kouzla:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "K seznamu dovedností a kouzel se dostaneš kliknutím levým tlačítkem myši na " "tlačítko 'KOUZLA' na informačním panelu. Zapamatovaná kouzla a kouzla " "dostupná z hole jsou zde zobrazena. Kliknutí levým tlačítkem na kouzlo " "vybere dané kouzlo. Vybrané kouzlo lze vyčarovat tak, že klikneš pravým " "tlačítkem myši do herní obrazovky." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Používání Rychlé Volby pro Kouzla:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Kliknutí levým tlačítkem na aktuální kouzlo v informačním panelu otevře " "'Rychlou Volbu', kde si můžeš rychle zvolit dovednost nebo kouzlo k použití. " "Vybrané kouzlo lze vyčarovat tak, že klikneš pravým tlačítkem myši do herní " "obrazovky." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Kliknutím Shift + Levé tlačítko myši na aktuální kouzlo můžeš odebrat " "vybrané kouzlo." #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Nastavení Klávesových Zkratek pro Kouzla:" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Můžeš nastavit až 4 Klávesové zkratky pro dovednosti, svitky nebo kouzla. " "Nejprve otevři 'Rychlou Volbu' jak je popsáno o sekci nahoře. Poté myší " "najeď na kouzlo, na které chceš nastavit klávesovou zkratku a zmáčkni jednu " "z kláves F5, F6, F7 nebo F8." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Knihy Kouzel:" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Čtením více než jedné knihy zvyšuješ svou znalost daného kouzla, což ti " "umožní čarovat dané kouzlo efektivněji." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Nápověda k Shareware Hellfire" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Nápověda k Hellfire" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Nápověda k Shareware Diablu" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Nápověda k Diablu" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Stiskni ESC pro ukončení, šipky pro posun." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "Nelze vytvořit hlavní okno" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Olej Přesnosti" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Olej Mistrovství" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Olej Ostrosti" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Olej Smrti" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Olej Dovednosti" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Kovářský Olej" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Olej Statečnosti" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Olej Stálosti" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Olej Tvrdnutí" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Olej Nepropustnosti" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{0} {1} {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{0} {1} {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} {1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "zvyšuje u zbraně" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "šanci na zásah" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "výrazně zvyšuje" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "u zbraně šanci na zásah" #: Source/items.cpp:1652 msgid "damage potential" msgstr "potenciál poškození" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "výrazně zvyšuje u zbraně" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "potenciál poškození - ne u luku" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "snižuje potřebné atributy" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "pro používání zbroje a zbraní" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "doplňuje 20% z" #: Source/items.cpp:1664 msgid "item's durability" msgstr "výdrže předmětu" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "zvyšuje současnou a" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "maximální výdrž předmětu" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "udělá předmět nezničitelným" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "zvýší obranné číslo" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "zbroje nebo štítu" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "výrazně zvýší obranné číslo" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "zbroje nebo štítu" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "nastraží ohnivou past" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "nastraží bleskovou past" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "nastraží past zkamenění" #: Source/items.cpp:1695 msgid "restore all life" msgstr "doplní všechny životy" #: Source/items.cpp:1698 msgid "restore some life" msgstr "doplní část životů" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "doplní část many" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "doplní všechnu manu" #: Source/items.cpp:1707 msgid "increase strength" msgstr "zvýší sílu" #: Source/items.cpp:1710 msgid "increase magic" msgstr "zvýší magii" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "zvýší obratnost" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "zvýší vitalitu" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "doplní část životů a many" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "doplní všechny životy a manu" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Zobrazení - klikni Pravým" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Použití - klikni Pravým" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Přečtení - klikni Pravým, poté\n" "klikni Levým na cíl" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Přečtení - klikni Pravým" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Aktivuj pro zobrazení" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Otevři inventář pro použití" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Aktivuj pro použití" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Vyber si z knihy kouzel, a poté\n" "vyčaruj kouzlo pro přečtení" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Aktivuj pro přečtení" #: Source/items.cpp:1800 #, fuzzy, c++-format #| msgid "Activate to view" msgid "{} to view" msgstr "Aktivuj pro zobrazení" #: Source/items.cpp:1806 #, fuzzy, c++-format #| msgid "{:+d} to strength" msgid "{} to use" msgstr "{:+d} síla" #: Source/items.cpp:1809 #, fuzzy, c++-format #| msgid "" #| "Select from spell book, then\n" #| "cast spell to read" msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Vyber si z knihy kouzel, a poté\n" "vyčaruj kouzlo pro přečtení" #: Source/items.cpp:1811 #, fuzzy, c++-format #| msgid "Activate to read" msgid "{} to read" msgstr "Aktivuj pro přečtení" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Patro: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Zdvojnásobí kapacitu hromádek zlata" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Požadavky:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Síla" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Magie" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Obrat" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "Kniha - {:s}" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "Ucho od {:s}" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "šance na zásah: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% poškození" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "na zásah: {:+d}%, {:+d}% poškození" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% obrana" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "obranné číslo: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Odolnost Oheň: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Odolnost Oheň: {:+d}% MAX" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Odolnost Blesky: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Odolnost Blesky: {:+d}% MAX" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Odolnost Magie: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Odolnost Magie: {:+d}% MAX" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Odolnost Vše: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Odolnost Vše: {:+d}% MAX" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "kouzla navýšena o {:d} úroveň" msgstr[1] "kouzla navýšena o {:d} úrovně" msgstr[2] "kouzla navýšena o {:d} úrovní" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "kouzla snížena o {:d} úroveň" msgstr[1] "kouzla snížena o {:d} úrovně" msgstr[2] "kouzla snížena o {:d} úrovní" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "úroveň kouzel beze změn" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Použití navíc" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} použití" msgstr[1] "{:d} {:s} použití" msgstr[2] "{:d} {:s} použití" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Poškození ohněm: {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Poškození ohněm: {:d}-{:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Poškození bleskem: {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Poškození bleskem: {:d}-{:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} síla" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} magie" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} obratnost" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} vitalita" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} ke všem statistikám" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} poškození od nepřátel" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Životy {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Mana: {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "vysoká výdrž" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "snížená výdrž" #: Source/items.cpp:3960 msgid "indestructible" msgstr "nezničitelný" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% osvětlení" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% osvětlení" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "střílí více šípů současně" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "poškození ohnivým šípem: {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "poškození ohnivým šípem: {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "poškození bleskovým šípem: {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "poškození bleskovým šípem: {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "poškození ohnivou koulí: {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "poškození ohnivou koulí: {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "útočník je poškozen za 1-3" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "uživatel ztratí všechnu manu" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "pohlcuje polovinu poškození z pastí" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "odhodí nepřítele dozadu" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% poškození proti démonům" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Všechny Odolnosti jsou 0" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "zásah vysaje 3% many" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "zásah vysaje 5% many" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "zásah vysaje 3% životů" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "zásah vysaje 5% životů" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "prorazí obranu nepřítele" #: Source/items.cpp:4010 msgid "quick attack" msgstr "hbitý útok" #: Source/items.cpp:4012 msgid "fast attack" msgstr "rychlý útok" #: Source/items.cpp:4014 msgid "faster attack" msgstr "rychlejší útok" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "nejrychlejší útok" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Další dovednost (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "rychlé vzpamatování ze zásahu" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "rychlejší vzpamatování ze zásahu" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "nejrychlejší vzpamatování ze zásahu" #: Source/items.cpp:4027 msgid "fast block" msgstr "rychlé blokování" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "přidá {:d} bod k poškození" msgstr[1] "přidá {:d} body k poškození" msgstr[2] "přidá {:d} bodů k poškození" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "střílí šípy náhodnou rychlostí" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "předmět má neobvyklé poškození" #: Source/items.cpp:4035 msgid "altered durability" msgstr "upravená výdrž" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "jednoruční meč" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "neustále budeš ztrácet životy" #: Source/items.cpp:4041 msgid "life stealing" msgstr "vysátí životů" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "nepožaduje sílu" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "poškození bleskem: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "poškození bleskem: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "bleskové střely při zásahu" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "občas ztrojnásobí poškození" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "snižující se {:+d}% poškození" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x poškození do nepřítele, 1x do tebe" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Náhodné 0 - 600% poškození" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "nízká výdrž, {:+d}% poškození" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "extra OČ proti démonům" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "extra OČ proti nemrtvým" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% Many přesunuto do Životů" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% Životů přesunuto do Many" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "poškození: {:d} Nezničitelný" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "poškození: {:d} Výdrž: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "poškození: {:d}-{:d} Nezničitelný" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "poškození: {:d}-{:d} Výdrž: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "obrana: {:d} Nezničitelný" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "obrana: {:d} Výdrž: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Použití: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "unikátní předmět" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Není Identifikován" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Doupě Krále Kostlivců" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Komnata Kostí" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Bludiště" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Otrávené Zásoby Vody" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Doupě Arcibiskupa Lazaruse" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "" #: Source/levels/setmaps.cpp:33 #, fuzzy #| msgid "Hellfire" msgid "Hell Arena" msgstr "Hellfire" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Dolů do sklepení" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Dolů do katakomb" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Dolů do jeskyní" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Dolů do pekla" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Dolů do hnízda" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Dolů do krypty" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Nahoru do patra {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Nahoru do města" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Dolů do patra {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Dolů do Hnízda úrovně {:d}" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Nahoru do Hnízda úrovně {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Nahoru do Krypty úrovně {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Základní Kámen Světa" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Dolů do Krypty úrovně {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Zpět do Patra {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Nelze otevřít archiv souboru s uloženou hrou" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Neplatný soubor s uloženou hrou" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "Hráč je v úrovni pouze pro Hellfire" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Neplatný stav hry" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "Nelze zobrazit mainmenu" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "Zvíře" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Démon" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Nemrtvý" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Typ: {:s} Zabito: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Celkem zabito: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Životy: {:d}-{:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Žádná magická odolnost" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Odolnosti:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Magie" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Oheň" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Blesky" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Imunita:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Typ: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Žádné odolnosti" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Žádné imunity" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Má nějaké Magické Odolnosti" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Má nějaké Magické Imunity" #: Source/mpq/mpq_writer.cpp:174 #, fuzzy #| msgid "Failed to open stash archive for writing." msgid "Failed to open archive for writing." msgstr "Nepodařilo se otevřít archiv bedny k zápisu." #: Source/msg.cpp:1701 #, fuzzy, c++-format #| msgid "{:s} has cast an illegal spell." msgid "{:s} has cast an invalid spell." msgstr "{:s} vyčaroval nedovolené kouzlo." #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "{:s} vyčaroval nedovolené kouzlo." #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Hráč '{:s}' (úroveň {:d}) se připojil do hry" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "Hra byla ukončena" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "Nelze získat data o patře" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "Hráč '{:s}' opustil hru" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Hráč '{:s}' zabil Diabla a opustil hru!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "Hráč '{:s}' byl odpojen (timeout)" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Hráč '{:s}' (úroveň {:d}) už je připojen do hry" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Tajemný" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Skrytý" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Ponurý" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "Divný" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Kouzelný" #: Source/objects.cpp:132 msgid "Stone" msgstr "Kamenný" #: Source/objects.cpp:133 msgid "Religious" msgstr "Náboženský" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Očarovaný" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Divotvorný" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Fascinující" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Záhadný" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Podivný" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Tajuplný" #: Source/objects.cpp:141 msgid "Divine" msgstr "Božský" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "Svatý" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Posvátný" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Duchovní" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Strašidelný" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Opuštěný" #: Source/objects.cpp:147 msgid "Creepy" msgstr "Strašidelný" #: Source/objects.cpp:148 msgid "Quiet" msgstr "Tichý" #: Source/objects.cpp:149 msgid "Secluded" msgstr "Odloučený" #: Source/objects.cpp:150 msgid "Ornate" msgstr "Ozdobený" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "Mihotavý" #: Source/objects.cpp:152 msgid "Tainted" msgstr "Poskvrněný" #: Source/objects.cpp:153 msgid "Oily" msgstr "Mastný" #: Source/objects.cpp:154 msgid "Glowing" msgstr "Zařící" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "Žebrákův" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "Jiskřivý" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "Třpytivý" #: Source/objects.cpp:159 msgid "Solar" msgstr "Sluneční" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "Murphyho" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "Velký Konflikt" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "Mzdou Hříchu je Válka" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "Příběh o Horadrimech" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "Temný Exil" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "Hříšná Válka" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "Spoutání Trojice" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "Sféry Mimo" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "Příbeh o Třech" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "Černý Král" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "Deník: Očarování" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "Deník: Setkání" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "Deník: Tiráda" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "Deník: Jeho Síla Roste" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "Deník: NA-KRUL" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "Deník: Konec" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "Kniha Kouzel" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "Ukřižovaný Kostlivec" #: Source/objects.cpp:4799 msgid "Lever" msgstr "Páka" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "Otevřené dveře" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "Zavřené dveře" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "Zablokované dveře" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "Prastará Kniha" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "Kniha Ohavností" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "Páka s lebkou" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "Mýtická Kniha" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "Malá Truhla" #: Source/objects.cpp:4833 msgid "Chest" msgstr "Truhla" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "Velká Truhla" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "Sarkofág" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "Police na knihy" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "Knihovna" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "Barel" #: Source/objects.cpp:4851 msgid "Pod" msgstr "Zámotek" #: Source/objects.cpp:4854 msgid "Urn" msgstr "Urna" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "{:s} Oltář" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "Kostěná Kniha" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "Kniha v Knihovně" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "Krvavá Fontána" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "Setnuté Tělo" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "Kniha Slepých" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "Kniha Krve" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "Očišťující Pramen" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "Brnění" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "Stojan na Zbraně" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "Oltář Kozlů" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "Velký Kotel" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "Kalná Kaluž" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "Fontána Slz" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "Ocelová Kniha" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "Podstavec Krve" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "Houbový Porost" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "Odporný Stojan" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Zabitý Hrdina" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "{:s} s pastí" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (deaktivováno)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "ZAP" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "VYP" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Herní Mód" #: Source/options.cpp:422 #, fuzzy #| msgid "Gameplay Settings" msgid "Game Mode Settings" msgstr "Herní Nastavení" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Hraj Diablo nebo Hellfire." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Omezit na Shareware" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Udělá hru kompatibilní s demoverzí. Umožní hrát hru více hráčů s kamarády, " "kteří nemají plnou verzi Diabla." #: Source/options.cpp:442 msgid "Start Up" msgstr "Spuštění" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Nastavení pro Spuštění" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Intro" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Zobrazení Intro filmečku." #: Source/options.cpp:455 msgid "Splash" msgstr "Úvodní obrazovka" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Zobrazení úvodní obrazovky." #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Logo a Úvodní Obrazovka" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Jen Úvodní Obrazovka" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Specifická nastavení pro Diablo" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Specifická nastavení pro Hellfire" #: Source/options.cpp:501 msgid "Audio" msgstr "Zvuk" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Nastavení Zvuku" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Zvuky Chození" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Hráč vydá zvuk když chodí." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Zvuk Auto Přezbrojení" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" "Automatické přezbrojení předmětů při sebrání vydá zvuk daného předmětu." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Zvuk Sebrání Předmětu" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Sbírání předmětů vydá zvuk sebrání předmětu." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Vzorkovací Frekvence" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Výstupní vzorkovací frekvence (Hz)." #: Source/options.cpp:508 msgid "Channels" msgstr "Kanály" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Počet zvukových kanálů." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Velikost Bufferu" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Udává velikost bufferu (počet snímků na kanál)." #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Kvalita Převzorkování" #: Source/options.cpp:510 #, fuzzy #| msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "Udává kvalitu převzorkování, od 0 (nejnižší) po 10 (nejvyšší)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Udává interní rozlišení hry a určuje velikost herního výhledu. Pozn.: Toto " "se může lišit od rozlišení obrazovky, pokud je použito Škálování, " "Celočíselné Škálování nebo Přizpůsobit Obrazovce." #: Source/options.cpp:574 msgid "Resampler" msgstr "" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "" #: Source/options.cpp:631 msgid "Device" msgstr "" #: Source/options.cpp:631 #, fuzzy #| msgid "Audio Settings" msgid "Audio device" msgstr "Nastavení Zvuku" #: Source/options.cpp:688 msgid "Graphics" msgstr "Grafika" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Nastavení Grafiky" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Na celou obrazovku" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Zobrazí hru v okně nebo na celou obrazovku." #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "Přizpůsobit Obrazovce" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "Automaticky upraví herní okno na rozlišení a poměr stran tvojí plochy." #: Source/options.cpp:700 msgid "Upscale" msgstr "Škálování" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Zapne škálování obrazu z rozlišení hry na rozlišení monitoru. Zabrání " "přepínání rozlišení monitoru a umožní změnu velikosti okna." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Kvalita Škálování" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "Zapne volitelné filtry pro výstup obrazu při škálování." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Nejbližší Pixel" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Bilineární" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Anisotropické" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Celočíselné Škálování" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Zapne škálování obrazu pomocí celočíselného poměru." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Vertikální Synchronizace" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "" #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Blesky" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Barevné Cyklování" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "Efekt barevného cyklování používaný pro animaci vody, lávy a kyseliny." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Alternativní vykreslení hnízda" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "Použije alternativní paletu barev v Hellfire lokaci hnízdo." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Hardwarový Kurzor" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Použije Hardwarový kurzor" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Hardwarový Kurzor pro Předměty" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Použije Hardwarový kurzor pro předměty." #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Max Velikost Hardwarového Kurzoru" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Maximální šířka a výška hardwarového kurzoru. Větší kurzory se vykreslí " "softwarově." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Zobraz FPS" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Ukáže FPS v levém horním rohu obrazovky." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Gameplay" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Herní Nastavení" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Zapne běhání/rychlé chození ve městě pro Diablo i Hellfire. Tato možnost " "byla přidána v datadisku." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Zachycení Vstupu" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "Po zapnutí bude myš zachycena k hernímu oknu." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Zapne úkol od Malé Holčičky." #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" "Zapne úkol od Jerseyho. Farmář Lester bude nahrazen postavou Úplný Blázen." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Přátelská Palba" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Zapne poškození šípy/kouzly mezi hráči při hře více hráčů, i když budou v " "přátelském módu." #: Source/options.cpp:790 #, fuzzy #| msgid "No pause in multiplayer" msgid "Full quests in Multiplayer" msgstr "Nelze pauzovat v multiplayeru" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "" #: Source/options.cpp:791 msgid "Test Bard" msgstr "Test Bard" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Zobrazí postavu typu Bard v menu tvorby hrdiny." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Test Barbar" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "Zobrazí postavu typu Barbar v menu tvorby hrdiny." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Lišta Zkušeností" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "Přidá Lištu Zkušeností do UI ve spodní části obrazovky." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" #: Source/options.cpp:795 msgid "Show health values" msgstr "Zobraz hodnoty životů" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "Ukáže aktuální a maximální hodnotu životů na kouli životů." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Zobraz hodnoty many" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "Ukáže aktuální a maximální hodnotu many na kouli many." #: Source/options.cpp:797 #, fuzzy #| msgid "Character Information" msgid "Show Party Information" msgstr "Informace o Postavě" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Lišta Životů Nepřítele" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "V horní části obrazovky zobrazí Lištu Životů Nepřítele." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Zlaťáky jsou automaticky sbírány když hráč prochází poblíž." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "Elixíry jsou automaticky sbírány když hráč prochází poblíž." #: Source/options.cpp:802 #, fuzzy #| msgid "" #| "Elixirs are automatically collected when in close proximity to the player." msgid "Oils are automatically collected when in close proximity to the player." msgstr "Elixíry jsou automaticky sbírány když hráč prochází poblíž." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Automatické sbírání předmětů ve městě." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Adria ti doplní manu když navštívíš její obchod." #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "Zapne automatické nasazení zbraně po sebrání nebo nákupu." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "Zapne automatické nasazení zbroje po sebrání nebo nákupu." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "Zapne automatické nasazení helmy po sebrání nebo nákupu." #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "Zapne automatické nasazení štítu po sebrání nebo nákupu." #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "Zapne automatické nasazení šperků po sebrání nebo nákupu." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "Náhodně vybere dostupné úkoly pro novou hru." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Zobraz Typy Monster" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "Přejetím myši přes monstrum zobrazí typ daného monstra v popisce v UI." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "" #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "Po použití předmětu z opasku doplní opasek předmětem z inventáře." #: Source/options.cpp:814 #, fuzzy #| msgid "" #| "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines " #| "and Sacred Shrines are not able to be clicked on and labeled as disabled." msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" "Je-li zapnuto, na Kotle, Fascinující Oltáře, Kozlí Oltáře, Ozdobené Oltáře a " "Posvátné Oltáře nepůjde kliknout a budou označeny jako deaktivované." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Rychlé Kouzlení" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Klávesové zkratky kouzel ihned vyčarují nastavené kouzlo namísto přepnutí na " "dané kouzlo." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Počet Léčivých Lektvarů k automatickému sebrání." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "Počet Plných Léčivých Lektvarů k automatickému sebrání." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Počet Mana Lektvarů k automatickému sebrání." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "Počet Plných Mana Lektvarů k automatickému sebrání." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "Počet Lektvarů Omlazení k automatickému sebrání." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "Počet Plných Lektvarů Omlazení k automatickému sebrání." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" #: Source/options.cpp:824 #, fuzzy msgid "Off" msgstr "Smyčka" #: Source/options.cpp:825 #, fuzzy #| msgid "Randomize Quests" msgid "Random Angles" msgstr "Náhodné Úkoly" #: Source/options.cpp:826 #, fuzzy #| msgid "Vertical Sync" msgid "Vertical Only" msgstr "Vertikální Synchronizace" #: Source/options.cpp:880 msgid "Controller" msgstr "Ovladač" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Nastavení Ovladačů" #: Source/options.cpp:889 msgid "Network" msgstr "Síť" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Nastavení Sítě" #: Source/options.cpp:901 msgid "Chat" msgstr "Chat" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Nastavení Chatu" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Jazyk" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Nastaví který jazyk bude použit ve hře." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Nastavení Jazyků" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Klávesové Zkratky" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Nastavení Klávesových Zkratek" #: Source/options.cpp:1260 #, fuzzy #| msgid "Keymapping" msgid "Padmapping" msgstr "Klávesové Zkratky" #: Source/options.cpp:1260 #, fuzzy #| msgid "Keymapping Settings" msgid "Padmapping Settings" msgstr "Nastavení Klávesových Zkratek" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 #, fuzzy #| msgid "Settings" msgid "Mod Settings" msgstr "Nastavení" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Úroveň" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Zkušenosti" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Další úroveň" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "Základ" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Nyní" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Síla" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Magie" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Obratnost" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Vitalita" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Body k distribuci" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Zlaťáky" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Obranné číslo" #: Source/panels/charpanel.cpp:176 #, fuzzy #| msgid "chance to hit" msgid "Chance To Hit" msgstr "šanci na zásah" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Poškození" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Životy" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Mana" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "Odolnost Magie" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Odolnost Oheň" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Odolnost Blesky" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "postava" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "úkoly" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "mapa" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "menu" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "inventář" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "kouzla" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "hlas" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "ztlumit" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Nepoužitelné" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Poš: 1/3 hp cíle" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Léčení: {:d} - {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Poškození: {:d} - {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Dovednost" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Hůl ({:d} použití)" msgstr[1] "Hůl ({:d} použití)" msgstr[2] "Hůl ({:d} použití)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Úroveň {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Mana: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Kouzlo" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Zraňuje pouze nemrtvé" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Svitek" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Hůl" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Klávesová Zkratka Kouzla {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "Nelze otevřít archiv" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "Nelze načíst postavu" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (úroveň {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "Historie Chatu (Zprávy: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} zlaťáků" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "Kolik zlaťáků chceš odebrat?" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Úroveň {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Zkušenosti: {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Maximální úroveň" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Další úroveň: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} na Úroveň {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "Hrobka Krále Leorica" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "Komnata Kostí" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Temný Průchod" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Bezbožný Oltář" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "Do: {:s}" #: Source/quick_messages.cpp:10 #, fuzzy #| msgid "I need help! Come Here!" msgid "I need help! Come here!" msgstr "Potřebuji pomoc! Pojď sem!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Následuj mě." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Tady je něco pro tebe." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "Teď chcípneš!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 #, fuzzy #| msgid "Thanks To" msgid "Thanks." msgstr "Poděkování" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 msgid "Griswold" msgstr "Griswold" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Pepin" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Ogden" #: Source/stores.cpp:135 msgid "Cain" msgstr "Cain" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Farnham" #: Source/stores.cpp:137 msgid "Adria" msgstr "Adria" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Gillian" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Wirt" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Zpět" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Poškození: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Obrana: {:d} " #: Source/stores.cpp:315 #, fuzzy, c++-format #| msgid "Dur: {:d}/{:d}, " msgid "Dur: {:d}/{:d}" msgstr "Výdrž: {:d}/{:d}, " #: Source/stores.cpp:317 #, fuzzy #| msgid "indestructible" msgid "Indestructible" msgstr "nezničitelný" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Vítejte v mém" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Kovářském obchodě" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Co byste rádi:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Mluvit s Griswoldem" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Koupit základní předměty" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Koupit prémiové předměty" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Prodat předměty" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Opravit předměty" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Odejít z obchodu" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Mám na prodej tyto věci:" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Mám na prodej tyto prémiové věci:" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "Nemáš nic co bych odkoupil." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "Které předměty jsou na prodej?" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "Nemáš nic k opravě." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "Který předmět mám opravit?" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Čarodějná chalupa" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Mluvit s Adrií" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Koupit předměty" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Nabít hole" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Odejít z chalupy" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "Nemáš žádné hole k nabití." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "Kterou hůl chceš nabít?" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "Nemáš dostatek zlaťáků" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "Nemáš dost místa v inventáři" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "Máme dohodu?" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "Opravdu chceš identifikovat tento předmět?" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "Opravdu chceš koupit tento předmět?" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "Opravdu chceš dobít tento předmět?" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "Opravdu chceš prodat tento předmět?" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "Opravdu chceš opravit tento předmět?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Wirt, kluk s protézou" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Mluvit s Wirtem" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Mám něco na prodej," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "ale bude tě stát 50 zlaťáků" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "aby ses mohl podívat. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "Co máš za předmět?" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Rozloučit se" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Mám na prodej tento předmět:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Odejít" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Domě Léčitele" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Mluvit s Pepinem" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Odejít z Léčitelova domu" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "Městský Stařešina" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Mluvit s Cainem" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Identifikovat předmět" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "Nemáš nic k identifikaci." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "Který předmět chceš identifikovat?" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "Tento předmět je:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Hotovo" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Mluvit s {:s}" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Mluvení s {:s}" #: Source/stores.cpp:1217 msgid "is not available" msgstr "není dostupné" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "v shareware" #: Source/stores.cpp:1219 msgid "version" msgstr "verzi" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Klábosit" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Vycházející Slunce" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Mluvit s Ogdenem" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Odejít z hospody" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Mluvit s Gillian" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Otevři Skladiště" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Opilec Farnham" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Mluvit s Farnhamem" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Rozloučit se" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Tvoje zlaťáky: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Zabitý Měšťák" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Kovář Griswold" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Léčitel Pepin" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Zraněný Měšťan" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Majitel hospody Ogden" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Stařešina Cain" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Čarodějnice Adria" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Barmanka Gillian" #: Source/towners.cpp:786 msgid "Cow" msgstr "Kráva" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Farmář Lester" #: Source/towners.cpp:788 msgid "Celia" msgstr "Celia" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Úplný Blázen" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Válečník" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Zlodějka" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Čaroděj" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Mnich" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Bard" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Barbar" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Zombík" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Ghůl" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Tlející Mrtvola" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Černá Smrt" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "Odpadlík" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Řezbář" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Ďáblík" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "Temný" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Skelet" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Kostra Sekerník" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Uhořelý" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Horor" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Mrchožrout" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Pojídač Moru" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Stínová Bestie" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Okusovač Kostí" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Kostra Lukostřelec" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Skelet Kapitán" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Kostra Kapitán" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Uhořelý Kapitán" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Horor Kapitán" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Neviditelný Pán" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Skrytý" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Stalker" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Nespatřený" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Tkadlec Iluzí" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Pán Satyr" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Klan Masa" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Klan Kamene" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Klan Ohně" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Klan Noci" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Pohůnek" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Šotek" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Pochmurník" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Posluhovač" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Kyselinová Bestie" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Jedovatý Plivač" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Bestie z Jámy" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Lávový Chřtán" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Král Kostlivců" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "Řezník" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Mocipán" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Bláťák" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Žabí Démon" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "Stažený z Kůže" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Wyrm" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Jeskynní Slimák" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Ďábelský Wyrm" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Požírač" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Magmový Démon" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Krvavý Kámen" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Pekelný Kámen" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Lávový Pán" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Rohatý Démon" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Zablácený Běžec" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Mrazivý Útočník" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Obsidiánový Pán" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "staré kosti" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Rudá Smrt" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Kostěný Démon" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Nemrtvý Balrog" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Podpalovač" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Plamenný Pán" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Ohnivá Záhuba" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Pekelný Podpalovač" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Rudá Bouře" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Bouřný Jezdec" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Bouřný Pán" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Bouřná Smršť" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Ukrutný Ďáblík" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Okřídlený Démon" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Chrlič" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Krvavý Dráp" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Křídla Smrti" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Zabiják" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Dozorce" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Pán Víru" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Balrog" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Jeskynní Zmije" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Ohnivý Dráček" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Zlatá Zmije" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Azurový Dráček" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Temný Rytíř" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Stráž Záhuby" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Ocelový Pán" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Krvavý Rytíř" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "Rozřezaný" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "Prázdný" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Pán Bolesti" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Tkadlec Reality" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Sukuba" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Sněhová Čarodějnice" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Pekelná Stvůra" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Palič Duší" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Poradce" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Soudce" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Kabalista" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Advokát" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Golem" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "Temný Pán" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Arci-Lich Malignus" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Gharbad Slaboch" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Šílený Zhar" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Kapka z Nosu" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Arcibiskup Lazarus" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Rudé Soužení" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Černý Nefrit" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Krvavý Válečník" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Horkův Démon" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "Znesvěcovač" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Kosťohlav Sekáček" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Ostrokožec Rozřezávač" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Dušohnis" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Krysodav Nečistý" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Trhač Kostí" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Hnilobný Hodovač" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Střevopich zvaný Hbitý" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Rozbitohlav Štítoprásk" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Bongo" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Tlející Masakr" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Stínohlodač" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Smrtící Oko" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Šílené Oko Mrtvola" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "El Chupacabras" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Hořící Lebka" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Zkroucená Lebka" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Nabodávač Jazyků" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Tepolezec" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Uctívač Měsíce" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Havraní Hněv" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Požírač Páteří" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Uhořelý zvaný Černý Popel" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Stínový Havran" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Snětikámen Slaboch" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Žlučopěňoch Pán Jámy" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Krvokožec Temnoluk" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "Odporné Křídlo" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Stínový Pijan" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Mlhový Úskočník" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Smrtiflus" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Krvežlab" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Smrtistín Tělotrhač" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Válečný Červ zvaný Šílený" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Sklolebec Rozeklaný" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Ohnivá Pohroma" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Noční Křídlo zvaný Chladný" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Kamenný Nabodávač" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Žhavá Bronzová Pěst" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Rozhněvaný Oheň zvaný Záhuba" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Ohnivý Zraňovač zvaný Ponurý" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Baron Splašky" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Pohromoroh Ocelová Palice" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Kvíleč Chaosu" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Záhuboškleb Tlející" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Šílený Hořák" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Lich Kostiřez" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Páteřolam" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Ďáblolebkoun Ostrá Kost" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Zlomený Bouřník" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Prokletý Bouřník" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Slintající Sliz" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Plamenná Zlatá Pohroma" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Černá Bouře" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Morohněv" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "Stahovač z Kůže" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Modroroh" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Zkroucený Oheň Zplozenec Pekla" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Drápokaz" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Lebkohnis" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Lebkolev zvaný Ohnutý" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Černý Jazyk" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Ohavný Hmatač" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Zmijoplamen" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Drápokožec" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Čarodějný Oheň zvaný Bezbožný" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Černá Lebka" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Sekač Duší" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Zplozenec Větrů" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Vládce Jámy" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Rezavý Tkadlec" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Odstín Kvílející Zloby" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Záhubomrak" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Ohnivá Duše Krvavý Měsíc" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Čarodějný Měsíc" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Nabodávač Hodovník" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Šedovojna Zabiják" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Hrůzný Soudce" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Čarodějnice Hvězdné Oko" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Ocelová Lebka zvaný Lovec" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Sir Gorash" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "Vizír" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Zamfír" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Krvežíznivec" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Pavučinová Vdova" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Masový Tanečník" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "Chmurný Bodec" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Zámek Záhuby" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Krátký Meč" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Pukléř" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Kyj" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Krátký Luk" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Krátká Hůl - Mana" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Sekáček" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "Nemrtvá Koruna" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Empyreanský Kroužek" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Kouzelný Kámen" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Optický Amulet" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Prsten Pravdy" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Vývěsní Štít" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Harlekýnský Odznak" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Ocelový Závoj" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Zlatý Elixír" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Kovadlina Zuřivosti" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Černá Houba" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Mozek" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Plesnivá Kniha" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Spektrální Elixír" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Krvavý Kámen" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Mapa Katedrály" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Lektvar Léčení" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Lektvar Many" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Svitek Identifikace" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Svitek Městského Portálu" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Arkainova Chrabrost" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Lektvar Plného Léčení" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Lektvar Plné Many" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Griswoldovo Ostří" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Hovězí Pláty" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Lazarusova Hůl" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Svitek Oživení" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Krátká Hůl" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Meč" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Dýka" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Runová Bomba" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Theodor" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Aurický Amulet" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Potrhaný Vzkaz 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Potrhaný Vzkaz 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Potrhaný Vzkaz 3" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Rekonstruovaný Vzkaz" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Hnědý Oblek" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Šedý Oblek" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Šišák" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Šišák s Nánosníkem" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Helma" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Hrncová Helma" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Koruna" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Římská Helma" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Kápě" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Hadry" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Plášť" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Róba" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Prošívané Brnění" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Kožené Brnění" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Brnění z Tvrdé Kůže" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Pobité Kožené Brnění" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Prošívaná Zbroj" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Zbroj" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Kroužková Zbroj" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Šupinová Zbroj" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Hrudní Pláty" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Pláty" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Lamelová Zbroj" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Plátová Zbroj" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Bitevní Pláty" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Gotické Pláty" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Plná Plátová Zbroj" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Štít" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Malý Štít" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Velký Štít" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Mandlový Štít" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Pavéza" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Gotický Štít" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Lektvar Omlazení" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Lektvar Plného Omlazení" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Olej" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Elixír Síly" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Elixír Magie" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Elixír Obratnosti" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Elixír Vitality" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Svitek Léčení" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Svitek Hledání" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Svitek Blesku" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Svitek Ohnivé Zdi" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Svitek Inferna" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Svitek Záblesku" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Svitek Infravize" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Svitek Fázování" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Svitek Manového Štítu" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Svitek Ohnivé Vlny" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Svitek Ohnivé Koule" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Svitek Kamenné Kletby" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Svitek Řetězového Blesku" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Svitek Ochránce" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Svitek Novy" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Svitek Golema" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Svitek Teleportace" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Svitek Apokalypsy" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Malchus" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Scimitar" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Claymore" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Čepel" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Palaš" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Dlouhý Meč" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Široký Meč" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Meč Bastard" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Obouruční Meč" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Velký Meč" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Malá Sekera" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Sekera" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Sekera" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Široká Sekera" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Bojová Sekera" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Velká Sekera" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Palice" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Jitřenka" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Válečné Kladivo" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Kladivo" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Ostnatý Kyj" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Řemdih" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Velká Palice" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Luk" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Lovecký Luk" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Dlouhý Luk" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Kompozitní Luk" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Krátký Bojový Luk" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Dlouhý Bojový Luk" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Krátký Válečný Luk" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Dlouhý Válečný Luk" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Dlouhá Hůl" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Kompozitní Hůl" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Krátká Hůl" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Válečná Hůl" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Prsten" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Amulet" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Runa Ohně" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Runa" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Runa Blesku" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Vyšší Runa Ohně" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Vyšší Runa Blesku" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Runa Kamene" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Krátká Hůl - Blesková Střela" #: Source/translation_dummy.cpp:368 #, fuzzy #| msgid "Mana Potion Pickup" msgid "Arena Potion" msgstr "Sbírání Mana Lektvarů" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "Řezníkův Sekáček" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Lightsabre" msgid "Lightforge" msgstr "Meč Světla" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "Trhlinový Luk" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "Jehelníček" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "Luk z Nebes" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Smrtící Lovec" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Luk Mrtvých" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "Luk z Černého Dubu" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Plamenná Šipka" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Masožrout" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Větrná Síla" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Orlí Roh" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Gonnagalův Nůž" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "Obránce" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Gryfův Dráp" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Černá Břitva" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Vypouklý Měsíc" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Ledová Kudla" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "Čepel Popravčího" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "Řezač Kostí" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Stínový Jestřáb" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Kouzelníkův Hrot" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Meč Světla" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "Sokolův Pařát" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Inferno" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "Nositel Záhuby" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "Grizzly" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "Praotec" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "Znetvořitel" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Ostrý Zobák" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Krvavý Zabiják" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "Sekera z Nebes" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "Zlovolná Sekera" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "Sekač Kamene" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "Aguinařina Sekyrka" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "Pekelný Zabiják" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "Messerschmidtův Plenitel" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "Rezavý Louskáček" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "Kladivo z Jholmu" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "Civerbův Obušek" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "Jitřenka z Nebes" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "Baranarova Jitřenka" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "Sukovitý Kořen" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "Mlátič Lebek" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "Schaeferovo Kladivo" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "Přiruba Snů" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "Stínová Hůl" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "Upalovač" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "Bouřná Věž" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "Třpytivá Píseň" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "Přivolavač Hromu" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "Ochránce" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "Najův Hádankář" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "Výkřik Mysli" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "Onanova Tyč" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "Helma Duchů" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "Přemýšlející Šišák" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "Mocipánova Helma" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "Hlupákův Odznak" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "Soumrak Bohů" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "Královská Čelenka" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "Roztrhané Maso Duší" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "Gladiátorova Zhouba" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "Duhový Plášť" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "Kožešina z Autu" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "Přehoz Moudrosti" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "Jiskřivá Zbroj" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "Mrchožroutův Krunýř" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "Kápě Noci" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "Najovy Odlehčené Pláty" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "Kabát z Ostnatého Démona" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "Deflektor" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "Štít Rozštěpené Lebky" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "Dračí Průlom" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "Štít z Černého Dubu" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "Svatý Obránce" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "Bouřný Štít" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "Réva" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "Prsten Reghy" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "Prsten Krvácení" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "Stahující Prsten" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "Zásnubní Prsten" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "Cínový" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "Mosazný" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "Bronzový" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "Železný" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "Ocelový" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "Stříbrný" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "Platinový" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "Mithrilový" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "Meteorický" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "Zvláštní" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "Nepoužitelný" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "Ohnutý" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "Slabý" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "Zubatý" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "Smrtící" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "Těžký" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "Krutý" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "Brutální" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "Masivní" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "Divoký" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "Bezohledný" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "Nemilosrdný" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "Nemotorný" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "Tupý" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "Ostrý" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "Skvělý" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "Bojovníkův" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "Vojákův" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "Lordův" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "Rytířův" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "Mistrův" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "Šampionův" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "Králův" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "Zranitelný" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "Zrezivělý" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "Silný" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "Velkolepý" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "Statečný" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "Slavný" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "Požehnaný" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "Posvátný" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "Úžasný" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "Božský" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "Červený" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "Rudý" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "Granátový" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "Rubínový" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "Modrý" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "Azurový" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "Lapisový" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "Kobaltový" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "Safírový" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "Bílý" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "Perleťový" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "Slonovinový" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "Krystalový" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "Diamantový" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "Topazový" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "Jantarový" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "Nefritový" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "Obsidiánový" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "Smaragdový" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "Hyeny" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "Žáby" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "Pavoukův" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "Havranův" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "Hadův" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "Ještěrův" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "Dráčkův" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "Drakův" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "Wyrmův" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "Hydří" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "Andělův" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "Arch-Andělův" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "Hojný" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "Bohatý" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "Planoucí" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "Bleskový" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "kvality" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "zmrzačení" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "zabíjení" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "nabodnutí" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "krveprolití" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "vraždění" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "bolesti" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "slz" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "života" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "ochrany" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "absorpce" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "odklonění" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "osmózy" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "křehkosti" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "slabosti" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "síly" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "mocnosti" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "moci" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "obrů" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "titánů" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "paralýzy" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "atrofie" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "obratnosti" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "dovednosti" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "přesnosti" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "preciznosti" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "dokonalosti" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "hlupáka" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "dyslexie" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "magie" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "mysli" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "oslnivosti" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "čarodějnictví" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "kouzelnictví" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "nemoci" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "choroby" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "vitality" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "nadšení" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "elánu" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "ráznosti" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "života" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "potíží" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "jámy" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "oblohy" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "měsíce" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "hvězd" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "nebes" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "zvěrokruhu" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "supa" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "šakala" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "lišky" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "jaguára" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "orla" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "vlka" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "tygra" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "lva" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "mamuta" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "velryby" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "křehkosti" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "lámavosti" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "pevnosti" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "řemesla" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "struktury" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "věků" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "temnoty" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "noci" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "světla" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "zářivosti" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "plamene" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "ohně" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "hoření" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "šoku" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "blesku" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "hromu" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "množství" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "hojnosti" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "trnů" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "zkaženosti" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "zlodějů" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "medvěda" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "netopýra" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "upíra" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "pijavice" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "krve" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "pronikavý" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "propíchnutí" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "mlácení" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "připravenosti" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "hbitosti" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "rychlosti" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "spěchu" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "rovnováhy" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "stability" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "harmonie" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "blokování" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "Kouzelný Kámen" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "Gharbad Slaboch" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "Zhar Šílenec" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "Řezník" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "Ogdenova Cedule" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "Síně Slepých" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "Valor" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "Krvavý Válečník" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "Kletba Krále Leorica" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "Arcibiskup Lazarus" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "Závažné Záležitosti" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "Farmářův Sad" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "Malá Holčička" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "Potulný Obchodník" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "Znesvěcovač" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "Jerseyho Trikot" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "Ohnivá Střela" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "Léčení" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "Blesk" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "Záblesk" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "Identifikace" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "Ohnivá Zeď" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "Městský Portál" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "Kamenná Kletba" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "Infravize" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "Fázování" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "Štít Many" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "Ohnivá Koule" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "Ochránce" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "Řetězový Blesk" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "Ohnivá Vlna" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "Had Zhouby" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "Krvavý Rituál" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "Nova" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "Neviditelnost" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "Inferno" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "Golem" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "Zuřivost" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "Teleportace" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "Apokalypsa" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "Odhmotnění" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "Oprava Předmětu" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "Dobití Hole" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "Odzbrojení Pasti" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "Elementál" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "Blesková Střela" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "Svatá Střela" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "Oživení" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "Telekineze" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "Léčení Druhých" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "Krvavá Hvězda" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "Kostěný Duch" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" " Ahh, příběh o našem Králi, je tak? Leoricův tragický pád byl pro tuto zemi " "hroznou ránou. Lidé Krále vždy milovali, a teď z něj mají smrtelný strach. " "Neustále si kladu otázku, jak mohl skončit tak daleko od Světla, protože " "Leoric byl vždy ten nejsvětější muž. Jedině ty nejodpornější síly Pekla by " "mohly tak kompletně zničit človeka zevnitř..." #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "Vesnice potřebuje tvou pomoc, dobrý člověče! Před několika měsíci byl unesen " "syn Krále Leorica, princ Albrecht. Král se strašlivě rozzuřil a svého " "ztraceného syna hledal ve vesnici. S každým dalšim dnem se zdálo, že Leoric " "upadá hlouběji do šílenství. Obviňoval nevinné vesničany ze zmizení svého " "syna a nechával je brutálně popravovat. Jen necelá polovina z nás přežila " "jeho šílenství...\n" " \n" "Královi Rytíři a Kneží se ho pokoušeli uklidnit, ale obrátil se proti nim a " "bohužel byli nuceni ho zabít. S posledním vydechnutím Král přivolal hrozivou " "kletbu na své dřívější následovníky. Přísahal, že mu budou v temnotě sloužit " "navěky...\n" " \n" "A tady věci nabraly ještě temnější spád, než jaký jsem si uměl představit! " "Náš bývalý Král povstal z věčného spánku a nyní velí zástupům nemrtvých " "přisluhovačů v Labyrintu. Jeho tělo bylo pohřbeno v hrobce tři patra pod " "Katedrálou. Prosím tě, dobrý člověče, upokoj jeho duši tím, že zničís jeho " "současnou prokletou podobu..." #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Jak jsem ti rekl, dobry pane, Kral byl pohrben tri patra pod Katedralou. Je " "tam dole a ceka ve zkazene temnote na svou sanci znicit tuto zemi..." #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "Kletba naseho Krale pominula, ale bojim se, ze to byla pouze cast nejakeho " "vetsiho zla. Avsak jiz mozna muzeme byt zachraneni pred temnotou, ktera " "suzuje nasi zem, protoze tve vitezstvi je dobrym znamenim. Necht te svetlo " "provazi na ceste." #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" "Ztrata syna byla pro Krale Leorica prilis tezka. Udelal jsem, co jsem mohl, " "abych zmirnil jeho silenstvi, ale nakonec ho premohlo. Od toho dne se nad " "jeho kralovstvim vznasi temna kletba, ale mozna pokud uvolnis jeho dusi z " "pozemskeho vezeni, kletba snad pomine..." #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" "Nerada myslim na to, jak Kral zemrel. Radeji vzpominam na jeho laskavost a " "spravedlivou vladu. Jeho smrt byla tak nestastna a vypadale velice spatne." #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" "Vyrobil jsem mnoho zbrani a hodne zbroji, kterymi Kral Leoric vybavil sve " "rytire. Take jsem pro nej vykoval ohromny obourucni mec z nejlepsiho mitrilu " "a take odpovidajici korunu do boje. Stale nemohu uverit tomu, jak zemrel, " "ale to, co ho uvrhlo do silenstvi, musela byt nejaka zlovestna sila!" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Nestaram se o to. Poslouchej, zadny kostlivec nemuze byt MUJ kral. Leoric je " "Kral. Kral, rozumis mi ? SLAVA KRALI!" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" "Mrtvi, kteri kraci mezi zivimi, nasleduji prokleteho Krale. Ma moc vzkrisit " "jeste vice valecniku do stale se rozrustajici armady nemrtvych. Jestli " "nezastavis jeho vladu, urcite se vyda do teto zeme a zavrazdi vsechny, kteri " "jsou jeste zivi." #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" "Podivej, ja tady delam obchody. Neprodavam informace a nestaram se o " "jakehosi Krale, ktery muze byt mrtvy dele nez ja jsem zivy. Jestli " "potrebujes neco proti tomu Krali nemrtvych, tak bych ti s tim mohl pomoct..." #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" "Teplo zivota vstoupilo do moji hrobky. Smrtelniku, priprav se slouzit memu " "Panu celou vecnost!" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" "Vidim, ze te to divne chovani docela popletlo. Jelikoz se mnoho demonu boji " "slunecniho svetla a veri, ze vladne velkou moci, tak se domnivam, ze " "vychazejici slunce zobrazene na znaku, o kterem mluvis, je presvedcilo, ze " "znak take vladne jakousi tajemnou silou. Hmm, mozna nejsou vsichni az tak " "chytri, jak jsme se obavali..." #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" "Pane, chci se s vami podelit o podivny zazitek. Vim, ze mas velke vedomosti " "o tech zrudach, ktere obyvaji labyrint, a toto je neco, cemu nemohu za cely " "zivot porozumet... V noci me probudilo nejake skrabani z venku mojeho " "hostince. Kdyz jsem vyhledl z moji loznice, videl jsem stiny nejakych potvor " "podobnych demonum na dvore hostince. Po kratke chvilce utekli pryc, ale " "predtim jeste ukradli znak mojeho hostince. Vubec nechapu, proc by demoni " "meli krast muj znak, ale nechat moji rodinu na pokoji... je to divne, ze?" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" "Oh, nemusel jsi mi prinest zpet muj znak, ale urcite mi to usetri vydaje za " "vyrobu noveho. Dobre, podivam se, co bych ti mohl dat jako odmenu za " "nalezeni mojeho znaku? Hmmm, copak tu mame... ah, ano! Tuhle capku nechal v " "jednom pokoji carodej, ktery tady pred nejakou dobou bydlel. Treba se ti " "muze nejak hodit." #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" "Muj boze, demoni behaji po nocich ve vesnici a pleni nase domovi - copak jim " "nic neni svate? Doufam, ze jsou Ogden a Garda v poradku. Myslim, ze by za " "mnou meli zajit, jestli nejsou zraneni..." #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" "Oh ne! Tak tam se podel ten znak? Moje babicka a ja jsme celou tu vec urcite " "zaspaly. Diky bohum, ze ty prisery nezautocili na hostinec." #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "Demoni ukradli Ogdenum znak, rikas? To nezni moc jako krutosti, o kterych " "jsem slysel - nebo je videl. \n" " \n" "Demoni se zajimaji o vytrhavani srdci z tela, ne o nejake znaky." #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" "Vis co si myslim? Nekdo vezme ten znak a budou za neho chtit hodne prachu. " "Kdybych ja byl Ogden... a ja nejsem, ale kdybych byl... proste bych koupil " "novy znak s nejakym fajnym obrazkem. Mozna pekny dzbanek pivka nebo kus " "sira..." #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Nikdo smrtelny nemuze doopravdy pochopit mysl demonu. \n" " \n" "Nikdy se nenech jejich divnymi ciny zmast, protoze to muze byt jejich plan." #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" "Co - rika, ze jsem to vzal ja? Predpokladam, ze Griswold je na jeho " "strane. \n" " \n" "Podivej, obycejneho kradeni znaku jsem nechal pred mesici. Na kusu dreva " "nemuzes vydelat." #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" "Hej - Ty, co zabijis vsechny! Dones mi Magicky Znak nebo mi zautocit! Ty " "neodejit zivy! Ty zabit velke osklivaky a donest Magii. Jdi za roh a dvere, " "najde osklivaky. Ty das, ty pujdes!" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "Ty zabit osklivaky, dostat znak. Ty prinest mi, jinak..." #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "Ty dal! Ano, dobre! Jdi, my silni. My zabit vsechny s velkou Magii!" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" "To nevesti dobre veci, protoze to potvrzuje me nejtemnejsi obavy. I kdyz " "jsem nechtel uverit davnym legendam, nyni je nemohu poprit. Mozna nastal cas " "k tomu, abych odhalil, kdo jsem.\n" " \n" "Me prave jmeno je Deckard Cain Starsi a jsem poslednim nasledovnikem davneho " "Bratrstva, ktere bylo zasveceno ochrane tajemstvi o nekonecnem zlu. Zlu, " "ktere bylo nyni ocividne uvolneno.\n" " \n" "Arcibiskup Lazarus, drive nejduveryhodnejsi poradce Krale Leorica, vedl do " "Labyrintu skupinu vesnicanu, aby nalezli Kralova ztraceneho syna, Albrechta. " "Uplynul nejaky cas nez se vratili a pouze nekolik jich uniklo zivych.\n" " \n" "Proklej me za blahovost! Mel jsem odhalit jeho skrytou zradu. Musel to byt " "sam Lazarus, kdo unesl Albrechta a ukryl ho v Labyrintu. Nechapu, proc se " "Arcibiskup obratil k temnote, nebo jake ma umysly s tim ditetem. Ledaze by " "ho chtel obetovat svemu temnemu panovi!\n" " \n" "To musel byt jeho plan! Ti, kteri prezili z jeho 'zachranne skupiny', " "rikaji, ze naposledy Lazara spatrili, jak bezi do nejhlubsiho nitra " "labyrintu. Musis si pospisit a zachranit prince pred obetnim nozem toho " "pomateneho zloducha!" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Musis spechat a zachranit Albrechta z rukou Lazara. Princ a lide tohoto " "kralovstvi na tebe spolehaji!" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" "Tvuj pribeh je dost hrozivy, priteli. Lazarus jiste shori v Pekle za sve " "hrozne ciny. Chlapec, ktereho jsi popsal, neni nas princ, ale verim, ze " "Albrecht jeste muze byt v nebezpeci. Symbol moci, o kterem mluvis musi byt " "portal do sameho srdce Labyrintu.\n" " \n" "Mel bys vedet toto, priteli - Zlo, proti kteremu bojujes je temny Pan Hruzy. " "Smrtelnikum je znam jako Diablo. To on byl pred mnoha staletimi uveznen v " "Labyrintu a verim, ze se opet snazi rozsevat chaos v lidskych risich. Musis " "projit portalem a znicit Diabla driv nez bude pozde!" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" "Lazarus byl Arcibiskup, ktery vedl mnoho vesnicanu do Labyrintu. Tenkrat " "jsem ztratil mnoho dobrych pratel a Lazarus se nikdy nevratil. Doufam, ze " "byl zabit spolu s ostatnimi. Jestli pro mne chces udelat laskavost, dobry " "pane - prosim nemluv o tom dni s Farnhamem." #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Byl jsem sokovan, kdyz jsem slysel, co vesnicane te noci planuji. Myslel " "jsem, ze Lazarus ma ze vsech lidi nejvice zdraveho rozumu. Byl Arcibiskup a " "vzdy se velmi staral o obcany Tristramu. Bylo jich zraneno prilis mnoho, " "nemohl jsem je zachranit vsechny..." #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Pamatuji si, ze Lazarus byl velmi laskavy a dobrotivy. Mluvil na pohrbu moji " "matky a pomahal moji babicce a mne v nejhorsich dobach. Kazdou noc se " "modlim, aby byl nejak nazivu a v bezpeci." #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Byl jsem tam, kdyz nas Lazarus vedl do Labyrintu. Mluvil o svate odplate, " "ale kdyz jsme zacali bojovat s temi zplozenci pekla, ani proti nim nepozvedl " "svuj palcat. Jenom bezel dale serymi, nekonecnymi komnatami, ktere byly plne " "sluzebniku temnoty!" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" "Bodaji, pak kousou, pak jsou vsude kolem tebe. Lhar! LHAR! Vsichni jsou " "mrtvi! Mrtvi! Slysis me? Jenom padaji a padaji... jejich krev se rozliva " "vsude po podlaze... vsechno je to jeho chyba..." #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" "Neznam toho Lazara, o kterem mluvis, ale citim v jeho dusi velky rozpor. " "Predstavuje velke nebezpeci a nezastavi se pred nicim, aby slouzil temnote, " "ktera ho prohlasila za sveho." #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" "Ano, spravedlivy Lazarus, ktery byl taak ucinny proti tem priseram tam dole. " "Nepomohl snad zachranit moji nohu, ze? Podivej, dam ti radu zadarmo. Zeptej " "se Farnhama, byl tam." #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" "Zapomen na svou blaznivou pout. Vse, co te ceka, je hnev meho Pana! Prisel " "jsi pozde, abys zachranil to dite. Ted se k nemu pridas v Pekle!" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" "Hmm, ani nevim, jestli ti muzu rici neco, co by ti nejak pomohlo. Voda, " "ktera plni nase studny, prichazi z podzemniho pramene. Slysel jsem o tunelu, " "ktery vede k velkemu jezeru - mozna jsou jedno a totez. Nanestesti nevim, co " "by mohlo zpusobit, ze jsou nase zasoby vody otravene." #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Vzdy jsem se ve sve zasobarne snazil udrzovat velke zasoby jidla a piti, ale " "kdyz nema cele mesto zadny zdroj ciste vody, i nase zasoby brzy dojdou. \n" " \n" "Prosim, udelej, co muzes, jinak nevim co budeme delat." #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "Jsem rad, ze jsem te zastihl! Voda z nasich studni se stala poloslanou a " "hnijici a nekteri vesnicane onemocneli, kdyz ji pili. Nase zasoby ciste vody " "rychle dochazeji. Verim, ze nekde je pruchod, ktery vede k pramenu, jez " "zasobuje cele nase mesto. Prosim, zjisti, co zpusobilo tuhle pohromu, jinak " "vsichni jistojiste zahyneme." #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Prosim, musis spechat. S kazdou dalsi hodinou jsme blize tomu, ze nebudeme " "mit co pit. \n" " \n" "Bez tve pomoci nemuzeme dlouho prezit." #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "Co to rikas - pouha pritomnost demonu zpusobila, ze se voda zkazila? Oh, pod " "nasim mestem ciha opravdu velke zlo, ale tva vytrvalost a odvaha nam dava " "nadeji. Prosim, vezmi si tento prsten - treba ti pomuze pri zniceni tech " "desivych priser." #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Moje babicka je velmi slaba a Garda rika, ze nemuzeme pit vodu ze studni. " "Prosim, muzes udelat neco, abys nam pomohl?" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" "Pepin ti rekl pravdu. Nalehave potrebujeme cistou vodu a co nejdrive. " "Zkousel jsem vycistit jednu z mensich studni, ale zase se zaplnila hnusnou " "spinou. Urcite je znecistena u pramene." #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "Ty pijes vodu?" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "Lide z Tristramu zemrou, pokud nemuzes dostat cistou vodu do jejich " "studni. \n" " \n" "Mel bys vedet, ze podstatou teto veci jsou demoni, ale vubec si nevsimaji " "toho, co zpusobili." #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Pro jednou s tebou souhlasim. Moje obchody vyschnou - to ti rikam - kdyz " "nebudu mit komu prodavat. Nejlip bys mel zjistit, co se deje!" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" "Kniha, ktera vypravi o komnate lidskych kosti? Dobra, Komnata kosti je " "zminovana v jistych starych spiscich, ktere jsem studoval v knihovnach na " "Vychode. Tyto svazky naznacovali, ze kdyz se Panove podsveti rozhodnou " "chranit velke poklady, vytvori oblast, kde budou vsichni ti, kteri se " "pokusili ukrast ten poklad, naveky svazani k jeho ochrane. Pokroucene, ale " "velmi ucinne, ze?" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" "Bojim se, ze o tom nevim nic, dobry pane. Cain ma mnoho knih, ktere ti mohou " "nejak pomoci." #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" "To zni jako velmi nebezpecne misto. Pokud si tam troufnes jit, prosim bud " "opatrny." #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Bohuzel jsem nikdy neslysela o nicem takovem. Mozna Cain Vypravec by ti mohl " "nejak pomoct." #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" "Nevim nic o tom miste, ale muzes se zkusit zeptat Caina Mluvi o mnoha vecech " "a neprekvapilo by me, kdyby mohl zodpovedet tvoje otazky." #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Oukej, tak poslouchej. Je tu ta drevena komnata, vis. A jeho zena, vis - ona " "- rika stromum... protoze musis pockat. Pak rikam, to proti nemu muze " "fungovat, ale jestli myslis, ze za to budu PLATIT... ty... uhh... jasne." #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Stanes se vecnym sluzebnikem temnych panu, pokud zahynes v teto proklete " "oblasti. \n" " \n" "Vstup do Komnaty kosti na vlastni riziko." #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "Ohromny a zahadny poklad, rikas? Mozna bych mel zajem vzit od tebe par " "veci... nebo jeste lepe, nepotrebujes nejake vzacne a drahe vybaveni, aby " "ses dostal skrz tu zkousku?" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" "Zda se, ze Arcibiskup Lazarus presvedcil mnoho vesnicanu k vyprave do " "Labyrintu, aby nalezli Kralova ztraceneho syna. Hral si s jejich strachem a " "udelal z nich zurivy dav. Nikdo z nich nebyl pripraven na to, co lezelo v " "chladne zemi... Lazarus je tam dole opustil - zanechal je ve sparech " "nepredstavitelnych priser - aby tam zemreli." #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" "Ano, Farnham mumlal cosi o priserne bestii, ktera se zurive ohanela zbrani. " "Myslim, ze mu rikal reznik." #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "Proboha, ja znam toho odporneho demona. Mnozi z tech, kteri ze skupiny " "vedene Lazarem prezili a vyplazili se z Katedraly, meli na tele jizvy, ktere " "jim zpusobil Reznikuv hnev. Nevim, cim rozrezaval sve obeti, ale nemuze to " "byt z tohoto sveta. Zanecha to rany hnisajici a dokonce i ja jsem shledaval " "temer nemoznym je lecit. Davej si velky pozor, jestli planujes valcit s tim " "hroznym demonem..." #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Kdyz Farnham rikal neco o reznikovi, co zabiji lidi, vubec jsem si toho " "nevsimala. Ale kdyz jsi s tim prisel ty, mozna je to pravda." #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" "Videl jsem to, cemu Farnham rika Reznik, jak se prosekaval skrz tela mych " "pratel. Ohanel se sekackem velkym jako sekera, odsekaval ruce a nohy a na " "miste zabijel statecne muze. Od bitky me oddelila banda malych jecicich " "demonu a nejak jsem nalezl schody vedouci ven. Uz nikdy jsem tu odpornou " "bestii nespatril, ale jeho krvavy vzhled me pronasleduje az do dnesnich dnu." #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "Velky! Velky sekacek zabiji moje pratele. Nemuzu ho zastavit, musim utect " "pryc, nemuzu jim pomoct. Chycen v mistnosti s tolika telama ... tolika " "prateli ... NEEEEEEEEEE!" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "Reznik je sadisticka prisera, ktera ma rozkos z muceni a bolesti jinych. " "Videl jsi jeho praci v ozralci Farnhamovi. Kdyz ho znicis, zajistis tak " "bezpeci teto vesnice." #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" "Vim o te potvore vic, nez si myslis. Jeho mali pratele me chytili a povedlo " "se jim useknout moji nohu nez me Griswold z te diry vytahl. \n" " \n" "Reknu to jednoduse - zabij ho driv nez on zabije tebe a prida si tvoji " "mrtvolu do sbirky." #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" "Prosim, pojd ke mne. Arcibiskup Lazarus nas zavedl dolu, abychom nasli " "ztraceneho prince. Ten bastard nas vedl do pasti! Ted jsou vsichni mrtvi... " "zabiti demonem, kteremu rikal Reznik. Pomsti nas! Najdi toho Reznika a zab " "ho, aby mohly nase duse odpocivat v pokoji..." #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" "Zarecitoval jsi zajimave verse, napsane stylem, ktery mi pripomnel jine " "dilo. Nech me premyslet - co to jen bylo?\n" " \n" "...Temnota zahali Skryte. Zarici oci nespatrenych a jen slysis zvuk ostrych " "drapu, ktere skrabou, aby mucily ty nebohe duse, ktere byly ucineny slepymi " "na vecnost. Vezeni pro tyto proklete se nazyva Sine slepych..." #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Nikdy jsem se moc nezajimal o poezii. Obcas jsem mel duvod najmout " "minstrely, kdyz se v hostinci darilo, ale to ted vypada, jako by to bylo uz " "davno. \n" " \n" "Co? Ah, ano... uh, dobra, predpokladam, ze muzes videt, co nekdo jiny vi." #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" "To mi zni jaksi povedomne. Myslim, ze si vzpominam, jak jsem cetl neco velmi " "podobneho te basni, kdyz jsem zkoumal historii demonickych trapeni. Je to " "misto velkeho zla... pockej - nechces se tam vypravit, ze ne?" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" "Jestli se ptas na slepotu, mel by sis promluvit s Pepinem. Vim, ze dal moji " "babicce lektvar, ktery pomohl vyjasnit jeji zrak, takze by ti s tim taky " "mohl pomoci." #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" "Obavam se, ze jsem nikdy nevidel ani neslysel o miste, ktere odpovida tvemu " "zivemu popisu, priteli. Mozna by ti nejak mohl pomoct Cain Vypravec." #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "Podivej se... to je docela vesely, he? Chapes? Slepy - podivej?" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Je to misto velkych muk a hruzy, a proto slouzi dobre svemu panu. \n" " \n" "Naslapuj opatrne, jinak muzes zustat dele nez ti bude mile." #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" "Hele, prodavam ti neco? Ne. Davas mi penize, abych ti o tom neco rekl? Ne. " "Odejdes ted a pujdes za vypravecem, ktery kvuli temhle vecem zije? Ano." #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" "Tvrdis, ze jsi mluvil s Lachdananem? Za sveho zivota byl velkym hrdinou. " "Lachdanan byl cestnym a spravedlivym clovekem, ktery po leta slouzil verne " "svemu Krali. Ale to uz samozrejme vis.\n" " \n" "Z tech, kteri byli chyceni ve sparech Kralovy kletby, by byl Lachdanan tim " "poslednim, kdo by se podrobil temnote bez boje, tudiz predpokladam, ze tvuj " "pribeh muze byt pravdivy. Kdybych byl na tvem miste, nalezl bych zpusob, jak " "ho uvolnit z jeho muk." #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "Mluvis o dlouho mrtvem statecnem bojovnikovi! V hostinci jsem neslysel o " "mluveni s davno mrtvymi, mnohokrat dekuji." #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Zlaty elixit, rikas. Nikdy drive jsem nevaril lektvar takove barvy, takze ti " "nemohu rict, jak by te ovlivnil, kdyby ses ho pokusil vypit. Jako tvuj " "lecitel ti radim - pokud najdes takovy elixir, udelej, o co te Lachdanan " "zada, a NEZKOUSEJ ho pouzit." #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Nikdy drive jsem o Lachdananovi neslysela. Promin, ale nemyslim, ze ti mohu " "nejak pomoci." #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" "Jestli je to opravdu Lachdanan, koho jsi potkal, pak ti radim, abys mu " "pomohl. Nekolikrat jsem s nim jednal a znal jsem ho jako cestneho a verneho " "cloveka. Kletba, ktera padla na nasledovniky Krale Leorica, na nej dopadla " "urcite zvlaste tvrde." #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" " Lachdanan je mrtvy. Kazdy to vi a ty me nemuzes oblbnout, abych si myslel " "neco jinyho. Nemuzes mluvit s mrtvyma. To vim!" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Muzes potkat lidi, kteri jsou polapeni v Labyrintu, jako je Lachdanan. \n" " \n" "Citim v nem cest a velky pocit viny. Pomoz mu a pomuzes vsem z Tristramu." #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" "Pockej, nech me hadat. Caina pohltila obrovska trhlina, ktera se pod nim " "otevrela. Pekelny ohen ho spalil na popel a uz nemuze odpovidat na tvoje " "otazky. Oh, tohle ze se nestalo? Tak potom si podle mne neco koupis nebo " "odejdes." #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" "Prosim, nezabijej me, jen me vyslechni. Kdysi jsem byl Kapitanem Rytiru " "Krale Leorica a udrzoval jsem zakony teto zeme spravedlive a cestne. Potom " "na nas padla temna Kletba za nasi ucast na kralove tragicke smrti. I kdyz " "moji pratele Rytiri podlehli svemu hroznemu osudu, ja jsem uprchl z Kralovi " "hrobky a snazim se najit zpusob, jak se uvolnit z Kletby. Selhal jsem...\n" " \n" "Slysel jsem o Zlatem Elixiru, ktery muze zrusit Kletbu a dovoli moji dusi " "odpocivat, ale nemohu ho nalezt. Ma sila nyni zanika a s ni take posledni " "kousky lidskosti. Prosim pomoz mi a najdi ten Elixir. Za tve usili se ti " "odvdecim - prisaham na svou cest." #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "Nenalezl jsi Zlaty Elixir. Bojim se, ze jsem odsouzen na vecnost. Prosim, " "pokus se jeste..." #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" "Zachranil jsi mou dusi pred zatracenim a za to jsem ti velmi dluzen. Jestli " "je nejaky zpusob, jak se ti mohu odvdecit zpoza hrobu, naleznu jej, ale " "prozatim - vezmi si mou helmu. Na ceste, na kterou se vydam, ji nebudu " "potrebovat. At te ochrani pred temnymi silami, ktere cihaji dole. Jdi za " "Svetlem, priteli..." #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" "Griswold mluvil o Kovadline Zurivosti - legendarnim artefaktu, dlouho " "hledanem, ale nenalezenem. Vyrobena z kovovych kosti demonu Pekelnych " "britev, Kovadlina Zurivosti byla tavena kolem kosti peti nejmocnejsich magu " "podsveti. Jsou na ni vyrezany runy moci a chaosu, jakakoliv zbran ci brneni " "vykovane na teto Kovadline, se ponori do rise Chaosu a naplni se magickymi " "schopnostmi. Rika se, ze kvuli nepredvidatelne povaze Chaosu je tezke urcit, " "jaky bude vysledek tohoto kovani..." #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" "Nemyslis, ze na tohle by ses mel radeji zeptat Griswolda? Je docela sikovny, " "vis." #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" "Pokud shanis informace o Palicce leceni nebo Stribrnem kalichu ocistovani, " "mohl bych ti pomoci, priteli. Jenze v tomhle pripade udelas lepe, kdyz si " "promluvis s Griswoldem nebo Cainem." #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" "Kdyz jsme vyrustali, Griswolduv otec nekterym z nas vypravel o obrovske " "kovadline, na ktere se vyrabeli mocne zbrane. Rikal, ze kdyz kladivo uderilo " "do te kovadliny, zem se otrasla velkou zurivosti. Kdykoliv se pohne zeme, " "vzdy si vzpomenu na ten pribeh." #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "Vitej! Vzdy je potesenim videt jednoho z mych nejlepsich zakazniku! Vim, ze " "se vydavas hloubeji do Labyrintu, a slychaval jsem pribeh, ktery by sis " "mozna rad poslechl...\n" " \n" "Jeden z muzu, kteri se vratili z Labyrintu mi vypravel o tajemne kovadline, " "na kterou narazil pri uteku. Jeho popis mi pripomnel legendy, ktere jsem " "slychaval v mladi, o horici Pekelne Vyhni, kde se vyrabeli mocne magicke " "zbrane. Legenda vypravela, ze hluboko v Pekelne Vyhni odpocivala Kovadlina " "Zurivosti! Tato Kovadlina obsahovala samotnou podstatu demonickeho " "podsveti...\n" " \n" "Rika se, ze jakakoliv zbran vykovana na te Kovadline je naplnena velkou " "silou. Jestli je tahle kovadlina Kovadlina Zurivosti, mozna bych ti mohl " "vykovat zbran, kterou bys porazil i nejtemnejsiho pana Pekla! \n" " \n" "Najdi pro me Kovadlinu a ja se dam do prace!" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" "Jeste nic, eh? Dobra, pokracuj v patrani. Zbran vykovana na Kovadline muze " "byt tvou nejlepsi nadeji a jsem si jist, ze ti mohu vyrobit opravdu " "legendarni." #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" "Nemohu tomu uverit! Toto je Kovadlina Zurivosti - dobra prace, priteli. Ted " "tem bastardum ukazeme, ze nejsou v Pekle hrozivejsi zbrane nez ty vykovane " "lidmi! Vezmi si toto a necht te Svetlo ochranuje." #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Griswold nemuze prodat svoji kovadlinu. Co by pak delal? Taky bych zuril, " "kdyby mi vzali kovadlinu!" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" "V Labyrintu je mnoho artefaktu, ktere vladnou silami, jez nemohou smrtelnici " "pochopit. Nektere z nich maji fantastickou moc, ktera muze byt pouzita " "Svetlem i Temnotou. Pokud ziskas Kovadlinu, mohlo by to zmenit prubeh Hrisne " "Valky ve prospech Svetla." #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" "Jestli najdes pro Griswolda ten artefakt, muze mi to hodne pokazit obchody. " "Awww, nikdy to nenajdes." #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" "Brana Krve a Sine Ohne jsou mista mystickeho puvodu. Kdekoliv spociva ta " "kniha, kterou jsi cetl, jiste je to misto velke sily.\n" " \n" "Legendy mluvi o podstavci, ktery je vytesan z obsidianu a na vrcholku " "pokrytem kostmi je kaluz vrici krve. Take jsou zde zminky o Kamenech krve, " "ktere otevrou dvere, jez strazi starodavny poklad...\n" " \n" "Povaha toho pokladu je zahalena domnenkami, priteli, ale rika se, ze davny " "hrdina Arkaine skryl svate brneni Udatnost v tajnem sklepeni. Arkaine byl " "prvnim smrtelnikem, ktery zmenil prubeh Hrisne Valky a pronasledoval legie " "temnoty zpet do Horoucich Pekel.\n" " \n" "Predtim nez Arkaine zemrel, bylo jeho brneni ukryto v tajne krypte. Rika se, " "ze az bude toto brneni opet potrebne, povstane hrdina, jez znovu oblekne " "Udatnost. Mozna jsi tim hrdinou ty..." #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" "Kazde dite slyselo pribeh valecnika Arkaina a jeho tajemneho brneni znameho " "jako Udatnost. Pokud naleznes misto jeho odpocinku, budes dobre chranen " "proti zlu v Labyrintu." #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Hmm... to zni jako neco, co bych si mel pamatovat, ale byl jsem tak " "zamestnan ucenim se novych leku a tvorenim lepsich elixiru, ze jsem to " "zapomnel. Promin..." #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" "Pribeh kouzelneho brneni nazyvaneho Udatnosti je neco, o cem si chlapci " "casto povidali. Radeji by ses mel zeptat nektero z muzu ve vesnici." #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" "Brneni zname jako Udatnost muze naklonit vahy ve tvuj prospech. Reknu ti, ze " "ho hledalo mnoho lidi - vcetne me samotneho. Arkaine ho skryl dobre, " "priteli, a budes potrebovat vice nez jen trochu stesti, abys odhalil " "tajemstvi, ktere ho ukryva jiz po mnoho let." #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" "Najdes-li tyto Kameny Krve, pouzij je velmi opatrne. \n" " \n" "Cesta je plna nebezpeci a tva jedina nadeje spociva ve tve vire v sebe sama." #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "Zamyslis najit brneni nazyvane Udatnost? \n" " \n" "Nikdo nikdy nezjistil, kde Arkaine schoval svoje veci, a jestli je nemuze " "najit muj kontakt, vazne pochybuji o tom, ze to zvladnes ty." #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" "Znam pouze jedinou legendu, jez vypravi o valecnikovi, ktereho jsi popsal. " "Jeho pribeh je zapsan v davnych kronikach o Hrisne Valce...\n" " \n" "Poskvrnen tisici let valky, krve a smrti stoji Vojevudce Krve na hore jeho " "roztrhanych obeti. Jeho temna cepel skuci cernou kletbu zivotu; muciva vyzva " "kazdemu, kdo se postavi Pekelnemu Popravcimu.\n" " \n" "Take je psano, ze ackoliv byl kdysi smrtelnikem, jez bojoval po boku Legii " "Temnoty behem Hrisne Valky, ztratil svou lidskost kvuli sve neukojitelne " "touze po krvi." #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Bojim se, ze jsem nikdy neslysel o takovem strasnem valecnikovi, dobry pane. " "Doufam, ze s nim nebudes muset bojovat, protoze to zni neobycejne nebezpecne." #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "Cain ti muze o necem takovem rict vice nez si budu kdy prat vedet." #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" "Jestli budes bojovat s tak hroznym protivinikem, necht te Svetlo vede a " "ochranuje. Budu na tebe stale myslet." #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" "Temne a strasne legendy se vypravi o Vojevudci Krve. Bud dobre pripraven, " "priteli, protoze nema s nikym vubec slitovani." #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" "Vzdycky musis mluvit o Krvi? Co treba kyticky a slunecni svit a to hezke " "devce, ktere roznasi piti. Poslechni, priteli - ses posedly, vis?" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" "Jeho zrucnost s mecem nahani hruzu a po tisice let zil pouze bojem. " "Promin... nemuzu videt, jestli ho porazis." #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Nikdy jsem neobchodoval s tim Vojevudcem, o kterem mluvis, ale zni to jako " "by potreboval spoustu mecu. Nevadilo by mi zasobovat jeho armadu..." #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" "Muj mec zpiva o tvoji krvi, smrtelniku a mym temnym panum jiste nebude " "odeprena." #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" "Griswold mluvil o Nebeskem kameni, ktery smeroval do mista na vychode. Byl " "tam prevazen kvuli dalsimu studiu. Ten kamen planul energii, ktera nejak " "zarucovala videni toho, co normalni clovek nemuze spatrit. Nevim, jaka " "tajemstvi skryva, priteli, ale nalezeni tohoto kamene se urcite ukaze velice " "cennym." #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" "Karavana se zde zastavila, aby nabrala zasoby pro cestu na vychod. Prodal " "jsem jim dost cerstveho ovoce a nejaky chleba, ktery Garda prave dopekla. Je " "to smula, co se jim stalo..." #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" "Nevim, co si mysleli, ze s tim kamenem uvidi, ale reknu ti jedno. Jestli " "padaji z oblohy kameny, radeji bys mel byt opatrny!" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Ano, karavana nejakych velmi dulezitych lidi se zde zastavila, ale bylo to " "uz pred nejakou dobou. Meli divny prizvuk a chystali se na dlouhou cestu, co " "si vzpominam. \n" " \n" "Nevim, jak muzes doufat, ze najdes neco z toho, co prevazeli." #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" "Postuj chvili - mam pribeh, ktery by te mohl zajimat. Pred nejakou dobou " "tudy projela karavana, ktera smerovala do vychodnich kralovstvi. " "Predpokladam, ze prevazeli kus nebes, ktery spadl na zem! Karavana byla na " "ceste severne odtud prepadena maskovanymi najezdniky. Hledal jsem v troskach " "ten kamen z nebe, ale nikde tam nebyl. Kdybys ho nasel, verim, ze bych z nej " "mohl pro tebe vytvorit neco uzitecneho." #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Stale cekam, kdy mi prineses ten kamen z oblohy. Vim, ze z nej muzu udelat " "neco mocneho." #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" "Podivejme se - ej... jo, je to jak, jsem veril. Pockej chvilku...\n" " \n" "Ah, tady mas. Vlozil jsem kousky kamene do stribrneho prstenu, ktery mi " "nechal muj otec. Doufam, ze ti bude k uzitku." #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" "Mel jsem pekny prsten; byl fakt drahej, s modrym a zelenym a cervenym a " "stribrnym. Jenze si nepamatuju, co se s nim stalo. Fakt mi ten prsten " "chybi..." #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "Nebesky kamen je velmi mocny a kdyby to byl nekdo jiny nez Griswold, kdo te " "ho poslal najit, branila bych ho. Spouta jeho sily a jeho uziti bude dobre " "pro nas vsechny." #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" "Jestli muze nekdo udelat neco z toho kamene,tak Griswold. Vi toho o sve " "praci hodne a i kdyz se mu snazim ukrast zakazniky, respektuji kvalitu jeho " "prace." #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" "Carodejka Adria hleda cernou houbu? O Cernych houbach toho vim tolik, co o " "Cervenych sledich. Mozna Pepin lecitel ti povi vice, ale v zednem z mych " "pribehu nebo knih takovou vec nenajdes." #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" "Reknu ti jenom tohle. Ani Garda ani ja vubec NIKDY nepodavame nasim ctenym " "hostum cerne houby. Jestli chce Adria uvarit nejake houby, tak je to jenom " "jeji vec, ale ja ti nemuzu pomoct s jejich hledanim. Cerne houby... jak " "nechutne!" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" "Carodejka mi rekla, ze hledas mozek demona, abys mi pomohl vytvorit muj " "elixir. Bude to mit velkou cenu pro mnoho lidi, kteri byly temi podlymi " "bestiemi poraneni, pokud se mi povede odhalit tajemstvi, ktere podle me " "ukryva. Pokud bys mohl vzit mozek demona, kdyz ho zabijes, byl bych ti velmi " "vdecny, kdybys mi ho prinesl." #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" "Vynikajici, presne tohle jsem si myslel. Mohl jsem elixir dokoncit i bez " "toho, ale nikdy neuskodi trochu studia. Zaneses tohle, prosim, carodejce? " "Verim, ze na to uz ceka." #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" "Myslim, ze Ogdem mozna ma v zasobarne nejake houby. Proc se nezeptas primo " "jeho?" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Pokud to Adria nemuze dostat, muzes se vsadit, ze je to velice vzacna vec. " "Nemuzu ti nabidnout lepsi pomoc nez tohle, ale zni to jako... velka, " "obrovita, nebezpecna, vypasena houba! Tak, stastny lov, predpokladam." #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Ogden micha UBOHOU cernou houbu, ale kdyz to piju, tak mi je blbe. Davej " "pozor, pozor... reknu ti tajemstvi - musis se mirnit!" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "Copak to tu mame? Zajimave, to vypada jako kniha o cinidlecg. Mej oci " "otevrene a hledej cernou houbu. Mela by byt docela velka a lehce " "rozpoznatelna. Jestli ji najdes, prineses mi ji, prosim?" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" "To, co potrebuji, je velka cerna houba. Ted bez a dones mi ji, abych ji " "mohla pouzit ve zvlastni smesi, na ktere prave pracuji." #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" "Ano, to je skvele do napoje, ktery vyrabim. Mimochodem, lecitel shani mozek " "nejakeho demona nebo neceho takoveho, aby mohl lecit ty, kteri byli otraveni " "jejich jedem. Verim, ze zamysli z neho udelat elixir. Jestli mu pomuzes to, " "co hleda, prosim, zkus mi prinest vzorek toho elixiru." #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "Proc mi to sem nosis? Zrovna ted mi je mozek demona k nicemu. Potrebuji " "trochu elixiru, na kterem pracuje Lecitel. To on potrebuje te groteskni " "organ, ktery drzis, a potom mi prines elixir. Je to tak jednoduche, kdyz se " "nad tim zamyslis, ze ano?" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "Coze? Ted jsi mi donesl ten elixir od Lecitele? Svuj napoj jsem mohla " "dokoncit bez nej. Proc si to proste nenechas..." #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" "Neprodavam zadne houby jakekoliv velikosti nebo barvy. Co takhle neco trochu " "uzitecnejsiho?" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" "Takze legenda o Mape je pravdiva. Ani ja jsem tomu opravdu neveril! " "Predpokladam, ze je cas rict ti pravdu o tom, kdo jsem, priteli. Vidis, ze " "nejsem tim, cim se zdam...\n" " \n" "Me prave jmeno je Deckard Cain a jsem poslednim nasledovnikem davneho " "Bratrstva, ktere bylo zasveceno udrzovani a ochrane tajemstvi o nekonecnem " "zlu. Zlu, ktere bylo nyni zrejme uvolneno...\n" " \n" "Zlo, proti kteremu stojis je temny Pan hruzy - smrtelnikum znamy jako " "Diablo. To on byl pred mnoha staletimi uveznen v Labyrintu. Mapa, kterou jsi " "prinesl byla pred davnymi veky vytvorena, aby oznacila dobu, kdy Diablo opet " "povstane ze sveho vezeni. Kdyz budou dve hvezdy vyznacene na mape v jedne " "rade, Diablo bude na vrcholu sve sily. Bude temer neznicitelny...\n" " \n" "Ted zavodis s casem, priteli! Najdi Diabla a znic ho drive nezse hvezdy " "seskupi, protoze uz mozna nebudeme mit zadnou dalsi sanci, jak vycistit svet " "od tohoto zla!" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "Nas cas se krati! Citim, jak se jeho temna sila zvetsuje a jedine ty muzes " "zabranit tomu, aby dosahl sve plne sily." #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Jsem si jist, ze jsi zkusil to nejlepsi, ale bojim se, ze ani tva sila a " "vule nestaci. Diablo je nyni na vrcholu sve pozemske sily a ty budes " "potrebovat vsechu odvahu a silu, abys ho porazil. Necht te Svetlo ochranuje " "a vede, priteli. Budu ti pomahat jakymkoliv zpusobem, ktery bude v mych " "silach." #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" "Kdyz ti nemuze pomoct carodejka a radi ti, abys sel za Cainem, proc si " "myslis, ze ti nejak pomohu ja? Zni to jako velmi zavazna zalezitost. Mel by " "sis pospisit a zajit za vypravecem, jak ti doporucila Adria." #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" "Ja si s tim napisem na mape moc neporadim, ale treba Adria nebo Cain by ti " "mohl pomoc rozlustit, o cem se zde pise. \n" " \n" "Vidim, ze to je mapa hvezd na nasi obloze, ale nic vic moje vedomosti " "nezahrnuji." #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "Na tuhle vec by ses mel zeptat nejlepe naseho vypravece. \n" " \n" "Cain je velice informovany o davnych svitcich a tohle je docela dobre " "nejstarsi kousek papiru, ktery jsem kdy videla." #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "Nikdy driv jsem takovou mapu nevidel. Kde jsi ji ziskal? I kdyz me nenapada, " "jak to precist, na tvoje otazky by ti mohli odpovedet Cain nebo Adria." #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Poslouchej, pojd bliz. Nevim, jestli vis, co vim, ale to co mas je opravdu " "neco. Tohleto je mapa." #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" "Oh, bojim se, ze to nevesti vubec nic dobreho. Tato mapa hvezd predvida " "velke nestesti, ale o jejich tajemstvich ti nic nepovim. Prisel cas, aby sis " "velmi vazne promluvil s Vypravecem..." #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" "Hledam mapu, ale tohle urcite neni ona. Mel bys to ukazat Adrii - nepochybne " "ti muze rict, co to je. Reknu ti jednu vec; vypada stare a stare obvykle " "znamena dost cenne." #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "Prooosim, neublizuj. Nezabijej. Nech zit a priste dam dobre." #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Neco pro tebe udelat. Zase, nezabijej Gharbada. Zit a dat dobre. \n" " \n" "Vem to, jako dukaz ja drzim slovo..." #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "Jeste ne! Skoro hotove. \n" " \n" "Mooc mocne, moc silne. Zit! Zit! \n" " \n" "Netrapit a ja dodrzet slib!" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Hodne dobre pro tebe. Hodne Mocne! Ty chces - ty ber!" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "Co?! Proc tu jsi? Z toho neustale vyrusovani by jeden zesilel. Tady, vezmi " "si to a nech me pracovat. Uz me neobtezuj!" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "Arrrrgh! Tvoje zvedavost zpusobi tvoji smrt!!!" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Vitej, priteli. Postuj chvili a poslouchej..." #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "Na svych vypravach hloubeji do Labyrintu muzes nalezt ukryte svazky, ve " "kterych jsou mnohe vedomosti. \n" " \n" "Precti je pozorne, protoze ti mohou rici veci, ktere ani ja neznam." #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Znam mnoho mytu a legend, jez mohou obsahovat odpovedi na tve otazky, ktere " "se mohou vynorit pri tvych cestach Labyrintem. Pokud narazis na vyzvy a " "otazky, o nichz se chces dozvedete vice, vyhledej me a ja ti reknu vse, co " "budu moci." #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" "Griswold - muz velkych cinu a velke odvahy. Vsadim se, ze ti nikdy nerekl o " "tom, jak se vydal do Labyrintu, aby zachranil Wirta, ze? Dobre vedel, jaka " "nebezpeci tam na nej cihaji, ale vlastne - ty to vis take. Je velmi zrucny " "remeslnik a pokud tvrdi, ze ti muze velmi pomoci jakymkoliv zpusobem, muzes " "se spolehnout na jeho cestnost a dovednost." #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Ogden vlastni a provozuje hostinec U Vychazejiciho slunce jiz temer ctyri " "roky. Koupil ho prave nekolik mesicu predtim nez tu slo vsechno k certu. On " "a jeho zena Garda nemaji penize, aby odesli, protoze vsechno vlozili do " "toho, aby si tady zaridili dobre zivobiti. Je to dobry muz s velkym smyslem " "pro odpovednost." #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" "Chudak Farnham. Je poznamenan vzpominkami na dav, ktery vstoupil do " "Katedraly s Lazarem v ten temny den. Utekl zivy, ale jeho odvaha a mnoho " "jeho zdraveho rozumu zustalo v te temne dire. Nyni nachazi utechu jedine na " "dne sveho korbele, ale mezi jeho neustlym blouznenim jsou obcas pohrbeny " "kousku pravdy." #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" "Carodejka, Adria, je v Tristramu trochu zvlastni. Prijela kratce pote, co " "byla Katedrala znesvecena, zatimco temer vsichni ostatni utikali. Postavila " "si malou chatrc na okraji mesta, zdanlive pres noc, a ma pristup k mnoha " "podivnym a tajemnym artefaktum a magickym svazkum, o kterych jsem ani ja " "drive neslysel." #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" "Wirtuv pribeh je desivy a tragicky. Byl vytrzen z naruci sve matky a " "odvlecen do labyrintu malymi, odpornymi demony, kteri nosili hnusne ostepy. " "V tech dnech bylo uneseno mnoho dalsich deti, vcetne syna Krale Leorica. " "Rytiri z palace se vydali dolu, ale nikdy se nevratili. Kovar chlapce nasel, " "ale az pote, co ho ty hnusne bestie zacali mucit kvuli svym sadistickym " "choutkam." #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "Ah, Pepin. Povazuji ho za praveho pritele - mozna nejblizsiho, co jsem kdy " "mel. Ted je trochu zmateny, ale nikdy neexistoval pozornejsi a ohleduplnejsi " "clovek. Jeho vedomostem a umeni se rovna malokdo a jeho dvere jsou stale " "otevrene." #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" "Gillian je dobra zena. Vsichni ji maji radi pro jejiho dobreho ducha a svezi " "smich, i ja ji mam velmi rad. Zustava v hostinci, aby se mohla starat o svou " "starou babicku, ktera je prilis nemocna a nemuze cestovat. Obcas se bojim o " "jeji bezpeci, ale vim, ze mnoho muzu by radeji zemrelo, jen aby se ji nic " "nestalo." #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "Vitej, dobry pane. Vitam te v hostinci U vychazejiciho slunce!" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" "Mnoho dobrodruhu zasedlo za stoly meho hostince a vypravelo se tu desetkrat " "vice pribehu nez bylo vypito piva. Jedina vec, co jsem slysel, na ktere " "vsichni,shodli je tato stara poucka. Treba ti nejak pomuze. Muzes sekat " "maso, ale musis rozdrtit kosti." #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" "Kovar Griswold toho vi o zbranich a brneni nesmirne mnoho. Pokud budes nekdy " "potrebovat spravit svou vyzbroj, urcite za nim zajdi." #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Farnham zde travi prilis mnoho casu utapenim sveho zarmutku v levnem pive. " "Vyhodil bych ho, ale kdyz byl v Labyrintu, velice trpel." #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Adria je na svuj vek velice moudra, ale musim se priznat - trochu me desi. \n" " \n" "Dobre, to nevadi. Pokud budes nekdy potrebovat kouzelny predmet, udrzuje " "svou chatrc za rekou podivne dobre zasobenou." #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Jestli chces vedet vice o historii nasi vesnice, vypravec Cain toho vi " "minulosti docela dost." #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Wirt je prohnany maly padouch. Vzdycky se dostane do problemu a vubec me " "neprekvapuje, co se mu stalo. \n" " \n" "Pravdepodobne se dostal nekam, kde by nemel byt. Toho chlapce je mi lito, " "ale nesnasim spolecnost, ve ktere se zdrzuje." #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Pepin je dobry muz - a urcite nejslechetnejsi ve vesnici. Vzdy se snazi " "pomahat ostatnim, ale zda se, ze potize toho ci onoho druhu ho nasleduji, " "kamkoliv jde..." #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" "Gillian, moje barmanka? Kdyby nebylo jeji pece o babicku, nejspis by odtud " "davno utekla. \n" " \n" "Bohove vi, ze jsem ji prosil, aby odesla a rikal ji, ze budu o jeji babicku " "pecovat, ale je prilis laskava a starostliva, aby to udelala." #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "Co te trapi, priteli?" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" "Objevil jsem velice zajimavou vec. Narozdil od nas se prisery v Labyrintu " "mohou lecit bez pomoci lektvaru ci magie. Pokud zranis nekterou z tech " "priser, ujisti se, ze je opravdu mrtva jinak se muze docela dobre vylecit " "sama." #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Predtim nez byla obsazena temi, cimkoliv co tam ciha, Katedrala byla mistem " "velkych vedomosti. Da se tam najit mnoho knih. Pokud nejake najdes, mel bys " "je vsechny precist, protoze v nich mohou byt ukryty tajemstvi o fungovani " "Labyrintu." #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" "Griswold toho vi o valecnem umeni tolik jako ja vim o umeni lecit. Je take " "mazany obchodnik, ale jeho praci se nikdo nevyrovna. Oh, domnivam se, ze to " "muze byt tim, ze je siroko daleko jediny kovar." #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" "Cain je pravy pritel a uceny mudrc. Udrzuje ohromnou knihovnu a ma " "prirozenou schopnost odhalit pravou povahu mnoha veci. Budes-li mit nekdy " "jakekoliv otazky, mel bys jit prave za nim." #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Ani me schopnosti nestacily na uplne vyleceni Farnhama. Oh, mohl jsem " "zahojit jeho telo, ale vyleceni jeho mysli a ducha je pro mne prilis tezke." #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" "I kdyz take pouzivam trochu magie k vytvareni lektvaru a elixiru, ktere zde " "skladuji, Adria je prava carodejka. Zda se, ze nikdy nespi, a vzdy ma " "pristup k mnoha mystickym svazkum a artefaktum. Verim, ze jeji chatrc je " "mnohem vice nez obycejna bouda, ale nikdy jsem se nemohl podivat dovnitr." #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Chudak Wirt. Udelal jsem pro nej vse, co bylo mozne, ale vim, ze se mu hnusi " "ta drevena noha, kterou jsem mu nasadil. Jeho rany byly priserne. Nikdo - a " "zvlaste ne takove mlade dite - by nemel tak moc trpet." #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" "Opravdu nechapu, proc Ogden zustava zde v Tristramu. Trpi slabsimi nervy, " "ale je inteligentni a pracovity clovek, ktery dela dobrotu, kamkoliv prijde. " "Domnivam se, ze to muze byt strachem z tolika vrazd, ktere se udaly v okolni " "zemi, nebo mozna prani jeho zeny, ktera se o nej a rodinu stara, kdekoliv " "jsou." #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "Ogdenova barmanka je sladka divka. Jeji babicka je dost nemocna a trpi " "preludy. \n" " \n" "Tvrdi, ze to jsou vize, ale nemam dukaz o jednom ani o druhem." #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "Dobry den! Cim mohu slouzit?" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Moje babicka mela sen, ze prijdes a budes se mnou mluvit. Vis, ona miva vize " "a muze videt do budoucnosti." #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "Zena na okraji mesta je carodejnice! Vypada docela prijemne a jeji jmeno, " "Adria, zni velice prijemne, ale ja se ji velmi bojim. \n" " \n" "Clovek, ktery by se podival, co tam provadi, musi byt statecny, jako ty." #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" "Nas kovar je chloubou Tristramu. Nejenom, ze je mistrovsky remeslnik, ktery " "vyhral mnoho turnaju v jeho cechu, ale pochvalil ho i sam nas Kral Leoric - " "necht jeho duse odpociva v pokoji. Griswold je take velky hrdina; vsak se " "zeptej Caina." #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Cain je vypravecem v Tristramu tak dlouho, co si pamatuji. Vi toho hodne a " "muze ti rict o cemkoliv skoro vsecho." #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" "Farnham je ozrala, ktery plni svoje bricho pivem a usi ostatnich nesmysly. \n" " \n" "Vim, ze Pepin i Ogden s nim maji pochopeni, ale ja jsem zklamana, kdyz ho " "pozoruji, jak stale vice a vice sklouzava do uplneho otupeni kazdou noc." #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" "Pepin zachranil moji babicce zivot a ja vim, ze mu to nikdy nebudu moci " "oplatit. Jeho schopnost lecit jakoukoliv nemoc je mocnejsi nez nejsilnejsi " "mec a zahadnejsi nez jakekoliv kouzlo. Jestli budes nekdy potrebovat leceni, " "Pepin ti urcite pomuze." #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" "Vyrustala jsem s Wirtovou matkou, Canace. I kdyz byla jen trochu zranena, " "kdyz ho ty odporne prisery unesly, nikdy se neuzdravila. Myslim, ze zemrela " "zalem. Z Wirta se stal necestny mladik, ktery se snazi jen vydelat na drine " "ostatnich. Vim, ze trpel a videl mnoho hruz, ktere si ani neumim predstavit, " "ale neco z te temnoty nad nim stale visi." #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Ogden a jeho zena vzali mne a moji babicku k sobe domu a nechavaji me " "vydelat par zlataku praci v hostinci. Tolik jim dluzim a doufam, ze jednoho " "dne opustime toto misto a pomuzu jim zalozit grand hotel na vychode." #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "Co pro tebe muzu udelat?" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" "Jestli shanis dobrou zbran, nech mne at ti ukazu tohle. Vezmi si zakladni " "tupou zbran, jako je palcat. Na vetsinu tech nehynoucich hruz tam dole " "pusobi bezvadne a neni nic lepsiho na rozbijeni tech vyzablych kostlivecku!" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" "Sekera? Jo, to je dobra zbran, vyvazena proti jakemukoliv nepriteli. " "Podivej, jak rozrazi vzduch a pak si v jeji ceste predstav nejakeho hnusneho " "demona. Mej na mysli, ze se s ni pomalu seka - ale rozdava pekne silne rany!" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Podivej na to ostri, tu vyvazenost. Mec v pravych rukach, a proti spravnemu " "nepriteli, je mistrem vsech zbrani. Jeho ostra cepel najde malo mista k " "sekani ci prorazeni na tech nemrtvych, ale proti zivemu, dychajicimu " "nepriteli mec krasne porcuje jeho maso!" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "Tvuj zapas proti Temnote se projevi i na tvych zbranich a zbroji. Jestli je " "prineses ke mne, muzu je, s trochou prace a horkou vyhni, opravit do " "nejlepsi bojove podoby." #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" "Zatimco ja musim kovy a naradi, ktere potrebuji od karavan, co projizdeji " "kolem naseho zatraceneho mesta, temer pasovat, zda se, ze ta carodejka Adria " "vzdy dostane vsechno, co chce. Kdybych o spoutani magie vedele jenomu trochu " "toho, co ona, mohla bych vyrabet skutecne neuveritelne veci." #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Gillian je hezke devce. Je smula, ze ma jeji babicka tak chatrne zdravi, " "jinak bych uz dohodl, aby je nektera z obchodnich karavan odvezla odtud." #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" "Nekdy si myslim, ze Cain mluvi prilis mnoho, ale tusim, ze to je jeho smysl " "zivota. Kdybych si mohl poddat ocel jako on tvoje usi, mohl bych vykovat " "brneni dobre i pro samotneho Cisare!" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Byl jsem s Farnhamem te noci, kdy nas Lazarus zavedl do Labyrintu. Uz nikdy " "jsem Arcibiskupa nevidel a nejspis bych neprezil, kdyby Farnham nebojoval se " "mnou. Bojim se, ze utok zmrzacil jeho dusi stejne jako jiny moji nohu. Tuhle " "bitvu pro nej nemohu vybojovat, ale udelal bych to, kdybych jen mohl." #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Dobry clovek, ktery vyvysuje potreby jinych nad svoje. V Tristramu - nebo " "kdekoliv jinde - nenajdes jedineho cloveka, ktery by mohl rict neco spatneho " "o nasem leciteli." #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" "Ten hoch se urcite dostane do velkych problemu... tusim, ze bych mohl rict, " "uz zase. Zkousel jsem v nem vzbudit zajem o praci u mne a nauceni se radneho " "remesla, ale on dava prednost vysokemu zisku pri obchodu se zbozim " "pochybneho puvodu. Nemohu to po nem chtit po tom, co se mu stalo, ale preji " "si, aby byl alespon opatrny." #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" "Hostinsky zde ma male obchody a zadny zpusob vydelku. Trochu si to vynahradi " "tim, ze poskytuje jidlo a nocleh tem, kteri sem tam prochazeji vesnici, ale " "ti se pravdepodobne odplizi v noci, aby nemuseli platit. Kdyby nebylo jeho " "zasob obili a suseneho masa, ktere ma ve sklepe, vetsina z nas by urcite " "umrela hlady v tom prvni roce, kdy bylo cele okoli zaplaveno demony." #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "Copak nemuzu v klidu pit?" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "Holka, co nosi piti? Oh, jo, to je pjekna divka. A prijemna taky." #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "Proc ta stara baba nedela taky neco jinyho. Jasne, jasne, ma ruzny nesmysly, " "ale poslouchej... ona neni normalni. Nikdy sem ji nevidel jist nebo pit - a " "nemuzes verit nekomu, kdo aspon trochu nepije." #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Cain neni, co rika, ze je. Jasne, jasne, povida dobry pribehy... nektery " "jsou fakt strasidelny nebo vesely... ale myslim, ze vi vic, nez vi, ze vi." #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Griswold? Dobry Griswold. Mam ho rad jako bratra! Bojovali jsme spolu, vis, " "kdyz jsme... my... Lazarus... Lazarus... Lazarus... Lazarus!!!" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" "Hehehe, mam rad Pepina. Fakt se snazi, vis. Poslouchej, mel bys ho fakt " "poznat. Dobry clovek a lidi, co potrebuou pomoct. Hej, hadam, ze je trochu " "jako ty, he hrdina? Ja sem taky bejval hrdina..." #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "Wirt je decko a ma vic problemu nez ja a ja vim o problemech vsecko. " "Poslouchej - ten kluk ma fajn obchod, ale byl tam dole, vis? Prisel o nohu! " "Musi chodit na kousku dreva. Smutne, smutne..." #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Ogden je nejlepsi chlap ve meste. Nemyslim, ze mne jeho zena ma moc rada, " "ale dokud on ovlada pipu, tak mi nevadi. Zda se, ze u Ogdena travim vic " "casu, nez je mozne, ale on je na mne tak hodny..." #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" "Chci ti neco rict, protoze vim o techhle vecech vsechno. Je to moje " "specialita. Tohle tady je nejlepsi... neeeejlepsi! Tohle druhe pivko neni " "tak dobre, kvuli tem blbym psum..." #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" "Nikdo mne nepos... neposloucha. Nekde - nevim to jiste - ale nekde pod " "kostelem je velka kupa zlata. Leskne se a sviti a jenom ceka na nekoho, kdo " "si to vezme." #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" "Vim, ze mas vlastni myslenky, a vim, ze tomu nebudes verit, ale ta zbran, co " "tu mas - to neni tak dobre proti tem velkym bestiim! Oh, nezajima mne, co " "rika Griswold, proste nemuzou udelat nic podobneho jako to delali kdysi..." #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" "Kdybych byl ty... a nejsem... ale kdybych byl, prodal bych to vsechno a " "rychle odcud vypadl. Ten kluk tam... Vzdycky ma neco dobryho, ale musis mu " "dat naky prachy jinak ti ani neukaze, co ma." #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "Uvedomuji si dusi v hledani odpovedi..." #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "Zadny uceny z nebe nespadl. Objevis-li svazek vedeni, zjisti, co rika. I " "kdybys mel jiz vedomosti o davnych tajemstvich popsanych v knize, pamatuj si " "- uroven mistrovstvi se vzdy muze zvysit." #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" "Nejvetsi sila ma casto nejkratsi zivot. Na svitku muzes nalezt zapsan davna " "slova moci. Sila techto svitku je v tom, ze je dokaze seslat i uplny " "zacatecnik i mistr. Jejich slabinou je, ze musi byt nejprve nahlas precteny " "a nikdy se ti neudrzi v mysli. Take bys mel vedet, ze tyto svitky mohou byt " "precteny pouze jednou, a proto je pouzivej s rozvahou." #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" "Ackoliv je zar nesrovnatelny, pouhy plaminek svice je nebezpecnejsi. Zadna " "energie, nezalezi jak silna, nemuze byt pouzita bez patricneho soustredeni. " "Kouzelne Hole s mnoha kouzly mohou byt nescetnekrat nabity magickou energii. " "Mam schopnost obnovit jejich silu - ale sam vis, ze nic neni zadarmo." #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "Mnozstvi nasich vedomosti zavisi na mnozstvi lidi. Najdes-li knihu ci " "svitek, ktery nemuzes rozlustit, nevahej a prines ho ke mne. Pokud ho " "pochopim, podelim se z tebou o to, co zjistim." #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "Pro muze, ktery zna pouze Zelezo, neni vetsi magie nez Ocel. Kovar Griswold " "je vice carodejem nez vi. Jeho schopnosti rozehrat ohen a kovat zelezo neni " "v teto zemi rovno." #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "Zkazenost ma silu podvodu, ale nevinnost vladne moci cistoty. Mlada zena " "Gillian ma ciste srdce a uprednostnuje potreby sve pramatky pred svymi. Boji " "se mne, ale je to pouze kvuli tomu, ze mi nerozumi." #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" "Truhla otevrena v temnote neskryva vetsi poklad nez, kdyz je otevrena na " "svetle. Vypravec Cain je zahadou, ale jen pro ty, kteri se nedivaji. Jeho " "vedomosti o tom, co lezi pod katedralou, jsou jeste vetsi nez si on sam " "uvedomuje." #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" "Cim vice veris jednomu muzi, tim horsi je pad. Farnham ztratil svou dusi, " "ale ne kvuli nejakemu demonovi. Ztratil ji, kdyz videl, jak Arcibiskup " "Lazarus zradil jeho pratele. Ma dulezite vedomosti, ale musis oddelit " "skutecnost od fantazie." #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" "Ruce, srdce a mysl mohou provadet zazraky, pokud jsou v dokonalem souladu. " "Lecitel Pepin se v tele vyzna tak, jak to ani ja nedokaze. Jeho schopnost " "uzdravovat nemoci a poraneni je jeste zvetsena tim, ze chape vyrobu elixiru " "a lektvaru. Je to velky spojenec, ktereho muzes v Tristramu mit." #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" "Nemuzeme videt mnoho z budoucnosti, ale az to prijde, prinese to dite. " "Chlapec Wirt ma na dusi cernotu, ale pro mesto nebo jeho obyvatele " "nepredstavuje hrozbu. Jeho tajne obchody se zlodejicky a podezrelymi cechy z " "blizkych mest mu zarucuje pristup k mnoha predmetum, ktere v Tristramu " "nejsou lehce dostupne. I kdyz muzes vycitat jeho metody, Wirt muze zajistit " "pomoc pro tvou bitvu proti pripravujici se Temnote." #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" "Hlinene zdi a doskova strecha nedelaji domov. Hostinsky Ogden slouzi ve " "meste k vice vecem nez mnohy chape. Poskytuje pristresek Gillian a jeji " "pramatce, udrzuje to, co zivot zanechal Farnhamovi, a pomaha vsem, kteri ve " "meste zustali, aby byl Tristram jako driv. Jeho krcma, a prosta poteseni, " "ktera se zde daji stale nalezt, obstaravaji zablesky zivota, jaky si lide " "pamatuji. Jsou to tyto vzpominky, ktere stale udrzuji jejich nadeji, ze " "uspejes." #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "Pssst... tady..." #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "Nikdo v Tristramu nema pouziti - nebo odbyt - pro vsechno, co najdes v " "labyrintu. Ani ja ne, i kdyz je tezke tomu uverit. \n" " \n" "Nekdy budes jenom ty schopen najitr vyuziti pro nektere ty veci." #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "Never vsemu, co rika ten opilec. Prilis mnoho piva zamlzilo jeho zrak a jeho " "zdravy rozum." #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" "Jestli sis toho jeste nevsiml, nekupuju nic z Tristramu. Jsem dovozcem " "kvalitniho zbozi. Jestli chces nabizet vetes, musis jit za Griswoldem, " "Pepinem nebo za tou carodejnici Adrii. Jsem si jist, ze skoci po vsem, co " "jim prineses..." #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Soudim, ze dluzim kovari zivot - co z neho zbylo. Jiste, Griswold mi nabidl " "uceni v kovarne a je docela prijemny chlapik, ale nikdy bych neziskal dost " "zlata na... dobre, reknu ri jenom, ze mam urcite plany, na ktere potrebuju " "hodne zlata." #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Kdybych byl o par let starsi, zahrnul bych ji vsim bohatstvim, co bych " "sehnal, a ujistuji te, ze se mohu dostat k opravdu pekne zbozi. Gillian je " "krasne devce, ktere by melo vypadnout z Tristramu hned, jak to bude " "bezpecne. Hmmm... mozna, ze ji vezmu s sebou, az pujdu..." #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" "Cain vi prilis hodne. Dost me otravuje - vic nez ta zenska za rekou. Porad " "mi vypravi, jak jsem stastny, ze jsem nazivu, a jak se muj pribeh prenasi v " "legendach. Myslim, ze mu trochu preskocilo." #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Farnham - to je clovek s vaznymi problemy a ja vim o vaznych problemech " "vsechno. Prilis veril v poctivost jednoho muze a Lazarus ho zavedl do sameho " "chrtanu smrti. Oh, ja vim, jake to je tam dole, tak mi nezacni povidat o " "svych planech na zniceni zla, ktere zije v tom Labyrintu. Jenom si davej " "pozor na nohy..." #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Dokud nepotrebujes neco pripojit, je stary Pepin hodne dobry. \n" " \n" "Kdybych tenkrat mel nektery z tech lektvaru, co vari, mohl jsem mit nohu..." #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Adria me opravdu trapi. Jiste, Cain ti muze rict hodne o minulosti, ale " "carodejka vidi do tvoji minulosti. Vzdycky najde nejaky zpusob, jak dostat " "vsechno, co chce. Adriinyma rukama proslo vice zbozi, nez jsem kdy videl " "projit branami Kralovskeho Bazaru behem Velke Slavnosti." #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" "Ogden je blazen, ze tu zustava. Mohl bych ho dostat z mesta za velmi " "rozumnou cenu, ale on trva na tom, ze povede tu hloupou hospodu. Alespon " "dava Gillian misto, kde muze pracovat, a jeho zena Garda pece skvely Ovcacky " "kolac..." #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" "Za Sinemi hrdinu lezi Komnata kosti. Vecna smrt ceka na kazdeho, kdo by se " "snazil ukrast poklady chranene v teto sini. Tak pravil Pan hruzy a tak jest " "to psano." #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" "...a tak, zamceno za Branou krve a Sinemi ohne, ceka Udatnost na Hrdinu " "svetla, aby procitlo..." #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Nemuzes videt to co ja.\n" "Oko se mlzi,obzor zakryva.\n" "Otocis se a jsou pryc,\n" "Slysis sumot a nic vic.\n" "Potom vidis, co nemuze byt,\n" "Stin se hybe, kde svetlo ma byt.\n" "Pryc z temna z myslenek tvych,\n" "Svrzen jsi do Sine slepych." #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Zbrojnice Pekla jsou domovem Vojevudce Krve. Za nim lezi zohavena tela " "tisicu. Andele i lide byli zabijeni, aby slouzili pri jeho nekonecnem " "obetovani Temnym, kteri prahnou pouze po jedine veci - po krvi." #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" "Pozor dej a svedc o pravdach, jez zde ctes, jelikoz jsou poslednim odkazem " "Horadrim. Prave ted zuri valka, ne na bitevnim poli, ktere zname - mezi " "vybornym kralovstvim Vysokeho Nebe a chaotickou dirou Horoucich Pekel. Tato " "valka jest znama jako Velky konflikt a zuri a plane dele nez hvezdy na " "obloze. Zadna ze stran nikdy neziskala prevahu na dlouhou dobu, kdyz sily " "Svetla a Temnoty souperi o vladu nad vsemi stvorenimi." #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" "Pozor dej a svedc o pravdach, jez zde ctes, jelikoz jsou poslednim odkazem " "Horadrim. Kdyz se Vecny konflikt mezi Vysokym Nebem a Horoucim Peklem " "presunul do zeme smrtelniku, byl nazyvan Hrisna Valka. Andele a Demoni " "kraceli v prevleku mezi lidmi, bojovali ve skrytu, vzdaleni od vsetecnych " "oci smrtelniku. Nekteri odvazni a mocni smrtelni se pripojili k nektere ze " "stran a pomahali urcovat vyvoj Hrisne Valky." #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Pozor dej a svedc o pravdach, jez zde ctes, jelikoz jsou poslednim odkazem " "Horadrim. Pred temer tremi stovkami let se stalo, ze Tri Prvotni Zla z " "Horoucich Pekel zahadne vstoupila do naseho sveta. Tri Bratri pustosili po " "desetileti zeme vychodu, zatimco se lide v jejich stopach trasli strachem. " "Nas Rad - Horadrim - byl zalozen tajnou skupinou magu, aby vypatral a " "uveznil Tri Zla pro jednou a naveky.\n" " \n" "Puvodne Horadrim uveznili dva ze Tri v mocnych artefaktech, jez jsou znamy " "jako Kameny duse, a pohrbili je hluboko pod piskem vychodnich pousti. Treti " "Zlo uniklo zajmuti a uprchlo na zapad, pronasledovano mnoha Horadrim. Treti " "Zlo - zname jako Diablo, Pan Hruzy - bylo nakonec dopadeno, jeho podstata " "ulozena do Kamenu duse a pohrbena v tomto Labyrintu.\n" " \n" "Budiz kazdy varovan, ze Kamen duse nesmi byt objeven lidmi bez viry. Bude-li " "Diablo uvolnen, vyhleda telo, jez muze jednoduse ovladat, protoze bude " "velice slaby - treba telo starce ci ditete." #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "I stalo se, ze v Horoucich Peklech propukla revoluce znama jako Temne " "vyhnanstvi. Nizsi Zla svrhla Tri Prvotni Zla a vyhostila jejich duchovni " "formy do rise smrtelniku. Demonove Belial (Pan Lzi) a Azmodan (Pan Hrichu) " "bojovali o ziskani vlady nad Peklem behem nepritomnosti Tri Bratru. Cele " "Peklo se rozdelilo na frakce Beliala a Azmodana, zatimco sily Vysokeho Nebe " "neustale dorazely az temer na same Brany Pekla." #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Mnoho demonu putovalo po risich smrtelniku a hledalo Tri Bratry. Tito demoni " "byli do smrtelne roviny nasledovani Andeli, kteri je pronasledovali v " "ohromnych mestech Vychodu. Andele se spojili s tajnym Radem smrtelnych magu, " "znamym jako Horadrim, kteri se rychle stali odborniky na patrani po " "demonech. Take si udelali mnoho temnych nepratel v podsveti." #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" "I stalo se, ze Tri Prvotni Zla byal v duchovni forme vyhnana do rise " "smrtelniku a po desetiletich rozsevani chaosu na Vychode, byla ulovena " "prokletym Radem smrtelniku Horadrim. Horadrim pouzili artefakty zvane Kameny " "duse, aby v nich zachytili podstatu Mefista, Pana Nenavisti, a jeho bratra " "Baala, Pana Zniceni. Nejmladsi z bratru - Diablo, Pan Hruzy - uprchl na " "zapad.\n" " \n" "Nakonec Horadrim uveznili i Diabla do Kamenu duse a pohrbili ho pod davnou, " "zapomenutou Katedralou. Tam Pan Hruzy spi a ocekava cas sveho znovuzrozeni. " "Vez, ze bude hledat telo mlade a nadane, aby ho ovladl - takove, ktere je " "nevinne a snadno ovladatelne. Pote povstane, aby uvolnil sve Bratry a znovu " "rozviril plameny Hrisne Valky..." #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" "Vsichni velebi Diabla - Pana Hruzy a Toho, jez prezil Temne vyhnanstvi. Kdyz " "se probudil z dlouheho spanku, muj Pan a Mistr se mnou mluvil o tajemstvich, " "jez zna pouze par smrtelniku. Rekl mi o vecne valce kralovstvi Vysokeho Nebe " "a diry Horoucich Pekel. Odhalil mi sily, ktere prinesly tento nesvar do rise " "lidi. Muj Pan nazval tuto bitvu o nas svet a vse, co zde existuje, Hrisna " "Valka." #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" "Slavu a podporu Diablovi - Panu Hruzy a Vudci Tri. Muj Pan mi rekl o svych " "dvou Bratrech, Mefistovi a Baalovi, kteri byli pred davnymi veky vyhnani do " "tohoto sveta. Muj Pan si preje vyckat na svuj cas a sbirat sve straslive " "sily, aby mohl osvobodit sve uveznene bratry z jejich hrobky pod poustemi " "vychodu. Jakmile muj Pan uvolni sve Bratry, Hrisna Valka opet pozna zurivost " "Tri." #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" "Slava a obetovani Diablovi- Panu Hruzy a Niciteli dusi. Kdyz jsem probudil " "meho Pana ze spanku, pokusil se ziskat smrtelnou podobu. Diablo zkusil " "ziskat telo Krale Leorica, ale muj Pan byl z uvezneni prilis slaby. Muj Pan " "pozadoval jednoduchou a nevinnou cestu do tohoto sveta a zjistil, ze je pro " "tento ukol nejvhodnejsi chlapec Albrecht. Zatimco dobry Kral Leoric zesilel " "po neuspesnem Diablove pokusu o jeho ovladnuti, unesl jsem jeho syna " "Albrechta a privedl ho pred meho Mistra. Nyni ocekavam Diablovo volani a " "preji si, abych byl odmenen, kdyz se konecne vynori jako Pan tohoto sveta." #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "Díky bohu, že ses vrátil!\n" "Hodně se toho změnilo od doby kdy jsi tu žil, příteli. Všude byl klid dokud " "nepřišli temní nájezdníci a nezničili naši vesnici. Mnozí byli na místě " "sťati a ti, kteří pozvedli zbraně, byli zabiti nebo odvlečeni, aby se stali " "otroky - nebo hůř. Kostel na konci mesta byl znesvěcen a je používán pro " "temné rituály. To vřískání co se rozléhá nocí je nelidské, ale nekteří " "našich vesničanů ještě možná přežijí. Jdi po cestě ležící mezi mojím " "hostincem a kovárnou, abys našel kostel, a zachraň koho můžeš. \n" " \n" "Snad ti řeknu více když si opět promluvíme. Hodně štěstí." #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" "Pokracuj ve svem ukolu. Hledani ztraceneho pokladu neni jednoduche. Najit " "poklad, ktery nekdo schoval je jednodussi. Necham te nad tim premyslet. " "Nedovol, aby cas zmatl tve hledani." #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" "Coze?! To je hloupost. Tady v Tristramu neni zadny zakopany poklad. Ukaz mi " "to!! Ah, podivej se na to. Tyhle kresby jsou nepresne. Neodpovidaji nasemu " "mestu. Pamatoval bych si na to, co lezi pod katedralou, ale ne co lezi pod " "nasimi kopci." #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" "Opravdu nemam cas, diskutovat s tebou o nejake mape. Mam mnoho nemocnych " "lidi, kteri potrebuji mou pomoc a ty taky." #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" "Pysny Iswall byl uveznen hluboko pod povrchem tohoto sveta. Jeho cest " "zmizela a vzhled se zmenil. Je uveznen v nekonecnych mukach a trapenich. " "Tajne doufa, ze se jednou stane velka vec, ktera ho osvobodi." #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" "Vsadim se, ze te Wirt videl prichazet a nahral to tak, aby se mohl pozdeji " "smat, az budes behat kolem mesta s nosem ve spine. Ignoroval bych to." #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" "Byly casy, kdy bylo tohle mesto castou zastavkou obchodniku z dalekeho " "okoli. Monoho se od tech dob zmenilo. Ale skryte jeskyne a pohrbene poklady " "jsou jenom detske povidacky. Wirt se malokdy oddava detskym hram. Mozna ze " "je to jenom jeho predstavivost." #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Poslouchej. Pojd bliz. Nevim, jestli vis co vim ja, ale ty mas opravdu jednu " "zajimavou vec. Tohle je mapa." #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Moje babicka mi casto vypravela pribehy o podivnych armadach, ktere obyvaly " "hrbitov kolem kostela. A mozna by te mohl zajimat jeden, ktery jsem slysela. " "Ona rikala, ze jestli nechas nejakou obet na hrbitove, vstoupis do " "katedraly, kde se budes modlit k mrtvym, a pak se vratis, tak ta obet by se " "mohla zmenit v neco docela jineho. Nevim, mozna ze to jsou pouze reci jedne " "stare nemocne zeny, ale v soucasne dobe je mozne vsechno." #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" "Hmmm. Obrovsky a tajemny poklad rikas? Mmmm. Mozna bych si od tebe par veci " "vzal. Nebo radeji, nepotrebujes nejake vzacne a drahe zasoby veci, s ktere " "ti muzou pomoci projit touhle zkouskou?" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "Tak ty jsi ten hrdina o kterem kazdy mluvi. Mozna bys mohl pomoci chudemu " "farmari z jeho problemu? Na konci tohoto ovocneho sadu, jizne odtud, se " "objevila nejaka podivna vec, ktera vyleza za zeme! Kvuli ni se nemuzu dostat " "k me urode ani pytlum se senem a moje kravy jsou hladove. Carodejnice mi " "dala tohle a rikala, ze to tu vec odstrani. Jestli muzes, jdi a znic to, " "budu ti vdecny. Udelal bych to sam, ale nekdo tady musi zustat s kravami..." #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Vedel jsem, ze to nebude tak jednoduche, jak carodejnice rikala. Je to " "spatny svet, kdyz nemuzes verit ani svym sousedum." #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "Hotovo? Poslal jsi to zpet do temnych zakouti Hadesova sveta? Coze? Oh, " "nerikej mi, ze jsi to ztratil! Tyhle veci nejsou jen tak k dostani, vis. " "Musis to najit a potom odstranit tu hruzu pryc z naseho mesta." #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" "Slysel jsem tu explozi! Mnohokrat ti dekuji cizince. Co se vsemi temito " "vecmi, co vylezaji ze zeme, monstry nicici chramy a tak dale, tohle je cas " "zkousek. Ja jsem jenom obycejny chudy farmar, ale tady -- vezmi si tohle a " "mnohokrat dekuji." #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" "Oh, takove problemy co mam... mozna... Ne, nemuzu te vyuzit, co tedy s temi " "dalsimi problemy. Mozna az vycistis kostel od nekterych priser, mohl by ses " "vratit... a usetrit svou pomoci trochu casu jednomu chudemu farmari?" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Waaaah! (smrka) Waaaah! (smrka)" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" "Ztratil jsem Thea! Meho nejlepsiho pritele! Hrali jsme si u reky, a Theo " "rikal, ze se chce jit podivat na tu velkou zelenou vec. Rikal jsem mu, ze " "bychom to nemeli delat, ale potom najednou vylezl ten BROUK! Utikali jsme " "pryc, ale Theo upadl a ten brouk ho chytil a odnesl pryc!" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" "Nasel jsi ho? Jdi najit Theodora, prosim te! Je jeste maly. Neumi se o sebe " "jeste postarat! Prosim te!" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "Ty jsi ho nasel! Nasel jsi ho! Dekuji ti! Oh Theo, vylekali te ti strasni " "brouci? Hey! Ugh! Tady je nejaka vec, ktera se na tebe lepi. Ick! Pojd Theo, " "pujdeme domu! Jeste jednou ti dekuji cizince!" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Dlouho jsme necinne lezeli a ted nastal cas se probudit. Po nasem dlouhem " "spanku mame velky hlad. Uz brzo, ted, prijde cas se nakrmit..." #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "Radoval ses, maly savce? Jak pateticke. Ten tvuj maly svet nebude mit zadnou " "sanci." #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Tyhle plochy budou znesveceny a nase rasa zaplavi vsechny pole, ktere volaji " "lidi domu. Nase upony obklopi tenhle svet a my budeme oslavovat na telech " "jeho obyvatel. Lide se stanou nasimi otroky a potravou." #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Ah, citim te... jsi blizko! Blizko! Ssss... vune krve a strachu... jak je to " "lakave..." #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "A v roce Zlateho Svetla bylo rozhodnuto o stavbe velke Katedraly. Zakladni " "kamen tohoto svateho mista byl vytesan z prusvitneho kamene Antyrael, ktery " "dostal jmeno po Andelovi, ktery sdilel svou silu s Horadrim. \n" " \n" "V Roce Barevnych stinu nastalo zemetreseni a Katedrala se narusila a spadla. " "Kdyz zacala stavba katakomb a vezi a muzi byli postaveni proti pustoseni " "Hrisne Valky, byly ruiny rozebrany kvuli potrebe kameni. A tak se stalo, ze " "zakladni kamen zmizel z jejich oci. \n" " \n" "Tento kamen byl soucast tohoto sveta -- a soucast vsech svetu -- stejne jako " "Svetlo je na pocatku vsech veci i na konci vsech veci. Svetlo a jednota jsou " "produkty tohoto svateho zrizeni, jednota vsech smyslu a jednota vlastnictvi." #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "Muu." #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "Rikam, Muuu." #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "Podivej, jsem krava, OK?" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" "Dobre, dobre. Nejsem opravdova krava. Normalne tohle nedelam; ale sedel jsem " "takhle doma, premyslel o obchode a najednou zacaly pres podlahu vylezat " "vsechny tyhle divne veci & brouci & rostliny... bylo to silene! Kdybych mel " "neco normalniho na sebe, nebylo by to tak hrozne. Hey! Mohl by jsi zajit ke " "mne domu a prinest mi obleceni? To hnede, ne to sedive, to je na vecer. " "Udelal bych to sam, ale nechci, aby me takhle nekdo videl. Tady, vezmi si " "tohle. Mozna to budes potrebovat... na zabiti tech veci, ktere mi rostou na " "zahrade. Muj dum nemuzes minout, je na jihu od mista, kde se reka " "rozdeluje... vsak uvidis... je to ten, u ktereho je zarostla zahradka." #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "Proc plytvas casem? Jdi hledat muj oblek! A rychle! Tamhleten Holstein se na " "mne porad diva!" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Hej, donesl jsi mi muj oblek? Rychle, nevsimej si toho. Tyhle usi svedi " "stejne jako ty neveris!" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" "Ne ne ne ne! Tohle je SEDIVY oblek! Ten je na večer! Tenhle si NEMUZU " "obleci. Copak jsi to za hlupaka? Potrebuju HNEDY oblek." #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" "Ahh, to je mnohem lepsi. Whew! Konecne, nejaka dustojnost! Mam rohy rovne? " "Dobre. Podivej, diky, ze jsi mi pomohl. Tady, vezmi si to jako malou odmenu. " "A jeste jeden maly modni tip... muzes pouzit maly... muzes pouzit novy... " "vis co myslim? Cely motiv dobrodruha je tak... retro. Takova mala rada, ze? " "Ciao." #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Podivej. Ja jsem krava. A ty jsi navnada pro prisery. Naber par zkusenosti a " "pak si promluvime..." #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Musi to opravdu byt strasny ukol, ktery jsem pred tebe postavil. Kdybych ti " "mohl jakkoliv... pomohla by ti sklenice pekneho, cerstveho mleka?" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "Oh, mozna bys mi mohl pomoci, ale snad az pote, co vycistis katakomby od " "tehletech nevericich bestii." #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Potrebuji neco udelat, ale nemuzu duverovat cizinci. Mozna az tady budes " "nejaky cas, budu se citit lepe, kdyz s tebou budu mluvit." #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "Vidim v tobe velike moznosti. Mozna nekdy, az budes znat svuj osud, mohl by " "ses u mne zastavit a udelat mi jednu laskavost?" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" "Myslim, ze bys mi pravdepodobne mohl pomoci, ale snad az budes trosku " "silnejsi. Nemuzu dopustit, aby padla jedina sance nasi vesnice na zniceni " "toho zla v chramu!" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Ja jsem samorostla krava. Udelej neco se sebou a... potom si promluvime." #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "Nebudu se zpovidat kazdemu turistovi, ktery pujde kolem! Nemas zabit par " "monster? Mozna si promluvime pozdeji. Pokud budes jeste nazivu..." #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" "Prestan me otravovat. Hledam opravdoveho hrdinu. A to ty nejsi. Ty jsi jen " "potrava pro prisery... Potrebuji nejakeho zkuseneho hrdinu." #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" "Dobra. Nechci te plest. Sedel jsem takhle doma, kdyz najednou se zacalo " "vsechno menit. Obrovske stada priser se zacala valit z podlahy! Zpanikaril " "jsem. Jedine, co jsem byl schopen udelat, bylo obleknout si tuhle Jersey " "kdyz jsem utikal ze dveri. No a ted vypadam takhle smesne. Kdybych ale mel " "neco normalniho na sebe, nebylo by to tak zle. Hey! Mohl bys zajit ke mne " "domu a donest mi obleceni? To hnede, ne to sedive, to je na vecer. Udelal " "bych to sam, ale nechci, aby me takhle nekdo videl. Tady, vezmi si tohle. " "Mozna to budes potrebovat... na zabiti tech veci, ktere mi rostou na " "zahrade. Muj dum nemuzes minout, je na jihu od mista, kde se reka " "rozdeluje... vsak uvidis... je to ten, u ktereho je zarostla zahradka." #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "Vyzkousel jsem kouzla, hrozby, prisahy a smlouvani s temihle odpornymi tvory " "-- bez uspechu. Moje metody na zotroceni mensich priser nemaji na techto " "bestiich zadny uspech." #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" "Muj domov se pomalu kazi, diky temhletem nezvanym veznum. Krypty jsou plne " "stinu, ktere se pohybuji pouze v zakoutich me mysli. Nejasne naznaky drapu " "tanci na okrajich meho sluchu. Hledaji, myslim si to, hledaji novinky." #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" "Mezi recmi jim uniklo jeho jmeno -- Na-Krul. Pokusil jsem se prozkoumat " "tohle jmeno, ale mali demoni mi nejak znicili mou knihovnu. Na-Krul... To " "jmeno me naplnuje obavami. Radeji o nem budu uvazovat jako o Prisere. Je to " "lepsi nez jeho prave jmeno." #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" "Sileny vriskot zajatych priser mi neda spat. Jsou nastvane na toho, kdo je " "vyslal do Pustiny a svolavaji na me kletby za to, ze je tady veznime. Tyhle " "jejich slova naplnuji me srdce strachem a nedari se mi zbavit se tech hlasu." #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" "Cas mi rychle ubiha. Musim zaznamenat zpusob, jak oslabit demona a pak ukryt " "tenhle text, pro pripad, ze by jeho prisluhovaci nasli zpusob, jak pomoci " "mych znalosti osvobodit sveho vudce. Doufam, ze ten kdo tohle najde bude " "hledat vedomost." #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Kdokoliv nasel tenhle svitek, je schopen porazit demonickou stvuru, ktera je " "za touto zdi. Muj cas vyprsel. Prave ted, jejich prisluhovac skrabe na " "dvere, vedle kterych se schovavam. \n" " \n" "Zpoutal jsem demona posvatnym kouzlem a uveznil ho za silnou zdi, ale citim, " "ze to nestaci. \n" " \n" "Tri listiny nalezene u mne se postaraji o odstraneni tohoto chraneneho " "vstupu do jeho kralovstvi, ale pouze pokud se pouziji ve spravnem poradi. " "Paky na vstupni ceste odstrani barieru a osvobodi demona; nedotykej se jich! " "Pouzij pouze svitky k dosazeni vstupu jinak jeho sila bude velika a ty ho " "nebudes moci porazit." #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "Praedictum Otium." #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "Pekelné Prase" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "Žihadlo" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "Psychokoule" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "Arachnid" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "Padlá Dvojčata" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "Horkův Potěr" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "Jedovatý Ocas" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "Nekrokoule" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "Pavoučí Pán" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "Šlehající Červ" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "Hořák" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "Pekelný Brouk" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "Hrobník" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "Hrobová Krysa" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "Ohnivý Netopýr" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "Okřídlená Lebka" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "Lich" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "Démon z Krypty" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "Pekelný Netopýr" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "Kostěný Démon" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "Arci-Lich" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "Biklop" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "Věc z Masa" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "Smrťák" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "Obrův Kloub" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "Rtuťový Prsten" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "Xorinin Prsten" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "Karikův Prsten" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "Prsten Magmatu" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "Prsten Mystiků" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "Prsten Hromu" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "Ochranný Amulet" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "Komáří Bodnutí" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "Spalovač" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "Brnění Pochmurnosti" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "Bleskovník" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "Zahřmění" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "Bílá Čepel" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "Pojídač Duší" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "Diamantové Ostří" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "Kostěné Kroužkové Brnění" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "Démonické Plátové Brnění" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "Amulet Ministranta" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "Gladiátorův Prsten" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "Šaškův" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "Krystalický" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "Dvojníkův" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "devastace" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "rozkladu" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "nebezpečí" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "Mana" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "od Mága" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "od Šaška" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "Blesková zeď" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "Upálení" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "Warp" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "Odražení" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "Berserk" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "Ohnivý Prstenec" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "Hledání" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "Runa Ohně" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "Runa Světla" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "Runa Novy" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "Runa Upálení" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "Runa Kamene" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "," #~ msgid "Options:" #~ msgstr "Nastavení:" #~ msgid "version {:s}" #~ msgstr "verze {:s}" #~ msgid "Decrease Gamma" #~ msgstr "Snížit Jas" #~ msgid "Increase Gamma" #~ msgstr "Zvýšit Jas" #~ msgid "No automap available in town" #~ msgstr "Automapa není dostupná ve městě" #~ msgid "Restart In Town" #~ msgstr "Restartuj Hru ve Městě" #~ msgid "Heart" #~ msgstr "Srdce" #~ msgid "recover life" #~ msgstr "doplní životy" #~ msgid "deadly heal" #~ msgstr "smrtící léčení" #~ msgid "decrease strength" #~ msgstr "sníží sílu" #~ msgid "decrease dexterity" #~ msgstr "sníží obratnost" #~ msgid "decrease vitality" #~ msgstr "sníží vitalitu" #~ msgid "you can't heal" #~ msgstr "nemůžeš se léčit" #~ msgid "hit monster doesn't heal" #~ msgstr "zasažený nepřítel se nemůže léčit" #~ msgid "Faster attack swing" #~ msgstr "Rychlejší útočný švih" #~ msgid "see with infravision" #~ msgstr "máš infra vidění" #~ msgid "Trying to drop a floor item?" #~ msgstr "Zkoušíš zahodit předmět na zemi?" #~ msgid "" #~ "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " #~ "frame. Disabling it can help with mouse lag on some systems." #~ msgstr "" #~ "Vynutí čekání na Vertikální Synchronizaci. Zabrání efektu trhání " #~ "obrazovky. Vypnutí může pomoct se zpožděním myši na některých systémech." #~ msgid "FPS Limiter" #~ msgstr "Omezovač FPS" #~ msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." #~ msgstr "" #~ "Omezení FPS zabrání vysokému vytížení CPU. Limit počítá s rychlostí " #~ "obnovení." #~ msgid "To hit" #~ msgstr "Šance na zásah" #~ msgid "Failed to open player archive for writing." #~ msgstr "Nepodařilo se otevřít archiv hráče k zápisu." #~ msgid "Unable to read to save file archive" #~ msgstr "Nelze přečíst z archivu uložené hry" #~ msgid "Unable to write to save file archive" #~ msgstr "Nelze zapsat do archivu uložené hry" #~ msgid "Indestructible, " #~ msgstr "Nezničitelný, " #~ msgid "No required attributes" #~ msgstr "Požadavky: nic" #~ msgid "" #~ "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits " #~ "any who would seek to steal the treasures secured within this room. So " #~ "speaks the Lord of Terror, and so it is written." #~ msgstr "" #~ "Za Sinemi hrdinu lezi Komnata kosti. Vecna smrt ceka na kazdeho, kdo by " #~ "se snazil ukrast poklady chranene v teto sini. Tak pravil Pan hruzy a tak " #~ "jest to psano." #~ msgid "" #~ "The armories of Hell are home to the Warlord of Blood. In his wake lay " #~ "the mutilated bodies of thousands. Angels and man alike have been cut " #~ "down to fulfill his endless sacrifices to the Dark ones who scream for " #~ "one thing - blood." #~ msgstr "" #~ "Zbrojnice Pekla jsou domovem Vojevudce Krve. Za nim lezi zohavena tela " #~ "tisicu. Andele i lide byli zabijeni, aby slouzili pri jeho nekonecnem " #~ "obetovani Temnym, kteri prahnou pouze po jedine veci - po krvi." #~ msgid "" #~ "Cloudy and cooler today. Casting the nets of necromancy across the void " #~ "landed two new subspecies of flying horror; a good day's work. Must " #~ "remember to order some more bat guano and black candles from Adria; I'm " #~ "running a bit low." #~ msgstr "" #~ "Dneska je chladno a oblacno. Vrhani magickych siti na nove druhy " #~ "letajicich priser, to je dobra prace na dnesek. Musim si ale objednat " #~ "vice netopyru a cernych svici od Adrie. Zacinaji mi dochazet." ================================================ FILE: Translations/da.po ================================================ # Translation of DevilutionX to Danish # Anders Jenbo , 2021. # msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:19+0200\n" "PO-Revision-Date: 2025-10-02 15:19+0200\n" "Last-Translator: Anders Jenbo \n" "Language-Team: \n" "Language: da\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Spildesign" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Senior designere" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Yderligere design" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Ledende programmør" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Senior programmører" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Programmering" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Særlige gæsteprogrammører" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Battle.net-programmering" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Seriel kommunikationsprogrammering" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Installationsprogrammering" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Kunst instruktører" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Kunst" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Teknisk kunst" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Filmregissører" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D filmisk kunst" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Filmisk teknisk kunst" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Overordnet instruktør" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Instruktør" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Associeret producent" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Strike Team" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Musik" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Lyddesign" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Filmisk musik og lyd" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Stemmeproduktion, retning og casting" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Manuskript og historie" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Stemmeredigering" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Stemmer" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Optagelsesingeniør" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Design af manual og layout" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Manualkunst" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Foreløbig direktør for QA (Lead tester)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "QA-angrebsteam (testere)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "QA-frømandskorps (kompatibilitetstestere)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "QA-artilleristøtte (ekstra testere) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "QA-modintelligens" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Bestilling af netværksinformationstjenester" #: Source/DiabloUI/credits_lines.cpp:136 #, fuzzy #| msgid "Support" msgid "Customer Support" msgstr "Support" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Salg" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Drønnert" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Hr. Dabiris baggrundsvokalister" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "PR" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Marketing" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Internationalt salg" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "U.S.-salg" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Produktion" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Juridisk & forretning" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Særlig tak til" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Tak til" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "Til minde om" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Meget Særlig tak til" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "" #: Source/DiabloUI/credits_lines.cpp:223 #, fuzzy #| msgid "Art Directors" msgid "Art Director" msgstr "Kunst instruktører" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "" #: Source/DiabloUI/credits_lines.cpp:230 #, fuzzy #| msgid "Game Design" msgid "Design" msgstr "Design af manual og layout" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "" #: Source/DiabloUI/credits_lines.cpp:240 #, fuzzy msgid "Quality Assurance Lead" msgstr "Foreløbig direktør for QA (Lead tester)" #: Source/DiabloUI/credits_lines.cpp:243 #, fuzzy msgid "Testers" msgstr "QA-artilleristøtte (ekstra testere) " #: Source/DiabloUI/credits_lines.cpp:248 #, fuzzy #| msgid "Manual Artwork" msgid "Manual" msgstr "Design af manual og layout" #: Source/DiabloUI/credits_lines.cpp:257 #, fuzzy #| msgid "Additional Design" msgid "\tAdditional Work" msgstr "Yderligere design" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 #, fuzzy #| msgid "Thanks To" msgid "Thanks to" msgstr "Tak til" #: Source/DiabloUI/credits_lines.cpp:267 #, fuzzy #| msgid "Copyright © 1996-2001 Blizzard Entertainment" msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "Meget Særlig tak til" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:277 #, fuzzy msgid "Quality Assurance Lead Tester" msgstr "Foreløbig direktør for QA (Lead tester)" #: Source/DiabloUI/credits_lines.cpp:280 #, fuzzy msgid "Main Testers" msgstr "QA-angrebsteam (testere)" #: Source/DiabloUI/credits_lines.cpp:283 #, fuzzy #| msgid "Additional Design" msgid "Additional Testers" msgstr "QA-artilleristøtte (ekstra testere) " #: Source/DiabloUI/credits_lines.cpp:288 #, fuzzy msgid "Product Marketing Manager" msgstr "Marketing" #: Source/DiabloUI/credits_lines.cpp:291 #, fuzzy #| msgid "Public Relations" msgid "Public Relations Manager" msgstr "PR" #: Source/DiabloUI/credits_lines.cpp:294 #, fuzzy #| msgid "Associate Producer" msgid "Associate Product Manager" msgstr "Associeret producent" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Ringen af et tusinde" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tIngen sjæle blev solgt i fremstillingen af dette spil." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "OK" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Vælg klasse" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Annuller" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Ny multiplayer-helt" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Ny singleplayer-helt" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Gem filen findes" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Indlæs spil" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Nyt spil" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Singleplayer-karakterer" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Skytten og Troldmanden er kun tilgængelige i den fulde detailversion af " "Diablo. Besøg https://www.gog.com/game/diablo for at købe den." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Indtast navn" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Ugyldigt navn. Et navn kan ikke indeholde mellemrum, reserverede tegn eller " "reserverede ord.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Kunne ikke oprette karakter." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Niveau:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Styrke:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Magi:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Smidighed:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Vitalitet:" #: Source/DiabloUI/hero/selhero.cpp:515 #, fuzzy #| msgid "Save Game" msgid "Savegame:" msgstr "Gem spil" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Vælg helt" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Ny helt" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Slet" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Multiplayer-karakteren" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Slet multiplayer-helt" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Slet singleplayer-helt" #: Source/DiabloUI/hero/selhero.cpp:617 #, fuzzy, c++-format #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Singleplayer" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Multiplayer" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Support" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Vis kredit" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Afslut Hellfire" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Afslut Diablo" #: Source/DiabloUI/mainmenu.cpp:71 #, fuzzy #| msgid "Pepin the Healer" msgid "Shareware" msgstr "Healeren Pepin" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Klient-server (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 #, fuzzy msgid "Offline" msgstr "Loopback" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Multiplayer-spil" #: Source/DiabloUI/multi/selconn.cpp:74 #, fuzzy #| msgid "Requirements:" msgid "Requirements:" msgstr "Krav:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "ingen gateway" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Vælg forbindelse" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Skift gateway" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Alle computere skal være tilsluttet et TCP-kompatibelt netværk." #: Source/DiabloUI/multi/selconn.cpp:126 #, fuzzy #| msgid "All computers must be connected to a TCP-compatible network." msgid "All computers must be connected to the internet." msgstr "Alle computere skal være tilsluttet et TCP-kompatibelt netværk." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Spil alene uden nogen netværkseksponering." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Understøttede spillere: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "" #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Vælg handling" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Opret spil" #: Source/DiabloUI/multi/selgame.cpp:164 #, fuzzy #| msgid "Create Game" msgid "Create Public Game" msgstr "Opret spil" #: Source/DiabloUI/multi/selgame.cpp:165 #, fuzzy #| msgid "Join TCP Games" msgid "Join Game" msgstr "Spildesign" #: Source/DiabloUI/multi/selgame.cpp:169 #, fuzzy #| msgid "Quit Diablo" msgid "Public Games" msgstr "Indlæs spil" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "" #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "ANNULLER" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Opret et nyt spil med sværhedsgrad efter eget valg." #: Source/DiabloUI/multi/selgame.cpp:232 #, fuzzy #| msgid "Create a new game with a difficulty setting of your choice." msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "Opret et nyt spil med sværhedsgrad efter eget valg." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Normal" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Mareridt" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Helvede" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "" #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Vælg sværhedsgrad" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Deltag i et {:s}-spil" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:366 #, fuzzy #| msgid "Enter Name" msgid "Enter address" msgstr "Indtast adgangskode" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Normal sværhedsgrad\n" "Det er her en nybegynder skal starte sin stræben efter at besejre Diablo." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Mareridt sværhedsgrad\n" "Borgerne i labyrinten er styrket og vil vise sig at være en større " "udfordring. Dette anbefales kun til erfarne helte." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Helvede sværhedsgrad\n" "De mest magtfulde af underverdenens væsener lurer ved indgangen til helvede. " "Kun de mest erfarne helte skal vove sig ind i denne verden." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Din karakter skal nå niveau 20, før du kan deltage i et multiplayer-spil med " "mareridt-sværhedsgrad." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Din karakter skal nå niveau 30, før du kan deltage i et multiplayer-spil med " "helvede-sværhedsgrad." #: Source/DiabloUI/multi/selgame.cpp:508 #, fuzzy msgid "Select Game Speed" msgstr "Vælg forbindelse" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:531 #, fuzzy #| msgid "" #| "Normal Difficulty\n" #| "This is where a starting character should begin the quest to defeat " #| "Diablo." msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Normal sværhedsgrad\n" "Det er her en nybegynder skal starte sin stræben efter at besejre Diablo." #: Source/DiabloUI/multi/selgame.cpp:535 #, fuzzy #| msgid "" #| "Nightmare Difficulty\n" #| "The denizens of the Labyrinth have been bolstered and will prove to be a " #| "greater challenge. This is recommended for experienced characters only." msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Mareridt sværhedsgrad\n" "Borgerne i labyrinten er styrket og vil vise sig at være en større " "udfordring. Dette anbefales kun til erfarne helte." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Indtast adgangskode" #: Source/DiabloUI/selstart.cpp:49 #, fuzzy #| msgid "Exit Hellfire" msgid "Enter Hellfire" msgstr "Afslut Hellfire" #: Source/DiabloUI/selstart.cpp:50 #, fuzzy #| msgid "Exit Diablo" msgid "Switch to Diablo" msgstr "Diablo Strike Team" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Ja" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Nej" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "" #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "" #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Forrige menu" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" #: Source/DiabloUI/title.cpp:67 #, fuzzy #| msgid "Copyright © 1996-2001 Blizzard Entertainment" msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Copyright © 1996-2001 Blizzard Entertainment2" #: Source/appfat.cpp:63 msgid "Error" msgstr "Fejl" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/appfat.cpp:83 #, fuzzy msgid "Data File Error" msgstr "Skrivebeskyttet mappe fejl" #: Source/appfat.cpp:84 #, fuzzy, c++-format #| msgid "" #| "Unable to open {:s}.\n" #| "\n" #| "Make sure that it is in the game folder and that the file name is in all " #| "lowercase." msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Kan ikke åbne {:s}.\n" "\n" "Sørg for, at den findes i spilmappen, og at filnavnet er med små bogstaver." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Skrivebeskyttet mappe fejl" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/automap.cpp:1416 #, fuzzy msgid "Game: " msgstr "Multiplayer-spil" #: Source/automap.cpp:1424 #, fuzzy msgid "Offline Game" msgstr "Loopback" #: Source/automap.cpp:1426 #, fuzzy msgid "Password: " msgstr "Indtast adgangskode" #: Source/automap.cpp:1429 #, fuzzy #| msgid "Quit Diablo" msgid "Public Game" msgstr "Indlæs spil" #: Source/automap.cpp:1443 #, fuzzy, c++-format msgid "Level: Nest {:d}" msgstr "Understøttede spillere: {:d}" #: Source/automap.cpp:1446 #, fuzzy, c++-format msgid "Level: Crypt {:d}" msgstr "Understøttede spillere: {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 #, fuzzy msgid "Town" msgstr "Genstart i byen" #: Source/automap.cpp:1452 #, fuzzy, c++-format #| msgid "Level:" msgid "Level: {:d}" msgstr "Niveau:" #: Source/control.cpp:203 msgid "Tab" msgstr "" #: Source/control.cpp:203 msgid "Esc" msgstr "" #: Source/control.cpp:203 #, fuzzy #| msgid "Enter Name" msgid "Enter" msgstr "Indtast navn" #: Source/control.cpp:206 #, fuzzy msgid "Character Information" msgstr "Kunne ikke oprette karakter." #: Source/control.cpp:207 msgid "Quests log" msgstr "" #: Source/control.cpp:208 msgid "Automap" msgstr "" #: Source/control.cpp:209 #, fuzzy msgid "Main Menu" msgstr "Forrige menu" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "" #: Source/control.cpp:211 msgid "Spell book" msgstr "" #: Source/control.cpp:212 msgid "Send Message" msgstr "" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr "" #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "" #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "" #: Source/control.cpp:653 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "What arena do you want to visit?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "" #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "" #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "" #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "" #: Source/control.cpp:800 msgid "[command]" msgstr "" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "" #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "" #: Source/control.cpp:802 msgid "" msgstr "" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "" #: Source/control.cpp:803 #, fuzzy msgid "" msgstr "Ringen af et tusinde" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "" #: Source/control.cpp:1311 #, fuzzy msgid "Player friendly" msgstr "Multiplayer-spil" #: Source/control.cpp:1313 #, fuzzy msgid "Player attack" msgstr "Singleplayer" #: Source/control.cpp:1316 #, fuzzy, c++-format msgid "Hotkey: {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "" #: Source/control.cpp:1331 #, fuzzy msgid "Hotkey: 's'" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, fuzzy, c++-format msgid "{:s} Skill" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, fuzzy, c++-format msgid "{:s} Spell" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, fuzzy, c++-format msgid "Spell Level {:d}" msgstr "Understøttede spillere: {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Scroll of {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, fuzzy, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "Understøttede spillere: {:d}" msgstr[1] "Understøttede spillere: {:d}" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Staff of {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, fuzzy, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "Understøttede spillere: {:d}" msgstr[1] "Understøttede spillere: {:d}" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, fuzzy, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "Understøttede spillere: {:s}" msgstr[1] "Understøttede spillere: {:s}" #: Source/control.cpp:1489 #, fuzzy #| msgid "Requirements:" msgid "Requirements not met" msgstr "Krav:" #: Source/control.cpp:1518 #, fuzzy, c++-format msgid "{:s}, Level: {:d}" msgstr "Niveau:" #: Source/control.cpp:1519 #, fuzzy, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Understøttede spillere: {:d}" #: Source/control.cpp:1525 #, fuzzy msgid "Right click to inspect" msgstr "Skrivebeskyttet mappe fejl" #: Source/control.cpp:1573 #, fuzzy #| msgid "Level:" msgid "Level Up" msgstr "Niveau:" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "" msgstr[1] "" #: Source/cursor.cpp:621 #, fuzzy msgid "Town Portal" msgstr "Genstart i byen" #: Source/cursor.cpp:622 #, fuzzy, c++-format msgid "from {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "" #: Source/cursor.cpp:636 #, fuzzy msgid "level 15" msgstr "Niveau:" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, fuzzy, c++-format #| msgid "Unable to create character." msgid "Unable to load data from file {0}" msgstr "Kunne ikke oprette karakter." #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 #, fuzzy msgid "Print this message and exit" msgstr "Afslut Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 #, fuzzy msgid "Print the version and exit" msgstr "Afslut Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 #, fuzzy msgid "Specify the location of diablo.ini" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 #, fuzzy msgid "Record a demo file" msgstr "Gem filen findes" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 #, fuzzy msgid "Play a demo file" msgstr "Gem filen findes" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 #, fuzzy #| msgid "Exit Hellfire" msgid "Force Hellfire mode" msgstr "Afslut Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 #, fuzzy msgid "Hellfire options:" msgstr "Indstillinger" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1551 #, fuzzy msgid "-- Network timeout --" msgstr "Bestilling af netværksinformationstjenester" #: Source/diablo.cpp:1552 #, fuzzy msgid "-- Waiting for players --" msgstr "Understøttede spillere: {:d}" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, fuzzy, c++-format #| msgid "Buy items" msgid "Belt item {}" msgstr "Køb varer" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "" #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "" #: Source/diablo.cpp:1809 #, fuzzy #| msgid "Previous Menu" msgid "Previous quick spell" msgstr "Forrige menu" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 msgid "Next quick spell" msgstr "" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "" #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "" #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 #, fuzzy msgid "Speedbook" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 #, fuzzy msgid "Open Speedbook." msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 #, fuzzy #| msgid "Save Game" msgid "Saves the game." msgstr "Gem spil" #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 #, fuzzy #| msgid "Load Game" msgid "Loads the game." msgstr "Indlæs spil" #: Source/diablo.cpp:1866 #, fuzzy #| msgid "Quit Diablo" msgid "Quit game" msgstr "Indlæs spil" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "" #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "" #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "" #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "" #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "" #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "" #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 #, fuzzy msgid "Character" msgstr "Kunne ikke oprette karakter." #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "" #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "" #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 #, fuzzy msgid "Spellbook" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 #, fuzzy msgid "Open Spellbook." msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "" #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "" #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "" #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "" #: Source/diablo.cpp:2004 msgid "Pause Game (Alternate)" msgstr "" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy msgid "Decrease Brightness" msgstr "Styrke:" #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "" #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy msgid "Increase Brightness" msgstr "Styrke:" #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 #, fuzzy msgid "Increase screen brightness." msgstr "Styrke:" #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "" #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "" #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 #, fuzzy #| msgid "Game Design" msgid "Game info" msgstr "Spildesign" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "" #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, fuzzy, c++-format msgid "{:s} {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "" #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 msgid "Sort Inventory" msgstr "" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" #: Source/diablo.cpp:2144 #, fuzzy #| msgid "Select Action" msgid "Secondary action" msgstr "Vælg handling" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "" #: Source/diablo.cpp:2159 #, fuzzy #| msgid "Select Action" msgid "Spell action" msgstr "Vælg handling" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "" #: Source/diablo.cpp:2174 #, fuzzy #| msgid "Cancel" msgid "Cancel action" msgstr "Annuller" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "" #: Source/diablo.cpp:2200 msgid "Move up" msgstr "" #: Source/diablo.cpp:2201 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character up." msgstr "Multiplayer-karakteren" #: Source/diablo.cpp:2206 msgid "Move down" msgstr "" #: Source/diablo.cpp:2207 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character down." msgstr "Multiplayer-karakteren" #: Source/diablo.cpp:2212 msgid "Move left" msgstr "" #: Source/diablo.cpp:2213 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character left." msgstr "Multiplayer-karakteren" #: Source/diablo.cpp:2218 msgid "Move right" msgstr "" #: Source/diablo.cpp:2219 #, fuzzy #| msgid "Multi Player Characters" msgid "Moves the player character right." msgstr "Multiplayer-karakteren" #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "" #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "" #: Source/diablo.cpp:2310 msgid "Automap Move Up" msgstr "" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 msgid "Automap Move Down" msgstr "" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 msgid "Automap Move Left" msgstr "" #: Source/diablo.cpp:2323 msgid "Moves the automap left when active." msgstr "" #: Source/diablo.cpp:2328 msgid "Automap Move Right" msgstr "" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "" #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "" #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "" #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "" #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "" #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 #, fuzzy msgid "Right mouse click" msgstr "Skrivebeskyttet mappe fejl" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "" #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "" #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "" #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 #, fuzzy msgid "Toggle game menu" msgstr "Multiplayer-spil" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 #, fuzzy #| msgid "Save Game" msgid "Opens the game menu." msgstr "Gem spil" #: Source/diablo_msg.cpp:63 #, fuzzy #| msgid "Game Design" msgid "Game saved" msgstr "Spildesign" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "" #: Source/diablo_msg.cpp:67 #, fuzzy msgid "Not enough space to save" msgstr "Gem filen findes" #: Source/diablo_msg.cpp:68 #, fuzzy msgid "No Pause in town" msgstr "Pause" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "" #: Source/diablo_msg.cpp:71 #, fuzzy msgid "No pause in multiplayer" msgstr "Pause" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 #, fuzzy msgid "Wherever you go, there you are" msgstr "Har du lyst til:" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "" #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "" #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 #, fuzzy msgid "There's no place like home." msgstr "Forlad healers hjem" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 #, fuzzy msgid "You feel stronger." msgstr "Har du lyst til:" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 #, fuzzy msgid "You feel wiser." msgstr "Har du lyst til:" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 #, fuzzy msgid "You feel refreshed." msgstr "Har du lyst til:" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "" #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, fuzzy, c++-format #| msgid "Select Difficulty" msgid "{} difficulty" msgstr "Vælg sværhedsgrad" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 #, fuzzy msgid "In Menu" msgstr "Forrige menu" #: Source/dvlnet/loopback.cpp:117 #, fuzzy #| msgid "loopback" msgid "loopback" msgstr "Loopback" #: Source/dvlnet/tcp_client.cpp:112 #, fuzzy #| msgid "Unable to create character." msgid "Unable to connect" msgstr "Kunne ikke oprette karakter." #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq eller spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" #: Source/engine/demomode.cpp:181 Source/options.cpp:535 #, fuzzy #| msgid "Select Action" msgid "Resolution" msgstr "Vælg handling" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 #, fuzzy #| msgid "Restart In Town" msgid "Run in Town" msgstr "Genstart i byen" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 #, fuzzy msgid "Auto Oil Pickup" msgstr "Pause" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 #, fuzzy msgid "Auto Pickup in Town" msgstr "Pause" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Indstillinger" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Gem spil" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy msgid "Exit to Main Menu" msgstr "Forrige menu" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 #, fuzzy #| msgid "Quit Diablo" msgid "Quit Game" msgstr "Indlæs spil" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Gamma" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Musik deaktiveret" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Lyd" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Lyd deaktiveret" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Pause" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "" #: Source/help.cpp:33 #, fuzzy msgid "S: Open Speedbook" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "" #: Source/help.cpp:49 msgid "$Movement:" msgstr "" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" #: Source/help.cpp:53 msgid "$Combat:" msgstr "" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" #: Source/help.cpp:70 msgid "$Gold:" msgstr "" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" #: Source/help.cpp:88 #, fuzzy msgid "$Setting Spell Hotkeys:" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" #: Source/help.cpp:94 #, fuzzy msgid "$Spell Books:" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" #: Source/help.cpp:200 #, fuzzy msgid "Shareware Hellfire Help" msgstr "Afslut Hellfire" #: Source/help.cpp:200 #, fuzzy #| msgid "Exit Hellfire" msgid "Hellfire Help" msgstr "Afslut Hellfire" #: Source/help.cpp:202 #, fuzzy msgid "Shareware Diablo Help" msgstr "Diablo Strike Team" #: Source/help.cpp:202 #, fuzzy msgid "Diablo Help" msgstr "Afslut Diablo" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "" #: Source/init.cpp:130 #, fuzzy #| msgid "Unable to create character." msgid "Unable to create main window" msgstr "Kunne ikke oprette karakter." #: Source/inv.cpp:2228 msgid "No room for item" msgstr "" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, fuzzy, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, fuzzy, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, fuzzy, c++-format msgid "{0} {1} of {2}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, fuzzy, c++-format msgid "{0} of {1}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "" #: Source/items.cpp:1652 msgid "damage potential" msgstr "" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "" #: Source/items.cpp:1659 #, fuzzy msgid "reduces attributes needed" msgstr "ingen gateway" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "" #: Source/items.cpp:1664 msgid "item's durability" msgstr "" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "" #: Source/items.cpp:1674 #, fuzzy msgid "increases the armor class" msgstr "Vælg klasse" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "" #: Source/items.cpp:1679 #, fuzzy msgid "class of armor and shields" msgstr "Vælg klasse" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "" #: Source/items.cpp:1695 msgid "restore all life" msgstr "" #: Source/items.cpp:1698 msgid "restore some life" msgstr "" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "" #: Source/items.cpp:1707 #, fuzzy msgid "increase strength" msgstr "Styrke:" #: Source/items.cpp:1710 #, fuzzy msgid "increase magic" msgstr "Magi:" #: Source/items.cpp:1713 #, fuzzy msgid "increase dexterity" msgstr "Smidighed:" #: Source/items.cpp:1716 #, fuzzy msgid "increase vitality" msgstr "Vitalitet:" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Højreklik for at læse, og\n" "venstreklik derefter for at målrette" #: Source/items.cpp:1768 #, fuzzy msgid "Right-click to read" msgstr "Skrivebeskyttet mappe fejl" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Vælg fra stavebog, og\n" "kast derefter stave for at læse" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "" #: Source/items.cpp:1806 #, fuzzy, c++-format msgid "{} to use" msgstr "Styrke:" #: Source/items.cpp:1809 #, fuzzy, c++-format #| msgid "" #| "Select from spell book, then\n" #| "cast spell to read" msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Vælg fra stavebog, og\n" "kast derefter stave for at læse" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Niveau: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "" #: Source/items.cpp:1855 Source/stores.cpp:327 #, fuzzy #| msgid "Requirements:" msgid "Required:" msgstr "Krav:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, fuzzy, c++-format msgid " {:d} Str" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:1859 Source/stores.cpp:331 #, fuzzy, c++-format msgid " {:d} Mag" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:1861 Source/stores.cpp:333 #, fuzzy, c++-format msgid " {:d} Dex" msgstr "Understøttede spillere: {:d}" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Ear of {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/items.cpp:3874 #, fuzzy, c++-format msgid "chance to hit: {:+d}%" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3877 #, fuzzy, no-c-format, c++-format msgid "{:+d}% damage" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3880 Source/items.cpp:4062 #, fuzzy, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:3883 #, fuzzy, no-c-format, c++-format msgid "{:+d}% armor" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3886 #, fuzzy, c++-format msgid "armor class: {:d}" msgstr "Vælg klasse" #: Source/items.cpp:3890 #, fuzzy, c++-format msgid "Resist Fire: {:+d}%" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "" #: Source/items.cpp:3896 #, fuzzy, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "" #: Source/items.cpp:3902 #, fuzzy, c++-format msgid "Resist Magic: {:+d}%" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "" #: Source/items.cpp:3907 #, fuzzy, c++-format msgid "Resist All: {:+d}%" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "" #: Source/items.cpp:3920 #, fuzzy, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "" "Kan ikke skrive til lokation:\n" "{:s}" msgstr[1] "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3933 #, fuzzy, c++-format msgid "{:+d} to strength" msgstr "Styrke:" #: Source/items.cpp:3936 #, fuzzy, c++-format msgid "{:+d} to magic" msgstr "Magi:" #: Source/items.cpp:3939 #, fuzzy, c++-format msgid "{:+d} to dexterity" msgstr "Smidighed:" #: Source/items.cpp:3942 #, fuzzy, c++-format msgid "{:+d} to vitality" msgstr "Vitalitet:" #: Source/items.cpp:3945 #, fuzzy, c++-format msgid "{:+d} to all attributes" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "" #: Source/items.cpp:3951 #, fuzzy, c++-format msgid "Hit Points: {:+d}" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3954 #, fuzzy, c++-format msgid "Mana: {:+d}" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "" #: Source/items.cpp:3960 msgid "indestructible" msgstr "" #: Source/items.cpp:3962 #, fuzzy, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3964 #, fuzzy, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "" #: Source/items.cpp:3979 #, fuzzy, c++-format msgid "fireball damage: {:d}" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:3981 #, fuzzy, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "" #: Source/items.cpp:4010 msgid "quick attack" msgstr "" #: Source/items.cpp:4012 msgid "fast attack" msgstr "" #: Source/items.cpp:4014 msgid "faster attack" msgstr "" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "" #: Source/items.cpp:4027 msgid "fast block" msgstr "" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "" #: Source/items.cpp:4035 msgid "altered durability" msgstr "" #: Source/items.cpp:4037 #, fuzzy #| msgid "Enter Password" msgid "one handed sword" msgstr "Ringen af et tusinde" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "" #: Source/items.cpp:4041 msgid "life stealing" msgstr "" #: Source/items.cpp:4043 #, fuzzy msgid "no strength requirement" msgstr "Styrke:" #: Source/items.cpp:4046 #, fuzzy, c++-format msgid "lightning damage: {:d}" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:4048 #, fuzzy, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "" #: Source/items.cpp:4054 #, fuzzy, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "" #: Source/items.cpp:4113 Source/items.cpp:4154 #, fuzzy, c++-format msgid "damage: {:d} Indestructible" msgstr "Understøttede spillere: {:d}" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, fuzzy, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, fuzzy, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, fuzzy, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, fuzzy, c++-format msgid "armor: {:d} Indestructible" msgstr "Understøttede spillere: {:d}" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, fuzzy, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, fuzzy, c++-format msgid "Charges: {:d}/{:d}" msgstr "Understøttede spillere: {:d}" #: Source/items.cpp:4139 #, fuzzy #| msgid "Buy items" msgid "unique item" msgstr "Køb varer" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "" #: Source/levels/setmaps.cpp:33 #, fuzzy msgid "Hell Arena" msgstr "Helvede" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "" #: Source/levels/trigs.cpp:384 #, fuzzy msgid "Down to hell" msgstr "Helvede" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, fuzzy, c++-format msgid "Up to level {:d}" msgstr "Understøttede spillere: {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 #, fuzzy msgid "Up to town" msgstr "Genstart i byen" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, fuzzy, c++-format msgid "Down to level {:d}" msgstr "Understøttede spillere: {:d}" #: Source/levels/trigs.cpp:595 #, fuzzy #| msgid "Exit Diablo" msgid "Down to Diablo" msgstr "Diablo Strike Team" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Back to Level {:d}" msgstr "Understøttede spillere: {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 #, fuzzy #| msgid "Unable to create character." msgid "Unable to open save file archive" msgstr "Kunne ikke oprette karakter." #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 #, fuzzy msgid "Invalid save file" msgstr "Gem filen findes" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "" #: Source/loadsave.cpp:2772 #, fuzzy msgid "Invalid game state" msgstr "Spildesign" #: Source/menu.cpp:157 #, fuzzy msgid "Unable to display mainmenu" msgstr "Kunne ikke oprette karakter." #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "" #: Source/monster.cpp:2992 msgid "Demon" msgstr "" #: Source/monster.cpp:2994 msgid "Undead" msgstr "" #: Source/monster.cpp:4413 #, fuzzy, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/monster.cpp:4415 #, fuzzy, c++-format msgid "Total kills: {:d}" msgstr "Understøttede spillere: {:d}" #: Source/monster.cpp:4441 #, fuzzy, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/monster.cpp:4446 #, fuzzy msgid "No magic resistance" msgstr "Magi:" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Magi" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Ild" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Lyn" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "" #: Source/monster.cpp:4476 #, fuzzy, c++-format msgid "Type: {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "" #: Source/monster.cpp:4485 #, fuzzy msgid "Some Magic Resistances" msgstr "Magi:" #: Source/monster.cpp:4489 #, fuzzy msgid "Some Magic Immunities" msgstr "Magi:" #: Source/mpq/mpq_writer.cpp:174 #, fuzzy #| msgid "Unable to create character." msgid "Failed to open archive for writing." msgstr "Kunne ikke oprette karakter." #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "" #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "" #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "" #: Source/msg.cpp:2718 #, fuzzy msgid "The game ended" msgstr "Multiplayer-spil" #: Source/msg.cpp:2724 #, fuzzy #| msgid "Unable to create character." msgid "Unable to get level data" msgstr "Kunne ikke oprette karakter." #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "" #: Source/objects.cpp:128 msgid "Hidden" msgstr "" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "" #: Source/objects.cpp:131 Source/objects.cpp:138 #, fuzzy #| msgid "Magic:" msgid "Magical" msgstr "Magi:" #: Source/objects.cpp:132 msgid "Stone" msgstr "" #: Source/objects.cpp:133 msgid "Religious" msgstr "" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "" #: Source/objects.cpp:140 msgid "Eerie" msgstr "" #: Source/objects.cpp:141 msgid "Divine" msgstr "" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "" #: Source/objects.cpp:143 msgid "Sacred" msgstr "" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "" #: Source/objects.cpp:145 msgid "Spooky" msgstr "" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "" #: Source/objects.cpp:147 msgid "Creepy" msgstr "" #: Source/objects.cpp:148 msgid "Quiet" msgstr "" #: Source/objects.cpp:149 msgid "Secluded" msgstr "" #: Source/objects.cpp:150 msgid "Ornate" msgstr "" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "" #: Source/objects.cpp:152 msgid "Tainted" msgstr "" #: Source/objects.cpp:153 msgid "Oily" msgstr "" #: Source/objects.cpp:154 msgid "Glowing" msgstr "" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "" #: Source/objects.cpp:159 msgid "Solar" msgstr "" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 #, fuzzy #| msgid "The Ring of One Thousand" msgid "The Binding of the Three" msgstr "Ringen af et tusinde" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "" #: Source/objects.cpp:4799 #, fuzzy #| msgid "Level:" msgid "Lever" msgstr "Niveau:" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "" #: Source/objects.cpp:4833 msgid "Chest" msgstr "" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "" #: Source/objects.cpp:4851 msgid "Pod" msgstr "" #: Source/objects.cpp:4854 msgid "Urn" msgstr "" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, fuzzy, c++-format msgid "{:s} Shrine" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "" #: Source/objects.cpp:4899 #, fuzzy #| msgid "Select Hero" msgid "Slain Hero" msgstr "Ny multiplayer-helt" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, fuzzy, c++-format msgid "Trapped {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, fuzzy, c++-format #| msgid "Music Disabled" msgid "{:s} (disabled)" msgstr "Musik deaktiveret" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "" #: Source/options.cpp:422 Source/options.cpp:423 #, fuzzy #| msgid "Game Design" msgid "Game Mode" msgstr "Spildesign" #: Source/options.cpp:422 #, fuzzy #| msgid "Game Design" msgid "Game Mode Settings" msgstr "Spildesign" #: Source/options.cpp:423 #, fuzzy msgid "Play Diablo or Hellfire." msgstr "Gem filen findes" #: Source/options.cpp:429 #, fuzzy #| msgid "Pepin the Healer" msgid "Restrict to Shareware" msgstr "Healeren Pepin" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" #: Source/options.cpp:442 msgid "Start Up" msgstr "" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "" #: Source/options.cpp:455 msgid "Splash" msgstr "" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "" #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "" #: Source/options.cpp:458 msgid "Title Screen" msgstr "" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "" #: Source/options.cpp:487 #, fuzzy msgid "Hellfire specific Settings" msgstr "Indstillinger" #: Source/options.cpp:501 msgid "Audio" msgstr "" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "" #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "" #: Source/options.cpp:507 msgid "Sample Rate" msgstr "" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "" #: Source/options.cpp:508 msgid "Channels" msgstr "" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "" #: Source/options.cpp:509 msgid "Buffer Size" msgstr "" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "" #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "" #: Source/options.cpp:510 msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "" #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" #: Source/options.cpp:574 msgid "Resampler" msgstr "" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "" #: Source/options.cpp:631 msgid "Device" msgstr "" #: Source/options.cpp:631 msgid "Audio device" msgstr "" #: Source/options.cpp:688 msgid "Graphics" msgstr "" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "" #: Source/options.cpp:691 #, fuzzy #| msgid "Pepin the Healer" msgid "Fit to Screen" msgstr "Healeren Pepin" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" #: Source/options.cpp:700 msgid "Upscale" msgstr "" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "" #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "" #: Source/options.cpp:710 msgid "Bilinear" msgstr "" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "" #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "" #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Lyn" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "" #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" #: Source/options.cpp:746 msgid "Show FPS" msgstr "" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "" #: Source/options.cpp:782 msgid "Gameplay" msgstr "" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" #: Source/options.cpp:785 msgid "Grab Input" msgstr "" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "" #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "" #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" #: Source/options.cpp:790 #, fuzzy msgid "Full quests in Multiplayer" msgstr "Pause" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "" #: Source/options.cpp:791 #, fuzzy msgid "Test Bard" msgstr "QA-artilleristøtte (ekstra testere) " #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "" #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "" #: Source/options.cpp:793 msgid "Experience Bar" msgstr "" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "" #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" #: Source/options.cpp:795 msgid "Show health values" msgstr "" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "" #: Source/options.cpp:796 msgid "Show mana values" msgstr "" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "" #: Source/options.cpp:797 #, fuzzy msgid "Show Party Information" msgstr "Kunne ikke oprette karakter." #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "" #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "" #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "" #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "" #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "" #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "" #: Source/options.cpp:814 msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" #: Source/options.cpp:815 msgid "Quick Cast" msgstr "" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "" #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "" #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "" #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "" #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "" #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "" #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" #: Source/options.cpp:824 #, fuzzy msgid "Off" msgstr "Loopback" #: Source/options.cpp:825 msgid "Random Angles" msgstr "" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "" #: Source/options.cpp:880 msgid "Controller" msgstr "" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "" #: Source/options.cpp:889 #, fuzzy #| msgid "Artwork" msgid "Network" msgstr "Kunst" #: Source/options.cpp:889 msgid "Network Settings" msgstr "" #: Source/options.cpp:901 msgid "Chat" msgstr "" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "" #: Source/options.cpp:1029 msgid "Language Settings" msgstr "" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 msgid "Mod Settings" msgstr "" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "" #: Source/panels/charpanel.cpp:139 #, fuzzy msgid "Next level" msgstr "Niveau:" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "" #: Source/panels/charpanel.cpp:149 #, fuzzy #| msgid "No" msgid "Now" msgstr "Nej" #: Source/panels/charpanel.cpp:150 #, fuzzy #| msgid "Strength:" msgid "Strength" msgstr "Styrke:" #: Source/panels/charpanel.cpp:154 #, fuzzy #| msgid "Magic:" msgid "Magic" msgstr "Magi:" #: Source/panels/charpanel.cpp:158 #, fuzzy #| msgid "Dexterity:" msgid "Dexterity" msgstr "Smidighed:" #: Source/panels/charpanel.cpp:161 #, fuzzy #| msgid "Vitality:" msgid "Vitality" msgstr "Vitalitet:" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "" #: Source/panels/charpanel.cpp:174 #, fuzzy msgid "Armor class" msgstr "Vælg klasse" #: Source/panels/charpanel.cpp:176 msgid "Chance To Hit" msgstr "" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "" #: Source/panels/charpanel.cpp:193 #, fuzzy msgid "Resist magic" msgstr "Magi:" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "" #: Source/panels/mainpanel.cpp:94 #, fuzzy msgid "menu" msgstr "Forrige menu" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 #, fuzzy #| msgid "Voices" msgid "voice" msgstr "Stemmeproduktion, retning og casting" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, fuzzy, c++-format msgid "Heals: {:d} - {:d}" msgstr "Understøttede spillere: {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, fuzzy, c++-format msgid "Damage: {:d} - {:d}" msgstr "Understøttede spillere: {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "" #: Source/panels/spell_book.cpp:176 #, fuzzy, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Understøttede spillere: {:d}" msgstr[1] "Understøttede spillere: {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, fuzzy, c++-format #| msgid "Level:" msgctxt "spellbook" msgid "Level {:d}" msgstr "Niveau:" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, fuzzy, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Understøttede spillere: {:d}" #: Source/panels/spell_list.cpp:159 #, fuzzy msgid "Spell" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/panels/spell_list.cpp:162 #, fuzzy msgid "Damages undead only" msgstr "Skrivebeskyttet mappe fejl" #: Source/panels/spell_list.cpp:173 #, fuzzy msgid "Scroll" msgstr "Understøttede spillere: {:d}" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "" #: Source/panels/spell_list.cpp:194 #, fuzzy, c++-format msgid "Spell Hotkey {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/pfile.cpp:762 #, fuzzy #| msgid "Unable to create character." msgid "Unable to open archive" msgstr "Kunne ikke oprette karakter." #: Source/pfile.cpp:764 #, fuzzy #| msgid "Unable to create character." msgid "Unable to load character" msgstr "Kunne ikke oprette karakter." #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, fuzzy, c++-format msgid "{:s} (lvl {:d}): " msgstr "" "{:s}\n" "\n" "Fejlen opstod ved: {:s} linje {:d}" #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "" #: Source/qol/itemlabels.cpp:113 #, fuzzy, c++-format msgid "{:s} gold" msgstr "Understøttede spillere: {:s}" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "" #: Source/qol/xpbar.cpp:146 #, fuzzy msgid "Maximum Level" msgstr "Niveau:" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "To {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/quick_messages.cpp:10 msgid "I need help! Come here!" msgstr "" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "" #: Source/quick_messages.cpp:12 #, fuzzy msgid "Here's something for you." msgstr "Har du lyst til:" #: Source/quick_messages.cpp:13 #, fuzzy msgid "Now you DIE!" msgstr "Har du lyst til:" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 #, fuzzy #| msgid "Thanks To" msgid "Thanks." msgstr "Tak til" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 #, fuzzy #| msgid "Talk to Pepin" msgid "Griswold" msgstr "Tal med Pepin" #: Source/stores.cpp:132 msgid "Pepin" msgstr "" #: Source/stores.cpp:134 msgid "Ogden" msgstr "" #: Source/stores.cpp:135 msgid "Cain" msgstr "" #: Source/stores.cpp:136 #, fuzzy #| msgid "Talk to Pepin" msgid "Farnham" msgstr "Tal med Pepin" #: Source/stores.cpp:137 msgid "Adria" msgstr "" #: Source/stores.cpp:138 Source/stores.cpp:1267 #, fuzzy #| msgid "Talk to Pepin" msgid "Gillian" msgstr "Tal med Pepin" #: Source/stores.cpp:139 msgid "Wirt" msgstr "" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr "" #: Source/stores.cpp:311 #, fuzzy, c++-format msgid "Damage: {:d}-{:d} " msgstr "Understøttede spillere: {:d}" #: Source/stores.cpp:313 #, fuzzy, c++-format msgid "Armor: {:d} " msgstr "Understøttede spillere: {:d}" #: Source/stores.cpp:315 #, fuzzy, c++-format msgid "Dur: {:d}/{:d}" msgstr "Understøttede spillere: {:d}" #: Source/stores.cpp:317 #, fuzzy msgid "Indestructible" msgstr "Understøttede spillere: {:d}" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Velkommen til" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Har du lyst til:" #: Source/stores.cpp:390 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Griswold" msgstr "Tal med Pepin" #: Source/stores.cpp:391 #, fuzzy #| msgid "Buy items" msgid "Buy basic items" msgstr "Køb varer" #: Source/stores.cpp:392 #, fuzzy #| msgid "Buy items" msgid "Buy premium items" msgstr "Køb varer" #: Source/stores.cpp:393 Source/stores.cpp:689 #, fuzzy #| msgid "Buy items" msgid "Sell items" msgstr "Køb varer" #: Source/stores.cpp:394 #, fuzzy msgid "Repair items" msgstr "Køb varer" #: Source/stores.cpp:395 #, fuzzy #| msgid "Leave Healer's home" msgid "Leave the shop" msgstr "Forlad healers hjem" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "" #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "" #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "" #: Source/stores.cpp:687 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Adria" msgstr "Tal med Pepin" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Køb varer" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "" #: Source/stores.cpp:691 #, fuzzy msgid "Leave the shack" msgstr "Forlad healers hjem" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "" #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "" #: Source/stores.cpp:945 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to identify this item?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/stores.cpp:951 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to buy this item?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/stores.cpp:954 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to recharge this item?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/stores.cpp:958 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to sell this item?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/stores.cpp:961 #, fuzzy #| msgid "Are you sure you want to delete the character \"{:s}\"?" msgid "Are you sure you want to repair this item?" msgstr "Er du sikker på, at du vil slette karakteren \"{:s}\"?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "" #: Source/stores.cpp:978 Source/stores.cpp:985 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Wirt" msgstr "Tal med Pepin" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "" #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "" #: Source/stores.cpp:982 msgid "What have you got?" msgstr "" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "" #: Source/stores.cpp:1013 #, fuzzy msgid "Leave" msgstr "Forlad healers hjem" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Healers hjem" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Tal med Pepin" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Forlad healers hjem" #: Source/stores.cpp:1079 #, fuzzy msgid "The Town Elder" msgstr "Genstart i byen" #: Source/stores.cpp:1081 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Cain" msgstr "Tal med Pepin" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "" #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "" #: Source/stores.cpp:1204 msgid "Done" msgstr "" #: Source/stores.cpp:1213 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Talk to {:s}" msgstr "Tal med Pepin" #: Source/stores.cpp:1216 #, fuzzy, c++-format #| msgid "Talk to Pepin" msgid "Talking to {:s}" msgstr "" "Kan ikke skrive til lokation:\n" "{:s}" #: Source/stores.cpp:1217 #, fuzzy msgid "is not available" msgstr "" "Skytten og Troldmanden er kun tilgængelige i den fulde detailversion af " "Diablo. Besøg https://www.gog.com/game/diablo for at købe den." #: Source/stores.cpp:1218 #, fuzzy #| msgid "Pepin the Healer" msgid "in the shareware" msgstr "Healeren Pepin" #: Source/stores.cpp:1219 #, fuzzy msgid "version" msgstr "" "Skytten og Troldmanden er kun tilgængelige i den fulde detailversion af " "Diablo. Besøg https://www.gog.com/game/diablo for at købe den." #: Source/stores.cpp:1246 msgid "Gossip" msgstr "" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "" #: Source/stores.cpp:1257 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Ogden" msgstr "Tal med Pepin" #: Source/stores.cpp:1258 #, fuzzy msgid "Leave the tavern" msgstr "Forlad healers hjem" #: Source/stores.cpp:1269 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Gillian" msgstr "Tal med Pepin" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "" #: Source/stores.cpp:1282 #, fuzzy #| msgid "Talk to Pepin" msgid "Talk to Farnham" msgstr "Tal med Pepin" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Healeren Pepin" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "" #: Source/towners.cpp:781 #, fuzzy #| msgid "Pepin the Healer" msgid "Cain the Elder" msgstr "Healeren Pepin" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "" #: Source/towners.cpp:786 msgid "Cow" msgstr "" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "" #: Source/towners.cpp:788 msgid "Celia" msgstr "" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Kriger" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Skytte" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Troldmand" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "" #: Source/translation_dummy.cpp:21 #, fuzzy msgctxt "monster" msgid "Fallen One" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "" #: Source/translation_dummy.cpp:24 #, fuzzy msgctxt "monster" msgid "Dark One" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "" #: Source/translation_dummy.cpp:28 #, fuzzy #| msgid "Error" msgctxt "monster" msgid "Horror" msgstr "Fejl" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "" #: Source/translation_dummy.cpp:47 #, fuzzy #| msgid "Nightmare" msgctxt "monster" msgid "Night Clan" msgstr "Mareridt" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "" #: Source/translation_dummy.cpp:61 #, fuzzy msgctxt "monster" msgid "Flayed One" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "" #: Source/translation_dummy.cpp:68 #, fuzzy #| msgid "Buy items" msgctxt "monster" msgid "Hell Stone" msgstr "Helvede" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "" #: Source/translation_dummy.cpp:81 #, fuzzy msgctxt "monster" msgid "Hell Burner" msgstr "Helvede" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "" #: Source/translation_dummy.cpp:91 #, fuzzy #| msgid "Single Player" msgctxt "monster" msgid "Slayer" msgstr "Singleplayer" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "" #: Source/translation_dummy.cpp:104 #, fuzzy msgctxt "monster" msgid "Hollow One" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "" #: Source/translation_dummy.cpp:109 #, fuzzy msgctxt "monster" msgid "Hell Spawn" msgstr "Helvede" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "" #: Source/translation_dummy.cpp:111 #, fuzzy #| msgid "Dunsel" msgctxt "monster" msgid "Counselor" msgstr "Drønnert" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "" #: Source/translation_dummy.cpp:130 #, fuzzy #| msgid "Pepin the Healer" msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Healeren Pepin" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "" #: Source/translation_dummy.cpp:163 #, fuzzy #| msgid "Nightmare" msgctxt "monster" msgid "Blightfire" msgstr "Mareridt" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "" #: Source/translation_dummy.cpp:183 #, fuzzy #| msgid "Single Player" msgctxt "monster" msgid "The Flayer" msgstr "Singleplayer" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "" #: Source/translation_dummy.cpp:225 #, fuzzy #| msgid "Magic:" msgid "Magic Rock" msgstr "Magi:" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "" #: Source/translation_dummy.cpp:227 #, fuzzy msgid "Ring of Truth" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "" #: Source/translation_dummy.cpp:243 #, fuzzy msgid "Scroll of Town Portal" msgstr "Genstart i byen" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "" #: Source/translation_dummy.cpp:248 #, fuzzy #| msgid "Single Player" msgid "Bovine Plate" msgstr "Singleplayer" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "" #: Source/translation_dummy.cpp:279 #, fuzzy msgid "Ring Mail" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "" #: Source/translation_dummy.cpp:301 #, fuzzy msgid "Elixir of Strength" msgstr "Styrke:" #: Source/translation_dummy.cpp:302 #, fuzzy msgid "Elixir of Magic" msgstr "Magi:" #: Source/translation_dummy.cpp:303 #, fuzzy #| msgid "Dexterity:" msgid "Elixir of Dexterity" msgstr "Smidighed:" #: Source/translation_dummy.cpp:304 #, fuzzy msgid "Elixir of Vitality" msgstr "Vitalitet:" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "" #: Source/translation_dummy.cpp:338 #, fuzzy #| msgid "Create Game" msgid "Great Axe" msgstr "Opret spil" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "" #: Source/translation_dummy.cpp:359 #, fuzzy msgid "Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Nightmare" msgid "Lightforge" msgstr "Mareridt" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "" #: Source/translation_dummy.cpp:391 #, fuzzy #| msgid "Nightmare" msgid "Lightsabre" msgstr "Mareridt" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "" #: Source/translation_dummy.cpp:430 #, fuzzy msgid "Torn Flesh of Souls" msgstr "\tIngen sjæle blev solgt i fremstillingen af dette spil." #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "" #: Source/translation_dummy.cpp:437 #, fuzzy #| msgid "Nightmare" msgid "Nightscape" msgstr "Mareridt" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "" #: Source/translation_dummy.cpp:447 #, fuzzy msgid "Ring of Regha" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "" #: Source/translation_dummy.cpp:449 #, fuzzy msgid "Constricting Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:450 #, fuzzy msgid "Ring of Engagement" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "" #: Source/translation_dummy.cpp:478 #, fuzzy #| msgid "Warrior" msgid "Warrior's" msgstr "Kriger" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "" #: Source/translation_dummy.cpp:531 #, fuzzy #| msgid "Vitality:" msgid "quality" msgstr "Vitalitet:" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "" #: Source/translation_dummy.cpp:542 #, fuzzy #| msgid "Select Action" msgid "deflection" msgstr "Vælg handling" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "" #: Source/translation_dummy.cpp:546 #, fuzzy #| msgid "Strength:" msgid "strength" msgstr "Styrke:" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "" #: Source/translation_dummy.cpp:553 #, fuzzy #| msgid "Dexterity:" msgid "dexterity" msgstr "Smidighed:" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "" #: Source/translation_dummy.cpp:557 #, fuzzy #| msgid "Select Action" msgid "perfection" msgstr "Vælg handling" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "" #: Source/translation_dummy.cpp:560 #, fuzzy #| msgid "Magic:" msgid "magic" msgstr "Magi:" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "" #: Source/translation_dummy.cpp:563 #, fuzzy #| msgid "Sorcerer" msgid "sorcery" msgstr "Troldmand" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "" #: Source/translation_dummy.cpp:567 #, fuzzy #| msgid "Vitality:" msgid "vitality" msgstr "Vitalitet:" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "" #: Source/translation_dummy.cpp:583 #, fuzzy #| msgid "Pepin the Healer" msgid "the eagle" msgstr "Healeren Pepin" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "" #: Source/translation_dummy.cpp:616 #, fuzzy #| msgid "Manufacturing" msgid "puncturing" msgstr "Produktion" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "" #: Source/translation_dummy.cpp:622 #, fuzzy #| msgid "Cancel" msgid "balance" msgstr "Annuller" #: Source/translation_dummy.cpp:623 #, fuzzy #| msgid "Vitality:" msgid "stability" msgstr "Vitalitet:" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "" #: Source/translation_dummy.cpp:626 #, fuzzy msgid "The Magic Rock" msgstr "Magi:" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "" #: Source/translation_dummy.cpp:654 #, fuzzy msgctxt "spell" msgid "Town Portal" msgstr "Genstart i byen" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "" #: Source/translation_dummy.cpp:681 #, fuzzy #| msgid "Healer's home" msgctxt "spell" msgid "Heal Other" msgstr "Healers hjem" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "" #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" #: Source/translation_dummy.cpp:725 #, fuzzy msgid "You drink water?" msgstr "Har du lyst til:" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "" #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "" #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "" #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "" #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "" #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "" #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "" #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "" #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "" #: Source/translation_dummy.cpp:979 #, fuzzy #| msgid "Hell" msgctxt "monster" msgid "Hellboar" msgstr "Helvede" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "" #: Source/translation_dummy.cpp:990 #, fuzzy #| msgid "Hell" msgctxt "monster" msgid "Hell Bug" msgstr "Helvede" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "" #: Source/translation_dummy.cpp:997 #, fuzzy #| msgid "Hell" msgctxt "monster" msgid "Hellbat" msgstr "Helvede" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "" #: Source/translation_dummy.cpp:1004 #, fuzzy msgid "Mercurial Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1005 #, fuzzy msgid "Xorine's Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1006 #, fuzzy msgid "Karik's Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1007 #, fuzzy msgid "Ring of Magma" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1008 #, fuzzy msgid "Ring of the Mystics" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1009 #, fuzzy #| msgid "The Ring of One Thousand" msgid "Ring of Thunder" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "" #: Source/translation_dummy.cpp:1022 #, fuzzy msgid "Gladiator's Ring" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "" #: Source/translation_dummy.cpp:1037 #, fuzzy msgctxt "spell" msgid "Ring of Fire" msgstr "Ringen af et tusinde" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "" #, fuzzy #~| msgid "Options" #~ msgid "Options:" #~ msgstr "Indstillinger" #, fuzzy #~ msgid "version {:s}" #~ msgstr "" #~ "Kan ikke skrive til lokation:\n" #~ "{:s}" #, fuzzy #~ msgid "Increase Gamma" #~ msgstr "Magi:" #, fuzzy #~ msgid "No automap available in town" #~ msgstr "Genstart i byen" #~ msgid "Restart In Town" #~ msgstr "Genstart i byen" #, fuzzy #~ msgid "decrease strength" #~ msgstr "Styrke:" #, fuzzy #~ msgid "decrease dexterity" #~ msgstr "Smidighed:" #, fuzzy #~ msgid "decrease vitality" #~ msgstr "Vitalitet:" #, fuzzy #~ msgid "you can't heal" #~ msgstr "Har du lyst til:" #, fuzzy #~| msgid "Unable to create character." #~ msgid "Unable to read to save file archive" #~ msgstr "Kunne ikke oprette karakter." #, fuzzy #~| msgid "" #~| "Unable to write to location:\n" #~| "{:s}" #~ msgid "Unable to write to save file archive" #~ msgstr "" #~ "Kan ikke skrive til lokation:\n" #~ "{:s}" ================================================ FILE: Translations/de.po ================================================ # Translation of DevilutionX to German # Mathieu Maret , 2021. # @ChaosMarc, 2021. # @sheng-luwei, 2021. # msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:19+0200\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Spieldesigner" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Seniordesigner" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Zusätzliches Design" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Chefprogrammierer" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Seniorprogrammierer" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Programmierung" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Besondere-Gäste-Programmierung" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Battle.net-Programmierung" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Programmierung der seriellen Kommunikation" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Installerprogrammierung" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Künstlerische Leitung" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Bildgestaltung" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Technische Bildgestaltung" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Filmkunstdirektor" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D-Filmgestaltung" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Technische Filmgestaltung" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Ausführender Produzent" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Produzent" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Assoziierter Produzent" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Strike Team" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Musik" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Sound-Design" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Filmische Musik & Sound" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Stimmenproduktion, Regie und Casting" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Drehbuch und Geschichte" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Stimmenbearbeitung" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Stimmen" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Aufnahmetechniker" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Handbuchdesign und -layout" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Künstlerische Gestaltung des Handbuchs" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Kommissarischer Leiter QS (Cheftester)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "QS-Überfallkommando (Tester)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "QS-Spezialeinheiten (Kompatibilitätstester)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "QS-Artillerieunterstützung (zusätzliche Tester) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "QS-Spionageabwehr" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Sortierung der Netzwerkinformationsdienste" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Kundendienst" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Verkauf" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Dunsel" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Mr. Dabiris Hintergrundsänger*innen" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Öffentlichkeitsarbeit" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Marketing" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Verkauf International" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "US-Verkäufe" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Produktion" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Recht- und Geschäftliches" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Besonderen Dank an" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Dank an" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "In Erinnerung an" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Ganz besonderen Dank an" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Generaldirektor" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Softwareentwicklung" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Kunstdirektor" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Künstler" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Design" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Sound-Design, SFX- & Audiotechnik" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Leiter der Qualitätssicherung" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Tester" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Handbuch" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\tZusätzliche Arbeit" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Questtexter" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Dank an" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tVielen Dank an Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Qualitätssicherungsdirektor" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Qualitätssicherungscheftester" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Haupttester" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Zusätzliche Tester" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Marketingdirektor" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Direktor für Öffentlichkeitsarbeit" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Projektleiter" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Der Ring der Eintausend" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tKeine Seelen wurden während der Entstehung dieses Spiels verkauft." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "OK" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Klasse wählen" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Zurück" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Neuer Mehrspielerheld" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Neuer Einzelspielerheld" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Spielstand existiert" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Spiel laden" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Neues Spiel" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Einzelspielercharaktere" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Die Jägerin und der Magier sind nur in der Vollversion von Diablo enthalten. " "Besuche https://www.gog.com/game/diablo um das Spiel zu erwerben." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Namen eingeben" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Ungültiger Name. Ein Name darf keine Leerzeichen, reservierte Zeichen oder " "reservierte Wörter enthalten.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Charaktererstellung fehlgeschlagen." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Level:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Stärke:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Magie:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Agilität:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Vitalität:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Spielstand:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Charakterwahl" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Neuer Held" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Löschen" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Mehrspielercharaktere" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Mehrspielercharakter löschen" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Einzelspielercharakter löschen" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Bist Du sicher, dass Du den Charakter \"{:s}\" löschen willst?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Einzelspieler" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Mehrspieler" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Einstellungen" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Hilfe" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Mitwirkende" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Hellfire verlassen" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Diablo verlassen" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Shareware" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Client-Server (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 msgid "Offline" msgstr "Offline" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Mehrspieler-Spiel" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Beschreibung:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "" "Kein Gateway\n" "erforderlich" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Verbindungsart" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Gateway wechseln" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Verbindung über ein TCP-kompatibles Netzwerk." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Alle Computer müssen mit dem Internet verbunden sein." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Ermöglicht das netzwerklose Solospiel." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Unterstützte Spieler: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Diablo Shareware" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Hellfire Shareware" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "Der Host betreibt ein anderes Spiel als Du." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "Der Spielersteller verwendet einen anderen Spielmodus ({:s}) als du." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "" "Deine Version {:s} stimmt nicht mit der des Hosts {:d}.{:d}.{:d} überein." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Beschreibung:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Aktion wählen" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Spiel erstellen" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Offenes Spiel erstellen" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Spiel beitreten" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Öffentliche Spiele" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Lade..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "Keine" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "ABBRECHEN" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "" "Erstelle ein neues Spiel mit Passwort und dem von Dir gewählten " "Schwierigkeitsgrad." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" "Erstelle ein neues offenes Spiel mit freiem Beitritt und dem von Dir " "gewählten Schwierigkeitsgrad." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "Einem laufenden Spiel via Spiel-ID beitreten." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "Einem laufenden Spiel via IP-Adresse oder Hostnamen beitreten." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "Einem laufenden öffentlichen Spiel beitreten." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Normal" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Alptraum" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Hölle" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Schwierigkeitsgrad: {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Geschwindigkeit: Normal" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Geschwindigkeit: Schnell" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Geschwindigkeit: Schneller" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Geschwindigkeit: Am schnellsten" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Spieler: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Schwierigkeitsgrad" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Trete {:s}-Spiel bei" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Spiel-ID eingeben" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Adresse eingeben" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Normal\n" "Hier sollte ein neuer Charakter seine Abenteuer in der Welt von Diablo " "beginnen." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Alptraum\n" "Die Bewohner des Labyrinths sind stärker und erweisen sich als größere " "Herausforderung. Nur für erfahrene Charaktere geeignet." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Hölle\n" "Hier warten die stärksten Dämonen an den Toren der Hölle auf Dich. Nur die " "stärksten Charaktere sind dieser Herausforderung gewachsen." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Dein Charakter muss mindestens Level 20 erreicht haben bevor er einem " "Mehrspieler-Spiel der Stufe \"Alptraum\" beitreten kann." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Dein Charakter muss mindestens Level 30 erreicht haben bevor er einem " "Mehrspieler-Spiel der Stufe \"Hölle\" beitreten kann." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Spielgeschwindigkeit" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Schnell" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "Schneller" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Am Schnellsten" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Normale Geschwindigkeit\n" "Hier sollte ein neuer Charakter seine Abenteuer in der Welt von Diablo " "beginnen." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Schnelle Geschwindigkeit\n" "Die Bewohner des Labyrinths sind schneller und erweisen sich als größere " "Herausforderung. Nur für erfahrene Charaktere geeignet." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "Schnellere Geschwindigkeit\n" "Die meisten Monster der Unterwelt werden Dich schneller aufspüren als je " "zuvor. Nur ein erfahrener Meister sollte sein Glück hier versuchen." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Schnelle Geschwindigkeit\n" "Die Gegner der Unterwelt werden unerbittlich schnell angreifen. Nur ein " "wahrer Geschwindigkeitsfanatiker sollte diese Gangart wählen." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Passwort eingeben" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Hellfire spielen" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Zu Diablo wechseln" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Ja" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Nein" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "Drücke eine beliebige Taste um diese zu ändern." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Festgelegte Taste:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Eine beliebige Taste drücken um sie festzulegen." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Festgelegte Taste freigeben" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "Gebundene Knopfkombination:" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "Entbinde Knopfkombination" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Vorheriges Menü" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Wir unterhalten einen Chatserver unter Discord.gg/devilutionx rund um Diablo " "und das Hellfire-Addon. Du bist herzlich eingeladen, unserer Community " "beizutreten." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX ist ein Projekt von Diasurgical. Probleme und Fehler bitte hier " "melden: https://github.com/diasurgical/devilutionX. Damit wir Dir helfen " "können, teile uns bitte Deine Versionsnummer, Dein Betriebssystem und die " "Art Deines Problems mit." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Haftungsausschluss:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX wird weder von GOG.com noch von Blizzard Entertainment " "unterstützt oder betrieben oder zwecks Gewährleistung auf Qualität oder " "Kompatibilität getestet. Alle Anfragen bezüglich DevilutionX sollten direkt " "an Diasurgical gerichtet werden, nicht an Blizzard Entertainment oder an " "GOG.com." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tDieser Port verwendet Charis SIL, New Athena Unicode, Unifont und Noto, " "lizenziert unter der SIL Open Font License, sowie Twitmoji, lizenziert unter " "CC-BY 4.0. Der Port verwendet außerdem SDL, lizenziert unter der zlib-" "Lizenz. Mehr Details in der ReadMe." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Urheberrecht © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Fehler" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Fehler an Position {:s} Zeile {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Dateifehler" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Das Hauptdatenarchiv ({:s}) kann nicht geöffnet werden.\n" "\n" "Stelle sicher, dass es sich im Spielordner befindet." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Nur-Lesen-Verzeichnisfehler" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "An folgenden Speicherort kann nicht geschrieben werden:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Spiel: " #: Source/automap.cpp:1424 msgid "Offline Game" msgstr "Offlinespiel" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Passwort: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Offenes Spiel" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Level: Stock {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Level: Krypta {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Heimeliger" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Level {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Tab" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Enter" #: Source/control.cpp:206 msgid "Character Information" msgstr "Charakterinformation" #: Source/control.cpp:207 msgid "Quests log" msgstr "Questtagebuch" #: Source/control.cpp:208 msgid "Automap" msgstr "Automatische Karte" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Hauptmenü" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Inventar" #: Source/control.cpp:211 msgid "Spell book" msgstr "Zauberbuch" #: Source/control.cpp:212 msgid "Send Message" msgstr "Nachricht senden" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "Verfügbare Befehle:" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "Befehl " #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr " ist unbekannt." #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "Beschreibung: " #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" "\n" "Parameter: Kein zusätzlicher Parameter erforderlich." #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" "\n" "Parameters: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "Arenen nur in Multiplayer unterstützt." #: Source/control.cpp:653 msgid "What arena do you want to visit?" msgstr "Welche Arena willst du besuchen?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "Untültige Arenen-Nummer. Gültige Nummern sind:" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" "Zum Betreten einer Arena, brauchst musst du in der Stadt oder in einer " "anderen Arena sein." #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "Die Inspektion wird nur im Mehrspielermodus unterstützt." #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "Stoppen den Spieler zu inspizieren." #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "Keine Spieler mit so einem Namen gefunden" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "Inspiziere Spieler: " #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "Gebe Hilfeübersicht oder Hilfe für einen bestimmten Befehl." #: Source/control.cpp:800 msgid "[command]" msgstr "[befehl]" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "Betrete die PvP Arena." #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "Gebe Arena Tränke." #: Source/control.cpp:802 msgid "" msgstr "" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "Überprüfe Statistiken und Ausrüstung eines anderen Spielers." #: Source/control.cpp:803 msgid "" msgstr "" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "Zeige Samen Informationen für das aktuelle Level." #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Spieler ist freundlich" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Spieler greift an" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Schnelltaste: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Zauberauswahlknopf" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Schnelltaste: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "{:s}-Fertigkeit" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "{:s}-Zauber" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Zauberlevel 0 - Unbenutzbar" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Zauberlevel {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "Schriftrolle der {:s}" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d}-Schriftrolle" msgstr[1] "{:d}-Schriftrollen" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "Stab mit {:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d}-Ladung" msgstr[1] "{:d}-Ladungen" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} Goldstück" msgstr[1] "{:s} Goldstücke" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Anforderungen nicht erfüllt" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Level: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Trefferpunkte {:d} von {:d}" #: Source/control.cpp:1525 #, fuzzy #| msgid "Right-click to use" msgid "Right click to inspect" msgstr "Rechtsklick zum Benutzen" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Level-Up" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Du hast {:s} Goldstück. Wieviele möchtest Du abheben?" msgstr[1] "Du hast {:s} Goldstücke. Wieviele möchtest Du abheben?" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Stadtportal" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "von {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Portal" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "Zum unheiligen Altar" #: Source/cursor.cpp:636 msgid "level 15" msgstr "Zu Level 15" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, fuzzy, c++-format #| msgid "Unable to load character" msgid "Unable to load data from file {0}" msgstr "Fehler beim Laden des Charakters" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Diese Nachricht ausgeben und beenden" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Version ausgeben und beenden" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Speicherort der diabdat.mpq definieren" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Speicherort der Spielstandsdateien definieren" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Speicherort der diablo.ini definieren" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "Bestimme den Sprachencode (e.g. de or de-deu)" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Introvideos überspringen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Bilder pro Sekunde anzeigen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "\"Verbose logging\" aktivieren" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Demo-Datei aufnehmen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Demo-Datei abspielen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Alle Frame-Begrenzungen bei Demo-Wiedergabe ausschalten" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Spielauswahl:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Shareware erzwingen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Diablo erzwingen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Hellfire erzwingen" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Hellfire-Optionen:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "Bugs melden unter https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" "Bitte aktualisiere devilutionx.mpq und fonts.mpq auf die neuste Version" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Fehler beim Laden des Interfaces.\n" "\n" "devilutionx.mpq muss im Spielordner vorhanden und auf dem neuesten Stand " "sein." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "Bitte aktualisiere fonts.mpq auf die neueste Version" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Netzwerkunterbrechung --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Warten auf Spieler --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "Hilfe nicht verfügbar" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "während des Dialogs" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Gürtelgegenstand {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Gürtelgegenstand verwenden." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Schnellzauber {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Schnelltaste für Fertigkeiten oder Zauber." #: Source/diablo.cpp:1809 #, fuzzy #| msgid "Previous Menu" msgid "Previous quick spell" msgstr "Vorheriges Menü" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 #, fuzzy #| msgid "Quick spell {}" msgid "Next quick spell" msgstr "Schnellzauber {}" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "Nutze den Heiltrank" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "Nutze den Heiltrank vom Gürtel." #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "Benutze den Mana Trank" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "Benutze die Mana Tränke vom Gürtel." #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Schnellzauberleiste" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Schnellzauberleiste öffnen." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Schnellspeichern" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Speichert das Spiel." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Schnellladen" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Lädt das Spiel." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Spiel beenden" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Beendet das Spiel." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Held anhalten" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Hält den Helden an und unterbricht anstehende Aktionen." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Gegenstand-Hervorhebung" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Zeigt/Versteckt Gegenstände auf dem Boden." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Dauerhafte Gegenstand-Hervorhebung" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Zeigt/Versteckt Gegenstände auf dem Boden dauerhaft." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Karte an/ausschalten" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Aktiviert / Deaktiviert die Karte." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Öffnet das Inventar." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Charakterübersicht" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Öffnet die Charakterübersicht." #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Questtagebuch" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Öffnet das Questtagebuch." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Zauberbuch" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Öffnet das Zauberbuch." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Schnellnachricht {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Schnellnachricht im Chat verwenden." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Infoanzeigen verstecken" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Versteckt alle Infoanzeigen." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Zoom" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Zoom näher an das Spiel heran." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Pause" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Pausiert das Spiel." #: Source/diablo.cpp:2004 #, fuzzy #| msgid "Pause Game" msgid "Pause Game (Alternate)" msgstr "Pause" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy #| msgid "Increase screen brightness." msgid "Decrease Brightness" msgstr "Erhöht die Bildschirmhelligkeit." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Verringert die Bildschirmhelligkeit." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy #| msgid "Increase screen brightness." msgid "Increase Brightness" msgstr "Erhöht die Bildschirmhelligkeit." #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Erhöht die Bildschirmhelligkeit." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Hilfe" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Öffnet das Hilfemenü." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Bildschirmfoto" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Erzeugt ein Bildschirmfoto." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Spielinfo" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Zeigt Spielinformationen an." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Chatverlauf" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Zeigt den Chatverlauf an." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 #, fuzzy #| msgid "Inventory" msgid "Sort Inventory" msgstr "Inventar" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "Primitive Attacke" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" "Attackiere Monster, Rede mit den Stadtbewohnern, Inventargegenstände anheben " "und platzieren." #: Source/diablo.cpp:2144 msgid "Secondary action" msgstr "Zweite Aktion" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "Öffene Kisten, interagiere mit den Türen, Sammle Items ein." #: Source/diablo.cpp:2159 msgid "Spell action" msgstr "Aktion auswählen" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "Wirke den aktiven Zauber." #: Source/diablo.cpp:2174 msgid "Cancel action" msgstr "Aktion abbrechen" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "Schließe Menüs." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "Bewege hoch" #: Source/diablo.cpp:2201 msgid "Moves the player character up." msgstr "Bewege den Charakter des Spieler nach oben." #: Source/diablo.cpp:2206 msgid "Move down" msgstr "Bewege runter" #: Source/diablo.cpp:2207 msgid "Moves the player character down." msgstr "Bewege den Charakter des Spieler nach unten." #: Source/diablo.cpp:2212 msgid "Move left" msgstr "Gehe nach links" #: Source/diablo.cpp:2213 msgid "Moves the player character left." msgstr "Bewege den Charakter des Spieler nach links." #: Source/diablo.cpp:2218 msgid "Move right" msgstr "Gehe nach rehcts" #: Source/diablo.cpp:2219 msgid "Moves the player character right." msgstr "Bewege den Charakter des Spieler nach rechts." #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "auf dem Boden stehen." #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "Halte gedrückt um dem Spieler vorm bewegen zu hindern." #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "Standfläche umschalten" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "Schalte um wenn der Spieler sich bewegt." #: Source/diablo.cpp:2310 #, fuzzy #| msgid "Automap" msgid "Automap Move Up" msgstr "Automatische Karte" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 #, fuzzy #| msgid "Move down" msgid "Automap Move Down" msgstr "Bewege runter" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 #, fuzzy #| msgid "Move left" msgid "Automap Move Left" msgstr "Gehe nach links" #: Source/diablo.cpp:2323 #, fuzzy #| msgid "Moves the player character up." msgid "Moves the automap left when active." msgstr "Bewege den Charakter des Spieler nach oben." #: Source/diablo.cpp:2328 #, fuzzy #| msgid "Move right" msgid "Automap Move Right" msgstr "Gehe nach rehcts" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "Ziehe Maus nach oben" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "Simuliere die Aufwärtsbewegung der Maus." #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "Ziehe die Maus nach unten" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "Simuliere die Rückwärtsbewegung der Maus." #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "Ziehe die Maus nach links" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "Simuliere die Bewegung der Maus nach links." #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "Ziehe Maus nach rechts" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "Simuliere die Bewegung der Maus nach rechts." #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "Linksklick" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "Simuliere den linken Maus-Button." #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "Rechtsklick" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "Simuliere den rechten Maus-Button." #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "Gamepad-Hotspell-Menü" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "Halte gedrückt um den Zauber-Hotkey fest zulegen oder zu nutzen." #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "Gamepad Menunavigator" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "Halte zum betreten des Gamepad Navigationsmenü." #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 msgid "Toggle game menu" msgstr "Schalte Spielmenü" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 msgid "Opens the game menu." msgstr "Öffnet das Spielmenü." #: Source/diablo_msg.cpp:63 #, fuzzy #| msgctxt "spell" #| msgid "Flame Wave" msgid "Game saved" msgstr "Flammenwelle" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "Keine Mehrspielerfunktionalität in der Demoversion" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Direct-Sound-Initialisierung fehlgeschlagen" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "In der Shareware-Version nicht verfügbar" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "Nicht genug Platz zum Speichern" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Keine Pause in der Stadt" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Das Kopieren auf eine Festplatte wird empfohlen" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Mehrspielersynchronisationsproblem" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Keine Pause im Mehrspielermodus" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Speichere..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Manche werden schwächer, wenn einer erstarkt" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Neue Kraft, geschmiedet durch Zerstörung" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Wer verteidigt, kann nicht zugleich angreifen" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "Das Schwert der Gerechtigkeit ist flink und scharf" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Ist der Geist wachsam, gedeiht der Körper" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Die Kraft des Manas erneuert auf frischer Bahn" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "Der Macht des Stahls kann die Zeit nichts anhaben" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "Magie ist nicht immer das was sie zu sein scheint" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Was einst geöffnet, ist nun verschlossen" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "Sättigung auf Kosten der Weisheit" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "Arkane Kraft bringt Zerstörung" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Was nicht gebunden werden kann, bleibt unverdorben" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Purpur und Azur werden wie die Sonne" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Wissen und Weisheit auf Kosten Eurer selbst" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Trinkt und erfrischt Euch" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Viele Wege führen zum Ziel" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "Energie auf Kosten der Weisheit" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Manchmal liegt das Gold auf der Straße" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "Wo die Habgier scheitert, wird Geduld belohnt" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "Gesegnet von einem wohlwollenden Gefährten!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Möge das Schicksal die Hände der Menschen lenken" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "Himmlischer Glaube spendet Stärke" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "Die Essenz des Lebens fließt in Euch" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "Von oben betrachtet wird der Weg klar" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "Erlösung auf Kosten der Weisheit" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Geheimnisse im Licht der Vernunft gelüftet" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Die Letzten werden die Ersten sein" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "Großzügigkeit ist ihr eigener Lohn" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Dafür müsst Ihr mindestens Level 8 sein." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Dafür müsst Ihr mindestens Level 13 sein." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Dafür müsst Ihr mindestens Level 17 sein." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "Arkanes Wissen gewonnen!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "Was Euch nicht umbringt..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "Wissen ist Macht." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Gebt und Ihr werdet empfangen." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Manche Erfahrung wird durch Berührung gewonnen." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "Es gibt keinen Ort wie Zuhause." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Spirituelle Energie wird wiederhergestellt." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Ihr fühlt Euch agiler." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Ihr fühlt Euch stärker." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Ihr fühlt Euch klüger." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Ihr fühlt Euch erfrischt." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "Was den Willen brechen kann." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Kathedrale" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Katakomben" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Höhlen" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Nest" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Krypta" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Lv {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "Schwierigkeitsgrad: {}" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "Im Hauptmenü" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "Loopback" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "Keine Verbindung möglich" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "Fehler: 0 Byte vom Server lesen" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq oder spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Einige Hellfire-MPQs fehlen" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Nicht alle Hellfire-MPQs vorhanden.\n" "Bitte besorge alle hf*.mpq-Dateien." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Auflösung" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Rennen in der Stadt" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "Theodor-Quest" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "Kuh-Quest" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Gold autom. aufheben" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Elixire autom. aufheben" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 msgid "Auto Oil Pickup" msgstr "Autom. Öl aufheben" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Autom. Aufheben in der Stadt" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Adria füllt Mana auf" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Waffen autom. anlegen" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Rüstungen autom. anlegen" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Helme autom. anlegen" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Schilde autom. anlegen" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Schmuck autom. anlegen" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Zufallsquests" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "Zeig Gegenstandbeschriftung" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Gürtel autom. nachfüllen" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Spielzerstörende Schreine deaktivieren" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Heiltränke autom. aufheben" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Große Heiltränke autom. aufheben" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Manatränke autom. aufheben" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Große Manatränke autom. aufheben" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Genesungstränke autom. aufheben" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Große Genesungstränke autom. aufheben" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Optionen" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Spiel speichern" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy #| msgid "Main Menu" msgid "Exit to Main Menu" msgstr "Hauptmenü" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Spiel beenden" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Gamma" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Geschw." #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Musik Aus" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Ton" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Ton Aus" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Pause" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Tastaturkürzel:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Hilfemenü" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Hauptmenü" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Tab: Automatische Karte" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Space: Alle Fenster ausblenden" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Schnellzauberleiste" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Zauberbuch" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Inventar" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Charakterübersicht" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "Q: Aufträge ansehen" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Helligkeit verringern" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "G: Helligkeit erhöhen" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Zoom" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Autom.-Karte-Zoom" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Gürtelplätze" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Schnelltaste für Zauber festlegen" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Linke Maustaste: Angreifen im Stand" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Linke Maustaste (in der Charakterübersicht): Alle Statuspunkte " "vergeben" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Linke Maustaste (im Inventar): Ggst. bewegen oder ausrüsten bzw. " "ablegen" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "Shift + Linke Maustaste (im Gürtel): Ggst. ins Inventar bewegen" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Bewegung:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" "Wenn Du den Mauszeiger festhältst, bewegt sich der Charakter kontinuierlich " "in diese Richtung." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Kampf:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "Mit Shift + Linksklick greift der Charakter an, ohne sich zu bewegen." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Autom. Karte:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "Rufe die automatische Karte über den \"Karte\"-Knopf oder die TAB-Taste auf. " "Zoomen kann man innerhalb der autom. Karte mit den Tasten + und -. Mit den " "Pfeiltasten verschiebt man die Karte." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Objekte aufheben:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Kleine verwendbare Gegenstände wie Tränke oder Schriftrollen werden " "automatisch im Gürtel platziert und mit einer kleinen Nummer versehen. Diese " "Gegenstände können durch Drücken der entsprechenden Nummerntaste oder " "Rechtsklick mit der Maus benutzt werden." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Gold:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Mit Rechtsklick auf einen Goldhaufen kannst Du eine bestimmte Menge Gold " "abheben." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Fertigkeiten & Zauber:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "Rufe Deine Liste der Fertigkeiten und Zauber über den \"Zauber\"-Knopf oder " "die B-Taste auf. Erlernte und durch Stäbe verfügbare Zauber werden hier " "angezeigt. Linksklick zum Auswählen eines Zaubers. Rechtsklick in das " "Spielfeld zum Auslösen des Zaubers." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Verwendung der Schnellzauberleiste:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Rufe die Schnellzauberleiste mit einem Linksklick auf den Knopf mit dem " "ausgewählten Zauber oder über die S-Taste auf. Linksklick auf eine " "Fertigkeit oder einen Zauber zum Auswählen. Rechtsklick in das Spielfeld zum " "Auslösen des Zaubers." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Shift + Linksklick auf den Knopf mit dem ausgewählten Zauber hebt die " "Auswahl auf." #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Zauber-Schnelltasten festlegen:" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Du kannst bis zu vier Schnelltasten für Zauber, Fertigkeiten oder " "Schriftrollen festlegen. Rufe dafür wie oben beschrieben die " "Schnellzauberleiste auf. Fahre dann mit der Maus über einen Zauber und weise " "ihm mit F5, F6, F7 oder F8 eine Schnelltaste zu." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Zauberbücher:" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Je mehr Bücher eines bestimmten Zaubers Du liest, desto größer wird Dein " "Wissen und Deine Effektivität beim Anwenden des Zaubers." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Shareware-Hellfire-Hilfe" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Hellfire-Hilfe" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Shareware-Diablo-Hilfe" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Diablo-Hilfe" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Escape zum Beenden oder Pfeiltasten um zu scrollen." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "Hauptfenster kann nicht erstellt werden" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "Kein Platz für das Item" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Öl der Genauigkeit" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Öl der Meisterschaft" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Öl der Schärfung" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Öl des Todes" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Öl der Agilität" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Öl der Reparatur" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Öl der Stärke" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Öl der Beständigkeit" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Öl des Härtens" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Öl der Undurchlässigkeit" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} mit {1}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{0}{1} mit {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{0}{1} de{2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{0}{1}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} de{1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "Erhöhung de" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "r Trefferchance" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "Starke Erhöhung de" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "r Waffentrefferchance" #: Source/items.cpp:1652 msgid "damage potential" msgstr "s Schadenspotentials" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "Starke Erhöhung de" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "s Schadenspotentials - außer Bögen" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "Verringerte Anforderungen" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "für Rüstungen und Waffen" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "Stellt 20% der" #: Source/items.cpp:1664 msgid "item's durability" msgstr "Haltbarkeit wieder her" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "Erhöht die aktuelle und" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "maximale Haltbarkeit" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "Macht einen Gegenstand unzerstörbar" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "Erhöhung des Rüstungswerts" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "von Rüstungen und Schilden" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "Große Erhöhung des Rüstungswerts" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "von Rüstungen und Schilden" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "legt eine Feuerfalle" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "legt eine Blitzfalle" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "legt eine Versteinerungsfalle" #: Source/items.cpp:1695 msgid "restore all life" msgstr "Volle Lebensaufladung" #: Source/items.cpp:1698 msgid "restore some life" msgstr "Partielle Lebensaufladung" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "Partielle Manaaufladung" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "Volle Manaaufladung" #: Source/items.cpp:1707 msgid "increase strength" msgstr "Erhöht die Stärke" #: Source/items.cpp:1710 msgid "increase magic" msgstr "Erhöht die Magie" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "Erhöht die Agilität" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "Erhöht die Vitalität" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "Lebens- und Manaaufladung" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "Volles Leben und Mana" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "(funktioniert nur in Arenen)" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Rechtsklick zum Ansehen" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Rechtsklick zum Benutzen" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "Rechtsklick zum Lesen, dann Linksklick zum Zielen" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Rechtsklick zum Lesen" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Aktivieren zum Ansehen" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Zum Benutzen das Inventar öffnen" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Aktivieren zum Benutzen" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Aus dem Zauberbuch wählen, dann\n" "Zaubern zum Lesen" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Linksklick zum Lesen" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "{} zum Ansehen" #: Source/items.cpp:1806 #, c++-format msgid "{} to use" msgstr "{} zum benutzen" #: Source/items.cpp:1809 #, c++-format msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Aus dem Zauberbuch wählen,\n" "dann {} lesen" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "{} zum Lesen" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Level {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Verdoppelt die Goldkapazität" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Benötigt:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Str" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Mag" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Agil" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "Buch mit {:s}" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "Ohr von {:s}" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "Trefferchance: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% Schaden" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "Treff.: {:+d}%, {:+d}% Schaden" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% Rüstung" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "Rüstungswert: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Feuerresistenz: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Feuerresistenz: {:+d}% MAX" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Blitzresistenz: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Blitzresistenz: {:+d}% MAX" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Magieresistenz: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Magieresistenz: {:+d}% MAX" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Resistenz gegen alles: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Resistenz gegen alles: {:+d}% MAX" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "+ {:d} auf alle Zauberlevel" msgstr[1] "+ {:d} auf alle Zauberlevel" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "- {:d} auf alle Zauberlevel" msgstr[1] "- {:d} auf alle Zauberlevel" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "Zauberlevel unverändert (?)" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Extraladungen" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} Ladung" msgstr[1] "{:d} {:s} Ladungen" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Feuerschaden: {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Feuerschaden: {:d}-{:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Blitzschaden: {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Blitzschaden: {:d}-{:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} zu Stärke" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} zu Magie" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} zu Agilität" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} zu Vitalität" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} zu allen Attributen" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} Schaden von Feinden" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Leben: {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Mana: {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "hohe Haltbarkeit" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "verringerte Haltbarkeit" #: Source/items.cpp:3960 msgid "indestructible" msgstr "Unzerstörbar" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% Lichtradius" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% Lichtradius" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "Mehrere Pfeile pro Schuss" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "Feuerpfeilschaden: {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "Feuerpfeilschaden: {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "Blitzpfeilschaden {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "Blitzpfeilschaden {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "Feuerballschaden: {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "Feuerballschaden: {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "Angreifer erleidet 1-3 Schaden" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "Träger verliert gesamtes Mana" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "Halber Fallenschaden" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "Zurückstoßung" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% Schaden gegen Dämonen" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Träger verliert alle Resistenzen" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "3% Manaabsaugung" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "5% Manaabsaugung" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "3% Lebensabsaugung" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "3% Lebensabsaugung" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "Durchschlägt gegnerische Rüstung" #: Source/items.cpp:4010 msgid "quick attack" msgstr "Flinker Angriff" #: Source/items.cpp:4012 msgid "fast attack" msgstr "schneller Angriff" #: Source/items.cpp:4014 msgid "faster attack" msgstr "schnellerer Angriff" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "schnellster Angriff" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Anderer Effekt (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "schnelle Erholung" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "schnellere Erholung" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "schnellste Erholung" #: Source/items.cpp:4027 msgid "fast block" msgstr "schnelles Blocken" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "{:d} Schadenspunkt extra" msgstr[1] "{:d} Schadenspunkte extra" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "Zufällige Pfeilgeschwindigkeit" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "Ungewöhnlicher Schaden" #: Source/items.cpp:4035 msgid "altered durability" msgstr "Veränderte Haltbarkeit" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "Einhändig" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "Kontinuierlicher LP-Verlust" #: Source/items.cpp:4041 msgid "life stealing" msgstr "Lebensabsaugung" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "Keine Mindeststärke" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "Blitzschaden: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "Blitzschaden: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "Geladene Blitze bei Treffer" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "5% Chance auf 3-fachen Gesamtschaden" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "{:+d}% Schaden, -5% pro Treffer" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x Gegnerschaden, 1x Selbstschaden" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Plötzlich 0 - 600% Schaden" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "Geringe Haltb., {:+d}% Schaden" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "Extra-Rüstung gegen Dämonen" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "Extra-Rüstung gegen Untote" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% Mana wird zu Leben" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% Leben wird zu Mana" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "Schaden: {:d} Unzerstörbar" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "Schaden: {:d} Haltb.: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "Schaden: {:d}-{:d} Unzerstörbar" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "Schaden: {:d}-{:d} Haltb.: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "Rüstung: {:d} Unzerstörbar" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "Rüstung: {:d} Haltb.: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Ladungen: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "Einzigartiger Gegenstand" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Nicht identifiziert" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Halle des Skelettkönigs" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Knochenkammer" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Labyrinth" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Die vergiftete Wasserquelle" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Refugium des Erzbischof Lazarus" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "Kirchen Arena" #: Source/levels/setmaps.cpp:33 msgid "Hell Arena" msgstr "Höllen Arena" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "Kreis des Lebens Arena" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Hinunter ins Labyrinth" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Hinunter in die Katakomben" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Hinunter in die Höhlen" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Hinunter in die Hölle" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Hinunter in den Stock" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Hinunter in die Krypta" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Hinauf zu Level {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Hinauf zur Stadt" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Hinab zu Level {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Hinunter zu Diablo" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Hinauf zu Stocklevel {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Hinauf zu Kryptalevel {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Grundstein des Universums" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Hinunter zu Kryptalevel {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Zurück zu Level {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Kann Spielstand nicht laden" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Ungültiger Spielstand" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "Spieler befindet sich auf einem Hellfire-Level" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Ungültiger Spielzustand" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "Kann Hauptmenü nicht öffnen" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "Tierisch" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Dämonisch" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Untot" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Typ: {:s} Getötet: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Insgesamt getötet: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Trefferpunkte: {:d}-{:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Kein magischen Resistenzen" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Resistenzen:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Magie" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Feuer" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Blitz" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Immunitäten:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Typ: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Keine Resistenzen" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Keine Immunitäten" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Einige magische Resistenzen" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Einige magische Immunitäten" #: Source/mpq/mpq_writer.cpp:174 msgid "Failed to open archive for writing." msgstr "Fehler beim öffnen des Archives beim schreiben." #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "Ungültiger Zauber von {:s}" #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "Illegaler Zauber von {:s}" #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Spieler '{:s}' (Level {:d}) ist dem Spiel beigetreten" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "Das Spiel ist zu Ende" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "Leveldaten nicht verfügbar" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "Spieler '{:s}' hat das Spiel verlassen" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Spieler '{:s}' hat Diablo getötet und das Spiel verlassen!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "Spieler '{:s}' wurde wegen Timeout entfernt" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Spieler '{:s}' (Level {:d}) ist bereits im Spiel" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Mysteriöser" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Versteckter" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Glimmernder" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "Verdrehter" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Magischer" #: Source/objects.cpp:132 msgid "Stone" msgstr "Steinerner" #: Source/objects.cpp:133 msgid "Religious" msgstr "Religiöser" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Verzauberter" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Thaumaturgischer" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Faszinierender" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Rätselhafter" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Unheimlicher" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Grausiger" #: Source/objects.cpp:141 msgid "Divine" msgstr "Göttlicher" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "Heiliger" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Geheiligter" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Spiritueller" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Gespenstischer" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Verlassener" #: Source/objects.cpp:147 msgid "Creepy" msgstr "Gruseliger" #: Source/objects.cpp:148 msgid "Quiet" msgstr "Stiller" #: Source/objects.cpp:149 msgid "Secluded" msgstr "Abgelegener" #: Source/objects.cpp:150 msgid "Ornate" msgstr "Verzierter" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "Glimmender" #: Source/objects.cpp:152 msgid "Tainted" msgstr "Verdorbener" #: Source/objects.cpp:153 msgid "Oily" msgstr "Öliger" #: Source/objects.cpp:154 msgid "Glowing" msgstr "Leuchtender" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "Bescheidener" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "Funkelnder" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "Schimmernder" #: Source/objects.cpp:159 msgid "Solar" msgstr "Sonniger" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "Murphys" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "Der große Zwist" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "Der Lohn des Sündenkriegs" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "Die Legende der Horadrim" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "Das finstere Exil" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "Der Sündenkrieg" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "Die Gefangennahme der Drei" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "Die Reiche des Jenseits" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "Die Sage von den Dreien" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "Der schwarze König" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "Tagebuch: Die Verhexung" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "Tagebuch: Die Begegnung" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "Tagebuch: Die Tirade" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "Tagebuch: Es wird stärker" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "Tagebuch: Na-Krul" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "Tagebuch: Das Ende" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "Ein Zauberbuch" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "Gekreuzigtes Skelett" #: Source/objects.cpp:4799 msgid "Lever" msgstr "Hebel" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "Offene Tür" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "Geschlossene Tür" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "Blockierte Tür" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "Alter Wälzer" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "Buch der Niedertracht" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "Schädel-Hebel" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "Mystisches Buch" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "Kleine Truhe" #: Source/objects.cpp:4833 msgid "Chest" msgstr "Truhe" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "Große Truhe" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "Sarkophag" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "Bücheregal" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "Bücherschrank" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "Fass" #: Source/objects.cpp:4851 msgid "Pod" msgstr "Exuvie" #: Source/objects.cpp:4854 msgid "Urn" msgstr "Urne" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "{:s} Schrein" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "Skelettbuch" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "Ledergebundenes Buch" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "Blutbrunnen" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "Enthauptete Leiche" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "Buch der Blinden" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "Buch des Blutes" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "Reinigende Quelle" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "Rüstung" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "Waffenständer" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "Ziegenschrein" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "Kessel" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "Trübes Becken" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "Fontäne der Tränen" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "Stahlbuch" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "Blutsockel" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "Pilzbeet" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "Abscheulicher Ständer" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Getöteter Held" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "Gesicherte {:s}" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (deaktiviert)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "An" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "Aus" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Spielmodus" #: Source/options.cpp:422 #, fuzzy #| msgid "Gameplay Settings" msgid "Game Mode Settings" msgstr "Spieleinstellungen" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Diablo oder Hellfire spielen." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Auf Demoversion beschränken" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Macht das Spiel zur Demo kompatibel. Ermöglicht das Zusammenspiel mit " "Freunden, die das Spiel nicht besitzen." #: Source/options.cpp:442 msgid "Start Up" msgstr "Spielstart" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Spielstarteinstellungen" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Intro" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Angezeigte Intro-Videos." #: Source/options.cpp:455 msgid "Splash" msgstr "Begrüßungsbildschirm" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Angezeigte Begrüßungsbildschirme." #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Logo- und Titelbildschirm" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Titelbildschirm" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Diablo-Einstellungen" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Hellfire-Einstellungen" #: Source/options.cpp:501 msgid "Audio" msgstr "Audio" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Audio Einstellungen" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Laufgeräusche" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Der Spieler verursacht Geräusche beim Laufen." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Autom.-Aufheben Geräusch" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" "Wenn Gegenstände automatisch aufgehoben werden, wird ein Geräusch abgespielt." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Gegenstand-Aufheben Geräusch" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Wenn Gegenstände aufgehoben werden, wird ein Geräusch abgespielt." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Abtastrate" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Ausgangsabtastrate (Hz)." #: Source/options.cpp:508 msgid "Channels" msgstr "Kanäle" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Legt die Anzahl der Ausgangskanäle fest." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Puffergröße" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Puffergröße (Anzahl der Frames pro Kanal)." #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Neuberechnungsqualität" #: Source/options.cpp:510 #, fuzzy #| msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "Qualität der Neuberechnung, von 0 (niedrig) bis 10 (hoch)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Verändert die interne Auflösung des Spiels und bestimmt den sichtbaren " "Bereich. Hinweis: Dieser kann bei gleicher Auflösung je nach Auswahl der " "Skalierungseinstellungen variieren." #: Source/options.cpp:574 msgid "Resampler" msgstr "Resampler" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "Audio resampler" #: Source/options.cpp:631 msgid "Device" msgstr "Gerät" #: Source/options.cpp:631 msgid "Audio device" msgstr "Audio Gerät" #: Source/options.cpp:688 msgid "Graphics" msgstr "Grafik" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Grafikeinstellungen" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Vollbild" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Zeigt das Spiel im Fenster- oder Vollbildmodus an." #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "An Bildschirmgröße anpassen" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Passt das Seitenverhältnis des Spielfensters automatisch an die aktuelle " "Bildschirmauflösung an." #: Source/options.cpp:700 msgid "Upscale" msgstr "Hochskalieren" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Aktiviert die Bildskalierung von Spiel- zur Desktopauflösung. Verhindert das " "Wechseln der Desktopauflösung und erlaubt es die Fenstergröße anzupassen." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Skalierungsqualität" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "Aktiviert optionale Filter für das Ausgabebild beim Hochskalieren." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Pixelwiederholung" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Bilinear" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Anisotropisch" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Ganzzahlige Skalierung" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Skaliert das Bild mit einem ganzzahligen Pixelverhältnis." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Vertikale Synchronisation" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "Zoomen an wenn aktiviert." #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Blitz" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Indizierte Farben" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "Verwendet indizierte Farben für Wasser-, Lava- und Säureanimationen." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Alternative Nest-Farbpalette" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "In Hellfire wird eine andere Farbpalette für das Nest verwendet." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Hardware-Cursor" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Einen Hardware-Cursor verwenden" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Hardware-Cursor für Gegenstände" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Verwendet einen Hardware-Cursor für Gegenstände." #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Hardware-Cursor Maximalgröße" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Legt die maximale Breite / Höhe für den Hardware-Cursor fest. Bei größeren " "Werten wird ein Software-Cursor verwendet." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Zeige FPS an" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Zeigt die FPS in der oberen linken Ecke des Bildschirms an." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Spiel" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Spieleinstellungen" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Aktiviert joggen / schnelles Laufen in der Stadt für Diablo und Hellfire. " "Diese Option wurde in der Erweiterung eingeführt." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Eingabe fangen" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "Hindert den Mauszeiger am Verlassen des Spielfensters." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Theodor Quest aktivieren." #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" "Kuh-Quest aktivieren. Lester der Farmer wird ersetzt durch den kompletten " "Narr." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Freundliches Feuer" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Erlaubt Pfeil- und Zauberschaden zwischen Spielern in Multiplayer, selbst " "wenn die Spieler sich freundlich gesinnt sind." #: Source/options.cpp:790 msgid "Full quests in Multiplayer" msgstr "Volle Quests im Mehrspielermodus" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "Aktiviert die voll / ungeschnitte Einzelspieler Version von Quest." #: Source/options.cpp:791 msgid "Test Bard" msgstr "Bardin aktivieren" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Zeigt den Charaktertyp Bardin in der Heldenauswahl an." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Barbar aktivieren" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "Zeigt den Charaktertyp Barabar in der Heldenauswahl an." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Erfahrungsleiste" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "" "Eine Erfahrungsleiste wird in die Benutzeroberfläche am unteren Rand des " "Bildschirms integriert." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "Zeigt Gegenständegrafiken im Store" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" "Zeigt Gestandgrafik zu der linken Gegenstandbeschreibung im Store Menu." #: Source/options.cpp:795 msgid "Show health values" msgstr "Lebenspunkte anzeigen" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "Zeigt die aktuellen / maximalen Lebenspunkte an der Lebenskugel an." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Manapunkte anzeigen" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "Zeigt die aktuellen / maximalen Manapunkte an der Manakugel an." #: Source/options.cpp:797 #, fuzzy #| msgid "Character Information" msgid "Show Party Information" msgstr "Charakterinformation" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Monsterlebensbalken" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "Ein Lebensbalken für Monster wird am oberen Bildschirmrand angezeigt." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Gold wird automatisch aufgehoben, wenn der Held in die Nähe kommt." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "" "Elixire werden automatisch aufgehoben, wenn der Held in die Nähe kommt." #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "Öle werden automatisch aufgehoben, wenn der Spieler in die Nähe kommt." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Gegenstände in der Stadt werden automatisch aufgehoben." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Adria füllt das Mana auf, wenn man sie in ihrem Laden besucht." #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" "Waffen werden automatisch angelegt, wenn sie aufgehoben oder gekauft werden." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" "Rüstungen werden automatisch angelegt, wenn sie aufgehoben oder gekauft " "werden." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" "Helme werden automatisch angelegt, wenn sie aufgehoben oder gekauft werden." #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" "Schilde werden automatisch angelegt, wenn sie aufgehoben oder gekauft werden." #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" "Schmuck wird automatisch angelegt, wenn er aufgehoben oder gekauft wird." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "Wählt die Quests bei neuen Spielen zufällig aus." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Monstertyp anzeigen" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Beim Halten des Mauszeigers auf ein Monster wird dessen Typ in der " "Beschreibungsbox am unteren Bildschirmrand angezeigt." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "Beschriftungen für Gegenstände auf dem Boden anzeigen, wenn aktiviert." #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "" "Füllt den Gürtel automatisch aus dem Inventar nach, wenn ein Gegenstand " "verbraucht wird." #: Source/options.cpp:814 #, fuzzy #| msgid "" #| "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines " #| "and Sacred Shrines are not able to be clicked on and labeled as disabled." msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" "Kessel, Faszinierende, Verzierte, Geheiligte und Ziegenschreine können nicht " "mehr angeklickt werden und werden als deaktiviert markiert." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Schnellzaubern" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Schnelltasten führen einen Zauber augenblicklich aus, statt ihn nur als " "ausgewählt zu markieren." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Anzahl der Heiltränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "" "Anzahl der großen Heiltränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Anzahl der Manatränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "" "Anzahl der großen Manatränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "Anzahl der Genesungstränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "" "Anzahl der großen Genesungstränke, die automatisch aufgehoben werden sollen." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "Aktivert schwebende Nummern" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" "Ermöglicht schwebende Zahlen für den Erhalt von XP, das Verursachen von " "Schaden usw." #: Source/options.cpp:824 msgid "Off" msgstr "Aus" #: Source/options.cpp:825 msgid "Random Angles" msgstr "Zufallsquests" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "Nur Vertikal" #: Source/options.cpp:880 msgid "Controller" msgstr "Controller" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Controllereinstellungen" #: Source/options.cpp:889 msgid "Network" msgstr "Netzwerk" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Netzwerkeinstellungen" #: Source/options.cpp:901 msgid "Chat" msgstr "Chat" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Chat Einstellungen" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Sprache" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Legt die Spielsprache fest." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Spracheinstellungen" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Tastaturbelegung" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Tastaturbelegungseinstellungen" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "Gamepadbelegung" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "Gamepadbelegungseinstellungen" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 #, fuzzy #| msgid "Settings" msgid "Mod Settings" msgstr "Einstellungen" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Level" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Erfahrung" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Nächstes Level" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "Basis" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Aktuell" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Stärke" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Magie" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Agilität" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Vitalität" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Punkte" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Gold" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Rüstung" #: Source/panels/charpanel.cpp:176 #, fuzzy #| msgid "chance to hit" msgid "Chance To Hit" msgstr "r Trefferchance" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Schaden" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Leben" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Mana" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "Magie-Res." #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Feuer-Res." #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Blitz-Res." #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "Char" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "Aufträge" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "Karte" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "Menü" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "Inv" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "Zauber" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "Sprechen" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "Stumm" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Unbenutzbar" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Schaden: 1/3 Leben des Ziels" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Heilt: {:d} - {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Schaden: {:d} - {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Fähigkeit" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Stab ({:d} Ladung)" msgstr[1] "Stab ({:d} Ladungen)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Level {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Mana: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Zauber" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Nur Schaden an Untoten" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Schriftrolle" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Stab" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Zauber-Schnelltaste {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "Fehler beim Öffnen des Archivs" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "Fehler beim Laden des Charakters" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (lvl {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "Chatverlauf (Nachrichten: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} Gold" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "Wieviel Gold möchtest du abheben?" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Level {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Erfahrung: {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Maximales Level" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Nächstes Level: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} zu Level {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "König Leorics Grabstätte" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "Die Knochenkammer" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Einem dunkler Durchgang" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Unheiliger Altar" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "Zu {:s}" #: Source/quick_messages.cpp:10 #, fuzzy #| msgid "I need help! Come Here!" msgid "I need help! Come here!" msgstr "Ich brauche Hilfe!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Folge mir." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Ich habe etwas für Dich." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "Stirb!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 #, fuzzy #| msgid "Thanks To" msgid "Thanks." msgstr "Dank an" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 msgid "Griswold" msgstr "Griswold" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Pepin" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Ogden" #: Source/stores.cpp:135 msgid "Cain" msgstr "Cain" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Farnham" #: Source/stores.cpp:137 msgid "Adria" msgstr "Adria" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Gillian" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Wirt" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Zurück" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Schaden: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Rüstung: {:d} " #: Source/stores.cpp:315 #, fuzzy, c++-format #| msgid "Dur: {:d}/{:d}, " msgid "Dur: {:d}/{:d}" msgstr "Haltb.: {:d}/{:d}, " #: Source/stores.cpp:317 #, fuzzy #| msgid "indestructible" msgid "Indestructible" msgstr "Unzerstörbar" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Willkommen im" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Laden des Schmieds" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Was möchtet Ihr:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Mit Griswold sprechen" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Gewöhnliches kaufen" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Besonderes kaufen" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Verkaufen" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Reparieren" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Schmiede verlassen" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Hier meine Angebote:" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Hier meine besonderen Angebote:" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "Ihr habt nichts zu verkaufen." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "Was wollt Ihr verkaufen?" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "Ihr habt nichts zu Reparieren." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "Was soll repariert werden?" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Hütte der Hexe" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Mit Adria sprechen" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Etwas kaufen" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Stäbe aufladen" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Hütte verlassen" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "Ihr habt nichts aufzuladen." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "Was soll ich aufladen?" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "Ihr habt nicht genug Gold" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "Ihr habt nicht genug Platz im Inventar" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "Abgemacht?" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "Seid Ihr sicher, dass ich das identifizieren soll?" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "Seid Ihr sicher, dass Ihr diesen Gegenstand kaufen wollt?" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "Seid Ihr sicher, dass ich diesen Stab aufladen soll?" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "Seid Ihr sicher, dass Ihr diesen Gegenstand verkaufen wollt?" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "Seid Ihr sicher, dass ich das reparieren soll?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Wirt mit dem Holzbein" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Mit Wirt sprechen" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Ich habe etwas Besonderes zu," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "verkaufen. Für 50 Gold-" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "stücke zeige ich es Dir. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "Was hast Du?" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Auf Wiedersehen" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Hier mein Angebot:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Verlassen" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Haus des Heilers" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Mit Pepin sprechen" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Heiler verlassen" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "Deckard Cain" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Mit Cain sprechen" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Gegenstand identifizieren" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "Ihr habt nichts zu identifizieren." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "Was soll ich identifizieren?" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "Folgende Gegenstände habe ich identifiziert:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Erledigt" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Sprecht mit {:s}" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Ihr sprecht mit {:s}" #: Source/stores.cpp:1217 msgid "is not available" msgstr "ist nicht verfügbar" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "in der Shareware" #: Source/stores.cpp:1219 msgid "version" msgstr "Version" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Tratsch" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Aufgehende Sonne" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Mit Ogden sprechen" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Taverne verlassen" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Mit Gillian sprechen" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Truhe öffnen" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Farnham, der Säufer" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Mit Farnham sprechen" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Auf Wiedersehen" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Euer Gold: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Getöteter Dorfbewohner" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Griswold, der Schmied" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Pepin, der Heiler" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Verwundeter Dorfbewohner" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Ogden, der Gastwirt" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Cain, der Dorfälteste" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Adria, die Hexe" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Gillian, die Barkeeperin" #: Source/towners.cpp:786 msgid "Cow" msgstr "Kuh" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Lester, der Farmer" #: Source/towners.cpp:788 msgid "Celia" msgstr "Celia" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Kompletter Narr" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Krieger" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Jägerin" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Magier" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Mönch" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Bardin" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Barbar" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Zombie" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Ghul" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Faulender Kadaver" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Pestbringer" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "Gefallener" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Kobold" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Teuflischer" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "Dunkle Kreatur" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Skelett" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Knochenkrieger" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Brennender Toter" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Horror" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Aasfresser" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Pestkäfer" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Schattenbiest" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Knochenreißer" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Knochen-Bogenschütze" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Skelett-Heerführer" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Knochen-Heerführer" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Brennender Heerführer" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Horror-Heerführer" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Fürst der Täuschung" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Phantom" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Anschleicher" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Ungesehener" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Illusionist" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Satyrfürst" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Blut-Clan" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Stein-Clan" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Feuer-Clan" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Nacht-Clan" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Fledermaus" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Blinzler" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Düsterflügel" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Familiar" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Säurespucker" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Giftspucker" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Grubenbiest" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Lavaschlund" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Skelettkönig" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "Der Schlächter" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Aufseher" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Entstellter" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Verdammter" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "Gehäuteter" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Lindwurm" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Höhlenlarve" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Teufelswurm" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Verschlinger" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Magmafürst" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Blutfürst" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Höllenfürst" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Lavafürst" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Gehörnter Drescher" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Schlammdrescher" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Frostdrescher" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Obsidianfürst" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "oldboned" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Roter Tod" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Lich-Dämon" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Untoter Balrog" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Brennende Seele" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Flammenfürst" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Feuer der Verdammnis" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Höllenbrand" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Gewittergeißler" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Sturmreiter" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Sturmfürst" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Mahlstrom" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Teuflische Brut" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Geflügelter Dämon" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Gargoyle" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Blutklaue" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Todesschwinge" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Feuergeißler" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Wächter" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Feuerwirbler" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Balrog" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Höhlenviper" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Salamander" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Goldviper" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Azurschlange" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Schwarzer Ritter" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Ritter der Verdammnis" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Ritter des Abgrunds" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Blutritter" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "Zerfetzte" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "Leere Hülle" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Herr der Schmerzen" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Trugbild-Weber" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Sukkubus" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Schneehexe" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Höllenhexe" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Sirene" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Justiziar" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Magistrat" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Kabbalist" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Palatin" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Golem" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "Der Dunkle Fürst" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Der Erz-Lich Malignus" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Gharbad der Schwächliche" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Zhar der Verrückte" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Snotspill" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Erzbischof Lazarus" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Die Hexe Vanth" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Aterjadus" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Kriegsherr des Blutes" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Grendel" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "Gothaur der Schlinger" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Knochenaxt der Kühne" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Klingenhaut der Schnitter" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Eitrige Seele" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Lygromath der Unreine" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Knochenbrecher" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Gatacheor der Hungrige" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Soor-Omash der Flinke" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Bhazman-Lath" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Bongo" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Garrach der Befallene" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Schattenbiss" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Todesauge" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Sarcon der Verblichene" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "El Chupacabras" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Feuerschädel" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Zyraell" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Drokvalath Bannschläger" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Carnifex" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Kalong" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Zornrabe" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Chyraptaroth" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Varmoth der Feuerteufel" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Schattenkrähe" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Beskatet der Ungestärkte" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Orkhabal der Grubenmeister" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Blutfell Düsterbogen" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "Moderschwinge" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Schattentrinker" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Bhalaved" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Zhelobb der Giftige" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Tosfarel" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Gamrafos der Todesschatten" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Carcharoth die Kriegsmade" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Tar-Gralid der Umbarmherzige" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Oribadesh" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Fidar der Kalte" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Nograch" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Bronzefaust Feuerstein" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Gayazhed der Gezeichnete" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Bolgramoth der Harte" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Inarius" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Fäulnishorn Stahlkeule" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Chaosheuler" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Beelroth der Verfaulte" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Uphostadil" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Rikrak die Knochensäge" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Rückenbrecher" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Entseelte Fratze" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Bahkauv" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Taar-Umush" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Ozedrol" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Goldfäule der Flamme" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Schwarzer Sturm" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Graglior der Pestbringer" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "Masabal der Häuter" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Buras der Pfähler" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Hyrug der Missgebildete" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Tarasque" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Rashgrador" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Ternobel der Gebeugte" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Schwarzzunge" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Schaurige Berührung" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Viperflamme" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Urzok der Tatzelwurm" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Thalac die Unheilige" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Der Schwarze Schnitter" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Scalptor" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Mazaruth" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Cellerarius" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Sartor der Rostige" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Asrud der Schattige" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Lanius der Unheilvolle" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Ragana Seelenfeuer" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Nyxana die Drude" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Addanc Blutmaul" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Ulschalal der Schlachter" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Der grausige Richter" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Die Hexe Sternenauge" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Zantharod der Jäger" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Sir Gorash" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "Varazhed der Wesir" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Zamphir" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Blutdürstige Hexe" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Schwarze Witwe" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Erichtho Wirbeltanz" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "Grimmzorn" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Volux der Vergessene" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Kurzschwert" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Beschützer" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Keule" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Kurzbogen" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Kurzstab des Manas" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Beil" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "Die Krone der Untoten" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Der himmlische Ring" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Magischer Stein" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Das Optische Amulett" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Ring der Wahrheit" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Tavernenschild" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Harlekinskrone" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Stählerner Schleier" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Goldenes Elixier" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Amboss des Zornes" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Schwarzer Pilz" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Gehirn" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Pilzbuch" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Spektralelixier" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Blutstein" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Karte der Kathedrale" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Heiltrank" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Manatrank" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Schriftrolle der Identifikation" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Schriftrolle des Stadtportals" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Arkaines Tapferstahl" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Großer Heiltrank" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Großer Manatrank" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Griswolds Schneide" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Prunkharnisch des Kuhkönigs" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Stab des Lazarus" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Schriftrolle der Wiederbelebung" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Kurzstab" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Schwert" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Dolch" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Runenbombe" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Theodor" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Auratisches Amulett" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Zerissene Notiz 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Zerissene Notiz 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Zerissene Notiz 3" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Wiederhergestellte Notiz" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Brauner Anzug" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Grauer Anzug" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Kappe" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Schädelkappe" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Helm" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Vollhelm" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Krone" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Kammhelm" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Mantel" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Lumpen" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Umhang" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Robe" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Gesteppte Rüstung" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Lederrüstung" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Gehärtetes Leder" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Nietenrüstung" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Ringpanzer" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Brünne" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Kettenhemd" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Schuppenpanzer" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Brustharnisch" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Panzer" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Schienenrüstung" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Kettenpanzer" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Feldrüstung" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Gotischer Harnisch" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Plattenpanzer" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Schild" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Kleiner Schild" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Großer Schild" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Drachenschild" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Turmschild" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Gotischer Schild" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Trank der Genesung" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Trank der vollen Genesung" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Öl" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Elixir der Stärke" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Elixir der Magie" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Elixir der Agilität" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Elixir der Vitalität" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Schriftrolle der Heilung" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Schriftrolle der Suche" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Schriftrolle des Blitzes" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Schriftrolle der Feuerwand" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Schriftrolle des Infernos" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Schriftrolle des Lichtblitzes" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Schriftrolle des Durchblicks" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Schriftrolle der Verschiebung" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Schriftrolle des Manaschilds" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Schriftrolle der Flammenwelle" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Schriftrolle des Feuerballs" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Schriftrolle des Steinfluchs" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Schriftrolle des Kettenblitzes" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Schriftrolle der Wächter" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Schriftrolle der Nova" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Schriftrolle des Golems" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Schriftrolle des Teleports" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Schriftrolle der Apokalypse" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Sarazenenschwert" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Krummsäbel" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Flamberg" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Klinge" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Säbel" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Langschwert" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Breitschwert" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Bastardschwert" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Bidenhänder" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Kriegsschwert" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Handbeil" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Axt" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Streitaxt" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Breitaxt" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Kampfaxt" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Kriegsaxt" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Stahlkeule" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Morgenstern" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Streithammer" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Hammer" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Nagelkeule" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Dreschflegel" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Kriegshammer" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Bogen" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Jagdbogen" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Langbogen" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Kompositbogen" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Recurvebogen" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Kampfbogen" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Prunkbogen" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Kriegsbogen" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Langstab" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Kompositstab" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Kampfstab" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Kriegsstab" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Ring" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Amulett" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Feuerrune" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Rune" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Blitzrune" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Große Feuerrune" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Große Blitzrune" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Steinrune" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Kurzstab des geladenen Blitzes" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "Arena Trank" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "Metzgerbeil des Schlächters" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Lightsabre" msgid "Lightforge" msgstr "Lichtsäbel" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "Der Riftbogen" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "Der Nadler" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "Der himmlische Bogen" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Tödlicher Jäger" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Bogen der Toten" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "Der Schwarzeichenbogen" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Feuerpfeil" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Nierenstecher" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Windmacht" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Adlerhorn" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Gonnagals Dolch" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "Der Verteidiger" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Greifenklaue" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Schwarzer Schnitter" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Affenmond" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Eiszapfen" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "Die Henkersklinge" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "Die Knochensäge" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Schattenfalke" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Keris des Zauberers" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Lichtsäbel" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "Die Falkenklaue" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Inferno" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "Todesbringer" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "Der Grizzly" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "Der Großvater" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "Der Zermalmer" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Scharfschnabel" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Blutschlächter" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "Die himmlische Axt" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "Bösartige Axt" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "Steinspalter" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "Aguinaras Kriegsbeil" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "Höllenschlächter" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "Messerschmidts Räuber" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "Brechrost" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "Hammer des Jholm" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "Civerbs Knüppel" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "Der himmlische Stern" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "Baranars Stern" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "Knorrige Wurzel" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "Der Schädelspalter" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "Dirks Streithammer" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "Alptraumtöter" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "Stab der Schatten" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "Immolator" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "Sturmspitze" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "Glitzerlied" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "Donnerruf" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "Der Beschützer" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "Najs Rätsler" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "Seelenjammer" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "Onans Stab" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "Helm der Geister" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "Die Denkkappe" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "Helm des Aufsehers" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "Narrenkappe" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "Götterdämmerung" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "Königlicher Reif" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "Fleisch der gequälten Seelen" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "Der Fluch des Gladiators" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "Der Regenbogenumhang" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "Auts Lederrüstung" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "Gewand der Weisheit" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "Funkenrüstung" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "Aasfressers Beute" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "Nachtschatten" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "Najs leichter Panzer" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "Stachelige Dämonenhaut" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "Der Deflektor" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "Spaltschädel-Schild" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "Drachentöter" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "Schwarzeichenschild" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "Heiliger Lodder" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "Sturmschild" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "Dornenzweig" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "Ring von Regha" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "Der Bluter" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "Würgering" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "Verlobungsring" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "Zinn-" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "Messing-" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "Bronze-" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "Eisen-" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "Stahl-" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "Silber-" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "Platin-" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "Mithril-" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "Meteorischer" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "Seltsamer" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "Unnützer" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "Verbogener" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "Schwacher" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "Gezackter" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "Tödlicher" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "Schwerer" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "Grimmiger" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "Brutaler" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "Massiver" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "Wilder" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "Rücksichtsloser" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "Gnadenloser" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "Ungeschickter" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "Stumpfer" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "Scharfer" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "Erlesener" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "Kriegerischer" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "Soldatischer" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "Fürstlicher" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "Ritterlicher" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "Meisterlicher" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "Heldenhafter" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "Königlicher" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "Verletzlicher" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "Verrosteter" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "Starker" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "Robuster" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "Tapferer" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "Glorreicher" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "Gesegneter" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "Geweihter" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "Großartiger" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "Göttlicher" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "Roter" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "Karmesin-" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "Granat-" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "Rubin-" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "Blauer" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "Azurblauer" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "Lazulith-" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "Kobalt-" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "Saphir-" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "Weißer" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "Perlen-" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "Elfenbein-" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "Kristall-" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "Diamant-" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "Topas-" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "Bernstein-" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "Jade-" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "Obsidian-" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "Smaragd-" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "Hyänen-" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "Frosch-" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "Spinnen-" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "Raben-" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "Schlangen-" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "Vipern-" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "Echsen-" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "Drachen-" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "Lindwurm-" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "Hydra-" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "Engels-" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "Erzengels-" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "Spendabler" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "Großzügiger" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "Flammender" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "Blitzender" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "r Qualität" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "r Verstümmelung" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "s Erschlagens" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "s Massakers" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "s Gemetzels" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "s Schlachtens" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "s Schmerzes" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "r Tränen" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "r Gesundheit" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "s Schutzes" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "r Absorption" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "r Deflektion" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "r Osmose" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "r Zerbrechlichkeit" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "r Schwäche" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "r Stärke" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "r Macht" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "r Kraft" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "r Giganten" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "r Titanen" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "r Paralyse" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "r Atropie" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "r Agilität" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "r Fertigkeit" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "r Genauigkeit" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "r Präzision" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "r Perfektion" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "s Narren" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "r Dyslexie" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "r Magie" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "s Geistes" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "r Brillanz" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "r Zauberei" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "r Hexerei" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "r Krankheit" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "r Seuche" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "r Vitalität" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "r Frische" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "s Schwungs" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "r Härte" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "s Lebens" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "r Sorgen" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "r Lähmung" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "s Himmels" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "s Mondes" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "r Sterne" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "r Herrlichkeit" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "s Tierkreises" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "s Geiers" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "s Schakals" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "s Fuchses" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "s Jaguars" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "s Adlers" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "s Wolfs" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "s Tigers" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "s Löwen" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "s Mammuts" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "s Wals" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "r Brüchigkeit" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "r Sprödigkeit" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "r Festigkeit" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "r Kunstfertigkeit" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "r Dauerhaftigkeit" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "r Ewigkeit" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "r Finsternis" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "r Nacht" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "s Lichts" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "s Glanzes" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "r Flamme" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "s Feuers" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "s Brennens" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "s Schocks" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "s Blitzes" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "s Donners" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "r Vielfalt" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "r Fülle" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "r Dornen" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "r Verderbtheit" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "r Diebe" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "s Bären" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "r Fledermaus" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "r Vampire" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "s Blutegels" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "s Blutes" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "r Perforation" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "s Durchbohrens" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "s Zerschmetterns" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "r Bereitschaft" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "r Gewandtheit" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "r Geschwindigkeit" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "r Schnelligkeit" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "r Balance" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "r Stabilität" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "r Harmonie" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "s Blockens" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "Der magische Stein" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "Gharbad der Schwächliche" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "Zhar der Verrückte" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "Der Schlächter" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "Ogdens Schild" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "Die Halle der Blinden" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "Heldenmut" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "Kriegsherr des Blutes" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "Der Fluch des König Leoric" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "Erzbischof Lazarus" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "Grabfunde" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "Lesters Obstgarten" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "Theodor" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "Der fliegende Händler" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "Gothaur der Schlinger" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "Das festliche Gewand" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "Feuerblitz" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "Heilung" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "Blitz" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "Lichtblitz" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "Identifikation" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "Feuerwand" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "Stadtportal" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "Steinfluch" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "Durchblick" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "Verschiebung" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "Manaschild" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "Feuerball" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "Wächter" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "Kettenblitz" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "Flammenwelle" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "Schlangen der Verdammnis" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "Blutritual" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "Nova" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "Unsichtbarkeit" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "Inferno" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "Golem" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "Wut" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "Teleport" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "Apokalypse" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "Ätherisieren" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "Reparatur" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "Stäbe aufladen" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "Falle entschärfen" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "Elementarwesen" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "Geladener Blitz" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "Heiliger Blitz" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "Wiederbelebung" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "Telekinese" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "Heilung Anderer" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "Blutstern" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "Knochengeist" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" " Ah, die Geschichte unseres Königs wollt Ihr hören, wie? Der tragische " "Untergang von Leoric war ein harter Schlag für dieses Land. Die Menschen " "haben den König immer geliebt, und nun leben sie in ständiger Todesangst vor " "ihm. Was ich mich immer wieder frage, ist Folgendes: Wie konnte er so tief " "sinken? Leoric war ein Heiliger unter den Menschen. Nur die übelsten Mächte " "der Hölle können einen Mann im Innersten so gründlich vernichten..." #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "Das Dorf braucht Eure Hilfe! Vor einigen Monaten wurde Prinz Albrecht " "gekidnappt, König Leorics Sohn. Der Monarch war außer sich vor Wut und " "durchsuchte das ganze Dorf nach seinem Erben. Mit jedem Tag, der ergebnislos " "verlief, rutschte Leoric tiefer in den Wahnsinn ab. Er beschuldigte " "schließlich wahllos unschuldige Untertanen und ließ sie grausam hinrichten. " "Weniger als die Hälfte von uns überlebte seinen Wahn. \n" " \n" "Die Ritter und Priester des Königs versuchten, ihn zu besänftigen, doch er " "stellte sich gegen sie, und zu ihrer tiefsten Schande waren sie gezwungen, " "ihn zu töten. Mit seinem letzten Atemzug sprach der König einen " "fürchterlichen Fluch über seine ehemaligen Gefolgsleute aus. Er schwor, dass " "sie ihm in der Finsternis auf ewig dienen würden... \n" " \n" "Und hier wird die Geschichte unheimlicher, als ich es jemals für möglich " "gehalten hätte. Unser früherer Herrscher stand von den Toten auf und " "befehligt jetzt eine Legion von Untoten im Labyrinth. Sein Körper wurde in " "einer Gruft drei Stockwerke unter der Kathedrale beigesetzt. Bitte, zerstört " "seinen untoten Körper und sorgt so dafür, dass seine Seele endlich Frieden " "findet." #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Wie ich Euch schon sagte, wurde unser König drei Stockwerke unter der Kirche " "beerdigt. Er ist jetzt da unten und wartet in der fauligen Dunkelheit auf " "seine Chance, das Land zu vernichten..." #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "Der Fluch unseres Königs ist vorüber, aber ich fürchte, er war nur ein Teil " "eines größeren Übels. Wir können jedoch noch vor der Dunkelheit gerettet " "werden, die unser Land verzehrt, denn Euer Sieg ist ein gutes Omen. Möge das " "Licht Euch auf Eurem Weg leiten, guter Meister." #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" "Der Verlust seines Sohnes war zuviel für König Leoric. Ich habe alles getan, " "was in meinen Kräften stand, um seine Wahnvorstellungen zu heilen, aber am " "Ende überwältigten sie ihn doch. Ein finsterer Fluch hing von diesem Tage an " "über diesem Königreich. Doch vielleicht endet er ja, wenn Ihr die Seele des " "Königs aus ihrem irdischen Gefängnis befreit." #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" "Ich mag gar nicht daran denken, wie der König gestorben ist. Ich erinnere " "mich lieber an ihn, wie er als freundlicher und gerechter Herrscher war. " "Sein Tod war so traurig und schien so unpassend." #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" "Ich habe viele der Waffen und die meisten der Rüstungen hergestellt, mit " "denen König Leoric seine Ritter ausgerüstet hat. Ich habe ihm sogar ein " "riesiges Zweihänder-Schwert aus feinstem Mithril geschmiedet und eine " "passende Feldkrone dazu. Ich kann immer noch nicht glauben, wie er gestorben " "ist, aber es müssen ganz finstere Mächte gewesen sein, die ihn in den " "Wahnsinn getrieben haben." #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Ist mir egal. Hör mal, kein Skelett wird jemals mein König sein. Leoric ist " "der König, klar? König, verstehst Du? LANG LEBE DER KÖNIG!" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" "Die Toten, die unter uns Lebenden wandeln, sie folgen dem verfluchten König. " "Er hat die Macht, noch mehr Krieger für die ständig wachsende Armee der " "Untoten zu rekrutieren. Wenn Ihr seine Herrschaft nicht beendet, wird er " "eines Tages über das Land marschieren und alle umbringen, die noch am Leben " "sind." #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" "Hör mal, ich mache hier Geschäfte! Ich verkaufe keine Informationen, und mir " "ist dieser König völlig egal, der länger tot ist, als ich überhaupt lebe. " "Aber wenn Du was brauchst, was Du gegen diesen Untoten-König als Waffe " "verwenden kannst, dann kann ich Dir helfen." #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" "Die stickige Wärme des Lebens ist in mein Grab eingedrungen! Bereite Dich " "darauf vor, Sterblicher, meinem Herrn bis in alle Ewigkeit zu dienen!" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" "Ich sehe, dass dieses merkwürdige Verhalten auch Euch verwundert. Ich kann " "nur vermuten, dass es die aufgehende Sonne auf dem Schild war, die die " "Dämonen zu der Annahme verleitet hat, das Schild selbst habe magische " "Kräfte. Ihr müsst wissen, dass die Dämonen das Sonnenlicht fürchten und " "glauben, es habe starke Zauberkräfte. Na ja, vielleicht sind sie dann doch " "nicht alle so schlau, wie wir befürchtet haben." #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" "Ich muss Euch von einem höchst merkwürdigen Erlebnis berichten, weil ich " "weiß, dass Ihr Euch mit diesen Monstrositäten auskennt, die im Labyrinth " "leben. Und ich verstehe es beim besten Willen nicht. Heute nacht wurde ich " "von einem kratzenden Geräusch vor meinem Gasthof geweckt. Als ich aus meinem " "Schlafzimmerfenster schaute, sah ich die Umrisse kleiner, dämonenartiger " "Wesen, die über den Hof huschten. Nach einiger Zeit liefen sie weg, aber sie " "nahmen dabei das Türschild des Gasthofes mit! Ich weiß nicht, warum Dämonen " "mein Holzschild stehlen, aber mich und meine Familie in Ruhe lassen sollten. " "Ist doch komisch, oder?" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" "Och, Ihr hättet mir das Schild aber nicht extra zurückbringen müssen! Na ja, " "so spare ich auf jeden Fall die Kosten für ein neues. Wartet mal, was könnte " "ich Euch denn als kleines Dankeschön geben...ah ja, ich weiß schon. Diese " "Mütze hier hat ein durchreisender Magier in einem der Zimmer liegenlassen. " "Vielleicht könnt Ihr ja was damit anfangen." #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" "Meine Güte, Dämonen laufen nachts durch das Dorf und plündern unsere Häuser " "- ist denen denn gar nichts mehr heilig? Ich hoffe, Ogden und Garda geht es " "gut? Na ja, wenn sie verletzt wären, hätten sie mich ja wohl schon besucht." #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" "Oh Himmel! Dahin ist das Schild also verschwunden. Meine Großmutter und ich " "müssen währenddessen friedlich geschlafen haben. Dem Licht sei Dank, dass " "diese Monster den Gasthof nicht überfallen haben." #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "Dämonen haben Ogdens Kneipenschild gestohlen? Das klingt aber gar nicht nach " "den Grausamkeiten, von denen ich gehört und einige auch selbst gesehen " "habe.\n" "\n" "Dämonen reißen Dir normalerweise den Kopf ab, nicht das Türschild!" #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" "Weißt Du, was ich glaube? Irgend jemand hat das Schild geklaut, und jetzt " "wollen sie 'ne riesige Menge Knete dafür. Wenn ich Ogden wäre, ... bin ich " "ja nicht, aber wenn ich's wäre... Ich würde einfach ein neues Schild kaufen, " "mit einem netten Bild drauf. So ein hübscher Bierkrug vielleicht oder ein " "Stückchen Käse." #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Kein Sterblicher kann je den Verstand eines Dämon wirklich ergründen. \n" "\n" "Lasst Euch nie von ihren ziellosen Taten verwirren, denn genau das könnte " "ihr Plan sein." #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" "Was denn, behauptet er etwa, ich hätte das gestohlen?\n" "\n" "Hör mal, über einfaches Schilderklauen bin ich seit Monaten weg. Aus so " "einem Stück Holz ist einfach kein Profit rauszuschlagen." #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" "Hey - Du sein doch der, der alle töten! Du mir holen magische Standarte, " "oder wir angreifen! Du nicht weggehen mit Leben! Du töten große Fieslinge " "und zurückgeben Magie! Gehen um Ecke und Türe, finden Fieslinge! Du geben, " "du gehen!" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "" "Du töten Fieslinge, holen Standarte. Du mir bringen, sonst,...ja sonst..." #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" "Du bringen! Ja, gut, gut! Jetzt gehen, wir stark! Wir alle töten mit große " "Magie! Hahahahaha!" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" "Das klingt nicht gut, denn es bestätigt meine schlimmsten Befürchtungen. " "Bisher habe ich nicht an die alten Legenden geglaubt, doch jetzt zweifle ich " "nicht mehr an ihnen. Vielleicht ist es an der Zeit, Euch zu sagen, wer ich " "wirklich bin. \n" "\n" "Mein wirklicher Name ist Deckard Cain der Ältere, und ich bin der letzte " "Nachfahre einer uralten Bruderschaft, die es sich zum Ziel gesetzt hatte, " "die Geheimnisse einer uralten bösen Macht zu bewahren. Einer Macht, die nun " "offensichtlich aus ihrem Gefängnis entkommen konnte. \n" "\n" "Erzbischof Lazarus, einst der vertrauteste Berater von König Leoric, hat " "einen Trupp einfacher Bürger ins Labyrinth geführt, um Albrecht zu suchen, " "den verschollenen Sohn des Königs. Es dauerte lange, bis jemand wiederkam, " "und nur wenige sind überhaupt mit dem Leben davongekommen. \n" "\n" "Was war ich bloß für ein Idiot! Ich hätte ihm damals schon auf die Schliche " "kommen müssen! Es muss Lazarus selbst gewesen sein, der Albrecht entführt " "hat und seitdem im Labyrinth versteckt hält. Ich weiß nicht, warum der " "Erzbischof zur Finsteren Seite hinübergewechselt ist oder was er mit dem " "Jungen vorhat, es sei denn, er will ihn seinem Dunklen Herrn opfern. \n" "\n" "Ja, das muss es sein! Die Überlebenden seines 'Rettungstrupps' haben " "erzählt, dass Lazarus, als sie ihn zum letzten Mal gesehen haben, " "hinunterrannte in die tiefsten Eingeweide des Labyrinths. Ihr müsst Euch " "beeilen und den Jungen vor dem Opfermesser dieses Verrückten retten!" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Ihr müsst Euch beeilen und Albrecht aus Lazarus' Gewalt befreien! Der Prinz " "und die Bewohner des ganzen Königreiches zählen auf Euch!" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" "Was Ihr da erzählt, klingt sehr ernst, mein Freund. Lazarus wird für seine " "schrecklichen Untaten in der Hölle schmoren. Der Junge, den Ihr mir " "beschrieben habt, ist nicht unser Prinz, aber ich glaube, dass Albrecht " "trotzdem in Gefahr sein könnte. Das Symbol der Macht, von dem Ihr mir " "berichtet habt, ist wahrscheinlich ein Portal, das direkt ins Herz des " "Labyrinthes führt.\n" "\n" "Doch wisset, mein Freund: Die böse Macht, mit der Ihr Euch messen wollt, ist " "der Finstere Lord des Terrors, unter den Sterblichen als Diablo bekannt. Er " "war es, der vor etlichen Jahrhunderten im Labyrinth eingesperrt worden ist, " "und ich fürchte, dass er erneut versucht, Chaos und Schrecken im Reiche der " "Menschen zu verbreiten. Ihr müsst Euch durch das Portal begeben und Diablo " "vernichten, bevor es zu spät ist!" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" "Lazarus war der Erzbischof, der einen Trupp der Dorfbevölkerung in das " "Labyrinth hinabgeführt hat. Ich habe an jenem Tag viele gute Freunde " "verloren, und Lazarus ist nie zurückgekommen. Ich vermute, er ist zusammen " "mit den anderen getötet worden. Wenn Ihr mir einen Gefallen tun wollt, dann " "sprecht bitte Farnham nie auf diesen schrecklichen Tag an." #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Ich war schockiert, als ich in jener Nacht von den Plänen der Leute hörte. " "Von allen Bewohnern des Dorfes hätte ich doch gerade Lazarus mehr Verstand " "zugetraut. Er war schließlich Erzbischof, und er schien sich immer sehr um " "die Leute von Tristram zu sorgen. So viele sind verletzt worden... ich " "konnte nicht alle retten." #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Ich erinnere mich an Lazarus als einen netten und großzügigen Mann. Er hat " "bei der Beerdigung meiner Mutter die Predigt gehalten, und er hat meiner " "Großmutter und mir in sehr schlimmen Zeiten Halt gegeben. Ich bete jede " "Nacht, dass er noch am Leben und in Sicherheit ist." #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Ich war dabei, als Lazarus uns in das Labyrinth geführt hat. Er sprach von " "heiliger Vergeltung, aber als wir anfingen, gegen diese Höllenbrut zu " "kämpfen, hat er noch nicht mal seinen Morgenstern gezückt. Er rannte einfach " "immer tiefer in die dunklen, endlosen Gänge des Labyrinthes, aus denen die " "Knechte der Finsternis auf uns einströmten." #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" "Sie stechen, dann beißen sie, dann sind sie überall um Dich rum. Lügner! " "LÜGNER! Sie sind alle tot! Tot! Versteht Du?! Sie fallen wie die Fliegen... " "der ganze Boden schwimmt vor lauter Blut... alles seine Schuld..." #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" "Ich habe diesen Lazarus nicht gekannt, von dem Ihr erzählt. Aber ich spüre " "eine tiefe Spaltung in seinem Wesen. Er ist eine große Gefahr und er wird " "alles tun für die Mächte der Dunkelheit, die ihn zu einem der Ihren gemacht " "haben." #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" "Oh ja, der heilige Lazarus, der ja soooo gut gegen diese Monster da unten " "gekämpft hat. Aber mein Bein hat er irgendwie auch nicht retten können, " "oder? Hör mal, ich gebe Dir einen Tipp - sogar gratis: Frag Farnham, der war " "dabei." #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" "Vergesst Euer törichtes Vorhaben! Das Einzige, was Euch hier erwartet, ist " "der Zorn meines Herrn. Ihr kommt zu spät, um den Jungen noch zu retten. Doch " "Ihr werdet ihm bald Gesellschaft leisten - in der Hölle!" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" "Hmm, ich weiß nicht, ob ich Euch darüber überhaupt etwas Hilfreiches " "erzählen kann. Das Wasser in unseren Brunnen stammt aus einer unterirdischen " "Quelle. Ich habe mal von einem Tunnel gehört, der zu einem großen " "unterirdischen See führen soll - vielleicht ist dort die Quelle. Aber " "unglücklicherweise weiß ich auch nicht, was unser Wasser so verseuchen " "könnte." #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Ich habe immer versucht, ausreichend große Vorräte an Getränken und " "Lebensmitteln im Keller zu lagern. Aber wenn jetzt die ganze Stadt kein " "frisches Wasser mehr hat, werden sogar meine Reserven bald aufgebraucht " "sein. \n" "\n" "Bitte, tut Euer Bestes, oder ich weiß nicht, wie es weitergehen soll!" #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "Ich bin ja so froh, dass ich Euch noch rechtzeitig erwische! Unsere Brunnen " "sind plötzlich alle vergiftet und brackig. Einige Leute im Dorf sind schon " "krank geworden, weil sie davon getrunken haben. Unsere Frischwasserreserven " "gehen rapide zur Neige. Ich vermute, es gibt einen Durchgang vom Labyrinth " "zu der Höhle, in der unsere Quellen liegen. Bitte findet den Grund für " "dieses Desaster, oder wir werden alle zugrunde gehen." #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Ich bitte Euch, beeilt Euch! Mit jeder Stunde kommen wir dem Verdursten " "näher!\n" "\n" "Ohne Eure Hilfe können wir nicht mehr lange durchhalten." #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "Was sagt Ihr? Die bloße Anwesenheit der Dämonen hat ausgereicht, das Wasser " "zu verseuchen? Da muss ja wirklich ein gewaltiges Übel unter unserer Stadt " "lauern... Aber Euer Mut und Eure Hartnäckigkeit geben uns Grund zur " "Hoffnung. Bitte, nehmt diesen Ring. Vielleicht wird er Euch bei der " "Vernichtung solch widerlicher Geschöpfe einmal hilfreich sein." #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Meine Großmutter ist sehr schwach, und Garda sagt, dass wir das Wasser aus " "den Brunnen nicht trinken können. Ich bitte Euch, könnt Ihr nicht etwas tun, " "um uns zu helfen?" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" "Pepin hat Euch die Wahrheit erzählt. Wir brauchen dringend frisches Wasser, " "und zwar schnell. Ich habe versucht, einen der kleineren Brunnen sauber zu " "kriegen, aber das Wasser selbst stinkt nach verfaultem Dreck. Es wird " "offensichtlich schon direkt an der Quelle vergiftet." #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "Ihr trinkt ... Wasser?" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "Die Leute in Tristram müssen sterben, wenn Ihr nicht dafür sorgt, dass aus " "den Quellen wieder frisches Wasser sprudelt. \n" "\n" "Wisset, dass Dämonen hinter all dem stecken, doch sie wissen nicht, was sie " "angerichtet haben." #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Zur Abwechslung bin ich mal ganz auf Deiner Seite. Mein Geschäft geht - " "bildlich gesprochen - den Bach runter, wenn ich keine Käufer mehr habe. Du " "solltest lieber schnell rausfinden, was da vor sich geht." #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" "Ein Buch, in dem eine Kammer aus Menschenknochen erwähnt wird? Na ja, eine " "Knochenkammer wurde in einigen der archaischen Schriften erwähnt, die ich in " "den Bibliotheken im Osten studiert habe. Diese Bücher meinten " "übereinstimmend, dass die Lords der Unterwelt zum Schutz großer Schätze " "solche Räume errichten könnten. Diejenigen, die bei dem Versuch starben, den " "Schatz zu stehlen, waren für alle Ewigkeit dazu verdammt, ihn zu beschützen. " "Ein schreckliches, aber merkwürdig passendes Ende..." #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" "Ich fürchte, über das Thema weiß ich so gut wie nichts. Aber Cain hat viele " "Bücher, in denen was darüber stehen könnte." #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" "Das klingt nach einem sehr gefährlichen Ort. Wenn Ihr dorthin geht, seid " "bitte besonders vorsichtig!" #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Es tut mir leid, aber darüber habe ich noch nie etwas gehört. Vielleicht " "kann Cain Euch weiterhelfen, unser Geschichtenerzähler?" #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" "Ich weiß nichts über diesen Ort, aber Ihr könntet Cain danach fragen. Er " "redet gerne und viel, und es würde mich nicht überraschen, wenn er Antworten " "auf Eure Fragen hätte." #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Ok, hör mal zu. Da ist dieses Zimmer aus Holz, verstehst Du? Und seine Frau, " "weißt Du, also die... erzählt's den Bäumen... weil Du drauf wartest. Und " "drauf sag ich zu ihm, das könnte schlecht laufen für ihn, aber wenn Du " "meinst, ich bezahle dafür, dann.... äh.... ja, genau." #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Solltet Ihr in diesem verfluchten Reich sterben, werdet Ihr auf ewige Zeiten " "den Dunklen Herrschern dienen müssen. \n" "\n" "Betretet die Knochenkammer auf eigene Gefahr!" #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "Ein riesiger und geheimnisvoller Schatz, sagst Du? Vielleicht könnte ich Dir " "ja nachher das eine oder andere Stück abkaufen. Oder noch besser, brauchst " "Du keine seltene und teure Spezialausrüstung für dieses gewagte Unternehmen?" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" "Es sieht so aus, als habe Erzbischof Lazarus eine ganze Reihe von " "Stadtbewohnern dazu überredet, im Labyrinth nach dem verschollenen Sohn des " "Königs zu suchen. Er stachelte ihre Angst an und verwandelte sie in einen " "aufgeputschten Pöbel. Keiner von ihnen hatte auch nur die leiseste Ahnung, " "was ihn wirklich in der kalten Erde erwartete. Lazarus ließ sie dort unten " "im Stich, wo sie in den Fängen unvorstellbarer Horrorgestalten umkamen." #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" "Ja, Farnham hat mal etwas über ein grausames Monster mit einem riesigen Beil " "erzählt. Ich glaube, er nannte es den Schlächter." #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "Beim Licht, ich kenne diesen bösen Dämon! Von den wenigen Überlebenden, die " "nach Lazarus' Expedition wieder aus dem Labyrinth zurückkehrten, trugen die " "meisten die Spuren seines Zornes auf dem Körper. Ich weiß nicht, womit er " "ihnen diese Wunden beigebracht hat, aber es kann keine gewöhnliche Waffe " "gewesen sein. Die Wunden waren eitrig und entzündet, und selbst ich konnte " "sie kaum heilen. Wenn Ihr gegen diesen Höllensohn kämpfen müsst, seht Euch " "vor!" #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Als Farnham irgendwas über einen Schlächter erzählte, der Leute umbringt, " "habe ich es sofort als eine seiner Faseleien abgetan. Aber da Ihr jetzt auch " "davon anfangt, ist da wohl doch etwas mehr dran." #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" "Ich habe dieses Ding gesehen, das Farnham den 'Schlächter' nennt, wie es in " "den Reihen meiner Freunde gewütet hat. Es schwang ein Metzgerbeil, so groß " "wie eine Zweihandaxt, schlug nach rechts und links, trennte Gliedmaßen und " "Köpfe ab. Ich wurde von einer Horde kleiner, kreischender Dämonen von der " "Haupttruppe abgedrängt und fand irgendwie eine Treppe nach oben. Ich habe " "das Monster seitdem nie wiedergesehen, doch sein blutverschmiertes Gesicht " "taucht immer noch in meinen schlimmsten Alpträumen auf." #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "Groß! Großes Hackebeil tötet alle meine Freunde! Konnte ihn nicht aufhalten, " "musste weglaufen, konnte sie nicht retten. Eingesperrt in einem Zimmer mit " "so vielen Leichen, so vielen Freunden...NEEEEEEEIIIIIN!" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "Der Schlächter ist ein sadistisches Geschöpf, das sich an Schmerz und Qual " "anderer Wesen erfreut. Ihr könnt die Spuren seines Wirkens nur zu gut an dem " "Trinker Farnham sehen. Die Vernichtung des Schlächters wäre ein großer " "Beitrag zur Sicherheit dieses Dorfes." #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" "Ich weiß mehr über diesen Mistkerl, als Du ahnst. Seine kleinen Freunde " "haben mich festgehalten und mir das Bein ausgerissen, bevor Griswold mich " "aus diesem Loch rausgezogen hat. Ich will's mal ganz simpel ausdrücken:\n" "\n" "Mach ihn fertig, bevor er Dich umbringt und ausgestopft in seine " "Leichensammlung packt!" #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" "Bitte, hört mir zu! Erzbischof Lazarus hat uns dort hinunter geführt, um den " "verschollenen Prinzen zu suchen. Der Bastard hat uns in eine Falle geführt! " "Jetzt sind alle tot... umgebracht von einem Dämon, den man den Schlächter " "nennt. Rächt uns! Findet diesen Schlächter und vernichtet ihn, damit unsere " "Seelen endlich Frieden finden..." #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" "Ihr zitiert da einen interessanten Reim, der mich vom Stil her an andere " "Werke erinnert...Lasst mich nachdenken, was war das doch gleich?\n" "\n" "...Dunkelheit verdeckt die Verborgenen. Augen glühen unbeobachtet, nur vom " "kurzen Kratzen rasiermesserscharfer Klauen begleitet, um die armen Seelen zu " "quälen, denen man für alle Ewigkeit das Augenlicht genommen hat. Das " "Gefängnis der so Gestraften heißt Kerker der Blinden..." #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Ich habe mir noch nie viel aus Gedichten gemacht. Manchmal habe ich fahrende " "Sänger engagiert, wenn der Gasthof gut lief, aber das ist jetzt schon lange " "her. \n" "\n" "Was? Ach ja, sicher, darüber weiß jemand anders bestimmt mehr als ich." #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" "Das kommt mir irgendwie bekannt vor. Ich erinnere mich, mal etwas ganz " "Ähnliches gelesen zu haben wie dieses Gedicht. Es war während meiner " "Forschungen über die Geschichte der Dämonenseuchen. Da war die Rede von " "einem Ort großen Übels, der... wartet mal, Ihr habt doch nicht etwa vor, da " "hinzugehen, oder?" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" "Wenn Ihr Fragen zum Thema Blindheit habt, solltet Ihr mit Pepin reden. Ich " "weiß noch, wie er meiner Großmutter einen Trank gab, der Ihr Augenlicht " "verbessert hat. Vielleicht kann er Euch auch helfen." #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" "Ich fürchte, ich habe niemals einen Ort gesehen, der Eurer sehr lebhaften " "Beschreibung entspricht, und auch zuvor nie davon gehört, mein Freund. " "Vielleicht kann Euch Cain weiterhelfen, der Geschichtenerzähler." #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "" "Also schau mal.... hihihihi, lustig, was? Mitgekriegt? Blinde, schau mal? " "Hihihihi..." #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Ein Ort des Terrors und der Panik, und damit dient er seinen Herren auf das " "Vortrefflichste. \n" "\n" "Seid vorsichtig, oder Ihr könntet viel länger dort bleiben müssen, als Euch " "lieb ist." #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" "Schauen wir doch mal: Verkaufe ich Dir gerade was? Nein. Gibst Du mir Geld " "dafür, dass ich Dir etwas darüber erzähle? Ebenfalls Nein. Haust Du jetzt " "also ab und nervst den Erzähler mit Deinen Fragen, der liebend gerne lang " "und breit über alles Mögliche redet? Ja." #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" "Ihr sagt, Ihr habt mit Lachdanan gesprochen? Er war Zeit seines Lebens ein " "großer Held. Lachdanan war ein ehrenhafter und gerechter Mann, der seinem " "König jahrelang treu gedient hatte. Aber das wisst Ihr natürlich längst. \n" "\n" "Von denen, die der Fluch des Königs befiel, wäre Lachdanan derjenige, der " "sich am ehesten weigern würde, sich der Finsternis kampflos zu ergeben. " "Daher vermute ich, dass Eure Geschichte wahr sein könnte. Wäre ich an Eurer " "Stelle, mein Freund, so würde ich einen Weg finden, ihn von seinen Qualen zu " "erlösen." #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "Ihr sprecht da von einem tapferen Krieger, der schon lange tot ist! Mit so " "einem Gerede über Unterhaltungen mit verstorbenen Seelen will ich nichts zu " "tun haben." #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Ein goldenes Elixier, sagt Ihr? Ich habe noch nie einen Trank in dieser " "Farbe gebraut, daher kann ich Euch nicht sagen, welche Wirkung er auf Euch " "hätte. Als Euer Hausheiler kann ich Euch nur empfehlen, zu Risiken und " "Nebenwirkungen auf Lachdanan zu hören und ein solches Elixier - wenn Ihr es " "überhaupt findet - auf keinen Fall zu trinken!" #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Ich habe noch nie von einem Lachdanan gehört. Tut mir leid, aber da kann ich " "Euch wohl nicht viel helfen." #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" "Wenn es wirklich Lachdanan ist, den Ihr da getroffen habt, dann würde ich " "Euch dringend raten, ihm zu helfen. Ich hatte mehrmals mit ihm zu tun, und " "ich halte ihn für sehr loyal und ehrlich. Der Fluch, der das Gefolge von " "König Leoric traf, muss ihn besonders hart getroffen haben." #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" " Lachdanan ist tot. Das weiß jeder, und Du wirst mir das auch nicht " "ausreden. Und mit den Toten kannst Du nicht reden. Weiß ich genau!" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Ihr trefft möglicherweise auf Menschen, die im Labyrinth gefangen sind, so " "wie Lachdanan etwa. \n" "\n" "Ich spüre in ihm viel Ehre, aber auch große Schuld. Helft ihm, und Ihr helft " "ganz Tristram." #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" "Moment, lass mich raten: Cain ist von einer riesigen Erdspalte verschluckt " "worden, die sich unter ihm aufgetan hat. Dabei wurde er in einen gewaltigen " "Feuerball verwandelt und zu Asche verbrannt, und jetzt kann er Deine Fragen " "nicht mehr beantworten, stimmt's? Wenn nicht, dann willst Du bestimmt was " "kaufen oder schnell wieder abhauen." #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" "Bitte, bringt mich nicht um! Hört mich an! Ich war einst Hauptmann in der " "Rittergarde von König Leoric, und ich habe das Gesetz dieses Landes mit Ehre " "und Gerechtigkeit verteidigt. Dann aber fiel sein Fluch auf uns, wegen der " "Rolle, die wir bei seinem tragischen Tod gespielt haben. Während sich meine " "Mitstreiter in ihr grausiges Schicksal ergaben, bin ich aus der Grabkammer " "des Königs geflohen. Seitdem suche ich nach einer Möglichkeit, mich von " "diesem Fluch zu befreien. Doch bisher habe ich versagt... \n" "\n" "Ich habe nur von einem Goldenen Elixier gehört, das den Fluch von mir nehmen " "und meiner Seele den Frieden des Todes bringen könnte, aber ich konnte es " "nie finden. Meine Kraft lässt nun langsam nach, und damit verfliegt auch der " "letzte Rest von Menschlichkeit in mir. Bitte helft mir und findet das " "Elixier! Ich werde Euch für Eure Mühe entlohnen - das schwöre ich bei meiner " "Ehre!" #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "Ihr habt das Goldene Elixier nicht gefunden. Ich fürchte, ich bin für alle " "Ewigkeit verdammt. Bitte, sucht weiter!" #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" "Ihr habt meine Seele vor der Verdammnis gerettet, und dafür stehe ich in " "Eurer Schuld. Wenn es überhaupt einen Weg gibt, wie ich diese Schuld von " "jenseits des Grabes zurückzahlen kann, dann werde ich ihn finden. Doch fürs " "Erste kann ich Euch nur meinen Helm geben. Auf der Reise, die ich jetzt " "antrete, werde ich wohl kaum Verwendung dafür haben. Möge er Euch gegen die " "Mächte der Finsternis dort unten beschützen. Geht mit dem Licht, mein Freund!" #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" "Griswold spricht vom Amboss des Zornes - einem legendären Artefakt, nach dem " "lange gesucht, das aber nie gefunden wurde. Er wurde gefertigt aus den " "Metallknochen der Dämonen aus der Klingengrube, aber nicht nur das: Der " "Amboss des Zornes wurde um die Schädel der fünf mächtigsten Zauberer der " "Unterwelt herumgegossen und mit gewaltigen Runen der Macht und des Chaos " "versehen. Jede Waffe oder Rüstung, die auf diesem Amboss geschmiedet wird, " "taucht in das Reich des Chaos ein und erhält dabei magische Eigenschaften. " "Doch die unberechenbare Natur des Chaos macht es unmöglich, das Resultat " "dieses Schmiedevorganges vorab zu ahnen." #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" "Findet Ihr nicht, dass Ihr Euch da lieber an Griswold wenden solltet? Er ist " "ziemlich geschickt, müsst Ihr wissen." #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" "Wenn Ihr mich jetzt nach der Silbernen Schale der Läuterung oder dem " "Heilenden Mörserstößel gefragt hättet, dann könnte ich Euch sofort " "weiterhelfen. Aber so wendet Ihr Euch lieber an Cain oder Griswold." #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" "Als wir noch Kinder waren, erzählte Griswolds Vater einigen von uns " "Geschichten über einen riesigen Amboss, auf dem mächtige Waffen geschmiedet " "wurden. Er sagte, bei jedem Schlag des Hammers auf diesem Amboss zitterte " "der Boden vor Zorn. Ich muss jedesmal an diese Geschichte denken, wenn die " "Erde erbebt." #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "Seid gegrüßt! Es ist immer wieder eine Freude, einen meiner besten Kunden " "begrüßen zu dürfen. Ich weiß, dass Ihr inzwischen tiefer ins Labyrinth " "vorgedrungen seid, und es gibt da eine Geschichte, die Euch vielleicht " "interessieren dürfte. \n" "\n" "Einer der Männer, die aus dem Labyrinth zurückkehrten, erzählte mir von " "einem mystischen Amboss, an dem er während seiner Flucht vorbeigekommen war. " "Seine Beschreibung erinnerte mich an eine alte Legende, die ich als Junge " "mal gehört habe und die von der Höllenschmiede handelte, wo mächtige " "magische Waffen geschmiedet worden sein sollen. Der Sage zufolge stand tief " "im Herzen dieser Höllenschmiede der Amboss des Zornes. \n" "\n" "Der Kern dieses Ambosses bestand aus der Quintessenz der dämonischen " "Unterwelt. Man sagt, dass jede Waffe, die auf diesem Amboss geschmiedet " "wird, große Kräfte verliehen bekommt. Wenn dieser Amboss, den der Mann " "gesehen hat, wirklich der Amboss des Zornes ist, könnte ich Euch darauf " "vielleicht eine Waffe schmieden, die selbst den finstersten Fürsten der " "Hölle besiegen kann!\n" "\n" "Sucht mir diesen Amboss, und ich mache mich gleich an die Arbeit." #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" "Noch immer nichts, wie? Na ja, sucht einfach weiter. Eine Waffe, die auf dem " "Amboss des Zornes geschmiedet wurde, wäre Eure einzige Hoffnung, und ich bin " "sicher, ich könnte Euch eine Klinge von ungeahnten Dimensionen herstellen." #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" "Ich kann es kaum glauben! Das ist wirklich der Amboss des Zornes! Gute " "Arbeit, mein Freund. Jetzt wollen wir diesen Bastarden in der Hölle mal " "beweisen, dass es dort keinen gefährlicheren Stahl gibt als den, den wir " "Menschen schmieden! Nehmt diese Waffe hier, und möge das Licht Euch behüten! " #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Griswold kann seinen Amboss nicht verkaufen. Dann hätte er doch keine Arbeit " "mehr! Und ich würde ja auch sauer, wenn jemand meinen Amboss klauen wollte..." #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" "Es gibt viele Gegenstände im Labyrinth, deren Kräfte das Verständnis jedes " "Sterblichen übersteigen. Einige davon können ihre Macht für das Licht oder " "für die Finsternis einsetzen. Wenn Ihr den Amboss in Sicherheit bringen " "könntet, würde das den Verlauf des Sündenkrieges zugunsten des Lichtes " "beeinflussen." #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" "Wenn Du dieses Artefakt für Griswold fändest, würde das meinem Geschäft hier " "sehr schaden. Ach was, das findest Du doch sowieso nie..." #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" "Das Blutportal und das Flammengewölbe sind Orte aus der Mystik. Wo immer " "dieses Buch auch liegt, in dem Ihr etwas darüber gelesen habt, es ist mit " "Sicherheit ein Ort voller Magie. \n" "\n" "Die Legenden berichten von einem Podest aus Obsidian, mit einem Tümpel aus " "kochendem Blut auf seiner knochenverkrusteten Oberfläche. Es werden auch " "Blutsteine erwähnt, mit deren Hilfe man ein Portal öffnen kann, das einen " "uralten Schatz behütet. \n" "\n" "Welche Art von Schatz das ist, darüber gibt es nur Spekulationen, mein " "Freund, aber man sagt, der Sagenheld Arkaine habe seine heilige Rüstung " "namens Tapferstahl in einer geheimen Gruft versteckt. Arkaine war der erste " "Sterbliche, der den Verlauf des Sündenkrieges veränderte und die Legionen " "der Finsternis in die Feuer der Hölle zurücktrieb. \n" "\n" "Kurz vor seinem Tod wurde seine Rüstung an einem geheimen Ort verborgen. Die " "Legende sagt, dass die Zeit kommen wird, in der man diese heilige Rüstung " "wieder braucht. Ein Held wird kommen und den Tapferstahl wieder in die " "Schlacht tragen.Vielleicht seid Ihr dieser Held..." #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" "Jedes Kind kennt die Geschichte von dem Krieger Arkaine und seiner magischen " "Rüstung Tapferstahl. Wenn Ihr das Versteck dieser Rüstung finden könntet, " "wäret Ihr vor dem Bösen im Labyrinth gut geschützt." #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Hmm... klingt nach etwas, an das ich mich eigentlich erinnern müsste, aber " "ich war so beschäftigt damit, neue Heilmittel zu erforschen und immer " "wirksamere Tränke zu mischen, dass es mir wohl entfallen sein muss. Tut mir " "leid..." #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" "Die Geschichte von der Heiligen Rüstung Tapferstahl habe ich von den Jungen " "im Dorf oft gehört. Ihr solltet da lieber einen der Männer fragen." #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" "Die Heilige Rüstung Tapferstahl könnte den Kampf für uns entscheiden. Lasst " "Euch gesagt sein, dass viele schon danach gesucht haben - mich selbst " "eingeschlossen. Aber Arkaine hat sie gut versteckt, mein Freund, und Ihr " "werdet mehr als nur ein bisschen Glück brauchen, um die Geheimnisse zu " "enträtseln, hinter denen sich die Rüstung all die Jahre verborgen hat." #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Schnarch..." #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" "Solltet Ihr diese Blutsteine wirklich finden, geht vorsichtig mit ihnen " "um. \n" "\n" "Der Weg steckt voller Gefahren, und als Eure einzige Hoffnung bleibt Euer " "Selbstvertrauen." #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "Du willst diese Rüstung finden, die man Tapferstahl nennt?\n" "\n" "Niemand hat je rausfinden können, wo Arkaine den Kram versteckt hat, und " "wenn meine Gewährsleute das nicht rausfinden konnten, schaffst Du es erst " "recht nicht." #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" "Ich kenne nur eine einzige Sage, in der von so einem Krieger die Rede ist, " "wie Ihr ihn beschreibt. Seine Geschichte steht in den alten Chroniken der " "Sündenkriege...\n" "\n" "'Besudelt von einem Jahrtausend voller Krieg, Blut und Tod steht der " "Blutfürst auf einem Berg seiner zerschmetterten Opfer. Seine schwarze Klinge " "schleudert den Lebenden einen finsteren Fluch entgegen, eine gequälte " "Herausforderung für alle, die sich diesem Henker der Hölle in den Weg zu " "stellen wagen.'\n" "\n" "Es steht auch noch geschrieben, dass er zwar früher mal ein Sterblicher war, " "der im Sündenkrieg auf der Seite der Finsternis gekämpft hat. Doch er " "opferte sein Menschsein seinem unersättlichen Durst nach Blut." #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Es tut mir leid, von einem solchen Krieger habe ich noch nie etwas gehört. " "Ich hoffe nur, Ihr müsst nicht gegen ihn kämpfen, er klingt ja fürchterlich " "gefährlich." #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" "Cain kann Euch darüber wahrscheinlich viel mehr erzählen, als ich überhaupt " "wissen möchte." #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" "Wenn Ihr gegen so einen schweren Widersacher antreten müsst, dann möge das " "Licht Euch beschützen und leiten. Ich werde in Gedanken bei Euch sein." #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" "Dunkle und böse Legenden ranken sich um den Blutfürsten. Bereitet Euch auf " "dieses Treffen gut vor, mein Freund, denn er kennt weder Gnade noch Pardon." #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" "Musst Du immer nur über Blut reden? Was ist denn mit Blümchen, Sonnenschein " "oder dem hübschen Mädel, das immer die Getränke bringt? Hör mal, mein Freund " "- das ist schon eine richtige Marotte von Dir, weißt Du das?" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" "Wie er mit einer Klinge umzugehen vermag, ist ehrfurchterregend, und er hat " "Tausende von Jahren nur für den Krieg gelebt. Es tut mir leid... ich wüsste " "nicht, wie Ihr ihn besiegen könntet." #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Ich habe noch nie mit diesem Herrn zu tun gehabt, von dem Du da erzählst, " "aber es klingt so, als bräuchte er eine Menge Schwerter. Hätte nichts " "dagegen, seine Armeen zu beliefern..." #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" "Meine Klinge schreit vor Hunger nach Eurem Blut, und bei meinem Dunklen " "Herren, sie soll nicht enttäuscht werden! " #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" "Griswold meint den Himmelsstein, der für die Enklave im Osten bestimmt war " "und dort weiter erforscht werden sollte. In dem Stein glühte eine Energie, " "die einen Menschen auf irgendeine Weise mehr erschauen ließ, als es ihm von " "Natur aus möglich war. Ich weiß nicht, welches Geheimnis sich in diesem " "Stein verbirgt, mein Freund, doch es ist auf jeden Fall der Mühe wert, ihn " "zu suchen! " #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" "Die Karawane hat hier Rast gemacht und einige Vorräte für die weite Reise in " "den Osten aufgeladen. Ich habe den Leuten eine Menge frisches Obst verkauft " "und einige süße Brote, die Garda gebacken hatte. Es ist eine Schande, was " "dann mit ihnen passiert ist..." #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" "Ich weiß ja nicht, was die mit diesem Stück Stein sehen wollten, aber eins " "weiß ich: Wenn jetzt schon die Steine vom Himmel fallen, dann seid lieber " "vorsichtig!" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Na ja, eine Karawane mit sehr wichtigen Leuten hat hier gehalten, aber das " "ist schon ziemlich lange her. Sie hatten alle so einen komischen Akzent und " "haben sich auf eine weite Reise vorbereitet, soweit ich mich erinnere. \n" "\n" "Ich verstehe nur nicht, wie Ihr irgendwas finden wollt, das die bei sich " "hatten? " #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" "Verweilt einen Moment - ich habe da eine Geschichte gehört, die Euch " "interessieren dürfte. Vor einiger Zeit kam hier eine Karawane durch, auf dem " "Weg in die östlichen Königreiche. Man erzählte sich, sie habe ein Stück des " "Himmels dabei, das auf die Erde gefallen sei. Doch nur wenige Meilen " "nördlich von hier wurde die Karawane von vermummten Reitern überfallen. Ich " "habe in den Überresten nach diesem Stein vom Himmel gesucht, aber nichts " "gefunden. Wenn er Euch in die Hände fallen sollte, könnte ich daraus " "bestimmt etwas Nützliches herstellen." #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Ich warte immer noch, dass Ihr mir diesen Stein des Himmels bringt. Ich " "weiß, dass ich etwas Machtvolles daraus herstellen könnte." #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" "Zeigt mir das mal kurz. Ja, genau, das habe ich mir gedacht. Gebt ihn mal " "gerade her... hier!\n" "\n" "Ich habe Splitter des Steines in einen Silberring eingelassen, den ich von " "meinem Vater geerbt habe. Ich hoffe, er hilft Euch." #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" "Ich hatte auch mal einen schönen Ring. War ein richtig teurer, so mit Blau " "und Rot und Grün und Silber. Weiß gar nicht, was damit passiert ist. Aber " "ich vermisse ihn wirklich." #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "Der Himmelsstein ist sehr mächtig, und würde Euch jemand anderer als " "Griswold bitten, ihn zu suchen, so würde ich es verhindern. Er aber wird die " "Kräfte des Steins zu bändigen und zum Wohle der gesamten Menschheit " "einzusetzen wissen." #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" "Wenn irgend jemand etwas aus diesem Stück Stein zaubern kann, dann Griswold. " "Er weiß genau, was er tut, und auch wenn ich versuche, ihm seine Kunden " "abspenstig zu machen, so respektiere ich doch die Qualität seiner Arbeit." #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" "Adria, die Hexe, benötigt einen schwarzen Pilz? Über schwarze Pilze weiß ich " "leider genauso viel wie über rosa Schnittlauch. Vielleicht kann Euch Pepin, " "der Heiler, etwas mehr darüber verraten. In meinen Büchern und Geschichten " "jedenfalls steht nichts davon." #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" "Dazu kann ich nur eines sagen: Weder Garda noch ich haben oder werden " "unseren zahlenden Gästen JEMALS schwarze Pilze servieren. Wenn Adria solche " "Pilze in ihren Eintopf schneiden möchte, ist das allein ihre Sache, aber ich " "werde Euch nicht auch noch helfen, schwarze Pilze zu finden... ist ja " "widerlich!" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" "Die Hexe hat mir erzählt, dass Ihr nach einem Dämonenhirn sucht, um mir bei " "der Herstellung meines Elixiers zu helfen. Es wäre von unschätzbarem Wert " "für alle, die von diesen elenden Monstern verwundet werden, wenn ich die " "Geheimnisse entschlüsseln könnte, die ich in der Alchemie dieser Hirne " "vermute. Wenn Ihr also wieder mal einen Dämon tötet, versucht bitte, sein " "Hirn unversehrt mitzubringen und mir zu geben, ja? " #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" "Exzellent, genau das, was ich mir vorgestellt hatte. Ich konnte zwar das " "Elixier auch ohne das Dämonenhirn fertigstellen, aber es kann ja nicht " "schaden, es als Studienobjekt hier zu haben. Würdet Ihr das hier bitte zur " "Hexe bringen? Ich glaube, sie wartet darauf..." #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" "Ich glaube, Ogden hat einige Pilze in seinem Vorratskeller. Warum fragt Ihr " "ihn nicht mal?" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Wenn Adria keinen hat, müssen die ja wirklich selten sein. Ich kann Euch " "nicht mehr dazu sagen, aber es klingt nach einem... irgendwie riesigen, " "aufgequollenen, gigantischen Pilz! Na ja, viel Spaß beim Suchen." #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Ogden kann einen tierischen Schwarzen Pilz mischen, aber davon muss ich " "immer kotzen. Hör mal, hör mal, ich verrate Dir was: Mäßigung ist das ganze " "Geheimnis. " #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "Was haben wir denn hier? Interessant... sieht aus wie ein Buch mit " "Reagenzien. Haltet Eure Augen offen nach einem schwarzen Pilz. Er müsste " "ziemlich groß sein und daher leicht zu identifizieren. Wenn Ihr einen " "findet, bringt ihn her, ja?" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" "Ich brauche einen großen, schwarzen Pilz. Los, geht jetzt und holt ihn mir, " "damit ich dieses spezielle Gebräu fertigmischen kann, an dem ich gerade " "arbeite." #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" "Ja, das ist die perfekte Zutat für meinen Trank. Übrigens, der Heiler " "benötigt das Hirn eines Dämon für seinen Heiltrank gegen Dämonengift. Wenn " "Ihr ihm helfen könnt, dann tut es bitte, aber versucht doch auch, eine Probe " "dieses Elixiers für mich zu bekommen." #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "Warum schleppt Ihr mir das Zeug hier an? Ich brauche im Moment kein " "Dämonenhirn. Was ich brauche, ist ein Schluck von dem Elixier, an dem der " "Heiler gerade arbeitet. Er braucht dieses.... groteske Organ, das Ihr da so " "stolz herumzeigt. Und dann könnt Ihr mir sein Elixier bringen. Ist doch ganz " "einfach, wenn man mal drüber nachdenkt, oder?" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "Was denn, jetzt bringt Ihr mir endlich das Elixier vom Heiler? Jetzt habe " "ich meinen Trank auch ohne das Zeug fertigbekommen. Ihr könnt es behalten..." #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" "Ich habe überhaupt keine Pilze zu verkaufen, egal wie groß oder in welcher " "Farbe. Wie wäre es denn statt dessen mit etwas Nützlicherem?" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" "Soso, die Legende über die Karte ist also wahr. Selbst ich habe nie wirklich " "daran geglaubt. Vielleicht ist es an der Zeit, Euch zu sagen, wer ich " "wirklich bin. Ihr müsst wissen, dass mein äußerer Anschein nicht ganz meiner " "wahren Natur entspricht. \n" " \n" "Mein wirklicher Name ist Deckard Cain der Ältere, und ich bin der letzte " "Nachfahre einer uralten Bruderschaft, die es sich zum Ziel gesetzt hatte, " "die Geheimnisse einer uralten bösen Macht zu bewahren. Einer Macht, die nun " "offensichtlich aus ihrem Gefängnis entkommen konnte. \n" " \n" "Die böse Macht, mit der Ihr Euch messen wollt, ist der Finstere Lord des " "Terrors, unter den Sterblichen als Diablo bekannt. Er war es, der vor " "etlichen Jahrhunderten im Labyrinth eingesperrt worden ist. Diese Karte in " "Eurer Hand ist vor Jahrhunderten angefertigt worden, um den Zeitpunkt " "festzulegen, an dem Diablo wieder aus seinem Gefängnis entkommen würde. Wenn " "die beiden Sterne auf der Karte zusammenkommen, ist Diablo auf dem Höhepunkt " "seiner Macht. Er ist dann praktisch unbesiegbar. \n" " \n" "Die Zeit arbeitet nun gegen Euch, mein Freund. Findet Diablo und vernichtet " "ihn, bevor die Sterne zusammenkommen, oder wir werden vielleicht nie wieder " "die Chance bekommen, die Welt vor seinem Würgegriff zu bewahren!" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "Unsere Zeit wird knapp! Ich kann spüren, wie seine finstere Macht wächst, " "und nur Ihr könnt verhindern, dass er all seine Kräfte wiedererlangt! " #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Ich bin sicher, dass Ihr Euer Bestes geben werdet, doch ich fürchte, nicht " "einmal Euer Arm und Euer Wille sind stark genug. Diablo ist nun auf dem " "Höhepunkt seiner irdischen Macht, und Ihr werdet all Euren Mut und Eure " "Kraft brauchen, um ihn zu besiegen. Möge das Licht Euch beschützen und " "leiten, mein Freund. Ich werde Euch helfen, soweit ich es vermag." #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" "Wenn die Hexe Euch nicht helfen kann und Euch schon zu Cain schickt, wieso " "glaubt Ihr dann, ich könnte etwas darüber wissen? Das klingt nach einer sehr " "ernsten Angelegenheit. Ihr solltet lieber schleunigst das tun, was Adria " "vorgeschlagen hat und zum Geschichtenerzähler gehen." #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" "Ich kann mit den Inschriften auf der Karte nicht viel anfangen, aber " "vielleicht wissen Adria oder Cain, was das bedeuten soll.\n" "\n" "Ich kann nur erkennen, dass es eine Karte unseres Sternenhimmels ist, aber " "mehr sagt mir das Ding nicht." #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "Bei solchen Sachen fragt Ihr am besten immer unseren Geschichtenerzähler.\n" "\n" "Cain weiß eine Menge über alte Schriften, und das hier ist ja nun wirklich " "das älteste Stück Papier, das mir jemals untergekommen ist." #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "So eine Karte habe ich noch nie gesehen. Wo habt Ihr die denn her? Ich kann " "sie zwar nicht entziffern, aber Cain oder Adria müssten Euch da weiterhelfen " "können." #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Hör mal, komm mal her. Ich weiß nicht, ob Du weißt, was ich weiß, aber weißt " "Du, dass das eine Karte ist? Eine richtige Landkarte! " #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" "Oh, ich fürchte, das hört sich gar nicht gut an. Diese Sternenkarte bedeutet " "großes Unheil, doch es ist nicht an mir, ihre Geheimnisse zu lüften. Die " "Zeit ist reif für ein ernstes Gespräch mit dem Geschichtenerzähler..." #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" "Ich habe nach einer Karte gesucht, aber bestimmt nicht nach der hier. Du " "solltest sie mal Adria zeigen - sie kann Dir eher sagen, was das ist. Ich " "weiß nur, dass sie ziemlich alt aussieht, und 'alt' bedeutet meist " "'wertvoll'." #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" "Bitte nicht wehtun. Nicht töten. Leben lassen, nächstes Mal Gutes Euch " "bringen." #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Etwas für Euch ich machen. Nicht töten Gharbad. Leben und Gutes geben. \n" "\n" "Hier das Ihr nehmen als Beweis ich halten Wort." #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "Noch nichts! Fast fertig aber. \n" "\n" "Sehr mächtig, sehr stark. Leben! Leben!\n" "\n" "Keine Wehtun, ich halten Versprechung." #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Dies zu gut für Euch. Sehr stark! Ihr wollen, Ihr Euch holen kommen!" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "Was? Warum seid Ihr hier? Bei diesen ganzen Störungen muss man ja verrückt " "werden! Hier, nehmt das und geht, ich muss arbeiten! Stört mich bloß nicht " "mehr!" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "Arrrrgh! Eure Neugier wird Euch noch mal umbringen. Und zwar jetzt!" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Hallo, mein Freund. Bleibt und hört mir ein wenig zu..." #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "Während Ihr tiefer und tiefer in das Labyrinth vordringt, werdet Ihr Bücher " "mit geheimem Wissen finden, die dort seit Generationen verborgen liegen. \n" "\n" "Lest sie sorgfältig, denn sie können Euch Dinge lehren, von denen nicht " "einmal ich weiß." #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Ich kenne viele Legenden und Mythen, und in einigen davon stecken " "wahrscheinliche Antworten auf ein paar der Fragen, die sich Euch im Laufe " "Eurer Abenteuer im Labyrinth stellen werden. Wenn Ihr also auf Rätsel und " "Herausforderungen stoßt, über die Ihr zu wenig wisst, kommt zu mir, und ich " "werde Euch alles sagen, was ich darüber weiß." #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" "Griswold - ein Mann der Tat, und ein ungeheuer mutiger Kämpfer. Ich wette, " "er hat Euch nie erzählt, dass er mal ins Labyrinth hinunter gestiegen ist, " "um Wirt zu retten, oder? Er kennt schon viele der Gefahren, die dort unten " "lauern, aber - die kennt Ihr inzwischen ja auch. Er ist ein geschickter " "Handwerker, und wenn er sagt, dass er Euch helfen kann, könnt Ihr Euch auf " "seine Ehrlichkeit und sein Können verlassen." #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Ogden ist jetzt seit fast vier Jahren Eigentümer des Gasthofs zum " "Sonnenaufgang. Er hat ihn erst ein paar Monate vor der Katastrophe hier " "gekauft. Er und seine Frau Garda haben kein Geld mehr, um woanders " "hinzuziehen. Sie haben alles investiert, um sich hier eine Existenz " "aufzubauen. Ogden ist ein guter, verantwortungbewusster Mann." #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" "Der arme Farnham. Er ist für uns eine bleibende Erinnerung an den traurigen " "Tag, an dem der Trupp Dorfbewohner dem Erzbischof Lazarus in das Labyrinth " "unter der Kathedrale folgte. Farnham kam mit dem Leben davon, aber seinen " "Mut und den größten Teil seines Verstandes hat er in diesem dunklen Loch " "verloren. Er hat sich ganz in seinen Bierkrug zurückgezogen, aber in seinem " "betrunkenen Gelalle steckt doch ab und zu ein Körnchen Wahrheit." #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" "Die Hexe Adria ist eine Außenseiterin hier in Tristram. Sie kam hier an, als " "alle anderen schon von hier flohen, kurz nachdem die Kathedrale entweiht " "worden war. Sie hat sich eine kleine Hütte am Stadtrand gebaut, scheinbar " "über Nacht, und sie besitzt viele seltsame, magische Gegenstände und Bücher, " "die selbst ich noch nie gesehen habe." #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" "Wirts Geschichte ist tragisch und furchterregend. Er wurde von kleinen " "Dämonen mit Speeren seiner Mutter aus den Armen gerissen und ins Labyrinth " "verschleppt. Am gleichen Tag wurden noch viele andere Kinder entführt, " "darunter auch der Sohn von König Leoric. Die Ritter des Palastes gingen " "sofort hinunter, doch sie kamen nie wieder zurück. Der Schmied fand den " "Jungen, doch erst, nachdem die Monster angefangen hatten, ihn zu ihrem " "sadistischen Vergnügen zu foltern." #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "Ah, Pepin. Ich zähle ihn zu meinen wahren Freunden - vielleicht ist er sogar " "der beste, den ich habe. Er ist manchmal etwas verwirrt, aber niemand kann " "freundlicher und fürsorglicher sein als er. Sein Wissen und sein Können sind " "unübertroffen, und seine Tür ist für alle stets offen." #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" "Gillian ist eine ganz besondere Frau. In meinem Herzen ist ein ganz " "besonderer Platz für sie reserviert, für ihre stets gute Laune und ihr " "fröhliches Lachen. Sie bleibt in der Taverne wegen ihrer alten Großmutter, " "die zu krank zum Reisen ist. Manchmal sorge ich mich um ihre Sicherheit, " "aber andererseits weiß ich, dass jeder Mann im Dorf lieber sterben würde als " "zuzulassen, dass ihr etwas zustößt." #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "Seid mir herzlich gegrüßt! Willkommen im Gasthof Zum Sonnenaufgang!" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" "Viele Abenteurer haben schon an den Tischen meines Gasthofs gesessen, und " "zehnmal so viele Geschichten sind bei ebenso vielen Krügen Bier erzählt " "worden. Das Einzige, worin sich alle immer einig waren, ist die alte " "Grundregel, die Ihr vielleicht auch ganz nützlich finden werdet: \"Fleisch " "kann man schneiden, aber Knochen muss man zerschmettern\"." #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" "Der Schmied Griswold kennt sich bei Waffen und Rüstungen extrem gut aus. " "Wenn Ihr jemals etwas an Euren Sachen auszubessern habt, solltet Ihr auf " "jeden Fall zu ihm gehen." #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Farnham verbringt viel zu viel Zeit hier. Er versucht, seine Sorgen in " "billigem Bier zu ertränken. Ich würde ihn ja rauswerfen, aber er hat so viel " "durchgemacht im Labyrinth." #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Adria ist weiser, als es ihre Jugend vermuten lässt, aber ich muss zugeben, " "dass sie mir auch etwas Angst macht. \n" "\n" "Na ja, ist ja auch egal. Jedenfalls, wenn Ihr jemals ein paar magische " "Sachen braucht oder eintauschen wollt, geht zu ihr. Sie hat eine Hütte " "jenseits des Flusses, und ihre Vorräte sind merkwürdigerweise immer " "reichlich." #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Wenn Ihr mehr über die Vergangenheit unseres Dorfes erfahren wollt, kann " "Euch der Geschichtenerzähler Cain bestimmt weiterhelfen." #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Wirt ist ein Taugenichts und ein kleiner Rumtreiber. Er hatte schon immer " "ein Talent dafür, sich Ärger einzuhandeln, und es überrascht mich nicht, " "dass ihm sowas zugestoßen ist. \n" "\n" "Hat sich wahrscheinlich irgendwo rumgetrieben, wo er nichts zu suchen hatte. " "Er tut mir ja schon irgendwie leid, aber sein Bekanntenkreis gefällt mir " "überhaupt nicht." #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Pepin ist ein guter Mensch - und mit Sicherheit der großzügigste der Stadt. " "Er kümmert sich immer um die Bedürfnisse der anderen, dabei hat er selbst " "immer genug Sorgen." #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" "Gillian, meine Kellnerin? Sie wäre schon vor langer Zeit von hier geflohen, " "wenn da nicht noch ihre Großmutter wäre. Sie würde sie niemals im Stich " "lassen. \n" "\n" "Ich habe ihr sogar angeboten, mich um die alte Dame zu kümmern und sie " "praktisch angefleht, sich doch in Sicherheit zu bringen, aber zwecklos." #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "Was fehlt Euch? Braucht Ihr Hilfe?" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" "Ich habe eine sehr interessante Entdeckung gemacht. Im Gegensatz zu uns " "können sich die Geschöpfe des Labyrinthes ohne die Hilfe von Tränken oder " "Magie selbst heilen! Wenn Ihr also gegen eines dieser Monster kämpft, " "vergewissert Euch, dass es auch wirklich tot ist, sonst regeneriert es sich, " "und Euer Kampf war umsonst." #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Bevor sie übernommen wurde von...na ja, was immer da unten lauert, war die " "Kathedrale ein Ort des Lernens. Es gibt dort heute noch viele Bücher. Wenn " "Ihr welche findet, solltet Ihr sie auf jeden Fall lesen, denn sie enthalten " "viele Hinweise darauf, was im Labyrinth vor sich geht." #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" "Griswold weiß so viel über die Kunst des Krieges wie ich über die Kunst des " "Heilens. Er ist ein gerissener Kaufmann, aber seine Arbeit ist " "unvergleichlich. Na ja, vielleicht auch deshalb, weil es keinen anderen " "Schmied mehr gibt, mit dessen Arbeit man sie vergleichen könnte..." #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" "Cain ist ein wahrer Freund und ein Weiser. Er hat eine riesige Bibliothek " "und besitzt die angeborene Gabe, die wahre Natur vieler Dinge zu erkennen. " "Wenn Ihr jemals Fragen habt, solltet Ihr zu ihm gehen." #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Selbst meine Kräfte reichen nicht aus, um Farnham völlig zu heilen. Oh ja, " "ich konnte seinen Körper wieder in Ordnung bringen, aber sein Geist und sein " "Verstand liegen jenseits meiner Möglichkeiten." #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" "Ich benutze zwar eine gewisse Art von Magie, um die Tränke und Elixiere zu " "brauen, die ich hier lagere. Aber Adria ist eine echte Hexe. Sie scheint " "niemals zu schlafen, und sie hat immer Zugang zu seltenen Büchern und " "Artefakten. Ich vermute, ihre Hütte ist viel mehr als der Holzschuppen, nach " "dem sie aussieht. Aber irgendwie habe ich es nie geschafft, hineinzukommen." #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Der arme Wirt. Ich habe für ihn getan, was ich konnte, aber ich weiß genau, " "dass er das Holzbein hasst, das ich an seinem Beinstumpf anbringen musste. " "Seine Wunden waren einfach grausam. Niemand - und schon gar kein Kind, wie " "er es war - sollte jemals so etwas erleiden müssen." #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" "Ich verstehe wirklich nicht, warum Ogden hier in Tristram bleibt. Er bekommt " "leicht nervöse Zustände, aber er ist ein intelligenter und unternehmerischer " "Mann, der überall gut über die Runden käme, egal, wo er hinginge. Ich " "vermute, er hat Angst wegen der vielen Morde in der Umgebung, oder " "vielleicht will seine Frau hier einfach nicht weg." #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "Ogdens Kellnerin ist ein süßes Mädchen. Ihre Großmutter ist ziemlich krank " "und leidet an Wahnvorstellungen. \n" "\n" "Sie sagt zwar, es seien Visionen, aber dafür gibt es keinerlei Beweise." #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "Einen guten Tag! Womit kann ich Euch dienen?" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Meine Großmutter hat im Traum gesehen, dass Ihr kommen und mit mir reden " "würdet. Sie hat Visionen, müsst Ihr wissen, und kann in die Zukunft sehen." #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "Diese Frau am Stadtrand ist eine Hexe! Sie scheint ja ganz nett zu sein, und " "ihr Name Adria klingt ja auch ganz angenehm, aber ich habe trotzdem Angst " "vor ihr. \n" "\n" "Man müsste schon sehr mutig sein, so wie Ihr, um nachzusehen, was sie da " "draußen überhaupt so macht." #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" "Auf unseren Schmied sind die Leute hier in Tristram besonders stolz. Er ist " "nicht nur ein meisterlicher Handwerker, der schon viele Wettbewerbe seiner " "Zunft gewonnen hat, sondern er wurde sogar von König Leoric selbst gelobt - " "möge seine Seele in Frieden ruhen. Griswold ist außerdem ein großer Held - " "fragt nur mal Cain!" #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Cain war schon immer der Geschichtenerzähler in Tristram, seit ich denken " "kann. Er weiß so viel und er kann Euch fast alles über fast jedes Thema " "berichten." #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" "Farnham ist ein Säufer, der seinen Bauch mit Fusel füllt und allen anderen " "die Ohren mit Unfug vollstammelt. \n" "\n" "Ich weiß, dass er sowohl Ogden als auch Pepin leid tut, aber mich frustriert " "es sehr, ihn jeden Abend immer weiter in den Rausch abrutschen zu sehen." #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" "Pepin hat meiner Großmutter das Leben gerettet, und ich weiß, dass ich das " "niemals wieder gutmachen kann. Seine Fähigkeit, alle Krankheiten zu heilen, " "ist stärker als das mächtigste Schwert und dabei geheimnisvoller als jeder " "nur erdenkliche Zauberspruch. Wenn Ihr jemals einen Heiler nötig habt, geht " "zu Pepin." #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" "Ich bin zusammen mit Wirts Mutter Canace aufgewachsen. Obwohl sie nur leicht " "verletzt wurde, als die Dämonen ihn entführt haben, hat sie sich davon nie " "wieder erholt. Ich glaube, sie ist an gebrochenem Herzen gestorben. Wirt hat " "sich zu einem hartgesottenen Jungen entwickelt, der nur durch den Schweiß " "anderer reich werden will. Ich weiß ja, dass er viel durchgemacht und " "Schrecken gesehen hat, die ich mir nicht mal ausmalen könnte, aber etwas von " "dieser Finsternis schwebt immer noch über ihm." #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Ogden und seine Frau haben Großmutter und mich bei sich aufgenommen, und sie " "lassen mich sogar ein paar Goldstücke im Gasthof verdienen. Ich schulde " "ihnen so viel, und ich wünsche mir nichts sehnlicher, als ihnen eines Tages " "helfen zu können, ein großes Hotel im Osten aufzumachen." #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "Was kann ich für Euch tun?" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" "Wenn Ihr auf der Suche nach der richtigen Waffe seid, kann ich Euch einiges " "erzählen. Nehmt zum Beispiel eine normale Schlagwaffe, etwa eine Keule. " "Gegen die meisten dieser Untoten da unten ist die einfach Gold wert; es gibt " "zum Beispiel nichts Besseres, um knochige kleine Skelette zu zertrümmern." #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" "Die Streitaxt? Ja, das ist eine richtig gute Waffe, ausgewogen gegen jeden " "Gegner. Seht, wie sie die Luft durchschneidet, und jetzt stellt Euch mal " "einen schönen, dicken Dämonenkopf genau in der Flugbahn vor. Denkt daran, " "dass sie sich nur langsam schwingen lässt, aber wenn sie trifft ... hmmmm." #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Schaut Euch nur die Schneide an, die Balance... Ein Schwert ist in den " "richtigen Händen und gegen den richtigen Gegner die beste aller Waffen. Zwar " "findet auch die schärfste Klinge an Untoten nur wenig zum Schneiden oder " "Durchbohren, doch gegen lebende, atmende Gegner gibt es nichts Besseres." #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "Eure Waffen und Rüstungen werden bald schon Spuren Eurer Kämpfe gegen die " "Finsternis zeigen. Wenn Ihr sie zu mir bringt, kann ich sie - mit ein wenig " "Arbeit und einem schönen, heißen Schmiedefeuer - wieder in Höchstform " "bringen." #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" "Während ich das Metall und meine Werkzeuge im wahrsten Sinne des Wortes von " "Karawanen am Stadtrand hereinschmuggeln muss, scheint diese Hexe Adria immer " "alles zu haben, was sie braucht. Wenn ich auch nur einen Funken ihrer " "magischen Fähigkeiten hätte, könnte ich einfach unglaubliche Sachen " "herstellen." #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Gillian ist ein nettes Mädchen. Schade, dass es ihrer Großmutter so schlecht " "geht, sonst würde ich sie beide mit einer der Handelskarawanen hier " "rausbringen lassen." #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" "Manchmal denke ich, Cain redet zuviel. Aber das ist wohl seine Bestimmung. " "Wenn ich mit Stahl so gut umgehen könnte wie er mit Worten, würde ich jeden " "Tag ein Kettenhemd schmieden, das sogar der Kaiser von China tragen würde!" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Ich war mit Farnham zusammen in jener Nacht, als Lazarus uns ins Labyrinth " "geführt hat. Ich habe den Erzbischof nie wiedergesehen und ich hätte wohl " "selbst nicht überlebt, wenn Farnham nicht bei mir gewesen wäre. Ich " "befürchte, der Angriff hat seine Seele so verkrüppelt, wie ein anderer mein " "Bein. Ich kann seinen Kampf jetzt nicht für ihn austragen, aber wenn ich es " "könnte, würde ich es sofort tun." #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Ein guter Mann, der die Bedürfnisse der anderen über seine eigenen stellt. " "Ihr werdet niemanden in Tristram - oder auch anderswo - finden, der etwas " "Schlechtes über den Heiler sagen würde." #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" "Dieser Kerl wird noch mal richtigen Ärger bekommen... oder besser gesagt, " "wieder mal. Ich habe versucht, ihm eine Lehre hier schmackhaft zu machen, " "damit er ein ehrliches Gewerbe erlernen könnte, aber er zieht es vor, das " "schnelle Geld zu machen, indem er mit Waren zweifelhafter Herkunft handelt. " "Kann ich ihm nicht mal verübeln, nach dem, was er durchgemacht hat, aber ich " "wünschte mir, er wäre wenigstens etwas vorsichtiger." #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" "Der Gastwirt hat nur wenige Gäste und kann nicht viel verdienen. Er kommt " "gerade so über die Runden, indem er den gelegentlichen Besuchern der Stadt " "Nahrung und Unterkunft bietet, aber statt ihn zu bezahlen, verdrückt sich " "die Hälfte seiner Kunden einfach im Schutze der Nacht. Doch ohne seine " "Getreidevorräte und das getrocknete Fleisch, das er in seinem Keller lagert, " "wären die meisten von uns schon im ersten Jahr verhungert, als die Dämonen " "das ganze Land hier niedergetrampelt haben." #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "Kann man hier nicht mal in Ruhe einen trinken?" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" "Das Mädel, das die Drinks serviert? Oh ja, die ist hübsch, Und tierisch nett." #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "Warum tut diese alte Krähe nicht mal was zur Abwechslung? Klar, die hat " "ihren ganzen Kram und so, aber ich sag Euch was: Sie ist unnatürlich. Ich " "habe sie nie essen oder trinken sehen. Und man kann einfach niemanden " "vertrauen, der nicht wenigstens ab und zu mal ein wenig trinkt..." #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Cain ist nicht der, für den er sich ausgibt. Klar, er kann prima Geschichten " "erzählen... manche sind richtig schaurig oder lustig. Aber ich glaube, er " "weiß mehr, als er weiß, dass er weiß, weißt Du...?" #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Griswold? Der gute alte Griswold. Ich liebe ihn wie einen Bruder! Wir haben " "zusammen gekämpft, weißt Du, damals, als wir... Lazarus... Lazarus... " "LAZARUS!!!" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" "Hihi, ich mag Pepin. Er gibt sich wirklich Mühe, weißt Du? Du musst ihn " "unbedingt kennenlernen. Sehr lieber Kerl, wo doch die Leute immer Hilfe von " "ihm wollen. He, das könnte Dir ja auch mal passieren, als Held und so. Ich " "war früher auch mal ein Held, weißt Du?" #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "Wirt hat sogar noch mehr Probleme als ich, und ich kenne mich mit Problemen " "nun wirklich gut aus. Weißt Du, der Kleine zeigt zwar die harte Schale, aber " "er war da, verstehst Du? Hat sein Bein da unten verloren. Muss auf 'nem " "verdammten Stück Holz rumlaufen. Verdammt traurige Geschichte..." #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Ogden ist der Beste hier in der Stadt. Ich glaube, seine Frau mag mich nicht " "besonders, aber solange sie die Krüge immer schön voll macht, finde ich sie " "ganz sympathisch. Ich schein' irgendwie mehr Zeit mit Ogden zu verbringen " "als die meisten anderen hier, aber er ist ja auch wirklich gut zu mir." #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" "Ich will Dir mal was sagen, weil ich mich mit diesen Sachen ja auskenne. Ist " "mein Spezialgebiet. Das hier ist einfach das Beste. Das Allerallerbeste. Das " "andere Bier ist einfach ungenießbar seit diese ...Hunde...." #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" "Niemand hört mi-HICKS-mir zu. Irgendwo - ich weiß nicht mehr genau, wo, aber " "irgendwo unter dieser Kirche liegt ein Riiiesenhaufen Gold. Glänzt und " "schimmert und wartet nur darauf, dass ihn jemand mitnimmt." #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" "Ich weiß, Du hast so Deine eigenen Vorstellungen, und ich weiß auch, dass Du " "mir nicht glauben wirst, aber ich sag's Dir trotzdem: Deine Waffe da ist " "völlig nutzlos gegen diese riesigen Monster! Ist mir egal, was Griswold " "sagt, heute gibt es keine echte Qualität mehr, nicht wie in den guten alten " "Zeiten!" #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" "Wenn ich Du wäre... bin ich ja nicht, aber wenn ich's wäre...dann würde ich " "den ganzen Kram verkaufen, den Du da hast, und von hier verschwinden. Der " "Junge da oben, der hat immer was Gutes, aber Du musst ihm sogar Geld dafür " "geben, dass er Dir überhaupt zeigt, was er hat." #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "Ah, eine dürstende Seele, die nach Antworten sucht..." #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "Weisheit muss man sich verdienen... man bekommt sie nicht geschenkt. Wenn " "Ihr ein Buch des Wissens findet, verschlingt seine Worte. Solltet Ihr schon " "mystisches Wissen um die Geheimnisse gesammelt haben, die in dem Werk " "beschrieben werden, so bedenket - auch ein Meister kann stets noch " "dazulernen." #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" "Die größte Macht ist oft die, die am schnellsten verlischt. Ihr werdet auf " "Eurem Weg uralte Worte der Macht finden, die auf Schriftrollen aus Pergament " "gebannt wurden. Ihre große Stärke liegt darin, dass sowohl Zauberlehrling " "wie auch Meistermagier sie mit gleicher Leichtigkeit und Gewalt einsetzen " "können. Ihre große Schwäche hingegen ist, dass Ihr sie niemals im Gedächtnis " "halten könnt, sondern sie jedesmal laut verlesen müsst. Wisset auch, dass " "Ihr jede Schriftrolle nur ein einziges Mal lesen könnt, setzt sie also mit " "Bedacht ein." #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" "Obwohl die Hitze der Sonne grenzenlos ist, stellt doch die bescheidene " "Flamme einer einzelnen Kerze eine weitaus größere Gefahr dar. Ohne den " "passenden Fokus lässt sich keine Energie einsetzen, egal wie groß. Viele " "Zaubersprüche lassen sich in magische Stäbe binden, oft sogar mehrfach. Ich " "habe die Fähigkeit, ihre Kräfte zu erneuern - doch Ihr könnt davon ausgehen, " "dass alles seinen Preis hat." #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "Die Summe unseres Wissens ist in der Summe der Menschen. Solltet Ihr ein " "Buch oder eine Schriftrolle finden, die Ihr nicht entziffern könnt, kommt " "einfach zu mir. Wenn ich etwas damit anfangen kann, werde ich mein Wissen " "mit Euch teilen." #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "Für einen Mann, der nur Eisen kennt, gibt es keine größere Magie als den " "Stahl. Griswold, der Schmied, ist ein größerer Zauberer, als er selbst weiß. " "Seine Fähigkeit, Feuer und Metall zu verschmelzen, ist einzigartig im ganzen " "Land." #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "Korruption hat die Stärke des Betruges, aber die Unschuld bezieht ihre Kraft " "aus der Reinheit. Die junge Gillian hat ein reines Herz und kümmert sich " "mehr um die Bedürfnisse ihrer Großmutter als um ihre eigenen Wünsche. Sie " "fürchtet sich vor mir, aber nur, weil sie mich nicht versteht." #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" "Eine Kiste, die man in der Dunkelheit öffnet, enthält dadurch keinen " "größeren Schatz, als wenn man sie bei Licht aufmacht. Der " "Geschichtenerzähler Cain ist ein Rätsel, aber nur für diejenigen, die nicht " "hinschauen. Sein Wissen um die dunklen Dinge, die unter der Kathedrale " "lauern, ist größer, als er sich sogar selbst eingestehen mag." #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" "Je mehr Vertrauen Ihr in einen Menschen setzt, um so größer kann die " "Enttäuschung sein. Farnham hat seine Seele verloren, aber nicht an " "irgendeinen Dämon.. Sie starb, als er mit ansehen musste, wie seine Nachbarn " "und Freunde von Erzbischof Lazarus hintergangen wurden. Er besitzt Wissen, " "das Euch nützen könnte, doch Ihr müsst sorgsam Tatsachen von " "Wahnvorstellungen trennen." #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" "Die Hand, das Herz und der Verstand können Wunder vollbringen, wenn sie " "harmonisch zusammenarbeiten. Der Heiler Pepin vermag in einen Körper " "hineinzuschauen, wie nicht einmal ich es kann. Zu seiner Fähigkeit, den " "Kranken und Verwundeten zu helfen, gesellen sich noch wertvolle Kenntnisse " "auf dem Gebiet der Tränke und Elixiere Er ist einer der besten Verbündeten, " "die Ihr in Tristram haben könnt." #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" "Es gibt vieles in der Zukunft, von dem wir noch nichts wissen können. Aber " "wenn es kommt, werden es unsere Kinder sein, die damit fertig werden müssen. " "Auf der Seele von Wirt, dem kleinen Rumtreiber, lasten dunkle Schatten, aber " "er ist deshalb noch lange keine Gefahr für diese Stadt oder ihre Bewohner. " "Durch seine Verbindungen und seine heimlichen Geschäfte mit der Unterwelt " "und den geheimen Gilden hat er Zugang zu Objekten, die man ansonsten in " "Tristram nicht so leicht bekommt. Seine Methoden kann man vielleicht " "kritisieren, aber Wirt kann Euch trotzdem eine wertvolle Hilfe sein in Eurem " "Kampf gegen die Mächte der Finsternis." #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" "Wände aus Lehm mit einem Dach aus Stroh machen alleine noch kein Zuhause " "aus. Der Gastwirt Ogden ist für diese Stadt wichtiger, als vielen überhaupt " "bewusst ist. Er bietet Gillian und ihrer Großmutter Schutz und Obdach, hält " "Farnhams schwachen Lebensfunken am Glimmen und erinnert alle, die noch in " "der Stadt geblieben sind, daran, wie es in Tristram früher einmal war. Seine " "Kneipe und die einfachen Vergnügungen, die man sich da noch leisten kann, " "sind ein schwacher Nachhall des früheren Lebens, an das sich die Menschen " "hier noch erinnern. Diese Erinnerungen sind es, die ihnen immer noch " "Hoffnung auf Euren Erfolg geben." #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "Pssst... hier drüben..." #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "Nicht jeder in Tristram hat Verwendung oder Kundschaft für jedes Teil, das " "Du im Labyrinth finden wirst. Nicht mal ich, so schwer das auch zu glauben " "sein mag. \n" "\n" "Manchmal wirst nur Du etwas damit anfangen können." #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "Hör nicht auf alles, was der Besoffene Dir erzählt. Die zahllosen Bierchen " "haben seinen Blick und seinen Verstand getrübt." #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" "Falls Du es noch nicht bemerkt hast, ich kaufe nichts aus Tristram. Ich bin " "Importeur für Qualitätswaren. Wenn Du Deinen Müll verscherbeln willst, musst " "Du Dich schon an Griswold, Pepin oder an Adria wenden, diese Hexe. Ich bin " "sicher, die reißen Dir alles aus den Händen, was Du ihnen anschleppst." #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Ich schätze, ich verdanke dem Schmied mein Leben - oder zumindest das, was " "davon noch übrig ist. Klar, Griswold hat mir eine Lehre in seiner Schmiede " "angeboten, und er ist ja auch ein ganz netter Kerl, aber da würde ich nie " "genug Geld verdienen, um... na ja, sagen wir mal, ich habe ganz präzise " "Zukunftspläne, für die ich eine größere Geldsumme benötige." #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Wenn ich ein paar Jährchen älter wäre, würde ich sie mit allen Reichtümern " "überschütten, die ich auftreiben könnte. Und glaub mir, ich komme an " "ziemlich ausgefallene Sachen ran, wenn ich will! Gillian ist ein wunderbares " "Mädchen, das Tristram verlassen sollte, sobald es gefahrlos möglich ist. " "Hmmm... vielleicht nehme ich sie mit, wenn ich gehe." #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" "Cain weiß zuviel. Er macht mir Höllenangst - mehr noch als diese Frau auf " "der anderen Seite des Flusses. Er erzählt mir dauernd, was für ein Glück ich " "habe, noch am Leben zu sein, und dass meine Geschichte in den Legenden " "vorhergesagt worden sei. Manchmal glaube ich, sein geistiger Sattel ist ein " "bisschen vom Pferd gerutscht." #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Farnham - tja, da haben wir mal einen Mann mit richtig ernsten Problemen. " "Und ich bin Fachmann, wenn es um ernste Probleme geht. Er hat der Integrität " "eines einzigen Mannes zu sehr vertraut, und Lazarus hat ihn mitten in die " "Tore der Hölle geführt. Ich weiß genau, wie es da unten aussieht, also " "erzähl mir nichts vom \"Ausrotten des Bösen, das da unten haust\" - pass " "einfach nur auf Deine Beine auf." #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Solange keine Teile neu an Deinem Körper befestigt werden müssen, ist der " "alte Pepin genau der Richtige. \n" "\n" "Hätte ich damals ein paar seiner Tränke dabei gehabt, hätte ich vielleicht " "noch beide Beine." #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Adria macht mir richtig Angst. Klar, Cain ist ziemlich unheimlich mit seinen " "Geschichten über die Vergangenheit, aber Adria... die kann in Deine " "Vergangenheit reingucken! Und sie bekommt irgendwie immer genau das, was sie " "will. Adria kommt an mehr Sachen ran, als früher während des Herbstfestes " "durch die Tore des Königsbazars geliefert wurden." #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" "Ogden ist ein Idiot, dass er hierbleibt. Ich könnte ihn zu einem sehr " "zivilen Preis aus der Stadt bringen, aber er besteht darauf, sein Glück hier " "mit diesem blöden Gasthof zu versuchen. Na ja, zumindest hat Gillian dadurch " "einen Arbeitsplatz, und seine Frau Garda macht einen prima Hirtenkuchen." #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" "Jenseits der Halle der Helden liegt die Knochenkammer. Ewiger Tod erwartet " "den Frevler, der die Schätze aus dieser Kammer zu stehlen versucht. So " "spricht der Herr des Terrors, und so steht es geschrieben." #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" "...und so wartet Tapferstahl hinter dem Blutportal und jenseits des " "Flammengewölbes auf den Tag, da der Held des Lichtes wieder erwacht..." #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Ich kann sehen, was niemand kann.\n" "Milchiger Blick, kein Auge dann.\n" "Wenn sie sich umdrehn, sind sie fort.\n" "Ihr heimlich Lied flüstert der Ort.\n" "Dann seht auch Ihr, was nicht sein darf.\n" "Wo Licht sein soll, der Schatten warf.\n" "Aus Dunkel, wild wie ein Berserker,\n" "unten in der Blinden Kerker." #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Die Waffenkammern der Hölle sind das Zuhause des Blutfürsten. In seinem " "Kielwasser treiben die verstümmelten Leichen von Tausenden. Engel und " "Menschen gleichermaßen wurden reihenweise niedergemäht, als Opfergaben für " "die Finsteren Herrscher, die es nur nach einem dürstet - nach Blut." #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" "Merket auf und vernehmt die Worte der Wahrheit, denn sie sind das letzte " "Vermächtnis der Horadrim. Genau in diesem Moment tobt ein Krieg, jenseits " "der uns bekannten Dimensionen, zwischen den Fürstentümern des Himmelreiches " "und den chaotischen Abgründen der brennenden Hölle. Dieser Krieg wird der " "Große Zwist genannt, und er tobt und wütet schon länger, als irgendein Stern " "am Firmament leuchtet. Keine Seite hat lange einen echten Vorteil in diesem " "Zwist, in dem die Kräfte des Lichts und der Finsternis beharrlich um die " "Macht über die gesamte Schöpfung kämpfen." #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" "Merket auf und vernehmt die Worte der Wahrheit, denn sie sind das letzte " "Vermächtnis der Horadrim. Als der Ewige Zwist zwischen Himmelreich und Hölle " "auf das Gebiet der Sterblichen übergriff, nannte man ihn den Sündenkrieg. " "Engel und Dämonen wandeln verkleidet unter den Menschen, kämpfen im " "Verborgenen, versteckt vor den Blicken der Sterblichen. Einige wagemutige, " "mächtige Sterbliche haben sich sogar der einen oder anderen Seite " "angeschlossen und so den Verlauf des Sündenkrieges mit beeinflusst." #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Merket auf und vernehmt die Worte der Wahrheit, denn sie sind das letzte " "Vermächtnis der Horadrim. Vor beinahe dreihundert Jahren wurde bekannt, dass " "die Drei Erzübel der Hölle auf mysteriöse Weise in unsere Welt gelangt " "waren. Die drei Brüder verwüsteten jahrzehntelang die Länder des Ostens, und " "die Menschheit konnte nur zitternd zu ihnen aufsehen. Heimlich wurde von " "einer Gruppe Magier unser Orden gegründet, die Horadrim, mit dem Zweck, die " "drei Erzübel ein für allemal zu fangen. \n" "\n" "Die Gründerväter der Horadrim schafften es auch wirklich, zwei der drei " "Brüder in mächtigen Artefakten einzuschließen, den sogenannten " "Seelensteinen, die sie anschließend tief unten im Sand der verlassenen " "Wüsten des Ostens vergruben. Doch das dritte Erzübel entkam und floh nach " "Westen, verfolgt von vielen Horadrim. Dieser Dämon, bekannt als Diablo, " "Fürst des Terrors, wurde schließlich auch gefangen, seine Essenz in einen " "Seelenstein eingesperrt und in diesem Labyrinth versteckt. \n" "\n" "Hütet Euch, dass niemand den Seelenstein finde, der nicht dem Orden " "angehört! Wenn Diablo wieder freikäme, würde er sich sofort einen neuen " "Körper suchen, wahrscheinlich erst einmal einen schwachen, den er leicht " "kontrollieren kann, etwa einen alten Mann oder ein Kind." #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "Und so geschah es, dass in den Tiefen der Hölle eine Revolution stattfand, " "bekannt als das Dunkle Exil. Die Geringen Übel stürzten die drei Erzübel und " "verbannten ihre geistigen Formen in das Reich der Sterblichen. Kaum waren " "die drei Fürsten der Finsternis fort, kämpften in der Hölle die Dämonen " "Belial (der Herr der Lügen) und Azmodan (der Herr der Sünden) um die " "Vorherrschaft. Die gesamte Hölle spaltete sich in diese beiden Lager auf, " "während die vereinten Kräfte des Lichts konstant auf die Pforten einstürmten." #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Viele Dämonen reisten in die Welt der Sterblichen, um die drei Brüder zu " "suchen. Doch jedem dieser Dämon folgten auch Engel in unsere Welt, die ihn " "in den riesigen Städten des Ostens jagten. Die Engel verbündeten sich dazu " "mit einem Orden sterblicher Magier, den Horadrim, die bald exzellente " "Dämonenjäger wurden. Doch dabei machten sie sich auch mächtige Feinde in der " "Unterwelt..." #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" "So geschah es, dass die drei Erzübel in geistiger Form in das Reich der " "Sterblichen verbannt wurden. Und nachdem sie jahrzehntelang das Chaos im " "Osten verbreitet hatten, wurden sie von dem verfluchten Orden der " "sterblichen Horadrim gejagt. Die Horadrim benutzten sogenannte Seelensteine, " "um die Essenz von Mephisto, dem Fürsten des Hasses, und von Baal, dem Herrn " "der Vernichtung, gefangenzunehmen und einzusperren. Der Jüngste der " "Gebrüder, Diablo, der Fürst des Terrors, entkam in den Westen. \n" "\n" "Doch schließlich setzten die Horadrim auch Diablo in einem Seelenstein " "gefangen, und sie vergruben ihn unter einer alten, vergessenen Kathedrale. " "Dort ruht der Fürst des Terrors nun und wartet auf die Zeit seiner " "Wiedergeburt. Wisset, dass er wiederkehren wird in einem Körper voller " "Jugend und Macht - einem Körper, der unschuldig und leicht zu kontrollieren " "ist. Dann wird er sich erheben, seine Brüder zu befreien und das Land aufs " "Neue mit der Fackel des Sündenkrieges in Brand zu stecken." #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" "Lob und Ruhm sei Diablo, dem Fürsten des Terrors und Überlebenden des " "Dunklen Exils. Als er aus seinem langen Schlummer erwachte, erzählte mir " "mein Herr und Meister von Geheimnissen, die nur wenigen Sterblichen je " "bekannt waren. Er erzählte mir von den Fürstentümern des Himmelreiches und " "den Abgründen der brennenden Hölle, und von einem ewigen Krieg zwischen den " "beiden. Er nannte mir auch die Mächte, die diesen Zwist ins Reich der " "Menschheit brachten. Mein Herr nannte die Schlacht um diese Welt und alle, " "die auf ihr leben, den Sündenkrieg..." #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" "Ruhm und Ehre sei Diablo, dem Fürsten des Terrors und Anführer der Drei. " "Mein Gebieter erzählte mir von seinen beiden Brüdern, Mephisto und Baal, die " "schon vor langer Zeit auf diese Welt verbannt worden sind. Mein Herr will in " "Ruhe abwarten, seine furchterregenden Kräfte sammeln und dann seine " "gefangenen Brüder aus ihrem Gefängnis unter dem Wüstensand des Ostens " "befreien. Und sobald mein Gebieter erst seine Brüder befreit hat, wird der " "Sündenkrieg wieder unter der Peitsche der Drei zu neuen Höhen getrieben." #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" "Gebete und Opfer für Diablo, den Fürsten des Terrors und Zerstörer der " "Seelen! Als ich meinen Herren aus seinem Schlaf weckte, versuchte er, sofort " "einen menschlichen Körper zu übernehmen. Er probierte, von König Leoric " "Besitz zu ergreifen, doch mein Gebieter war noch zu geschwächt von seiner " "langen Verbannung. Er braucht einen unschuldigen und einfachen Anker in " "dieser Welt, und so fand er, dass der Junge Albrecht die ideale Besetzung " "für diese Rolle sei. Und während der 'gute' König Leoric von Diablos " "erfolglosem Versuch der Übernahme in den Wahnsinn getrieben wurde, entführte " "ich seinen Sohn Albrecht und brachte ihn zu meinem Herrn. Nun warte ich " "darauf, dass Diablo mich ruft, und hoffe, dass ich meinen gerechten Lohn " "erhalte, wenn er letztendlich als Beherrscher dieser Welt auf dem Thron " "sitzt." #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "Dem Himmel sei Dank, Ihr seid wieder da!\n" "Vieles hat sich verändert, seit Ihr hier gewohnt habt, mein Freund. Alles " "war friedlich, bis die Dunklen Reiter kamen und unser Dorf zerstörten. Viele " "von uns starben, bevor sie sich wehren konnten. Und die, die zu ihren Waffen " "eilten, wurden umgebracht oder verschleppt, als Sklaven - oder Schlimmeres. " "Die Kirche am Dorfrand wurde entweiht und wird jetzt für dunkle Rituale " "missbraucht. Die Schreie, die man nachts hört, sind unmenschlich, doch " "vielleicht haben einige unserer Freunde und Nachbarn ja doch noch überlebt. " "Folgt dem Weg, der zwischen meinem Gasthof und der Schmiede entlang führt, " "dann kommt Ihr zur Kirche. \n" "\n" "Rettet, wen Ihr retten könnt, und vielleicht kann ich Euch schon mehr sagen, " "wenn wir uns das nächste Mal wiedersehen. Viel Glück." #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" "Haltet an Eurer Suche fest. Einen verlorenen Schatz zu finden ist nicht " "leicht. Einen versteckten Schatz noch weniger. Ich gebe Euch das mit auf den " "Weg: Lasst Euch bei Eurer Suche nicht vom Zahn der Zeit verwirren." #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" "Ein was?! So eine Torheit. Es gibt keinen vergrabenen Schatz hier in " "Tristram. Lasst mich das sehen!! Ah seht, diese Zeichnungen sind ungenau. " "Sie passen überhaupt nicht zu unserem Dorf. Ich würde mich lieber damit " "beschäftigen, was unter der Kathedrale liegt, und nicht damit, was unter " "unserem Ackerboden liegt." #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" "Ich habe wirklich keine Zeit um über irgendeine Karte zu besprechen, die Ihr " "sucht. Ich habe viele kranke Menschen, die meine Hilfe brauchen und Eure " "ebenso." #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" "Der einst stolze Izual ist tief unter der Oberfläche dieser Welt gefangen. " "Seiner Ehre beraubt und sein Antlitz verwandelt. Er ist in ewiger Qual " "gefangen, gezwungen dasjenige zu verbergen, das ihn befreien könnte." #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" "Ich wette, Wirt hat Euch kommen sehen und eine Schau abgezogen, damit er " "sich später über Euch lustig machen kann, wenn Ihr im Dorf herumlauft und " "Eure Nase in den Dreck steckt. Ich würde es ignorieren." #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" "Es gab eine Zeit, in der diese Stadt ein häufiger Zwischenstopp für Reisende " "aus nah und fern war. Seitdem hat sich viel verändert. Aber versteckte " "Höhlen und vergrabene Schätze sind gängige Fantasien jedes Kindes. Wirt " "frönt selten jugendlichen Spielen. Es könnte also nur seine Einbildung sein." #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Hör mal, komm mal her. Ich weiß nicht, ob Du weißt, was ich weiß, aber weißt " "Du, dass das eine Karte ist? Eine richtige Landkarte." #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Meine Großmutter erzählt mir oft Geschichten über die seltsamen Kräfte, die " "den Friedhof außerhalb der Kirche bewohnen. Und es könnte Dich " "interessieren, eine davon zu hören. Sie sagte, dass, wenn man die richtige " "Opfergabe auf dem Friedhof hinterlässt, die Kathedrale betritt, um für die " "Toten zu beten, und dann wiederkommt, die Opfergabe auf seltsame Weise " "verändert würde. Ich weiß nicht, ob dies nur das Gerede einer alten kranken " "Frau ist, aber heutzutage scheint alles möglich zu sein." #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" "Hmmm. Ein gewaltiger und rätselhafter Schatz, sagt Ihr. Mmmm. Ich würde Euch " "vielleicht das eine oder andere Kleinod abkaufen. Oder besser noch, benötigt " "Ihr nicht einiges an seltener und teurer Ausrüstung um diese Prüfung zu " "bestehen?" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "Also, Du musst der Held sein, über den alle reden. Vielleicht kannst du " "einem armen, einfachen Bauern aus einer furchtbaren Klemme helfen? Am Rande " "meiner Plantage, genau südlich von hier, quillt ein abscheuliches Ding aus " "dem Boden! Ich kann nicht zu meinen Pflanzen oder zu meinen Heuballen, und " "meine armen Kühe werden hungern. Die Hexe hat mir das hier gegeben und " "gesagt, dass es dieses Ding aus meinem Acker sprengen würde. Wenn Du es " "beseitigen könntest, wäre ich Dir ewig dankbar. Ich würde es ja selbst tun, " "aber jemand muss hier bei den Kühen bleiben..." #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Ich wusste, dass es nicht so einfach sein kann wie die Hexe behauptet hat. " "Es ist eine traurige Welt, wenn du noch nicht mal deinen Nachbarn vertrauen " "kannst." #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "Ist es weg? Hast du es in den dunklen Schoß des Hades zurückgeschickt, aus " "dem es gekrochen kam? Du hast was? Oh, erzähl mir nicht, du hast es " "verloren! Diese Dinger sind nicht billig, weißt du. Du musst es wiederfinden " "und dann dieses Grauen aus unserem Dorf sprengen." #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" "Ich habe die Explosion von hier aus gehört! Hab vielen Dank, freundlicher " "Fremdling. Mit diesen ganzen Dingern, die aus dem Boden kommen, Monstern, " "die die Kirche übernehmen, und so weiter, es sind schwierige Zeiten. Ich bin " "zwar nur ein armer Bauer, aber hier -- nimm das mit meinem großen Dank." #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" "Oh, solch einen Ärger habe ich ... möglicherweise ... nein, das kann ich dir " "nicht aufbürden, du hast sicher schon genug Probleme. Vielleicht könntest du " "zurückkommen, wenn du die Kirche von einigen dieser Kreaturen gesäubert " "hast ... und ein bisschen Zeit erübrigen, um einem armen Bauern zu helfen?" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Waaaah! (schnief) Waaaah! (schnief)" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" "Ich hab Theo verloren! Ich hab meinen besten Freund verloren! Wir haben auf " "der anderen Seite beim Fluss gespielt, und Theo sagte er will sich das große " "grüne Ding ansehen. Ich sagte, das sollten wir nicht, aber wir sind da " "hingeschlichen, und dann kam auf einmal dieser Käfer heraus! Wir sind " "weggerannt, aber Theo fiel hin und der Käfer packte ihn und nahm ihn weg!" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" "Hast du ihn gefunden? Du musst Theodore finden, bitte! Er ist noch so klein. " "Er kann nicht auf sich selbst aufpassen! Bitte!" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "Du hast ihn gefunden! Du hast ihn gefunden! Danke! Oh Theo, haben dir die " "bösen Käfer Angst gemacht? Hey! Pfui! Da steckt etwas in deinem Fell! Igitt! " "Komm schon, Theo, lass uns nach Hause gehen! Danke nochmal, Herr Held!" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Wir haben lange geschlafen, und die Zeit des Erwachens ist gekommen. Nach " "unserem langen Schlaf sind wir von großem Hunger erfüllt. Bald, jetzt, " "werden wir speisen..." #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "Hast du dich amüsiert, kleines Säugetier? Wie erbärmlich. Deine kleine Welt " "wird keine Herausforderung sein." #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Dieser Boden wird besudelt werden, und unsere Brut wird die Felder " "überrennen, die die Menschen zu Hause nennen. Unsere Ranken werden diese " "Welt einwickeln, und wir werden am Fleisch ihrer Bewohner schlemmen. Der " "Mensch wird unser Besitz und unsere Nahrung." #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Ah, ich kann dich riechen...du bist in der Nähe! Ganz nah! Ssss...der Geruch " "von Blut und Angst...wie verlockend..." #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "Und im Jahre des goldenen Lichts wurde es daher verfügt, dass eine große " "Kathedrale errichtet werde. Der Grundstein dieses heiligen Orts sollte aus " "dem durchsichtigen Stein Antyrael gemeißelt werden, benannt nach dem Engel, " "der seine Kräfte mit den Horadrim geteilt hat.\n" " \n" "In dem Jahr, als sich die Schatten abzeichneten, erbebte die Erde und die " "Kathedrale zerbrach und stürzte ein. Als der Bau der Katakomben und Burgen " "begann und die Menschen sich gegen die Verheerungen des Sündenkriegs " "stellten, wurden die Ruinen wegen der Steine geplündert. Und so geschah es, " "dass der Grundstein aus den Augen der Menschen verschwand.\n" " \n" "Der Stein war von dieser Welt -- und von allen Welten -- da das Licht sowohl " "in allen Dingen als auch jenseits aller Dinge ist. Licht und Einheit sind " "die Erzeugnisse dieser heiligen Grundfeste, eine Einheit des Zwecks und eine " "Einheit des Besitzes." #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "Muh." #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "Ich sagte, Muh." #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "Schau, ich bin nur eine Kuh, okay?" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" "Schon gut, schon gut. Ich bin nicht wirklich eine Kuh. Ich laufe " "normalerweise nicht so herum; aber, ich habe zu Hause gesessen und mich um " "meine eigenen Angelegenheiten gekümmert, als ganz plötzlich diese Käfer & " "Ranken & Knollen aus dem Fußboden kamen ... es war schrecklich! Wenn ich nur " "etwas Normales anzuziehen hätte, wäre es nicht so schlimm. Heeey! Könntest " "du zu meiner Stube zurückgehen und meinen Anzug für mich holen? Den braunen, " "nicht den grauen, der ist Abendgarderobe. Ich würde es selbst tun, aber ich " "will nicht, dass mich jemand so sieht. Hier, nimm das, du könntest es " "brauchen ... um diese Dinger zu töten, die alles überwuchert haben. Du " "kannst mein Haus nicht verfehlen. Es ist genau südlich von der " "Flussgabelung ... du weißt schon ... das mit dem überwucherten Gemüsegarten." #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "Wofür vergeudest du denn noch Zeit? Geh und hol meinen Anzug! Und beeil " "dich! Die Milchkuh da drüben zwinkert mir ständig zu!" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Hey, hast du meinen Anzug? Schnell, gib Ihn mir! Diese Ohren jucken, als " "würdest du es nicht glauben!" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" "Nein nein nein nein! Das ist mein grauer Anzug! Der ist Abendgarderobe! " "Formale Anlässe! Das kann ich nicht tragen. Was bist du, irgendein Spinner? " "Ich brauche den braunen Anzug." #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" "Ahh, das ist viel besser. Hui! Endlich etwas Würde! Steht mein Geweih " "akkurat? Gut. Hör mal, vielen Dank, dass du mir ausgeholfen hast. Hier, nimm " "das als Geschenk; und, weißt du ... ein kleiner Modetipp ... du könntest ein " "bisschen ... du könntest ein neues ... weißtwasichmein? Dieses ganze " "Abenteurermotiv ist einfach so ... retro. Nur ein gut gemeinter Rat, ja? " "Ciao." #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Schau, ich bin eine Kuh. Und du, du bist Monster-Frühstück. Sammel erstmal " "ein paar Erfahrungen! Wir sprechen uns noch." #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Es muss wirklich eine furchterregende Aufgabe sein, die ich Euch gestellt " "habe. Wenn es nur eine Möglichkeit gäbe... würde mir ein Krug frischer Milch " "helfen?" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "Oh, ich könnte deine Hilfe gebrauchen, aber vielleicht nachdem du die " "Katakomben vor ihrer Entweihung durch diese Biester bewahrt hast." #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Ich müsste etwas erledigt haben, aber ich könnte das nicht einem völlig " "Fremden aufbürden. Vielleicht würde ich mich eher dabei wohlfühlen, dich um " "einen Gefallen zu bitten, nachdem du eine Weile hier verbracht hast." #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "Ich sehe ein Potential zu echter Größe in dir. Vielleicht kannst du " "irgendwann mal, während du dein Schicksal erfüllst, vorbeischauen und mir " "einen kleinen Gefallen tun?" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" "Ich denke, Du könntest mir wahrscheinlich helfen, aber vielleicht, nachdem " "Du ein wenig mächtiger geworden bist. Ich möchte nicht die einzige Chance " "des Dorfes verspielen, die Bedrohung der Kirche ein für alle mal " "auszulöschen!" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Ich? Ich bin eine Selfmade-Kuh. Mach du erstmal was aus dir, und ... dann " "reden wir." #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "Ich muss mich nicht jedem Touristen erklären, der zufällig vorbeiläuft! Hast " "du keine Monster umzubringen? Vielleicht reden wir später. Wenn du dann noch " "lebst..." #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" "Hör auf mich zu nerven! Ich suche nach jemand wirklich Heldenhaftem. Und du " "bist es nicht. Ich kann dir nicht vertrauen, du würdest doch jetzt jederzeit " "von Monstern gegessen werden ... Ich brauche jemanden, der ein erfahrender " "Held ist." #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" "In Ordnung, ich breche das Schweigen. Ich wollte dich nicht in falsche " "Wasser führen. Ich saß zu Hause und fühlte mich mürrisch, als die Dinge " "wirklich instabil wurden; ein ganzer Ansturm von Monstern kam aus dem Boden! " "Ich habe mich nur eingeschüchtert. Ich habe gerade dieses Trikot getragen, " "als ich aus der Tür gerannt bin, und jetzt sehe ich lächerlich aus. Wenn ich " "nur etwas Normales zum Anziehen hätte, wäre es nicht so schlimm. Hallo! " "Kannst Du zu mir zurückgehen und meinen Anzug für mich holen? Den braunen, " "nicht den grauen, das ist für die Abendgarderobe. Ich würde es selbst tun, " "aber ich möchte nicht, dass mich jemand so sieht. Hier, nimm das, du " "brauchst es vielleicht... um die Dinger zu töten, die alles überwuchert " "haben. Du kannst mein Haus nicht verfehlen, es liegt südlich der Gabelung im " "Fluss... du weißt schon... das mit dem überwucherten Gemüsegarten." #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "Ich habe Zauber versucht, Drohungen, Widerrufung und Handel mit dieser " "unreinen Kreatur, aber vergeblich. Meine Methoden zur Versklavung geringerer " "Dämonen scheinen keine Auswirkung auf diese furchterregende Bestie zu haben." #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" "Allmählich legt die Niedertracht dieses ungebetenen Gefangenen ein Unheil " "über mein Heim. Die Krypta ist voller Schatten, die kaum mein Sichtfeld " "streifen. Das schwache Scharren von Klauen tanzt an der Grenze meines " "Hörsinns. Sie suchen, denke ich. Nach diesem Tagebuch." #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" "In seinem Fluchen hat diese Kreatur seinen Namen fallen lassen -- Na-Krul. " "Ich habe versucht, diesem Namen auf den Grund zu gehen, aber die geringeren " "Dämonen haben irgendwie meine Bibliothek zerstört. Na-Krul ... der Name " "erfüllt mich mit kaltem Grausen. Ich ziehe es vor, sie lieber als die " "\"Kreatur\" zu bezeichnen als über ihren wahren Namen nachzudenken." #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" "Das wütende Brüllen der gefangenen Kreatur hindert mich daran, dringend " "benötigten Schlaf zu bekommen. Er wütet gegen denjenigen, der ihn in den " "Abgrund geschickt hat, und er spricht schlimme Flüche über mich aus, weil " "ich ihn hier gefangen habe. Seine Worte erfüllen mein Herz mit Schrecken, " "und dennoch kann ich seine Stimme nicht verdrängen." #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" "Meine Zeit läuft schnell ab. Ich muss aufzeichnen, wie man den Dämon " "schwächen kann, und diesen Text dann verstecken, damit nicht seine Diener " "einen Weg finden, ihren Herrn mit Hilfe meines Wissens zu befreien. Ich " "hoffe, dass, wer immer dieses Tagebuch findet, er nach diesem Wissen suchen " "wird." #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Wer immer diese Schriftrolle findet, hat die Aufgabe, die dämonische Kreatur " "aufzuhalten, die hinter diesen Mauern weilt. Meine Zeit ist vorbei. Selbst " "in diesem Moment kratzen seine höllischen Diener an der zerbrechlichen Tür, " "hinter der ich mich verstecke.-\n" "\n" "Ich habe den Dämon mit einer geheimen Magie gebunden und in diesen mächtigen " "Mauern eingeschlossen, aber ich befürchte, das wird nicht genug sein.\n" "\n" "Die Zaubersprüche aus den drei Zauberbüchern werden Euch einen geschützten " "Zugang in seinen Bereich gewähren, aber nur, wenn sie in der richtigen " "Reihenfolge aufgesagt werden. Die Hebel am Eingang werden die Sperre " "beseitigen und den Dämon freisetzen; berühre sie nicht! Benutze nur die " "Zaubersprüche, sonst könnte er zu mächtig sein um ihn zu besiegen." #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "Praedictum Otium." #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "Höllenkeiler" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "Skorpion" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "Psychorb" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "Arachnon" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "Missgestalteter" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "Hungrige Brut" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "Giftschwanz" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "Nekromorb" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "Spinnenfürst" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "Peitschenwurm" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "Fackelträger" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "Höllenkäfer" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "Totengräber" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "Gruftratte" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "Feuerflügel" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "Sturmrufer" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "Lich" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "Geschmiedeter" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "Höllenflügel" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "Knochendämon" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "Erz-Lich" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "Byklop" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "Fleisch-Untier" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "Schnitter" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "Riesenknöchel" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "Lebhafter Ring" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "Xorines Ring" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "Kariks Ring" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "Ring des Magmas" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "Ring der Mystiker" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "Ring des Donners" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "Amulett des Widerstands" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "Moskitostich" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "Flambeau" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "Harnisch der Düsternis" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "Blitzen" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "Donnerschlag" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "Shirotachi" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "Seelenfresser" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "Diamantschneide" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "Knochenkettenrüstung" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "Dämonenpanzer" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "Amulett des Akolythen" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "Ring des Gladiators" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "Einfältiger" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "Gläserner" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "Doppelgänger-" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "r Verwüstung" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "s Verfalls" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "r Tücke" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "Mana-Ritual" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "Mantik" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "Torheit" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "Blitzwand" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "Deflagration" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "Warp" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "Dornen" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "Berserker" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "Feuerring" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "Suche" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "Feuerrune" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "Lichtrune" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "Novarune" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "Deflagrationsrune" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "Steinrune" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "," #~ msgid "/help" #~ msgstr "/hilfe" #~ msgid "/arena" #~ msgstr "/arena" #~ msgid "/arenapot" #~ msgstr "/arenapot" #~ msgid "/inspect" #~ msgstr "/inspect" #~ msgid "/seedinfo" #~ msgstr "/seedinfo" #~ msgid "Command \"" #~ msgstr "Befehl \"" #~ msgid "Decrease Gamma" #~ msgstr "Helligkeit verringern" #~ msgid "Increase Gamma" #~ msgstr "Helligkeit erhöhen" #~ msgid "No automap available in town" #~ msgstr "Die Karte ist in der Stadt nicht verfügbar" #~ msgid "Restart In Town" #~ msgstr "Neustart in Tristram" #~ msgid "Heart" #~ msgstr "Herz" #~ msgid "Trying to drop a floor item?" #~ msgstr "Versuchst Du, einen Bodengegenstand fallen zu lassen?" #~ msgid "" #~ "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " #~ "frame. Disabling it can help with mouse lag on some systems." #~ msgstr "" #~ "Erzwingt das Warten auf die vertikale Synchronisation. Verhindert den " #~ "Zerreißeffekt. Deaktivieren kann auf manchen Systemen bei ruckelnden " #~ "Mauszeigern helfen." #~ msgid "FPS Limiter" #~ msgstr "Maximale FPS" #~ msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." #~ msgstr "" #~ "Die FPS werden limitiert um eine hohe CPU-Last zu verhindern. Das Limit " #~ "berücksichtig die Bildwiederholungsrate." #~ msgid "To hit" #~ msgstr "Treffer%" #~ msgid "Indestructible, " #~ msgstr "Unzerstörbar, " #~ msgid "No required attributes" #~ msgstr "Ohne Anforderungen" #~ msgid "" #~ "Cloudy and cooler today. Casting the nets of necromancy across the void " #~ "landed two new subspecies of flying horror; a good day's work. Must " #~ "remember to order some more bat guano and black candles from Adria; I'm " #~ "running a bit low." #~ msgstr "" #~ "Heute bewölkt und kühler. Das Auswerfen der Netze der Nekromantie über " #~ "die Leere brachte zwei neue Unterarten des fliegenden Horrors an Land; " #~ "ein guter Arbeitstag. Muss daran denken, noch mehr Fledermaus-Guano und " #~ "schwarze Kerzen von Adria zu bestellen; Mein Vorrat geht ein langsam zur " #~ "Neige." ================================================ FILE: Translations/devilutionx.pot ================================================ #, fuzzy msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:22+0200\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: ;_;N_;ngettext:1,2;pgettext:1c,2;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "" #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "" #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "" #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:27 msgid "Offline" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "" #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "" #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "" #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "" #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "" #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "" #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "" #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "" #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "" #: Source/appfat.cpp:63 msgid "Error" msgstr "" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" #: Source/automap.cpp:1416 msgid "Game: " msgstr "" #: Source/automap.cpp:1424 msgid "Offline Game" msgstr "" #: Source/automap.cpp:1426 msgid "Password: " msgstr "" #: Source/automap.cpp:1429 msgid "Public Game" msgstr "" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "" #: Source/control.cpp:203 msgid "Tab" msgstr "" #: Source/control.cpp:203 msgid "Esc" msgstr "" #: Source/control.cpp:203 msgid "Enter" msgstr "" #: Source/control.cpp:206 msgid "Character Information" msgstr "" #: Source/control.cpp:207 msgid "Quests log" msgstr "" #: Source/control.cpp:208 msgid "Automap" msgstr "" #: Source/control.cpp:209 msgid "Main Menu" msgstr "" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "" #: Source/control.cpp:211 msgid "Spell book" msgstr "" #: Source/control.cpp:212 msgid "Send Message" msgstr "" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "" #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr "" #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "" #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "" #: Source/control.cpp:653 msgid "What arena do you want to visit?" msgstr "" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "" #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "" #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "" #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "" #: Source/control.cpp:800 msgid "[command]" msgstr "" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "" #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "" #: Source/control.cpp:802 msgid "" msgstr "" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "" #: Source/control.cpp:803 msgid "" msgstr "" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "" #: Source/control.cpp:1311 msgid "Player friendly" msgstr "" #: Source/control.cpp:1313 msgid "Player attack" msgstr "" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "" msgstr[1] "" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "" msgstr[1] "" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "" msgstr[1] "" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "" #: Source/control.cpp:1525 msgid "Right click to inspect" msgstr "" #: Source/control.cpp:1573 msgid "Level Up" msgstr "" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "" msgstr[1] "" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "" #: Source/cursor.cpp:636 msgid "level 15" msgstr "" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, c++-format msgid "Unable to load data from file {0}" msgstr "" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "" #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "" #: Source/diablo.cpp:1809 msgid "Previous quick spell" msgstr "" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 msgid "Next quick spell" msgstr "" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "" #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "" #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "" #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "" #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "" #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "" #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "" #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "" #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "" #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "" #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "" #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "" #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "" #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "" #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "" #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "" #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "" #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "" #: Source/diablo.cpp:2004 msgid "Pause Game (Alternate)" msgstr "" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 msgid "Decrease Brightness" msgstr "" #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "" #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 msgid "Increase Brightness" msgstr "" #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "" #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "" #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "" #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "" #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "" #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 msgid "Sort Inventory" msgstr "" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" #: Source/diablo.cpp:2144 msgid "Secondary action" msgstr "" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "" #: Source/diablo.cpp:2159 msgid "Spell action" msgstr "" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "" #: Source/diablo.cpp:2174 msgid "Cancel action" msgstr "" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "" #: Source/diablo.cpp:2200 msgid "Move up" msgstr "" #: Source/diablo.cpp:2201 msgid "Moves the player character up." msgstr "" #: Source/diablo.cpp:2206 msgid "Move down" msgstr "" #: Source/diablo.cpp:2207 msgid "Moves the player character down." msgstr "" #: Source/diablo.cpp:2212 msgid "Move left" msgstr "" #: Source/diablo.cpp:2213 msgid "Moves the player character left." msgstr "" #: Source/diablo.cpp:2218 msgid "Move right" msgstr "" #: Source/diablo.cpp:2219 msgid "Moves the player character right." msgstr "" #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "" #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "" #: Source/diablo.cpp:2310 msgid "Automap Move Up" msgstr "" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 msgid "Automap Move Down" msgstr "" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 msgid "Automap Move Left" msgstr "" #: Source/diablo.cpp:2323 msgid "Moves the automap left when active." msgstr "" #: Source/diablo.cpp:2328 msgid "Automap Move Right" msgstr "" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "" #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "" #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "" #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "" #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "" #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "" #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "" #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "" #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 msgid "Toggle game menu" msgstr "" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 msgid "Opens the game menu." msgstr "" #: Source/diablo_msg.cpp:63 msgid "Game saved" msgstr "" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "" #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "" #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "" #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 msgid "Auto Oil Pickup" msgstr "" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 msgid "Exit to Main Menu" msgstr "" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "" #: Source/help.cpp:49 msgid "$Movement:" msgstr "" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" #: Source/help.cpp:53 msgid "$Combat:" msgstr "" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" #: Source/help.cpp:70 msgid "$Gold:" msgstr "" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "" #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "" #: Source/items.cpp:1652 msgid "damage potential" msgstr "" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "" #: Source/items.cpp:1664 msgid "item's durability" msgstr "" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "" #: Source/items.cpp:1695 msgid "restore all life" msgstr "" #: Source/items.cpp:1698 msgid "restore some life" msgstr "" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "" #: Source/items.cpp:1707 msgid "increase strength" msgstr "" #: Source/items.cpp:1710 msgid "increase magic" msgstr "" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "" #: Source/items.cpp:1806 #, c++-format msgid "{} to use" msgstr "" #: Source/items.cpp:1809 #, c++-format msgid "" "Select from spell book,\n" "then {} to read" msgstr "" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr "" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr "" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr "" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "" #: Source/items.cpp:3956 msgid "high durability" msgstr "" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "" #: Source/items.cpp:3960 msgid "indestructible" msgstr "" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "" #: Source/items.cpp:4010 msgid "quick attack" msgstr "" #: Source/items.cpp:4012 msgid "fast attack" msgstr "" #: Source/items.cpp:4014 msgid "faster attack" msgstr "" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "" #: Source/items.cpp:4027 msgid "fast block" msgstr "" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "" msgstr[1] "" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "" #: Source/items.cpp:4035 msgid "altered durability" msgstr "" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "" #: Source/items.cpp:4041 msgid "life stealing" msgstr "" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "" #: Source/items.cpp:4139 msgid "unique item" msgstr "" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "" #: Source/levels/setmaps.cpp:33 msgid "Hell Arena" msgstr "" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "" #: Source/monster.cpp:2992 msgid "Demon" msgstr "" #: Source/monster.cpp:2994 msgid "Undead" msgstr "" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr "" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr "" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr "" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "" #: Source/mpq/mpq_writer.cpp:174 msgid "Failed to open archive for writing." msgstr "" #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "" #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "" #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "" #: Source/objects.cpp:128 msgid "Hidden" msgstr "" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "" #: Source/objects.cpp:132 msgid "Stone" msgstr "" #: Source/objects.cpp:133 msgid "Religious" msgstr "" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "" #: Source/objects.cpp:140 msgid "Eerie" msgstr "" #: Source/objects.cpp:141 msgid "Divine" msgstr "" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "" #: Source/objects.cpp:143 msgid "Sacred" msgstr "" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "" #: Source/objects.cpp:145 msgid "Spooky" msgstr "" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "" #: Source/objects.cpp:147 msgid "Creepy" msgstr "" #: Source/objects.cpp:148 msgid "Quiet" msgstr "" #: Source/objects.cpp:149 msgid "Secluded" msgstr "" #: Source/objects.cpp:150 msgid "Ornate" msgstr "" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "" #: Source/objects.cpp:152 msgid "Tainted" msgstr "" #: Source/objects.cpp:153 msgid "Oily" msgstr "" #: Source/objects.cpp:154 msgid "Glowing" msgstr "" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "" #: Source/objects.cpp:159 msgid "Solar" msgstr "" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "" #: Source/objects.cpp:4799 msgid "Lever" msgstr "" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "" #: Source/objects.cpp:4833 msgid "Chest" msgstr "" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "" #: Source/objects.cpp:4851 msgid "Pod" msgstr "" #: Source/objects.cpp:4854 msgid "Urn" msgstr "" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "" #: Source/options.cpp:422 msgid "Game Mode Settings" msgstr "" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "" #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" #: Source/options.cpp:442 msgid "Start Up" msgstr "" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "" #: Source/options.cpp:455 msgid "Splash" msgstr "" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "" #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "" #: Source/options.cpp:458 msgid "Title Screen" msgstr "" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "" #: Source/options.cpp:501 msgid "Audio" msgstr "" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "" #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "" #: Source/options.cpp:507 msgid "Sample Rate" msgstr "" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "" #: Source/options.cpp:508 msgid "Channels" msgstr "" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "" #: Source/options.cpp:509 msgid "Buffer Size" msgstr "" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "" #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "" #: Source/options.cpp:510 msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "" #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" #: Source/options.cpp:574 msgid "Resampler" msgstr "" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "" #: Source/options.cpp:631 msgid "Device" msgstr "" #: Source/options.cpp:631 msgid "Audio device" msgstr "" #: Source/options.cpp:688 msgid "Graphics" msgstr "" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "" #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" #: Source/options.cpp:700 msgid "Upscale" msgstr "" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "" #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "" #: Source/options.cpp:710 msgid "Bilinear" msgstr "" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "" #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "" #: Source/options.cpp:738 msgid "Per-pixel Lighting" msgstr "" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "" #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" #: Source/options.cpp:746 msgid "Show FPS" msgstr "" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "" #: Source/options.cpp:782 msgid "Gameplay" msgstr "" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" #: Source/options.cpp:785 msgid "Grab Input" msgstr "" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "" #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "" #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" #: Source/options.cpp:790 msgid "Full quests in Multiplayer" msgstr "" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "" #: Source/options.cpp:791 msgid "Test Bard" msgstr "" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "" #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "" #: Source/options.cpp:793 msgid "Experience Bar" msgstr "" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "" #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" #: Source/options.cpp:795 msgid "Show health values" msgstr "" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "" #: Source/options.cpp:796 msgid "Show mana values" msgstr "" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "" #: Source/options.cpp:797 msgid "Show Party Information" msgstr "" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "" #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "" #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "" #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "" #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "" #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "" #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "" #: Source/options.cpp:814 msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" #: Source/options.cpp:815 msgid "Quick Cast" msgstr "" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "" #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "" #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "" #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "" #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "" #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "" #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" #: Source/options.cpp:824 msgid "Off" msgstr "" #: Source/options.cpp:825 msgid "Random Angles" msgstr "" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "" #: Source/options.cpp:880 msgid "Controller" msgstr "" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "" #: Source/options.cpp:889 msgid "Network" msgstr "" #: Source/options.cpp:889 msgid "Network Settings" msgstr "" #: Source/options.cpp:901 msgid "Chat" msgstr "" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "" #: Source/options.cpp:1029 msgid "Language Settings" msgstr "" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 msgid "Mod Settings" msgstr "" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "" #: Source/panels/charpanel.cpp:176 msgid "Chance To Hit" msgstr "" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "" msgstr[1] "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "" #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "" #: Source/quick_messages.cpp:10 msgid "I need help! Come here!" msgstr "" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "" #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "" #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 msgid "Thanks." msgstr "" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 msgid "Griswold" msgstr "" #: Source/stores.cpp:132 msgid "Pepin" msgstr "" #: Source/stores.cpp:134 msgid "Ogden" msgstr "" #: Source/stores.cpp:135 msgid "Cain" msgstr "" #: Source/stores.cpp:136 msgid "Farnham" msgstr "" #: Source/stores.cpp:137 msgid "Adria" msgstr "" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "" #: Source/stores.cpp:139 msgid "Wirt" msgstr "" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr "" #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "" #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "" #: Source/stores.cpp:315 #, c++-format msgid "Dur: {:d}/{:d}" msgstr "" #: Source/stores.cpp:317 msgid "Indestructible" msgstr "" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "" #: Source/stores.cpp:394 msgid "Repair items" msgstr "" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "" #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "" #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "" #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "" #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "" #: Source/stores.cpp:982 msgid "What have you got?" msgstr "" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "" #: Source/stores.cpp:1013 msgid "Leave" msgstr "" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "" #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "" #: Source/stores.cpp:1204 msgid "Done" msgstr "" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "" #: Source/stores.cpp:1217 msgid "is not available" msgstr "" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "" #: Source/stores.cpp:1219 msgid "version" msgstr "" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "" #: Source/towners.cpp:786 msgid "Cow" msgstr "" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "" #: Source/towners.cpp:788 msgid "Celia" msgstr "" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "" #: Source/translation_dummy.cpp:370 msgid "Lightforge" msgstr "" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "" #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "" #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "" #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "" #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "" #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "" #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "" #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "" #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "" #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "" #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "" ================================================ FILE: Translations/el.po ================================================ msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:20+0200\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: Μιχάλης Χατζηβασιλείου\n" "Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Σχεδιασμός Παιχνιδιού" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Προϊστάμενοι Σχεδιασμού" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Επιπλέον Σχεδιαστές" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Κύριος Προγραμματιστής" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Προϊστάμενοι Προγραμματιστές" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Προγραμματιστές" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Ειδικοί Έκτακτοι Προγραμματιστές" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Προγραμματισμός Battle.net" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Προγραμματισμός Σειριακών Επικοινωνιών" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Προγραμματισμός Εγκατάστασης" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Καλλιτεχνικοί Διευθυντές" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Καλλιτεχνία" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Τεχνική Καλλιτεχνία" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Σκηνοθέτες Κινηματογραφικής Τέχνης" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D Κινηματογραφική Τέχνη" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Κινηματογραφική Τεχνική Τέχνη" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Εκτελεστικός Παραγωγός" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Παραγωγός" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Συνεργάτης Παραγωγός" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Ομάδα κρούσης Diablo" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Μουσική" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Σχεδιασμός Ήχου" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Κινηματογραφική Μουσική και Ήχος" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Παραγωγή φωνής, κατεύθυνση & κάστινγκ" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Σενάριο & Ιστορία" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Επεξεργασία Φωνής" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Φωνές" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Μηχανικός Εγγραφής Ήχου" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Σχεδιασμός και Μορφή Ενχειριδίου" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Καλλιτεχνία Εγχειριδίου" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Προσωρινός διευθυντής του QA (Προϊστάμενος Ελεγκτής)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "Ομάδα επίθεσης QA (Ελεγκτές)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "QA Ειδική Ομάδα Επιχειρήσεων (Ελεγκτές Συμβατότητας)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "Υποστήριξη πυροβολικού QA (πρόσθετοι ελεγκτές) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "Αντικατασκοπεία QA" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Ομάδα Υπηρεσιών Δικτυακών Πληροφοριών" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Υποστήριξη Πελατών" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Πωλήσεις" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Άχρηστα" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Τραγουδιστές Υπόβαθρου του κ. Dabiri" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Δημόσιες Σχέσεις" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Μάρκετινγκ" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Διεθνείς Πωλήσεις" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "Πωλήσεις U.S" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Παραγωγή" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Νομικά & Επιχειρηματικά" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Ειδικές Ευχαριστίες στους" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Ευχαριστούμε τους" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "Στην μνήμη του" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Πολύ Ειδικές Ευχαριστίες στους" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Γενικός Μάνατζερ" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Μηχανική Λογισμικού" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Διευθυντής Τέχνης" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Καλιτέχνες" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Σχεδιασμός" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Σχεδιασμός Ήχου, SFX & Μηχανική Φωνής" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Προϊστάμενος Διαπίστευσης Ποιότητας" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Έλεγχος" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Εγχειρίδιο" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\t Επιπλέον Εργασίες" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Συγγραφή Κειμένων Αποστολών" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Ευχαριστούμε τούς παρακάτω" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tΕιδικές Ευχαριστίες στην Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Μάνατζερ Διαπίστευσης Ποιότητας" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Προϊστάμενος και Ελεγκτής Διαπίστευσης Ποιότητας" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Κύριοι Ελεγκτές" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Επιπλέον Ελεγκτές" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Μάνατζερ Μάρκετινγκ Προϊόντος" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Μάνατζερ Δημοσίων Σχέσεων" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Βοηθός Μάνατζερ Προϊόντος" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Ο Κύκλος των Χιλίων" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tΔεν πουλήθηκαν ψυχές κατά την ανάπτυξη του παιχνιδιού." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "OK" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Επιλέξτε Κλάση" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Άκυρο" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Νέος Ήρωας για τετραπλό" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Νέος Ήρωας για μονό" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Το αρχείο παιχνιδιού Υπάρχει ήδη" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Φόρτωση Παιχνιδιού" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Νέο Παιχνίδι" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Ήρωες για Μονό" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Το Κλεφτρόνι και ο Μάγος είναι μόνο διαθέσιμοι στην πλήρης εμπορική έκδοση του " "Ντιάμπλο. Επισκεφτείτε https://www.gog.com/game/diablo για να το αγοράσετε." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Εισάγετε Όνομα" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Άκυρο όνομα. Ένα όνομα δεν μπορεί να περιλαμβάνει κενά, ειδικούς χαρακτήρες ή " "λέξεις\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Ανικανότητα δημιουργίας ήρωα." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Επίπεδό:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Δύναμη:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Μαγεία:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Επιδεξιότητα:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Ζωτικότητα:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Αρχείο Παιχνιδιού:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "ΔΙΑΛΕΞΕ ΗΡΩΑ" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Νέος Ήρωας" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Διαγ" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Ήρωες για Τετραπλό" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Διαγραφή Ήρωα για Τετραπλό" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Διαγραφή Ήρωα για Μονό" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Είσαι σίγουρος ότι θες να διαγράψεις τον ήρωα \"{:s}\";" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Ατομικό Παιχνίδι" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Τετραπλό Παιχνίδι" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Ρυθμίσεις" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Υποστήρηξη" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Συντελεστές" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Έξοδος" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Έξοδος" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Δωρεάν Έκδοση" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Διακομιστή-πελάτη (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 #, fuzzy msgid "Offline" msgstr "Βρόχος επιστροφής" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Τετραπλό Παιχνίδι" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Απαιτήσεις:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "χωρίς πύλη" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Σύνδεση" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Αλλαγή Πύλης" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Όλοι οι υπολογιστές πρέπει να είναι συνδεδεμένοι σε TCP-συμβατό δίκτυο." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Όλοι οι υπολογιστές πρέπει να είναι συνδεδεμένοι στο διαδίκτυο." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Παίξε μόνος χωρίς έκθεση σε κάποιο δίκτυο." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Mέγιστοι πάικτες: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Ντιάμπλο" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Ντιάμπλο Διαμοιραζόμενή Έκδοση" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Χελφαϊερ" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Χελφαϊερ Διαμοιραζόμενή Έκδοση" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "Ο εξυπηρετητής τρέχει διαφορετικό παιχνίδι σε σχέση με σένα." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "Ο εξυπηρετητής τρέχει διαφορετικό παιχνίδι ({:s}) σε σχέση με σένα." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "" "Η έκδοση του λογισμικού σου {:s} είναι διαφορετική από τον εξυπηρετητή {:d}." "{:d}.{:d}." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Περιγραφή:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Επιλέξτε Δράση" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Νέο Παιχνίδι" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Νέο Ανοικτό Παιχνίδι" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Είσοδος σε Παιχνίδι" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Ανοικτά Παιχνίδια" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Φόρτωση..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 Source/panels/charpanel.cpp:142 msgid "None" msgstr "Κανένα" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "Άκυρο" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Δημιούργησε νέο παιχνίδι με δυσκολία της επιλογής σας." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" "Δημιούργησε νέο δημόσιο παιχνίδι που οποιοσδήποτε μπορεί να μπει με δυσκολία " "της επιλογής σας." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "" "Εισάγετε ένα Αριθμό Ταυτότητας Παιχνιδιού (ID) για να εισέλθετε στο παιχνίδι " "που είναι σε εξέλιξη." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "" "Εισάγετε μια IP ή ένα όνομα εξυπηρετητή για να εισέλθετε στο παιχνίδι που " "είναι σε εξέλιξη." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "Εισέλθετε σε υπάρχων δημόσια ανοιχτό παιχνίδι που είναι σε εξέλιξη." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Κανονικό" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Εφιάλτης" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Κόλαση" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Δυσκολία: {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Ταχύτητα: Κανονική" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Ταχύτητα: Γρήγορη" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Ταχύτητα: Γρηγορότερη" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Ταχύτητα: Ταχύτατη" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Παίκτες: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Δυσκολία" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Είσοδος σε {:s} παιχνίδια" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Εισάγετε Αριθμό Ταυτότητας Παιχνιδιού (ID)" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Διεύθυνση" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Κανονική Δυσκολία\n" "Για την αρχή ενός νέου ήρωα που είναι στην αποστολή να νικήσει τον Ντιάμπλο." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Εφιαλτική Δυσκολία\n" "Οι κάτοικοι του Λαβύρινθου έχουν ενδυναμωθεί και είναι πολύ πιο δύσκολο να " "τους νικήσεις. Προτεινόμενο για έμπειρους." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into Hell. " "Only the most experienced characters should venture in this realm." msgstr "" "Κολασμένη Δυσκολία\n" "Τα πιο δυνατά πλάσματα του κάτω κόσμου σε περιμένουν στην πύλη μέσα στην " "Κόλαση. Μόνο για τους πιο έμπειρους." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game of " "Nightmare difficulty." msgstr "" "Ο ήρωας σου πρέπει να φτάσει στο επίπεδο 20 για να μπορέσεις να μπεις σε " "παιχνίδι πολλαπλών παικτών Εφιαλτικής δυσκολίας." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game of " "Hell difficulty." msgstr "" "Ο ήρωας σου πρέπει να φτάσει στο επίπεδο 30 για να μπορέσεις να μπεις σε " "παιχνίδι πολλαπλών παικτών Κολασμένης δυσκολίας." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Ταχύτητα" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Γρήγορο" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "Γρηγορότερο" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Ταχύτατο" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Κανονική Ταχύτητα\n" "Για την αρχή ενός νέου ήρωα που είναι στην αποστολή να νικήσει τον Ντιάμπλο." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Γρήγορη Ταχύτητα\n" "Οι κάτοικοι του Λαβύρινθου έχουν επιταχυνθεί και είναι πολύ πιο δύσκολο να " "τους νικήσεις. Προτεινόμενο για έμπειρους." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. Only " "an experienced champion should try their luck at this speed." msgstr "" "Γρηγορότερη Ταχύτητα\n" "Τα τέρατα του μπουντρουμιού θα σε αναζητήσουν πιο γρήγορα από ποτέ. Μόνο για " "πολύ έμπειρους." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Ταχύτατη Ταχύτητα\n" "Οι υποτακτικοί του κάτω κόσμου θα τρέξουν αστραπιαία να σου επιτεθούν χωρίς " "δισταγμό. Μόνο για δαίμονες της ταχύτητας." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Εισάγετε Κωδικό" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Εισαγωγή στο Χελφάϊερ" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Αλλαγή στο Ντιάμπλο" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Ναι" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Όχι" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "Πατήστε οποιοδήποτε πλήκτρο στο χειριστήριο για αλλαγή." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Δεσμευμένο πλήκτρο:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Πατήστε οποιοδήποτε πλήκτρο για αλλαγή." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Αποδέσμευσή πλήκτρου" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "Δεσμευμένο πλήκτρο συνδυασμένης κίνησης:" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "Άποδεσμευμένο πλήκτρο συνδιασμένης κίνησης" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Προηγούμενο Μενού" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Διατηρούμε εξυπηρετητή για συνομιλίες στο Discord.gg/devilutionx Ακολουθήστε " "τους συνδέσμους για να μπείτε στην κοινότητα μας όπου μιλάμε για θέματα " "σχετικά με το Ντιάμπλο, και την προέκταση του το Χελφαϊερ." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, and " "the nature of the problem." msgstr "" "Το DevilutionX το συντηρεί η ομάδα Diasurgical, Μπορείτε να μας ειδοποιήσετε " "για θέματα και σφάλματα σε αυτήν την διεύθυνση: https://github.com/diasurgical/" "devilutionX Για να μας βοηθήσετε να σας εξυπηρετήσουμε, παρακαλούμε να μας " "πείτε τον αριθμό έκδοσης, το λειτουργικό σας σύστημα/πλατφόρμα και την φύση " "του προβλήματος." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Αποποίηση Ευθύνης:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tΤο DevilutionX δεν υποστηρίζεται ή αναπτύσσεται από την Blizzard " "Entertainment, ή από το GOG.com. Ούτε η Blizzard Entertainment ή το GOG.com " "έχει τεστάρει ή πιστοποιήσει την ποιότητα ή την συμβατότητα του DevilutionX. " "Όλα τα ερωτήματα και τα αιτήματα σας σχετικά με το DevilutionX πρέπει να " "απευθυνθούν στον Diasurgical, και όχι στην Blizzard Entertainment ή στο " "GOG.com." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji which " "is licensed under CC-BY 4.0. The port also makes use of SDL which is licensed " "under the zlib-license. See the ReadMe for further details." msgstr "" "\tΤο παρών λογισμικό χρησιμοποιεί την γραμματοσειρες Charis SIL, New Athena " "Unicode, Unifont, και Noto που είναι άδειοδιτημένες κάτω από την Ανοικτή Άδεια " "Γραμματοσοειρών SIL, καθώς και την γραμματοσειρά Twitmoji, που είναι " "αδειοδοτημένη κάτω από το CC-BY 4.0. Το λογισμικό επίσης χρησιμοποιεί το SDL " "που είναι αδειοδοτημένο κάτω από την άδεια zlib. Δείτε το ReadMe για περεταίρω " "λεπτομέρειες." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Copyright © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Σφάλμα" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Το σφάλμα έλαβε χώρα στο: {:s} γραμμή {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Σφάλμα στο αρχείο δεδομένων" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Το κύριο αρχείο δεδομένων ({:s}) δεν βρέθηκε.\n" "\n" "Βεβαιωθείτε ότι βρίσκετε το φάκελο του παιχνιδιού." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Σφάλμα Καταλόγου Μόνο για Ανάγνωση" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Αδυναμία εγγραφής στην τοποθεσία:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Παιχνίδι: " #: Source/automap.cpp:1424 #, fuzzy msgid "Offline Game" msgstr "Βρόχος επιστροφής" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Κωδικός: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Δημόσιο Παιχνίδι" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Επίπεδο: Φωλιά {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Επίπεδο: Κρύπτη {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Πόλη" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Επίπεδο: {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Tab" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Enter" #: Source/control.cpp:206 msgid "Character Information" msgstr "Πληροφορίες Ήρωα" #: Source/control.cpp:207 msgid "Quests log" msgstr "Βιβλίο ταξιδιού" #: Source/control.cpp:208 msgid "Automap" msgstr "Χάρτης" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Κυρίως Μενού" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Αντικείμενα" #: Source/control.cpp:211 msgid "Spell book" msgstr "Βιβλίο με Ξόρκια" #: Source/control.cpp:212 msgid "Send Message" msgstr "Αποστολή Μυνήματος" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "Διαθέσιμες Εντολές:" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "Η Εντολή " #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr " είναι άγνωστη." #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "Περιγραφή: " #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" "\n" "Παράμετροι: Καμία επιπλέον παράμετρος δεν χρειάζεται." #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" "\n" "Παράμετροι: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "Οι Αρένες υποστηρίζονται μόνο στο τετραπλό παιχνίδι." #: Source/control.cpp:653 msgid "What arena do you want to visit?" msgstr "Ποια αρένα θέλεις να επισκεφτείς;" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "Άκυρος αριθμός αρένας. Έγκυροι αριθμοί είναι:" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "" "Για να εισέλθεις σε μια αρένα, πρέπει να είσαι στην πόλη ή σε μια άλλη αρένα." #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "H επιθεώρηση παίκτη υποστηρίζεται μόνο στο τετραπλό παιχνίδι." #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "Λήξη επιθεώρησης παίκτη." #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "Δεν βρέθηκε παίκτης με τέτοιο όνομα" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "Επιθεώρηση παίκτη: " #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "Εμφανίζει γενική βοήθεια, η βοήθεια για συγκεκριμένη εντολή." #: Source/control.cpp:800 #, fuzzy #| msgid "Command " msgid "[command]" msgstr "Η Εντολή " #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "Εισαγωγή σε PvP Αρένα." #: Source/control.cpp:801 #, fuzzy #| msgid "{arena-number}" msgid "" msgstr "{arena-number}" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "Σου δίνει φιλτρά αρένας." #: Source/control.cpp:802 #, fuzzy #| msgid "{number}" msgid "" msgstr "{number}" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "Επιθεώρηση στατιστικών και αντικειμένων ενός άλλου παίκτη." #: Source/control.cpp:803 msgid "" msgstr "<όνομα παίκτη>" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "Εμφανίζει τον σπόρο παραγωγής του τρέχοντος επιπέδου." #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Φιλικός προς Παίκτες" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Εχθρικός προς Παίκτες" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Πλήκτρο: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Επιλογή ξορκιού για ενεργοποίηση" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Πλήκτρο: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "Δεξιότητα \"{:s}\"" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "Ξόρκι \"{:s}\"" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Επίπεδο Ξορκιού 0 - Άχρηστο" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Επίπεδο Ξορκιού {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "Πάπυρος \"{:s}\"" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Πάπυρος" msgstr[1] "{:d} Πάπυροι" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "Ράβδος {:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Φορτίο" msgstr[1] "{:d} Φορτία" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} χρυσό νόμισμα" msgstr[1] "{:s} χρυσά νομίσματα" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Απαιτήσεις δεν έχουν εκπληρωθεί" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Επίπεδο: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Πόντοι Ζωής {:d} από {:d}" #: Source/control.cpp:1525 #, fuzzy #| msgid "Right-click to use" msgid "Right click to inspect" msgstr "Δεξί-κλικ για χρήση" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Νέο Επίπεδο" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Έχεις {:s} χρυσό νόμισμα. Πόσα θέλεις να αφαιρέσεις;" msgstr[1] "Έχεις {:s} χρυσά νομίσματα. Πόσα θέλεις να αφαιρέσεις;" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Πύλη Πόλης" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "από {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Πύλη πρός" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "Ο Ανίερος Βωμός" #: Source/cursor.cpp:636 msgid "level 15" msgstr "επίπεδο 15" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, fuzzy, c++-format #| msgid "Unable to load character" msgid "Unable to load data from file {0}" msgstr "Αποτυχία κατά την φόρτωση χαρακτήρα" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "" #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Εμφάνιση αυτού του μηνύματος και έξοδος" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Εμφάνιση της έκδοσης και έξοδος" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Ορισμός φακέλου για το diabdat.mpq" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Ορισμός φακέλου για τα αποθηκευμένα παιχνίδια" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Ορισμός φακέλου για το diablo.ini" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "Ορισμός κώδικα γλώσσας (π.χ. en ή pt_BR)" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Παράλειψη αρχικών βίντεο" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Προβολή αριθμού καρέ ανά δεπτερόλεπτο" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "Ενεργοποίηση λεπτομερών εγγραφών" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Εγγραφή αρχείου δράσης" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Αναπαραγωγή αρχείου δράσης" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Απενεργοποίηση ορίων καρέ κατά την αναπαραγωγή αρχείου δράσης" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Επιλογή παιχνιδιού:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Εξαναγκαστική κατάστασή Δοκιμαστικής έκδοσης" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Εξαναγκαστική κατάστασή σε Diablo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Εξαναγκαστική κατάστασή σε Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Επιλογές Χελφάϊερ:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "" "Αναφορά σφαλμάτων στην σελίδα https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" "Παρακαλώ ενημερώστε τα αρχεία devilutionx.mpq και fonts.mpq στην τελευταία " "τους έκδοση" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Αποτυχία φόρτωσης πόρων για το UI.\n" "\n" "Βεβαιωθείτε ότι το αρχείο devilutionx.mpq είναι στον φάκελο του παιχνιδιού και " "ότι είναι ενημερωμένο." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "Παρακαλώ ενημερώστε το αρχείο fonts.mpq στην τελευταία του έκδοση" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Τέλος χρονικού ορίου Δικτύου --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Αναμονή για παίκτες --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "Δεν υπάρχει διαθέσιμη βοήθεια" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "όσο βρίσκεσαι σε μαγαζί" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Αντικείμενο Ζώνης {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Χρήση αντικειμένου Ζώνης." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Γρήγορο ξόρκι {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Πλήκτρο για δεξιότητα ή ξόρκι." #: Source/diablo.cpp:1809 #, fuzzy #| msgid "Previous Menu" msgid "Previous quick spell" msgstr "Προηγούμενο Μενού" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1817 #, fuzzy #| msgid "Quick spell {}" msgid "Next quick spell" msgstr "Γρήγορο ξόρκι {}" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "" #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "Χρήση φίλτρου ζωής" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "Χρήση φίλτρου ζωής απο την ζώνη." #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "Χρήση φίλτρου μάνα" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "Χρήση φίλτρου μάνα απο την ζώνη." #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Γρήγορο Βιβλίου" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Άνοιγμα Γρήγορου Βιβλίου." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Άμεση αποθήκευση" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Αποθηκεύει το παιχνίδι." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Άμεση φώρτοση" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Φορτώνει το παιχνίδι." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Έξοδος παιχνιδιού" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Κλείνει το παιχνίδι." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Σταμάτημα ήρωα" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Σταματά το περπάτημα και ακυρώνει δράσεις που υπολείπονται." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Επισήμανση αντικειμένων" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Εμφανίζει ή κρύβει τα αντικείμενα στο έδαφος." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Εναλλαγή επισήμανσης αντικείμενων" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Μόνιμη εμφάνιση/απόκρυψη αντικειμένων στο έδαφος." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Εναλλαγή αυτόματου χάρτη" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Εναλλαγή εμφάνισης του αυτόματου χάρτη." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Ανοίγει την καρτέλα Αντικειμένων (σάκος)." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Ήρωας" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Ανοίγει την καρτέλα του ήρωα." #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Ιστορία" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Ανοίγει την καρτέλα με τις Αποστολές της Ιστορίας." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Ξόρκια" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Ανοίγει την Καρτέλα του Βιβλίου με τα Ξόρκια." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Άμεσο Μήνυμα {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Χρήση για άμεσο μήνυμα στην συνομιλία." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Κλείσιμο των καρτελών πληροφορίας" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Κλείσιμο όλων των καρτελών." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Μεγένθυση" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Μεγεθύνει την προβολή του παιχνιδιού." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Παύση Παιχνιδιού" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Σταματάει την δράση στο παιχνίδι." #: Source/diablo.cpp:2004 #, fuzzy #| msgid "Pause Game" msgid "Pause Game (Alternate)" msgstr "Παύση Παιχνιδιού" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy #| msgid "Increase screen brightness." msgid "Decrease Brightness" msgstr "Αύξηση φωτεινότητας." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Μείωση φωτεινότητας." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy #| msgid "Increase screen brightness." msgid "Increase Brightness" msgstr "Αύξηση φωτεινότητας." #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Αύξηση φωτεινότητας." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Βοήθεια" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Ανοίγει την Καρτέλα Βοήθειας." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Στιγμιότυπο οθόνης" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Λαμβάνει και αποθηκεύει την οθόνη σαν εικόνα." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Πληροφορίες Παιχνιδιού" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Εμφανίζει τις πληροφορίες του παιχνιδιού σε εξέλιξη." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Συνομιλίες" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Εμφανίζει τις συνομιλίες." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 #, fuzzy #| msgid "Inventory" msgid "Sort Inventory" msgstr "Αντικείμενα" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "" #: Source/diablo.cpp:2072 msgid "Console" msgstr "" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "" #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "Πρωτογενή δράση" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" "Επίθεση σε τέρας, ομιλία σε κάτοικο πόλης, μάζεμα και τοποθέτηση αντικειμένων " "στο σάκο." #: Source/diablo.cpp:2144 msgid "Secondary action" msgstr "Δευτερογενής δράση" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "Άνοιγμα σεντουκιών, αλληλεπίδραση με πόρτες, μάζεμα αντικειμένων." #: Source/diablo.cpp:2159 msgid "Spell action" msgstr "Δράση ξορκιού" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "Ρίψη του προεπιλεγμένου ξορκιού." #: Source/diablo.cpp:2174 msgid "Cancel action" msgstr "Ακύρωση δράσης" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "Κλείνει τα ανοιχτά μενού." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "Κίνηση πάνω" #: Source/diablo.cpp:2201 msgid "Moves the player character up." msgstr "Μετακίνηση του ήρωα προς τα πάνω." #: Source/diablo.cpp:2206 msgid "Move down" msgstr "Κίνηση κάτω" #: Source/diablo.cpp:2207 msgid "Moves the player character down." msgstr "Μετακίνηση του ήρωα προς τα κάτω." #: Source/diablo.cpp:2212 msgid "Move left" msgstr "Κίνηση αριστερα" #: Source/diablo.cpp:2213 msgid "Moves the player character left." msgstr "Μετακίνηση του ήρωα προς τα αριστερά." #: Source/diablo.cpp:2218 msgid "Move right" msgstr "Κίνηση δεξιά" #: Source/diablo.cpp:2219 msgid "Moves the player character right." msgstr "Μετακίνηση του ήρωα προς τα δεξιά." #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "Παρέμεινε ακίνητος" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "Κράτα το για να αποτρέψεις κίνηση του ήρωα." #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "Διακόπτης εναλλαγής κατάστασης ακινησίας." #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "Διακόπτης ορισμού ακινησίας ήρωα." #: Source/diablo.cpp:2310 #, fuzzy #| msgid "Automap" msgid "Automap Move Up" msgstr "Χάρτης" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 #, fuzzy #| msgid "Move down" msgid "Automap Move Down" msgstr "Κίνηση κάτω" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 #, fuzzy #| msgid "Move left" msgid "Automap Move Left" msgstr "Κίνηση αριστερα" #: Source/diablo.cpp:2323 #, fuzzy #| msgid "Moves the player character up." msgid "Moves the automap left when active." msgstr "Μετακίνηση του ήρωα προς τα πάνω." #: Source/diablo.cpp:2328 #, fuzzy #| msgid "Move right" msgid "Automap Move Right" msgstr "Κίνηση δεξιά" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "Κίνηση ποντικιού πάνω" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "Εξομοιώνει την προς τα πάνω κίνηση του ποντικού." #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "Κίνηση ποντικιού κάτω" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "Εξομοιώνει την προς τα κάτω κίνηση του ποντικού." #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "Κίνηση ποντικιού αριστερά" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "Εξομοιώνει την προς τα αριστερά κίνηση του ποντικού." #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "Κίνηση ποντικιού δεξιά" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "Εξομοιώνει την προς τα δεξιά κίνηση του ποντικού." #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "Αριστερό κλικ ποντικιού" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "Εξομοιώνει το αριστερό πάτημα πλήκτρου στο ποντίκι." #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "Δεξί κλικ ποντικιού" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "Εξομοιώνει το δεξί πάτημα πλήκτρου στο ποντίκι." #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "Μενού γρήγορου ξορκιού χειριστηρίου" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "Κράτα το πατημένο για να ορίσεις ή να χρησιμοποιήσεις κουμπιά ξορκιών." #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "Μενού πλοήγησης χειριστηρίου" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "Κράτα το πατημένο για να εμφανιστεί το μενού πλοήγησης του χειριστηρίου." #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 msgid "Toggle game menu" msgstr "Διακόπτης μενού παιχνιδιού" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 msgid "Opens the game menu." msgstr "Ανοίγει το κυρίως μενού του παιχνιδιού." #: Source/diablo_msg.cpp:63 #, fuzzy #| msgctxt "spell" #| msgid "Flame Wave" msgid "Game saved" msgstr "Κύμα Φωτιάς" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "Το δωρεάν παιχνίδι δεν επιτρέπει λειτουργίες για πολλούς παίκτες" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Αποτυχία δημιουργίας υποσυστήματος Direct Sound" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "Δεν είναι διαθέσιμο στην δωρεάν έκδοση" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "Δεν υπάρχει αρκετός ελεύθερος χώρος για αποθήκευση" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Δεν επιτρέπεται η Παύση στην Πόλη" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Προτείνουμε να αντιγράψετε το αρχείο στον σκληρό δίσκο" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Πρόβλημα συγχρονισμού τετραπλού παιχνιδιού" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Δεν επιτρέπεται η Παύση στο τετραπλό" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Αποθήκευση..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Κάποια αδυνατούν καθώς ένα δυναμώνει πολύ" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Νέα δύναμη σφυρηλατείται μέσα από την καταστροφή" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Όσοι αμύνονται σπάνια επιτίθενται" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "Το σπαθί της δικαιοσύνης είναι γρήγορο και κοφτερό" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Όταν το πνεύμα είναι άγρυπνο, το σώμα ευδοκιμάζει" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Η δύναμη του μάνα εστιασμένη ανανεώνει" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "Ο χρόνος δεν μπορεί να εξασθενίσει την δύναμη του ατσαλιού" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "Η μαγεία δεν είναι πάντα αυτό που φαίνεται" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Ότι ήταν κάποτε ανοικτό είναι πάλι κλειστό" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "Η σφοδρότητα έρχεται με κόστος την σοφία" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "Η απόκρυφη δύναμη φέρνει καταστροφή" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Αυτό που δεν πιάνεται δεν μπορεί να βλαφθεί" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Ερυθρό και κυανό γίνονται σαν τον ήλιο" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Γνώση και σοφία με κόστος ο εαυτό σου" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Πιές και αναζωογονήσου" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Όπου και αν πας, εκεί είσαι" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "Η ενέργεια έρχεται με κόστος την σοφία" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Βρίθεις από πλούτη εκεί που δεν το περιμένεις" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "Όταν η φιλαργυρία αποτυγχάνει, η υπομονή προσφέρει αμοιβή" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "Ένας καλοκάγαθος σύντροφός σου, σε ευλόγησε!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Τα χέρια των ανθρώπων καθοδηγούνται από την μοίρα" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "Η δύναμη ενισχύεται με την ουράνια πίστη" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "Η ουσία της ζωής ρέει εκ των έσω" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "Το μονοπάτι είναι ξεκάθαρο όταν το δείς από ψηλά" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "Η σωτήρια έρχεται με κόστος την σοφία" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Μυστήρια αποκαλύπτονται στο φώς της λογικής" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Αυτοί που είναι τελευταίοι, μπορεί τελικά τα γίνουν πρώτοι" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "Η γενναιοδωρία φέρνει τις δικές της αμοιβές" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Πρέπει να είσαι το λιγότερο επίπεδο 8 για να χρησιμοποιήσεις αυτό." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Πρέπει να είσαι το λιγότερο επίπεδο 13 για να χρησιμοποιήσεις αυτό." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Πρέπει να είσαι το λιγότερο επίπεδο 17 για να χρησιμοποιήσεις αυτό." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "Απόκτησες απόκρυφη γνώση!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "Ότι δεν σε σκοτώνει..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "Η γνώση είναι δύναμη." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Δώσε και θα λάβεις." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Κάποια εμπειρία αποκτάται με το άγγιγμα." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "Σπίτι μου, σπιτάκι μου και φτωχοκαλυβάκι μου." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Η πνευματική σου ενέργεια αποκαθίσταται." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Αισθάνεσαι πιο ευκίνητος." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Αισθάνεσαι πιο δυνατός." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Αισθάνεσαι πιο σοφός." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Αισθάνεσαι ανανεωμένος." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "Ότι σπάει θα σπάσει." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Καθεδρικός Ναός" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Κατακόμβες" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Σπήλαια" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Φωλιά" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Κρύπτη" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Επ. {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "Δυσκολία {}" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "Στο Μενού" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "βρόχος επιστροφής" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "Αδυναμία σύνδεσης" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "σφάλμα: ανάγνωση 0 bytes από τον εξυπηρετητή" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq ή spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Μερικά αρχεία MPQ του Χελφαϊερ, λείπουν" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Δεν βρέθηκαν όλα τα αρχεία MPQ του Χελφαϊερ.\n" "Παρακαλούμε αντιγράψτε όλα τα hf*.mpq αρχεία." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Ανάλυση" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Τρέξιμο στην Πόλη" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "Αποστολή του Θέο" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "Αποστολή Αγελάδας" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Αυτόματο Μάζεμα Χρυσού" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Αυτόματο Μάζεμα Ελιξιρίων" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 msgid "Auto Oil Pickup" msgstr "Αυτόματο Μάζεμα Λαδιών" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Αυτόματο Μάζεμα στην Πόλη" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Η Άντρια αναπληρώνει Μάνα" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Αυτόματος Εξοπλισμός Όπλων" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Αυτόματος Εξοπλισμός Πανοπλίας" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Αυτόματος Εξοπλισμός Κράνους" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Αυτόματος Εξοπλισμός Ασπίδας" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Αυτόματος Εξοπλισμός Χρυσαφικών" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Τυχαίες Αποστολές" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "Εμφάνιση Ετικετών Αντικειμένων" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Αυτόματο Γέμισμα Ζώνης" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Απενεργοποίηση Βλαβερών Βωμών" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Μάζεμα Φίλτρων Θεραπείας" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Μάζεμα Φίλτρων Πλήρους Θεραπείας" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Μάζεμα Φίλτρου Μάνα" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Μάζεμα Φίλτρου Πλήρωσης Μάνα" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Μάζεμα Φίλτρου Αναζωογόνησης" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Μάζεμα Φίλτρου Πλήρους Αναζωογόνησης" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Επιλογές" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Σώσιμο Παιχνιδιού" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy #| msgid "Main Menu" msgid "Exit to Main Menu" msgstr "Κυρίως Μενού" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Έξοδος Παχνιδιού" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Γάμα" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Ταχύτητα" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Μουσική Απενεργοποιημένη" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Ήχος" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Ήχος Απενεργοποιημένος" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Πάυση" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Συντομεύσεις Πληκτρολογίου:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Άνοιγμα Καρτέλας Βοήθειας" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Εμφάνιση Κεντρικού Μενού" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Tab: Εμφάνιση Χάρτη" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Space: Κλείσιμο όλων των καρτελών" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Άνοιγμα Γρήγορου Βιβλίου" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Άνοιγμα Βιβλίου με Ξόρκια" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Άνοιγμα καρτέλας Αντικειμένων (σάκος)" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Άνοιγμα καρτέλας Ήρωα" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "Q: Άνοιγμα καρτέλας Αποστολών (ιστορία)" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Μείωση φωτεινότητας οθόνης" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "F: Αύξηση φωτεινότητας οθόνης" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Μεγέθυνση Οθόνης Παιχνιδιού" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Αυξομείωση Μεγέθυνσης Χάρτη" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Χρήση αντικειμένου Ζώνης" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Ορισμός πλήκτρου για άμεσο ξόρκι ή δεξιότητα" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Αριστερό Κουμπί Ποντικιού: Επίθεση χωρίς κίνηση" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Αριστερό Κουμπί Ποντικιού (στην καρτέλα ήρωα): Τοποθέτηση όλων των " "ελεύθερων πόντων" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Αριστερό Κουμπί Ποντικιού (στην καρτέλα αντικειμένων): Μετακίνηση " "αντικειμένου στην ζώνη ή εξοπλισμός/ανεξοπλισμός του" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "" "Shift + Αριστερό Κουμπί Ποντικιού (στην ζώνη): Μετακίνηση αντικειμένου στον " "σάκο" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Κίνηση:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue to " "move in that direction." msgstr "" "Εάν κρατάτε το κουμπί του ποντικιού πατημένο καθώς κινήστε, ο ήρωας θα " "συνεχίσει να κινείται προς την κατεύθυνση αυτή." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Μάχη:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Κρατώντας πατημένο το πλήκτρο shift και κάνοντας αριστερό κλικ επιτρέπει τον " "ήρωα σας να επιτίθεται χωρίς να κινείται." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Χάρτης:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or press " "'TAB' on the keyboard. Zooming in and out of the map is done with the + and - " "keys. Scrolling the map uses the arrow keys." msgstr "" "Για να εμφανίσετε τον αυτόματο χάρτη, κάντε κλικ στο κουμπί 'χάρτης' στην " "μπάρα πληροφορίων κάτω ή πατήστε το πλήκτρο 'TAB' στο πληκτρολόγιο. Μεγέθυνση " "και σμίκρυνση μπορείτε να κάνετε με τα + και - πλήκτρα. Ανακύλιση τού χάρτη " "γίνεται με τα πλήκτρα με βελάκια." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Μαζεύοντας Αντικείμενα:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface bar . " "When an item is placed in the belt, a small number appears in that box. Items " "may be used by either pressing the corresponding number or right-clicking on " "the item." msgstr "" "Μικρά αντικείμενα μιας χρήσης όπως φίλτρα και πάπυροι, αυτόματα τοποθετούνται " "στην 'ζώνη', στην κορυφή της μπάρας πληροφοριών. Όταν ένα αντικείμενο είναι " "στην ζώνη, ένα μικρό νούμερο εμφανίζεται στο χώρο του. Μπορείτε να " "χρησιμοποιείται αυτά τα αντικείμενα πατώντας το πλήκτρο με τον αντίστοιχο " "αριθμό, ή κάνοντας δεξί κλικ στο αντικείμενο." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Χρυσός:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Μπορείς να διαλέξεις συγκεκριμένο ποσό χρυσού για να ρίξεις, κάνοντας δεξί " "κλικ σε έναν σωρό χρυσού στον σάκο σου." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Δεξιότητες & Ξόρκια:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the 'SPELLS' " "button in the interface bar. Memorized spells and those available through " "staffs are listed here. Left-clicking on the spell you wish to cast will ready " "the spell. A readied spell may be cast by simply right-clicking in the play " "area." msgstr "" "Μπορείς να έχεις πρόσβαση στην λίστα των ξορκιών και δεξιοτήτων σου κάνοντας " "αριστερό κλικ στο κουμπί 'ξόρκια' στην μπάρα πληροφοριών κάτω. Απομνημονευμένα " "ξόρκια και ξόρκια από ράβδους εμφανίζονται εδώ. Με αριστερό κλικ στο ξόρκι που " "θες να ρίξεις το ετοιμάζει κάτω δεξιά. Για να ρίξεις ένα προετοιμασμένο ξόρκι " "ή δεξιότητα απλά κάνε δεξί κλικ στον χώρο παιξίματος." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Χρησιμοποιόντας το γρήγορο βιβλίο για ξόρκια:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Με αριστερό κλικ στο κουμπί με το 'ετοιμασμένο ξόρκι' ανοίγει το γρήγορο " "βιβλίο το οποίο επιτρέπει την επιλογή ενός ξορκιού ή δεξιότητας για άμεση " "χρήση. Για να χρησιμοποιήσετε το ξόρκι ή την δεξιότητα απλά κάντε δεξί κλικ " "στον χώρο παιξίματος." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Shift + αριστερό κλικ στο κουμπί με το 'ετοιμασμένο ξόρκι' θα καθαρίσει το " "ετοιμασμένο ξόρκι." #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Ορίζοντας πλήκτρα για ξόρκια:" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Μπορείς να ορίσεις μέχρι τέσσερα πλήκτρα για δεξιότητες, ξόρκια ή παπύρους. " "Άνοιξε το γρήγορο βιβλίο όπως περιγράφεται στην ενότητα πάνω και πάτησε το F5, " "F6, F7 ή F8 πλήκτρο, αφού 'αγγίξεις' με τον δείκτη ποντικιού το ξόρκι που θες." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Βιβλία Ξορκιών:" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Διαβάζοντας παραπάνω από ένα βιβλίο σου αυξάνει την γνώση σου για το ξόρκι " "αυτό και σου επιτρέπει να το ρίχνεις πιο αποδοτικά." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Βοήθεια Δωρεάν έκδοσης Χελβαϊερ" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Βοήθεια Χελβαϊερ" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Βοήθεια Δωρεάν έκδοσης Ντιάμπλο" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Βοήθεια Ντιάμπλο" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Πατήστε ESC για κλείσιμο ή τα πλήκτρα με βελάκια για ανακύλιση." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "Ανικανότητα δημιουργίας του κυρίως παραθύρου" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "Δεν έχεις χώρο για το αντικείμενο" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Λάδι Ακρίβειας" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Λάδι της Μαεστρίας" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Λάδι Οξύτητας" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Λάδι του Θανάτου" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Λάδο της Δεξιότητας" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Λάδι του Σηδιρουργού" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Λάδι της Αντοχής" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Λάδι της Μονιμότητας" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Λάδι της Σκλήρυνσης" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Λάδι της Αδιαπερατότητας" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} με \"{1}\"" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{1} {0} με \"{2}\"" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{1} {0} και {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{1} {0}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} {1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "αυξάνει την πιθανότητα" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "ενός όπλου να χτυπήσει" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "αυξάνει κατά πολύ την πιθανότητα" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "ενός όπλου να χτυπήσει" #: Source/items.cpp:1652 msgid "damage potential" msgstr "δυνητικότητα ζημιάς" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "αυξάνει κατά πολύ την δυνητοκότητα" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "ενός όπλου για ζημιά - όχι σε τόξα" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "μειώνει τα απαιτούμενα στατιστικά" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "για να εξοπλίσεις όπλα και πανοπλίες" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "αποκαθιστά 20% της" #: Source/items.cpp:1664 msgid "item's durability" msgstr "ανθεκτικότητας ενος αντικειμένου" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "αυξάνει την μέγιστη και τρέχουσα" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "ανθεκτικότητα ενός αντικειμένου" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "κάνει ένα αντικείμενο άφθαρτο" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "αυξάνει την κλάση θωράκισης" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "μιας πανοπλίας ή ασπίδας" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "αυξάνει κατά πολύ την κλάση θωρακισης" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "μιας πανοπλίας ή ασπίδας" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "τοποθέτηση παγίδας φωτιας" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "τοποθέτηση παγίδας κεραυνού" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "τοποθέτηση παγίδας πέτρωσης" #: Source/items.cpp:1695 msgid "restore all life" msgstr "ολική αναπλήρωση της ζωής" #: Source/items.cpp:1698 msgid "restore some life" msgstr "μερική αναπλήρωση της ζωής" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "μερική αναπλήρωση των μάνα" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "ολική αναπλήρωση των μάνα" #: Source/items.cpp:1707 msgid "increase strength" msgstr "αυξάνει την δύναμη" #: Source/items.cpp:1710 msgid "increase magic" msgstr "αυξάνει την μαγεία" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "αυξάνει την επιδεξιότητα" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "αυξάνει την ζωτικότητα" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "μερική αναπλήρωση ζωής και μάνα" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "ολική αναπλήρωση ζωής και μάνα" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "(λειτουργεί μόνο στης αρένες)" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Δεξί-κλικ για προβολή" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Δεξί-κλικ για χρήση" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Δεξί-κλικ για διάβασμα, και\n" "αριστερό-κλικ στον στόχο" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Δεξί-κλικ για διάβασμα" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Ενεργοποίηση για προβολή" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Άνοιξε τον σάκο σου για να το χρησιμοποιήσεις" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Ενεργοποίησε το για να το χρησιμοποιήσεις" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Επιλέξτε από το βιβλίο με τα ξόρκια και μετά\n" "κάντε ξόρκι για ανάγνωση" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Ενεργοποίησε το για να το διαβάσεις" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "{} για προβολή" #: Source/items.cpp:1806 #, c++-format msgid "{} to use" msgstr "{} για χρήση" #: Source/items.cpp:1809 #, c++-format msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Επιλέξτε από το βιβλίο με τα ξόρκια,\n" " και μετά {} για ανάγνωση" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "{} για ανάγνωση" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Επίπεδο: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Διπλασιάζει τον αποθηκευτικό χώρο για τον χρυσό" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Απαιτεί:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Δυν" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Μαγ" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Επι" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "Βιβλίο: {:s}" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "Το αυτί του(ης) {:s}" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "πιθανότητα για χτύπημα: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% ζημιά" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "για χτύπημα: {:+d}%, {:+d}% ζημιά" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% θωράκιση" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "κλάση θωράκισης: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Αντοχή Φωτιάς: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Αντοχή Φωτιάς {:+d}% ΜΕΓΙΣΤΟ" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Αντοχή Κεραυνού: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Αντοχή Κεραυνού: {:+d}% ΜΕΓΙΣΤΟ" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Αντοχή Μαγείας: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Αντοχή Μαγείας: {:+d}% ΜΕΓΙΣΤΟ" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Αντοχή Πάντων: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Αντοχή Πάντων: {:+d}% ΜΕΓΙΣΤΟ" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "τα ξόρκια κερδίζουν {:d} επίπεδο" msgstr[1] "τα ξόρκια κερδίζουν {:d} επίπεδα" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "τα ξόρκια μειώνονται {:d} επίπεδο" msgstr[1] "τα ξόρκια μειώνονται {:d} επίπεδα" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "επίπεδα ξορκιών απαράλαχτα (?)" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Επιπλέον φορτία" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} φορτίο" msgstr[1] "{:d} {:s} φορτία" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Ζημιά χτυπήματος φωτιάς {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Ζημιά χτυπήματος φωτιάς {:d}-{:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Ζημιά χτυπήματος κεραυνού {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Ζημιά χτυπήματος κεραυνού {:d}-{:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} στην δύναμη" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} στην μαγεία" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} στην επιδεξιότητα" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} στην ζωτικότητα" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} σε όλα τα στατιστικά" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} ζημιά από εχθρούς" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Πόντοι Ζωής {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Μάνα {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "υψηλή ανθεκτικότητα" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "μειωμένη ανθεκτικότητα" #: Source/items.cpp:3960 msgid "indestructible" msgstr "άφθαρτο" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% εμβέλεια φωτός" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% εμβέλεια φωτός" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "πολλαπλά βέλη ανά βολή" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "ζημιά βέλους φωτιάς {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "ζημιά βέλους φωτιάς {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "ζημιά βέλους Κεραυνού {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "ζημιά βέλους Κεραυνού {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "ζημιά οβίδας φωτιάς: {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "ζημιά οβίδας φωτιάς: {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "ο επιτιθέμενος δέχεται 1-3 ζημιά" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "ο χρήστης χάνει όλα τα μάνα του" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "απορροφά την μισή ζημιά από παγίδες" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "σπρώχνει τον στόχο πίσω" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% ζημιά εναντίων δαιμονων" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Όλες οι Αντοχές γίνονται 0" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "με κτύπο κλέβει 3% μάνα" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "με κτύπο κλέβει 5% μάνα" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "με κτύπο κλέβει 3% ζωή" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "με κτύπο κλέβει 5% ζωή" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "διαπερνά θωράκιση" #: Source/items.cpp:4010 msgid "quick attack" msgstr "ταχεία επίθεση" #: Source/items.cpp:4012 msgid "fast attack" msgstr "γρήγορη επίθεση" #: Source/items.cpp:4014 msgid "faster attack" msgstr "γρηγορότερη επίθεση" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "γρηγορότατη επίθεση" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Άλλη δεξιότητα (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "γρήγορη ανάρρωση από χτύπημα" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "γρηγορότερη ανάρρωση από χτύπημα" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "γρηγορότατη ανάρρωση από χτύπημα" #: Source/items.cpp:4027 msgid "fast block" msgstr "γρήγορο μπλοκάρισμα" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "προσθέτει {:d} πόντο ζημιάς" msgstr[1] "προσθέτει {:d} πόντους ζημιάς" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "ρίχνει βέλη τυχαίας ταχύτητας" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "ασυνήθης ζημιά για αντικείμενο" #: Source/items.cpp:4035 msgid "altered durability" msgstr "αλλαγμένη ανθεκτικότητα" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "μονόχειρο σπαθί" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "χάνεις συνέχεια πόντους ζωής" #: Source/items.cpp:4041 msgid "life stealing" msgstr "κλέβει ζωή" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "καμία απαίτηση δύναμης" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "ζημιά κεραυνού: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "ζημιά κεραυνού: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "ρήψη Φορτισμένων Βλημάτων ανά κτύπημα" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "περιστασιακή τριπλή ζημιά" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "φθορά {:+d}% ζημιά" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x ζημ στους εχθρούς, 1x σε σένα" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Τυχαία ζημιά 0 - 600%" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "χαμηλή ανθ, {:+d}% ζημιά" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "επιπλέον ΚΘ εναντίων δαιμόνων" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "επιπλέον ΚΘ εναντίων απέθαντων" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% των Μάνα πάει στην Ζωή" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% Ζωής πάει στα Μάνα" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "ζημιά: {:d} Άφθαρτο" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "ζημιά: {:d} Ανθ: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "ζημιά: {:d}-{:d} Άφθαρτο" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "ζημιά: {:d}-{:d} Ανθ: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "θωράκιση: {:d} Άφθαρτη" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "θωράκιση: {:d} Ανθ: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Φορτία: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "μοναδικό Αντικείμενο" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Μη Ταυτοποιημένο" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Το Κρησφύγετο του Βασιλιά των Σκελετών" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Την Αίθουσα των Οστών" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Λαβυρινθος" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Η Δηλητηριασμένη Παροχή Νερού" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Το Κρησφύγετο του Αρχιεπισκόπου Λαζάρου" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "Αρένα Εκλησίας" #: Source/levels/setmaps.cpp:33 msgid "Hell Arena" msgstr "Αρένα Κόλασης" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "Αρένα Κύκλου Ζωής" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Κάτω στο Μπουντρούμι" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Κάτω στις Κατακόμβες" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Κάτω στα Σπήλαια" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Κάτω στην Κόλαση" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Κάτω στην Φωλιά" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Κάτω στην Κρύπτη" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Πάνω στο επίπεδο {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Πάνω στην πόλη" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Κάτω στο επίπεδο {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Κάτω στον Ντιάμπλο" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Πάνω στο επίπεδο Φωλιάς {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Πάνω στο επίπεδο Κρύπτης {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Ο Ακρογωνιαίος Λίθος του Κόσμου" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Κάτω στο επίπεδο Κρύπτης {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Πίσω στο Επίπεδο {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Αδυναμία ανοίγματος αρχείου παιχνιδιού" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Εσφαλμένο αρχείο παιχνιδιού" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "Ο παίκτης είναι σε επίπεδο που υπάρχει μόνο στο Χελφαϊερ" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Εσφαλμένη κατάσταση παιχνιδιού" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "Αδυναμία εμφάνισης του κυρίως μενού" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "Ζώο" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Δαίμονας" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Απέθαντος" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Είδος: {:s} Έσφαξες: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Σύνολο που έσφαξες: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Πόντοι Ζωής: {:d}-{:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Καμία μαγική αντοχή" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Αντοχές:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Μαγεία" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Φωτιά" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Κεραυνός" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Ανοσίες:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Είδος: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Καμία Αντοχή" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Καμία Ανοσία" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Μερικές Μαγικές Αντοχές" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Μερικές Μαγικές Ανοσίες" #: Source/mpq/mpq_writer.cpp:174 msgid "Failed to open archive for writing." msgstr "Αποτυχία ανοίγματος αρχείου παίκτη για εγγραφή." #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "Ο/Η {:s} έριξε μη ορθό ξόρκι." #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "Ο/Η {:s} έριξε παράνομο ξόρκι." #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Ο παίκτης '{:s}' (Επίπεδο {:d}) μόλις μπήκε στο παιχνίδι" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "Το παιχνίδι έληξε" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "Αδυναμία λήψης δεδομένων πίστας" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "Ο παίκτης '{:s}' μόλις βγήκε από το παιχνίδι" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Ο παίκτης '{:s}' σκότωσε τον Ντιάμπλο και βγήκε από το παιχνίδι!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "Ο παίκτης '{:s}' έπεσε λόγω δικτυακού χρόνου προθεσμίας" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Ο παίκτης '{:s}' (Επιπέδου {:d}) είναι ήδη στο παιχνίδι" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Μυστηριώδες" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Κρυφός" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Ζοφερός" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "του Περίεργου" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Μαγικός" #: Source/objects.cpp:132 msgid "Stone" msgstr "Πέτρινος" #: Source/objects.cpp:133 msgid "Religious" msgstr "Θρησκευτικός" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Μαγεμένος" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Θαυματουργικός" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Γοητευτικός" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Αινιγματικός" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Απόκοσμος" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Ανατριχιαστικός" #: Source/objects.cpp:141 msgid "Divine" msgstr "Θεϊκός" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "της Ιεροσύνης" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Ιερός" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Πνευματικός" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Τραμακτικός" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Εγκατελειμένος" #: Source/objects.cpp:147 msgid "Creepy" msgstr "Φοβιστικός" #: Source/objects.cpp:148 msgid "Quiet" msgstr "Ύσυχός" #: Source/objects.cpp:149 msgid "Secluded" msgstr "Απομονωμένος" #: Source/objects.cpp:150 msgid "Ornate" msgstr "Καλλωπισμένος" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "Αστραφτερός" #: Source/objects.cpp:152 msgid "Tainted" msgstr "Σπιλωμένος" #: Source/objects.cpp:153 msgid "Oily" msgstr "Λαδερός" #: Source/objects.cpp:154 msgid "Glowing" msgstr "Λαμπερός" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "Εεπαιτικός" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "Σπινθηροβόλος" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "Τρεμοπαικτικός" #: Source/objects.cpp:159 msgid "Solar" msgstr "Ηλιακός" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "Μερφικός" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "Η Μεγάλη Σύρραξη" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "Ο Μισθός της Αμαρτίας είναι ο Πόλεμος" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "Η Ιστορία των Χοράντριμ" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "Η Σκοτεινή Εξορία" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "Ο Πόλεμος της Αμαρτίας" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "Το Δέσιμο των Τριών" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "Οι Κόσμοι του Υπερπέραντος" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "Η Ιστορία των Τριών" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "Ο Μαύρος Βασιλιάς" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "Πρακτικά: Η Γητεία" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "Πρακτικά: Η Συνάντηση" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "Πρακτικά: Το Ξέσπασμα" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "Πρακτικά: Η Δύναμη του Μεγαλώνει" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "Πρακτικά: ΝΑ-ΚΡΟΥΛ" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "Πρακτικά: Το Τέλος" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "Ένα Βιβλίο με ξόρκια" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "Σταυρωμένος Σκελετός" #: Source/objects.cpp:4799 msgid "Lever" msgstr "Μοχλός" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "Ανοιχτή Πόρτα" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "Κλειστή Πόρτα" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "Μπλοκαρισμένη Πόρτα" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "Αρχαίος Τόμος" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "Βιβλίο της Αχρειότητας" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "Μοχλός με Νεκροκεφαλή" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "Μυθικό Βιβλίο" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "Μικρό Σεντούκι" #: Source/objects.cpp:4833 msgid "Chest" msgstr "Σεντούκι" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "Μεγάλο Σεντούκι" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "Σαρκοφάγος" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "Ράφι Βιβλίων" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "Βιβλιοθήκη" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "Βαρέλι" #: Source/objects.cpp:4851 msgid "Pod" msgstr "Λοβός" #: Source/objects.cpp:4854 msgid "Urn" msgstr "Τεφροδόχος" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "{:s} Βωμός" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "Σκελετωμένο Αναλόγιο" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "Αναλόγιο Εγγράφου" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "Σιντριβάνι Αίματος" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "Αποκεφαλισμένο Πτώμα" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "Το Βιβλίο των Τυφλών" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "Το Βιβλίο του Αίματος" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "Εξαγνιστική Πηγή" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "Θώρακας" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "Οπλοβαστός" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "Bωμός της Κατσίκας" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "Καζάνι" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "Θολή Δεξαμενή" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "Σιντριβάνι των Δακρύων" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "Ατσαλένιος Τόμος" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "Βωμός του Αίματος" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "Μανιτάρια" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "Αχρείa Βάση" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Σκοτωμένος Ήρωας" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "{:s} με παγίδα" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (απενεργοποιημένο)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "ON" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "OFF" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Λειτουργία παιχνιδιού" #: Source/options.cpp:422 #, fuzzy #| msgid "Gameplay Settings" msgid "Game Mode Settings" msgstr "Ρυθμίσεις του τρόπου που παίζετε το παιχνίδι" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Παίξε Ντιάμπλο ή Χέλλφαϊερ." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Περιορισμός στην Δωρεάν Έκδοση" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends who " "don't own a full copy of Diablo." msgstr "" "Κάνει το παιχνίδι συμβατό με την δωρεάν έκδοση. Επιτρέπει τετραπλό με φίλους " "σου που δεν έχουν την πλήρη έκδοση του Ντιάμπλο." #: Source/options.cpp:442 msgid "Start Up" msgstr "Εκκίνηση" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Ρυθμίσεις Εκκίνησης" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Εισαγωγή" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Εμφάνιση βίντεο εισαγωγής." #: Source/options.cpp:455 msgid "Splash" msgstr "Σπλάς" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Εμφάνιση Οθόνης Σπλάς." #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Οθόνη Λογότυπου και Τίτλου" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Οθόνη Τίτλου" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Ρυθμίσεις ειδικά για το Ντιάμπλο" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Ρυθμίσεις ειδικά για το Χελφαϊερ" #: Source/options.cpp:501 msgid "Audio" msgstr "Ήχος" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Ρυθμίσεις Ήχου" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Ήχος Βαδίσματος" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Ο παίκτης κάνει ήχο όταν κινείται." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Ήχος Αυτόματου εξοπλησμού" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" "Τα αυτόματα εξοπλισμένα αντικείμενα θα κάνουν τον ήχο εξοπλισμού όταν τα " "μαζεύεις." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Ήχος μαζέματος αντικειμένων" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Όταν μαζεύεις αντικείμενα θα ακούγεται ο ήχος αυτός." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Ρυθμός Δειγματοληψίας Ήχου" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Ρυθμός δειγμάτων (Hz)" #: Source/options.cpp:508 msgid "Channels" msgstr "Κανάλια Ήχου" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Αριθμός καναλιών εξόδου ήχου." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Μέγεθος Ενδιάμεσης Μνήμης" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Μέγεθος Ενδιάμεσης Μνήμης (Αριθμός πλαισίων ήχου ανά κανάλι)." #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Ποιότητα Αναδειγματοληψίας" #: Source/options.cpp:510 #, fuzzy #| msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "Ποιότητα της Αναδειγματοληψίας Ήχου, από 0 (κατώτερο) έως 10 (μέγιστο)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: This " "can differ from screen resolution, when Upscaling, Integer Scaling or Fit to " "Screen is used." msgstr "" "Αφορά την εσωτερική ανάλυση του παιχνιδιού και ορίζει πόσο μεγάλο είναι το " "πεδίο όρασης στο παιχνίδι. Σημ.: Είναι διαφορετική από την ανάλυση της οθόνης, " "όταν γίνεται χρήση κλιμάκωσης ή προσαρμογής στην οθόνη." #: Source/options.cpp:574 msgid "Resampler" msgstr "Αναδειγματολήπτης" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "Αναδειγματολήπτης ήχου" #: Source/options.cpp:631 msgid "Device" msgstr "Συσκευή" #: Source/options.cpp:631 msgid "Audio device" msgstr "Συσκευή εξόδου Ήχου" #: Source/options.cpp:688 msgid "Graphics" msgstr "Γραφικά" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Ρυθμίσεις Γραφικών" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Πλήρης Οθόνη" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Εμφάνιση του παιχνιδιού σε κατάσταση πλήρους οθόνης ή σε παράθυρο." #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "Προσαρμογή στην Οθόνη" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Αυτόματη κλιμάκωση του παιχνιδιού στην τρέχουσα ανάλυση και αναλογία της " "επιφάνειας εργασίας." #: Source/options.cpp:700 msgid "Upscale" msgstr "Κλιμάκωση" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Ενεργοποίηση της κλιμάκωσης από την ανάλυση του παιχνιδιού στην ανάλυση την " "οθόνης. Απενεργοποιεί την δυνατότητα αλλαγής της ανάλυσης οθόνης και επιτρέπει " "την αλλαγή των διαστάσεων του παραθύρου." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Ποιότητα Κλιμάκωσης" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "" "Ενεργοποιεί προαιρετικά φίλτρα υφής για την προβολή όταν γίνεται κλιμάκωση " "ανάλυσης." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Κοντινότερο Εικονοστοιχείο" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Διγραμικό" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Ανισοτροπικό" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Ακέραια Κλιμάκωση" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Κλιμακώνει την εικόνα με βάση μόνο ακέραιου πολλαπλασιαστή." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Οριζόντιος Συνχρόνισμός" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "Μεγέθυνση ενεργή στην επιλογή." #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Κεραυνός" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Ανακύλιση Χρωμάτων" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" "Ανακύλιση χρωμάτων για τα εφέ κινησης της λάβας, του νερού και του οξέος." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Χρήση εναλλακτικής παλέτας για την φωλιά" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" "Το παιχνίδι θα χρησιμοποιήσει άλλη παλέτα χρωμάτων για τα γραφικά πίστας της " "Φωλιάς στο Χελφαΐερ." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Κέρσορας Συστήματος" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Χρήση κέρσορα συστήματος (σε αντίθεση με κέρσορα λογισμικού)" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Κέρσορας συστήματος για Αντικείμενα" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Χρήση κέρσορα συστήματος όταν μεταφέρεις Αντικείμενα." #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Μέγεθος του Κέρσορα Συστήματος" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Μέγιστό μήκος και πλάτος για τον κέρσορα συστήματος. Τιμές πέρα από τα όρια " "στρέφουν στην χρήση κέρσορα λογισμικού." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Εμφάνιση ΚΑΔ" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "" "Εμφανίζει τα ΚΑΔ(Καρέ ανά Δευτερόλεπτο) στην πάνω αριστερή γωνία της οθόνης." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Παίξιμο Παιχνιδιού" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Ρυθμίσεις του τρόπου που παίζετε το παιχνίδι" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Ενεργοποίηση γρήγορου περπατήματος στην πόλη για το Ντιάμπλο και το Χελφάϊερ. " "Αυτή η επιλογή προστέθηκε στην προέκταση του παιχνιδιού." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Κλείδωμα εισαγωγής" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "" "Όταν είναι ενεργοποιημένο, το ποντίκι είναι κλειδωμένο στο παράθυρο του " "παιχνιδιού." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "" #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Ενεργοποίηση της αποστολή με το μικρό κοριτσάκι." #: Source/options.cpp:788 msgid "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" "Ενεργοποίηση της αποστολής του Τζέρσεϊ, Ο αγρότης Λέστερ, αντικαθίσταται από " "τον Ολοκληρωτικά Τρελό." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Φιλικό Πύρ" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the friendly " "mode is on." msgstr "" "Επιτρέπει τα βέλη και τα Ξόρκια να κάνουν ζημιά σε άλλους παίκτες ακόμα όταν " "είσαι φιλικός προς παίκτες." #: Source/options.cpp:790 msgid "Full quests in Multiplayer" msgstr "Όλες οι αποστολές ενεργές στο τετραπλό" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "Ενεργοποιεί τις πλήρες αποστολές απο το παιχνίδι ενός παίκτη." #: Source/options.cpp:791 msgid "Test Bard" msgstr "Δοκιμή Βάρδου" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Επιτρέπει να διαλέξεις τον Βάρδο κατά την δημιουργία ήρωα." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Δοκιμή Βάρβαρου" #: Source/options.cpp:792 msgid "Force the Barbarian character type to appear in the hero selection menu." msgstr "Επιτρέπει να διαλέξεις τον Βάρβαρο κατά την δημιουργία ήρωα." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Μπάρα Εμπειρίας" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "Προσθέτει μια Μπάρα Εμπειρίας στην κάτω καρτέλα στην οθόνη." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "Εμφάνιση γραφικών αντικειμένων στα Μαγαζιά" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" "Θα εμφανίζονται τα αντικείμενα με τα γραφικά τους στα αριστερά της κειμενικής " "τους περιγραφής στα μενού των μαγαζιών." #: Source/options.cpp:795 msgid "Show health values" msgstr "Εμφάνιση Αριθμού Ζωής" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "Δείχνει τον αριθμό των πόντων ζωής πάνω στην σφαίρα ζωής." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Εμφάνιση Αριθμού Μάνα" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "Δείχνει τον αριθμό των πόντων μάνα πάνω στην σφαίρα μάνα." #: Source/options.cpp:797 #, fuzzy #| msgid "Character Information" msgid "Show Party Information" msgstr "Πληροφορίες Ήρωα" #: Source/options.cpp:797 msgid "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Μπάρα Υγείας Εχθρών" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "Εμφανίζει την Μπάρα Υγείας Εχθρών στην κορυφή της οθόνης." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Ο χρυσός μαζεύεται αυτόματα όταν τον πλησιάσει ο παίκτης." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "Τα ελιξίρια μαζεύονται αυτόματα όταν τα πλησιάσει ο παίκτης." #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "Τα Λάδια μαζεύονται αυτόματα όταν τα πλησιάσει ο παίκτης." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Αυτόματα μαζεύει αντικείμενα στην πόλη." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Η Άντρια αναπληρώνει τα μάνα σου όταν επισκεφτείς το μαγαζί της." #: Source/options.cpp:805 msgid "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" "Άμα αγοράσεις ή μαζέψεις ένα όπλο, θα εξοπλιστεί αυτόματα, αν η επιλογή είναι " "ενεργοποιημένη." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" "Άμα αγοράσεις ή μαζέψεις μια πανοπλία, θα εξοπλιστεί αυτόματα, αν η επιλογή " "είναι ενεργοποιημένη." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" "Άμα αγοράσεις ή μαζέψεις ένα κράνος, θα εξοπλιστεί αυτόματα, αν η επιλογή " "είναι ενεργοποιημένη." #: Source/options.cpp:808 msgid "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" "Άμα αγοράσεις ή μαζέψεις μια ασπίδα, θα εξοπλιστεί αυτόματα, αν η επιλογή " "είναι ενεργοποιημένη." #: Source/options.cpp:809 msgid "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" "Άμα αγοράσεις ή μαζέψεις χρυσαφικά, θα εξοπλιστεί αυτόματα, αν η επιλογή είναι " "ενεργοποιημένη." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "Σε κάθε νέο παιχνίδι, επιλέγονται τυχαίες αποστολές." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Εμφάνιση Τύπου των Τεράτων" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Αν τοποθετήσεις τον κέρσορα πάνω σε ένα τέρας, θα εμφανιστεί ο τύπος του στο " "κουτί πληροφοριών στην οθόνη." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "" "Όταν είναι ενεργό θα εμφανίζονται ετικέτες για τα αντικείμενα στο έδαφος." #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "" "Η ζώνη σου γεμίζει ξανά από τον σάκο σου όταν καταναλώσεις ένα αντικείμενο." #: Source/options.cpp:814 #, fuzzy #| msgid "" #| "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines " #| "and Sacred Shrines are not able to be clicked on and labeled as disabled." msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and labeled " "as disabled." msgstr "" "Όταν είναι ενεργοποιημένο, ο Συναρπαστικός και Ιερός Βωμός καθώς και οι βωμοί " "Κατσίκας και Διακόσμησης, δεν μπορούν να γίνουν κλίκ, και είναι " "απενεργοποιημένοι." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Άμεσα Ξόρκια" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Τα πλήκτρα ξορκιών απευθείας ρίχνουν το ξόρκι, αντί να αλλάζουν το έτοιμο " "ξόρκι." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Αριθμός Φίλτρων Θεραπείας για αυτόματο μάζεμα." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "Αριθμός Φίλτρων Πλήρους Θεραπείας για αυτόματο μάζεμα." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Αριθμός Φίλτρων Μάνα για αυτόματο μάζεμα." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "Αριθμός Φίλτρων Πλήρωσης Μάνα για αυτόματο μάζεμα." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "Αριθμός Φίλτρων Αναζωογόνησης για αυτόματο μάζεμα." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "Αριθμός Φίλτρων Πλήρους Αναζωογόνησης για αυτόματο μάζεμα." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "Ενεργοποίηση αριθμών κινητής υποδιαστολής" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "" "Ενεργοποίηση αριθμών κινητής υποδιαστολής για τις αριθμητικές λειτουργίες των " "πόντων εμπειρίας / του υπολογισμού της ζημιάς και άλλων συστημάτων." #: Source/options.cpp:824 msgid "Off" msgstr "Απενεργοποιημένο" #: Source/options.cpp:825 msgid "Random Angles" msgstr "Τυχαίες Γωνίες" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "Κάθετα Μόνο" #: Source/options.cpp:880 msgid "Controller" msgstr "Xειριστήριο" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Ρυθμίσεις Χειριστηρίου" #: Source/options.cpp:889 msgid "Network" msgstr "Δίκτυο" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Ρυθμίσεις Δικτύου" #: Source/options.cpp:901 msgid "Chat" msgstr "Συνομιλία" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Ρυθμίσεις Συνομιλίας" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Γλώσσα" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Ορίζει ποια γλώσσα θα χρησιμοποιεί το παιχνίδι." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Ρυθμίσεις Γλώσσας" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Ορισμός Πλήκτρων" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Ρυθμίσεις Ορισμού Πλήκτρων" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "Ορισμός Πλήκτρων Χειριστηρίου" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "Ρυθμίσεις Ορισμού Πλήκτρων Χειριστηρίου" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 #, fuzzy #| msgid "Settings" msgid "Mod Settings" msgstr "Ρυθμίσεις" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Επίπεδο" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Εμπειρίες" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Επόμενο Επίπεδο" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "Βάση" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Τελικό" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Δύναμη" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Μαγεία" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Επιδξτητα" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Ζωτικότητα" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Πόντους Επιλογής" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Χρυσός" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Θωράκιση" #: Source/panels/charpanel.cpp:176 #, fuzzy #| msgid "chance to hit" msgid "Chance To Hit" msgstr "ενός όπλου να χτυπήσει" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Ζημιά" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Ζωή" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Μάνα" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "Αντοχή Μαγείας" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Αντοχή Φωτιάς" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Αντοχή Κεραυνού" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "ήρωας" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "στόχοι" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "χάρτης" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "μενού" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "σάκος" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "ξόρκια" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "φωνή" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "σίγαση" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Άχρηστο" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Ζημιά: 1/3 ζωής" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Θεραπεύει {:d} - {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Ζημια: {:d} - {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Δεξιότητα" #: Source/panels/spell_book.cpp:176 #, fuzzy, c++-format #| msgid "Staff ({:d} charge)" #| msgid_plural "Staff ({:d} charges)" msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Ράβδος ({:d} φορτίο}" msgstr[1] "Ράβδος ({:d} φορτία}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Επίπ. {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Μάνα: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Ξόρκι" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Κάνει ζημιά σε απέθαντους μόνο" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Πάπυρος" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Ράβδος" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Πλήκτρο για Ξόρκι {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "Αποτυχία κατά το άνοιγα αρχείου" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "Αποτυχία κατά την φόρτωση χαρακτήρα" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (Επίπεδο {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "Ιστορικό Συνομιλιών (Μηνύματα: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} χρυσά" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "Πόσα χρυσά νομίσματα θέλεις να κάνεις ανάληψη;" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Επίπεδο {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Εμπειρίες {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Μέγιστο Επίπεδο" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Επόμενο Επίπεδο: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} για Επίπεδο {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "Ο Τάφος του Βασιλιά Λεόρικ" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "Η Αίθουσα των Οστών" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Ένα σκοτεινό μονοπάτι" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Ανίερος Βωμός" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "Πρός {:s}" #: Source/quick_messages.cpp:10 #, fuzzy #| msgid "I need help! Come Here!" msgid "I need help! Come here!" msgstr "Χρειάζομαι βοήθεια! Έλα εδώ!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Ακολούθα με." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Έχω κάτι για σένα." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "Τώρα θα ΠΕΘΑΝΕΙΣ!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "" #: Source/quick_messages.cpp:16 #, fuzzy #| msgid "Thanks To" msgid "Thanks." msgstr "Ευχαριστούμε τους" #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "" #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "" #: Source/stores.cpp:131 msgid "Griswold" msgstr "Γκρίσουαλντ" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Πέπιν" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Όγκντεν" #: Source/stores.cpp:135 msgid "Cain" msgstr "Κάιν" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Φάρνχαμ" #: Source/stores.cpp:137 msgid "Adria" msgstr "Άντρια" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Γκίλλιαν" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Ουίρτ" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Πίσω" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Ζημιά: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Θωράκιση: {:d} " #: Source/stores.cpp:315 #, fuzzy, c++-format #| msgid "Dur: {:d}/{:d}, " msgid "Dur: {:d}/{:d}" msgstr "Ανθ: {:d}/{:d}, " #: Source/stores.cpp:317 #, fuzzy #| msgid "indestructible" msgid "Indestructible" msgstr "άφθαρτο" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Καλώς ήρθες στο" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Μαγαζί του Σιδηρουργού" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Θα ήθελες να:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Μιλήσεις στον Γκρίσγουαλντ" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Αγοράσεις απλά αντικείμενα" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Αγοράσεις αντικείμενα αξίας" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Πουλήσεις αντικείμενα" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Επιδιορθώσεις αντικείμενα" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Φύγεις από το Μαγαζί" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Έχω τα παρακάτω για πώληση:" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Έχω τα παρακάτω ακριβά για πώληση:" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "Δεν έχεις τίποτα που θέλω." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "Ποιο αντικείμενο είναι για πούλημα;" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "Δεν έχεις τίποτα για επιδιόρθωση." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "Πιο αντικείμενο θέλει επιδιόρθωση;" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Η Παράγκα της Μάγισσας" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Μιλήσεις στην Άντρια" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Αγοράσεις αντικείμενα" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Επαναφορτίσεις Ράβδους" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Φύγε από την παράγκα" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "Δεν έχεις τίποτα για επαναφόρτιση." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "Ποια ράβδος θες να φορτίσεις;" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "Δεν έχεις αρκετό χρυσό" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "Δεν έχεις χώρο στον σάκο σου" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "Είμαστε σύμφωνοι;" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "Σίγουρα θες να ταυτοποιήσεις το αντικείμενο αυτό;" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "Σίγουρα θες να αγοράσεις το αντικείμενο αυτό;" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "Σίγουρα θες να επαναφορτίσεις το αντικείμενο αυτό;" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "Σίγουρα θες να πουλήσεις το αντικείμενο αυτό;" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "Σίγουρα θες να επιδιορθώσεις το αντικείμενο αυτό;" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Ο Ουίρτ, με το Ξύλινο Πόδι" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Μίλα με τον Ουίρτ" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Έχω κάτι προς πώληση," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "αλλά θα δώσεις 50 χρυσά" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "μονό για να το δεις. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "Τι έχεις;" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Πες αντίο" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Έχω αυτό για πώληση:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Φύγε" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Το σπίτι του Θεραπευτή" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Μιλήσεις στον Πέπιν" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Φύγε από το σπίτι του Πέπιν" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "Ο Γέροντας της Πόλης" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Μιλήσεις στον Κάιν" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Ταυτοποιήσεις ένα αντικείμενο" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "Δεν έχεις τίποτα για ταυτοποίηση." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "Πιο αντικείμενο θέλεις να ταυτοποιήσω;" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "Αυτό το αντικείμενο είναι:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Τελειώσαμε" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Συνομιλία - {:s}" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Συνομιλία - {:s}" #: Source/stores.cpp:1217 msgid "is not available" msgstr "δεν είναι διαθέσιμο" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "στην δωρεάν" #: Source/stores.cpp:1219 msgid "version" msgstr "έκδοση" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Κουτσομπολιό" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Πανδοχείο 'Ανατέλλων Ήλιος'" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Μιλήσεις στον Όγκντεν" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Φύγε από την Ταβέρνα" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Μιλήσεις στην Γκίλλιαν" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Πρόσβαση Στην Αποθήκη" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Φάρνχαμ ο Μεθύστακας" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Μιλήσεις στον Φάρνχαμ" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Πες αντίο" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Ο χρυσός σου: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Σφαγμένος Κάτοικός" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Γκρίσουαλντ o Σιδηρουργός" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Πέπιν ο Θεραπευτής" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Τραυματισμένος Κάτοικος" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Όγκντεν ο ιδιοκτήτης της Ταβέρνας" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Κάιν ο Σοφός Γέροντας" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Άντρια η Μάγισσά" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Γκίλλιαν η Σερβιτόρα" #: Source/towners.cpp:786 msgid "Cow" msgstr "Αγελάδα" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Λέστερ ο Γεωργός" #: Source/towners.cpp:788 msgid "Celia" msgstr "Σέλια" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Τρελός" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Πολεμιστής" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Kλεφτρόνι" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Μάγος" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Μοναχός" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Βάρδος" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Βάρβαρός" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Ζόμπι" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Ζόμπι Βρυκόλακας" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Σαπισμένο Πτώμα" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Μαύρος Θάνατος" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "Έκπτωτος" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Χαράκτης" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Διάβολοσόϊ" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "Σκοτεινός" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Σκελετός" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Πτώμα με Τσεκούρι" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Φλεγόμενος Νεκρός" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Τρόμος" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Σαπροφάγος" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Καταβροχθέας Πανώλης" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Σκιώδες Θηρίο" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Χαρακωτής Κοκάλων" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Πτώμα με Τόξο" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Σκελετωμένος Λοχαγός" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Πτώμα Λοχαγού" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Φλεγόμενος Νεκρός Λοχαγός" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Λοχαγός του Τρόμου" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Αόρατος Άρχοντας" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Κρυφός" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Διώκτης" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Αθέαντος" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Υφαντής των ψευδαισθήσεων" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Άρχοντας Σάτυρος" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Η Φυλή της Σάρκας" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Η Φυλή της Πέτρας" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Η Φυλή της Φωτιάς" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Η Φυλή της Νύχτας" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Κτηνώδες Πετούμενο" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Αστραπιαίο Πετούμενο" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Ζοφερό Πετούμενο" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Μαγικός Ιπτάμενος Σύντροφος" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Όξινο Θηρίο" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Φτύστης Δηλητηρίου" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Θηρίο του Λάκου" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Στόμα Λάβας" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Βασιλιάς των Σκελετών" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "Ο Χασάπης" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Δεσπότης" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Λασπωμένος" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Δαιμονικός Βάτραχος" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "Γδαρμένος" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Δρακόντειος" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Γυμνοσάλιαγκας Σπηλαίων" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Διαβολικός Δράκοντας" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Καταβροχθέας" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Δαίμονας Μάγματος" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Ματωμένη Πέτρα" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Πέτρα της Κολάσεως" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Άρχονας της Λάβας" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Κερατάς Δαίμονας" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Δρομέας της Λάσπης" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Καταδρομέας του Παγετού" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Οψιανός Άρχοντας" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "γερασμένο Κόκαλο" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Κόκκινος Θάνατος" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Απέθαντος Μάγος Δαίμονας" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Απέθαντο Μπάλρογκ" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Αποτεφρωτής" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Άρχοντας της Φλόγας" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Φωτιά της Καταστροφής" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Καυστήρας της Κόλασης" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Κόκκινη Θύελλα" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Καβαλάρης της θύελλας" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Άρχοντας της θύελλας" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Ζωντανή Δίνη" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Κτηνώδες Διάβολοσόϊ" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Φτερωτός Δαιμονας" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Γκαργκόϊλ" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Ματωμένο Νύχι" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Φτερωτός Θάνατος" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Σφάχτης" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Προστάτης" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Άρχοντας της Δίνης" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Μπάλρογκ" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Οχιά της Σπιλιάς" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Δράκος της Φωτιάς" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Χρυσή Οχιά" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Κυανός Δράκος" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Μαύρος Ιππότης" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Φρουρός του Χαμού" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Ατσαλένιος Άρχοντας" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Ιππότης του Αίματος" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "Ο Κοματιασμένος" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "Κενό Πλάσμα" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Άρχοντας του Πόνου" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Υφαντής της Πραγματικότητας" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Σούκουμπους" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Μάγισσα του Χιονιού" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Γόνος της Κολάσεως" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Καυστήρας των Ψυχών" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Σύμβουλoς" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Δικαστής" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Μέλος Σάχτας" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Συνήγορος" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Πήλινος Μαχητής" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "Ο Σκοτεινός Άρχοντας" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Ο Απέθαντος Αρχιμάγος Μαλίγκνους" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Γκάρμπάντ Ο Αδύναμος" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Ζάρ ο Τρελός" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Μυξορίχτης" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Ο Αρχιεπίσκοπος Λάζαρος" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Κόκκινη Αγανάκτηση" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Ο Μαύρος Νεφρίτη" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Λακντάναν" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Ο Πολέμαρχος του Αίματος" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Σαρκοδαίμονας" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "Ο Βεβηλωτής" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "Να-Κρούλ" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Κοφτερό Σκελετωμένο Τσεκούρι" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Ο Λεπιδόδερμας ο Κόφτης" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Το Πύον της Ψυχής" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Το Εμετικό Ακάθαρτο Ποντίκι" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Κόκαλόξηλωτής" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Σαπιοφάγος Ο Πεινασμένος" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Εντεροκνήμης Ο Γρήγορος" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Ο Σπαζοκέφαλος Ασπιδοκτύπης" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Μπόνγκο" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Σαπισμένος Μακελάρης" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Σκιωδαγκανιάρης" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Νεκρομάτης" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Τρελομάτης ο Νεκρός" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "Ελ Τσουπακάμπρας" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Κρανιοφωτιά" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Το Διαστροφικό Κρανίο" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Αιματόγλωσσος" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Παλμοερπετό" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Ο Λυγιστής της Σελήνης" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Το Κοράκι της Οργής" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Ο Καταβροχθέας Σπονδύλων" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Η Μαύρη Στάχτη της Ανάφλεξης" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Σκιεροκόρακας" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Περονόπετρος, Ο Αδύναμος" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Ο Αφρίστης Χολής, Ο Άρχοντας του Λάκκου" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Αιματόδερμος, το Σκοτεινό Τόξο" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "To Βρωμερό Πτερύγιο" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Ο Πότης των Σκιών" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Ομιχλώδες Περπατητής" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Θανατοφτύχτης" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Το Αυλάκι Αίματος" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Η Σκιά του Θανάτου, η Σφύρα της Σάρκας" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Το Σκουλήκι του Πολέμου, ο Τρελός" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Γυαλιόκρανος ο Οδοντωτός" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Η Άρρωστη Φωτιά" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Το Κρύο Φτερό της Νύχτας" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Η Αιματοβαμμένη Πέτρα" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Χαλκόγρονθος, ο Φλογόπετρος" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Η Φωτιά της Οργής και του Χαμού" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Η Βλοσυρή Φλεγόμενη Πληγή" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Ο Βαρόνος της Γλίτσας" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Αρρωστοκέρατος, το Ατσαλένιο Σκήπτρο" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Ουρλιαχτός του Χάους" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Χαμοφέρνης ο Σάπιος" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Τρελόεμπρηστής" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Κοκαλοπρίονος, Ο Απέθαντος Μάγος" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Σπονδυλοσπάστης" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Δαιμονόκρανος, το Αιχμηρό Κόκαλο" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Η Σπασμένη Καταιγίδα" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Ο Όλεθρος της Καταιγίδας" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Λασποσαλιαρίδης" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Χρυσοφθορός της Φλόγας" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Ο Μαύρος Καταδρομέας" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Η Οργή της Πανούκλας" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "Ο Γδάρτης" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Το Μπλέ Κέρατο" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Η Διατροφική Φλόγα, η Κολασμένη" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Οδοντόσπειρα" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Πυόκρανος" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Λεοντόκρανιος ο Λυγισμένος" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Η Μαύρη Γλώσσα" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Το Αχρείο Άγγιγμα" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Η Φλεγόμενη Οχιά" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Οδοντόδερμα" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Η Ανίερη Φλόγα της Μάγισσάς" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Το Μαύρο Κρανίο" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Χαράκτης των Ψυχών" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Ο Γόνος του Ανέμου" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Άρχοντας του Λάκκου" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Υφαντής της Σκουριάς" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Το Θυμωμένο Ουρλιαχτό, το Σκιώδες" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Σύννεφο του Χαμού" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Ματωμένη Σελήνη, Η Φωτιά της Ψυχής" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Η Μάγισσα της Σελήνης" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Αιματογλέντης" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Γκριζοπόλεμος ο Σφάχτης" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Ο Δικαστής του Τρόμου" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Αστρομάτα, Η Μάγισσα" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Ατσαλόκρανιος Ο Κυνηγός" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Σερ Γκόρας" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "Ο Βεζίρης" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Ζαμφίρ" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Αιμολάγνα" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Η Χήρα του Ιστού" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Ο Χορευτής της Σάρκας" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "To Βλοσυρό Δόρυ" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Μάντης του Χαμού" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Ξίφος" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Πόρπη" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Ρόπαλο" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Κοντό Τόξο" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Κοντή Ράβδος του Μάνα" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Μπαλτάς" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "Η Απέθαντη Κορώνα" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Δακτύλιος των Ουρανών" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Μαγική Πέτρα" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Οπτικό Φυλακτό" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Δαχτυλίδι της Αλήθειας" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Πινακίδα Ταβέρνας" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Λοφίο Αρλεκίνου" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Πέπλο από Ατσάλι" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Χρυσό Ελιξίριο" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Το Αμόνι της Οργής" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Μαύρο Μανιτάρι" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Εγκέφαλος" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Μυκητιακός Τόμος" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Φασματικό Ελιξίριο" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Πέτρα Αίματος" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Χάρτης Καθεδρικού Ναού" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Φίλτρο Θεραπείας" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Φίλτρο Μάνα" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Πάπυρoς Ταυτοποίησης" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Πάπυρος Πύλης Πόλης" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Η Ανδρεία του Αρκαϊν" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Φίλτρο Πλήρους Θεραπείας" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Φίλτρο Πλήρωσης Μάνα" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Η Άκρη του Γκρίσγουαλντ" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Βοδινή Πανοπλία" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Ράβδος του Λαζάρου" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Πάπυρος Ανάστησης" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Κοντή Ράβδος" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Σπαθί" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Μάχαιρα" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Βόμβα Ρούνων" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Θεόδωρος" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Φυλακτό του Χρυσού" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Σκισμένο Σημείωμα 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Σκισμένο Σημείωμα 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Σκισμένο Σημείωμα 2" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Συναρμολογημένο Σημείωμα" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Καφέ Στολή" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Γκρίζα Στολή" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Σκούφος" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Φολιδωτο Κράνος" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Ελαφρύ Κράνος" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Ολομεταλλικό Κράνος" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Κορώνα" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Περικεφαλαία" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Κάπα" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Κουρέλια" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Μανδύας" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Ρόμπα" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Kαπιτονέ πανοπλία" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Δερμάτινος Θώράκας" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Σκληρός Δέρματος Θώρακας" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Δερμάτινος Θώρακας με Καρφιά" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Δαχτυλιδωτός Θώρακας" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Αλυσιδωτός Θώρακας" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Αλυσιδωτή Πανοπλία" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Φολιδωτή Πανοπλία" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Μεταλλικός Θώρακας" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Ολομεταλλικός Θώρακας" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Σκληθρωτή Πανοπλία" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Ολομεταλλική Πανοπλία" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Πλήρης Ολομεταλλικός Θώρακας" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Γοτθική Ολομεταλλική Πανοπλία" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Πλήρης Ολομεταλλική Εξάρτυση" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Ασπίδα" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Μικρή Ασπίδα" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Μεγάλη Ασπίδα" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Ασπίδα Χαρταετού" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Ασπίδα Πύργου" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Γοτθική Ασπίδα" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Φίλτρο Αναζωογόνησης" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Φίλτρο Πλήρους Αναζωογόνησης" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Λάδι" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Ελιξίριο Δύναμης" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Ελιξίριο Μαγείας" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Ελιξίριο Επιδεξιότητας" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Ελιξίριο Ζωτικότητας" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Πάπυρος Θεραπείας" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Πάπυρος Αναζήτησης" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Πάπυρος Κεραυνού" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Πάπυρος Τείχους Φωτιάς" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Πάπυρος Λαίλαπας" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Πάπυρος Λάμψης" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Πάπυρος Υπέρυθρης Όρασης" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Πάπυρος Φασικής Μεταφοράς" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Πάπυρος Ασπίδας από Μάνα" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Πάπυρος Κύματος Φωτιάς" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Πάπυρος Οβίδας Φωτιάς" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Πάπυρος Κατάρας Πέτρωσης" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Πάπυρος Αλυσιδωτών Κεραυνών" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Πάπυρος Προστάτη" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Πάπυρος Αστρικής Έκρηξης" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Πάπυρος Πήλινου Μαχητή" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Πάπυρος Τηλεμεταφοράς" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Πάπυρος Αποκάλυψης" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Φάλτσιο" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Γιαταγάνι" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Δίκοπο Ξίφος" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Λεπίδα" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Φάλξ" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Σπάθα" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Ρομφαία" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Μπάσταρδη Σπάθα" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Δίχειρη Βαριά Σπάθα" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Κατάνα" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Τσεκουράκι" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Τσεκούρι" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Πέλεκυς" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Φαρδύ Τσεκούρι" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Δικέφαλο Τσεκούρι" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Μεγάλο Βαρύ Τσεκούρι" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Κεφαλοθραύστης" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Αγκαθωτός Κεφαλοθραύστης" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Σφύρα Πολέμου" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Σφύρί" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Ρόπαλο με Καρφιά" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Κόπανος" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Βαριά Σφύρα Πολέμου" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Τόξο" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Κυνηγετικό Τόξο" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Μακρύ Τόξο" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Σύνθετο Τόξο" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Κοντό Τόξο Μάχης" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Μακρύ Τόξο Μάχης" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Κοντό Τόξο Πολέμου" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Μακρύ Τόξο Πολεμού" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Πλούσια Ράβδος" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Σύνθετη Ράβδος" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Μακριά Ράβδος" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Πολεμική Ράβδος" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Δαχτυλίδι" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Φυλακτό" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Ρούνος της Φωτιάς" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Ρούνος" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Ρούνος του Κεραυνού" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Ανώτερος Ρούνος της Φωτιάς" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Ανώτερος Ρούνος του Κεραυνού" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Ρούνος της Πέτρας" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Κοντή Ράβδος των Φορτισμένων Σφαιριδίων" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "Φίλτρο Αρένας" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "Ο Μπαλτάς του Χασάπη" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Lightsabre" msgid "Lightforge" msgstr "Φωτόσπαθο" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "Το Τόξο του Ρήγματος" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "Ο Βελονιστής" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "Το Τόξο των Ουρανών" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Ο Θανατηφόρος Κυνηγός" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Το Τόξο των Νεκρών" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "Το Τόξο της Μαύρης Βελανιδιάς" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Βέλος της Φλόγας" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Το Κεντρί της Σάρκας" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Η Δύναμη του Ανέμου" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Το Κέρας του Αετού" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Το Ξιφίδιο του Γκόναγκαλ" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "Ο Αμυνόμενος" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Το Νύχι του Γρυπαετού" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Το Μαύρο Ξυράφι" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Η Αμφίκυρτος Σελήνη" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Το Μαχαίρι του Πάγου" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "Η Λεπίδα του Εκτελεστή" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "Το Πριόνι για Κόκκαλα" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Το Γεράκι της Σκιάς" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Η Αιχμή του Μάγου" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Φωτόσπαθο" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "Το Νύχι του Αετού" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Λαίλαπα" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "O Μόρος" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "Η Γρίζλι" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "Ο Προπάππους" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "Ο Ξεσκιστής" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Το Aιχμηρό Ράμφος" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Ο Φονιάς του Αίματος" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "Το Ουράνιο Τσεκούρι" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "Το Κακόβουλο Τσεκούρι" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "Ο Μπαλτάς της Πέτρας" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "Το Τσεκούρι της Αγκουϊνάρας" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "Ο Σφάχτης της Κολάσεως" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "O Θεριστής του Μέσσερσμιντ" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "Η Σκουριασμένη Ρωγμή" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "Το Σφυρί του Γιόλμ" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "Το Ρόπαλο του Κίβερμπ" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "Το Ουράνιο Άστρο" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "Το Άστρο του Μπάραναρ" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "Οζώδης Ρίζα" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "Ο Κόπανος των Κρανίων" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "Η Σφύρα του Σχάφερ" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "Ο Κεφαλοθραύστης των Ονείρων" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "Η Ράβδος των Σκιών" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "Ο Αναφλέκτηρας" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "Ο Πυργίσκος της Θύελας" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "Το Λαμπερό Τραγούδι" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "Το Κάλεσμα του Κεραυνού" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "Ο Προστάτης" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "Ο Γρίφος του Νάτζ" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "Το Κλάμα του Νού" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "Η Ράβδος του Ονάν" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "Το Κράνος των Πνευμάτων" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "Σκούφος Σκέψης" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "Η Περικεφαλαία του Άρχοντα" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "Το Λοφίο του Βλαμμένου" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "To Λυκόφως των Θεών" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "Βασιλικό Διάδεμα" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "Η Σκισμένη Σάρκα των Ψυχών" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "Το Μαρτύριο του Μονομάχου" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "Ο Μανδύας του Ουράνιου Τόξου" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "Το Δέρμα του Αούτ" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "Το Περιτύλιγμα της Σοφίας" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "Ο Αστραφτερός Αλυσιδωτός Θώρακας" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "To Καύκαλο του Σαπροφάγου" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "Νυχτερινό Τοπίο" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "Ο Ολομεταλλικός Θώρακας του Νάτζ" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "Το Παλτό της Δαιμόνιας Αιχμής" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "Ο Εκτροπέας" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "Η Ασπίδα του Ραγισμένου Κρανίου" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "Η Ρωγμή του Δράκου" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "Η Ασπίδα της Μαύρης Βελανιδιάς" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "Ο Άγιος Υπερασπιστής" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "Η Ασπίδα της Θύελλας" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "Ο Βάτος" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "Το Δακτυλίδι της Ρέγκχας" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "Το Ρεμάλι" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "Το Περιοριστικό Δακτυλίδι" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "Το δακτυλίδι της Συμπλοκής" #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "του Κασσίτερου" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "του Oρείχαλκου" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "του Μπρούτζου" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "του Σιδήρου" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "του Ατσαλιού" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "του Ασημιού" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "της Πλατίνας" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "του Μίθριλ" #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "του Μετεωρίτη" #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "του Παράξενου" #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "του Άχρηστου" #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "του Λυγισμένου" #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "του Αδύναμου" #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "της Οδόντωσης" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "του Θανατηφόρου" #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "της Ενίσχυσης" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "του Φαύλου" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "της Κτηνωδίας" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "του Ογκού" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "της Αγριότητας" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "του Αδίστακτου" #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "του Ανελέητου" #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "του Άτσαλου" #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "της Άμβλυσης" #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "της Αιχμηρότητας" #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "της Εξοχότητας" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "του Μαχητή" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "του Στρατιώτη" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "του Άρχονα" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "του Ιππότη" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "του Μαέστρου" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "του Πρωταθλητή" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "του Βασιλιά" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "του Ευάλωτου" #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "της Σκουριάς" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "της Δύναμης" #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "της Μεγαλότητας" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "του Θάρρους" #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "της Δόξας" #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "της Ευλογίας" #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "του Άγιου" #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "του Φανταστικού" #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "του Θεού" #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "του Κόκκινου" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "του Πορφυρού" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "του Λυχνίτη" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "του Ρουμπινιού" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "του Μπλέ" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "του Γαλάζιου" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "του Λάπις" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "του Κοβάλτιου" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "του Ζαφειριού" #: Source/translation_dummy.cpp:505 msgid "White" msgstr "του Λευκού" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "του Μαργαριταριού" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "του Ελεφαντόδοτου" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "του Κρυστάλου" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "του Διαμαντιού" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "του Τόπαζ" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "του Κεχριμπαριού" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "του Νεφρίτη" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "του Οψιανού" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "του Σμαραγδιού" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "της Ύαινας" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "του Βατράχου" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "της Αράχνης" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "του Κόρακα" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "του Φιδιού" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "του Ερπετού" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "της Σαύρας" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "του Δρακόμορφου" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "του Δράκοντα" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "της Ύδρας" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "των Αγγέλων" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "των Αρχαγγέλων" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "της Αφθονίας" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "της Πλουσιότητας" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "της Φλόγας" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "του Κεραυνού" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "της Ποιότητας" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "της Ακρωτηρίασης" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "του Φόνου" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "της Αιματοχυσίας" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "του Μακελιού" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "της Σφαγής" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "του Πόνου" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "των Δακρύων" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "της Υγείας" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "της Προστασίας" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "της Απορόφησης" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "της Απόκρουσης" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "της Όσμωσης" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "της Ευθραυστότητας" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "της Αδυναμίας" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "της Δύναμης" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "της Ισχύς" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "της Εξουσίας" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "των Γιγάντων" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "των Τιτάνων" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "της Παράλυσης" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "της Ατροφίας" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "της Επιδεξιότητας" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "της Δεξιότητας" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "της Ευστοχίας" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "της Ακρίβειας" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "της Τελειότητας" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "του Χαζού" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "της Δυσλεξίας" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "της Μαγείας" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "του Μυαλού" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "της Λαμπρότητας" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "της Βασκανείας" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "των Μάγιστρων" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "της Αρρώστιας" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "της Επιδημίας" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "της Ζωτικότητας" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "του Πάθους" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "της Ζωντάνιας" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "του Σφρίγους" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "της Ζωής" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "του Μπελά" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "της Τρύπας" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "του Ουρανού" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "της Σελήνης" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "των Άστρων" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "των Ουρανών" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "του Ζωδιακού Κύκλου" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "του Αρπακτικού" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "του Τσακαλιού" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "της Αλεπούς" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "του Ιαγουάρου" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "του Αετού" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "του Λύκου" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "της Τίγρης" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "του Λιονταριού" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "του Μαμούθ" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "της Φάλαινας" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "της Ευθραυστότητας" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "της Διαλύσης" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "της Ανθεκτικότητας" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "της Αριστοτεχνείας" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "της Δομής" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "των Εποχών" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "του Σκότους" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "της Νύχτας" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "του Φωτός" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "της Ακτινοβολίας" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "της Φλόγας" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "της Φωτιάς" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "της Ανάφλεξης" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "του Ηλεκτροσόκ" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "της Βροντής" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "του Κεραυνού" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "των Πολλών" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "της Αφθονίας" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "των Αγκαθιών" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "της Διαφθόρας" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "των Κλεφτών" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "της Αρκούδας" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "της Νυχτερίδας" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "του Βρυκόλακα" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "της Βδέλλας" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "του Αίματος" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "της Διαπέρασης" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "του Τρυπήματος" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "του Ξυλοδαρμού" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "της Ετοιμότητας" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "της Σβελτάδας" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "της Ταχύτητας" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "της Βιασύνης" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "της Ισορροπίας" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "της Σταθερότητας" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "της Αρμονίας" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "της Παρεμπόδισης" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "Ο Μαγικός Βράχος" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "Γκάρμπάντ Ο Αδύναμος" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "Ζάρ ο Τρελός" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "Λακντάναν" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "Ο Χασάπης" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "Η Ταμπέλα του Όγκντεν" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "Η Αίθουσες των Τυφλών" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "Ανδρεία" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "Ο Πολέμαρχος του Αίματος" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "Η Κατάρα του Βασιλιά Λεόρικ" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "Λάζαρος Ο Αρχιεπίσκοπος" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "Τα ζητήματα του Τάφου" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "Ο Οπωρώνας του Αγρότη" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "Μικρό Κοριτσάκι" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "Πλανόδιος Έμπορας" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "Ο Βεβηλωτής" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "Να-Κρούλ" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "Η Στολή της Αγελάδας" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "Σφαιρίδιο Φωτιάς" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "Θεραπεία" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "Κεραυνός" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "Λάμψη" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "Ταυτοποίηση" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "Τείχος Φωτιάς" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "Πύλη Πόλης" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "Κατάρα Πέτρωσης" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "Υπέρυθρη Όραση" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "Φασική Μεταφορά" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "Ασπίδα από Μάνα" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "Οβίδα Φωτιάς" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "Προστάτης" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "Αλυσιδωτοί Κεραυνοί" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "Κύμα Φωτιάς" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "Ερπετά του Χαμού" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "Τελετή Αίματος" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "Αστρική Έκρηξη" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "Εξαφάνιση" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "Λαίλαπα" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "Πήλινος Μαχητής" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "Οργή" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "Τηλεμεταφορά" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "Αποκάλυψη" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "Αιθεροποιήση" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "Επισκευή Αντικειμένου" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "Επαναφόρτιση Ράβδου" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "Αφόπλιση Παγίδας" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "Στοιχείο Φωτιάς" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "Φορτισμένα Σφαιρίδια" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "Ιερό Σφαιρίδιο" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "Ανάστηση" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "Τηλεκίνηση" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "Θεραπεία Άλλου" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "Άστρο Αίματος" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "Πνεύμα των Οστών" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh blow " "to this land. The people always loved the King, and now they live in mortal " "fear of him. The question that I keep asking myself is how he could have " "fallen so far from the Light, as Leoric had always been the holiest of men. " "Only the vilest powers of Hell could so utterly destroy a man from within..." msgstr "" " Αχχ, η ιστορία του βασιλιά μας; Η τραγική πτώση του Λεόρικ ήταν ένα βάναυσο " "χτύπημα για την χώρα. Ο Λαός πάντα αγαπούσε τον Βασιλιά, αλλά τώρα ζούνε με " "θανάσιμο φόβο για αυτόν. Η ερώτηση που κάνω συνέχεια στον εαυτό μου είναι πώς " "μπόρεσε και έπεσε τόσο μακριά από το φώς, γιατί ο Λεόρικ ήταν από τους πιο " "άγιους άντρες. Μόνο οι πιο χυδαίες δυνάμεις την Κολάσεως μπορούν κα " "καταστρέψουν έναν άνθρωπο εκ των έσω..." #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "Το χωριό χρειάζεται την βοήθεια σου, καλέ/ή μου άρχοντα/ίσσα! Πρίν μερικούς " "μήνες ο γιός του Βασιλιά Λεόρικ, ο Πρίγκιπάς Άλμπρεχτ απήχθη. Ο Βασιλιάς " "εξαγριώθηκε και όργωσε το χωριό μας ψάχνοντας το αγνοούμενο παιδί. Καθώς " "περνούσαν οι μέρες, ο Λεόρικ φαινόταν να γλιστράει πιο βαθιά στην τρέλα. " "Προσπάθησε να κατηγορήσει τους αθώους κατοίκους της πόλης για την εξαφάνιση " "του αγοριού και άρχισε να τους εκτελεί με βάναυσο τρόπο. Λιγότεροι από τους " "μισούς από εμάς επέζησαν από την παραφροσύνη του...\n" "Οι Ιππότες και οι Ιερείς του Βασιλιά προσπάθησαν να τον κατευνάσουν, αλλά " "στράφηκε εναντίον τους και δυστυχώς, αναγκάστηκαν να τον σκοτώσουν. Με την " "ετοιμοθάνατη ανάσα του, ο Βασιλιάς έριξε μια τρομερή κατάρα στους πρώην " "ακόλουθους του. Ορκίστηκε ότι θα τον υπηρετήσουν στο σκοτάδι για πάντα...\n" "\n" "Εδώ είναι που τα πράγματα παίρνουν μια ακόμη πιο σκοτεινή τροπή από ό,τι " "νόμιζα ότι είναι δυνατόν! Ο πρώην βασιλιάς μας έχει σηκωθεί από τον αιώνιο " "ύπνο του και τώρα διοικεί μια λεγεώνα από απέθαντους μέσα στον Λαβύρινθο. Το " "σώμα του θάφτηκε σε έναν τάφο τρία επίπεδα κάτω από τον καθεδρικό ναό. Σε " "παρακαλώ, καλέ/ή μου άρχοντα/ίσσα, ηρέμησε την ψυχή του καταστρέφοντας την " "καταραμένη πλέον μορφή του..." #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Όπως σου είπα, καλέ/ή μου άρχοντα/ίσσα, ο Βασιλιάς ήταν ενταφιασμένος τρία " "επίπεδα πιο κάτω. Είναι εκεί κάτω, περιμένοντας στο σάπιο σκοτάδι την ευκαιρία " "του να καταστρέψει αυτή τη γη..." #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "Η κατάρα του Βασιλιά μας πέρασε, αλλά φοβάμαι ότι ήταν μόνο ένα μικρό μέρος " "ενός μεγαλύτερου κακού. Ωστόσο, μπορεί ακόμη να σωθούμε από το σκοτάδι που " "κατατρώει τη γη μας, γιατί η νίκη μας είναι καλός οιωνός. Είθε το Φως να σε " "καθοδηγήσει στο δρόμο σου, καλέ/ή μου άρχοντα/ίσσα." #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over this " "kingdom from that day forward, but perhaps if you were to free his spirit from " "his earthly prison, the curse would be lifted..." msgstr "" "Η απώλεια του γιου του ήταν υπερβολική για τον Βασιλιά Λέορικ. Έκανα ό,τι " "μπορούσα για να απαλύνω την τρέλα του, αλλά στο τέλος αυτή τον νίκησε. Μια " "μαύρη κατάρα κρεμόταν πάνω από αυτό το βασίλειο από εκείνη την ημέρα και μετά, " "αλλά ίσως αν απελευθερώσεις το πνεύμα του από την επίγεια φυλακή του, η κατάρα " "θα αρθεί..." #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for the " "kind and just ruler that he was. His death was so sad and seemed very wrong, " "somehow." msgstr "" "Δεν μου αρέσει να σκέφτομαι πώς πέθανε ο Βασιλιάς. Μου αρέσει να τον θυμάμαι " "για τον ευγενικό και δίκαιο κυβερνήτη που ήταν. Ο θάνατός του ήταν τόσο " "λυπηρός και φαινόταν πολύ λάθος, κατά κάποιο τρόπο." #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe how " "he died, but it must have been some sinister force that drove him insane!" msgstr "" "Έφτιαξα πολλά από τα όπλα και τις περισσότερες από τις πανοπλίες που " "χρησιμοποιούσε ο Βασιλιάς Λεόρικ για να εξοπλίσει τους ιππότες του. Έφτιαξα " "ακόμη και ένα τεράστιο δίχειρο σπαθί από το καλύτερο μίθριλ για εκείνον, καθώς " "και ένα πολύτιμο στέμμα για να ταιριάζει. Ακόμα δεν μπορώ να πιστέψω πώς " "πέθανε, αλλά πρέπει να ήταν κάποια απαίσια δύναμη που τον τρέλανε!" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Δεν με νοιάζει αυτό. Άκου, κανένας σκελετός δεν θα γίνει ο βασιλιάς ΜΟΥ. Ο " "Λέορικ είναι βασιλιάς. Βασιλιά, με ακούς; ΧΑΙΡΕ ΒΑΣΙΛΙΑ!" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the power " "to raise yet more warriors for an ever growing army of the undead. If you do " "not stop his reign, he will surely march across this land and slay all who " "still live here." msgstr "" "Οι νεκροί που περπατούν ανάμεσα στους ζωντανούς ακολουθούν τον καταραμένο " "Βασιλιά. Έχει τη δύναμη να σηκώσει ακόμα περισσότερους πολεμιστές για έναν " "ολοένα αυξανόμενο στρατό από νεκρούς. Εάν δεν σταματήσεις τη βασιλεία του, " "σίγουρα θα βαδίσει σε αυτή τη γη και θα σκοτώσει όλους όσοι ζουν ακόμα εδώ." #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't care " "about some King that's been dead longer than I've been alive. If you need " "something to use against this King of the undead, then I can help you out..." msgstr "" "Κοίτα, τρέχω μια επιχείρηση εδώ. Δεν πουλάω πληροφορίες και δεν με νοιάζει " "κάποιος Βασιλιάς που είναι νεκρός περισσότερο από όσο έχω ζήσει εγώ. Εάν " "χρειάζεσαι κάτι να χρησιμοποιήσεις εναντίον αυτού του Βασιλιά των απέθαντων, " "τότε μπορώ να σε βοηθήσω..." #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve my " "Master for eternity!" msgstr "" "Η ζεστασιά της ζωής έχει μπει στον τάφο μου. Ετοιμάσου, θνητέ, να υπηρετήσεις " "τον Κύριό μου στην αιώνιότητα!" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has led " "them to believe that it too holds some arcane powers. Hmm, perhaps they are " "not all as smart as we had feared..." msgstr "" "Βλέπω ότι αυτή η περίεργη συμπεριφορά σε προβληματίζει και εσένα. Θα υπέθετα " "ότι εφόσον πολλοί δαίμονες φοβούνται το φως του ήλιου και πιστεύουν ότι έχει " "μεγάλη δύναμη, ότι μπορεί ο ανατέλλοντας ήλιος που απεικονίζεται στην πινακίδα " "για την οποία αναφέρεσαι, να τους έκανε να πιστέψουν ότι έχει αντίστοιχες " "απόκρυφες δυνάμεις. Χμ, ίσως δεν είναι όλοι τους τόσο έξυπνοι όσο φοβόμασταν " "τελικά..." #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I looked " "out from my bedroom, I saw the shapes of small demon-like creatures in the inn " "yard. After a short time, they ran off, but not before stealing the sign to my " "inn. I don't know why the demons would steal my sign but leave my family in " "peace... 'tis strange, no?" msgstr "" "Άρχοντα/ίσσα, έχω μια περίεργη εμπειρία να διηγηθώ. Ξέρω ότι έχεις μεγάλη " "γνώση αυτών των τεράτων που κατοικούν στον λαβύρινθο, αλλά αυτό είναι κάτι που " "δεν μπορώ να καταλάβω, μα τη ζωή μου... Με ξύπνησε κατά τη διάρκεια της νύχτας " "ένας ήχος ξυσίματος ακριβώς έξω από την ταβέρνα μου. Όταν κοίταξα έξω από την " "κρεβατοκάμαρά μου, είδα τις φιγούρες μικρών πλασμάτων που έμοιαζαν με δαίμονες " "στην αυλή του πανδοχείου. Μετά από λίγο, έφυγαν τρέχοντας, αλλά όχι πριν " "κλέψουν την ταμπέλα του πανδοχείου μου. Δεν ξέρω γιατί οι δαίμονες έκλεψαν την " "πινακίδα μου ενώ άφησαν την οικογένειά μου απείραχτη... Είναι περίεργο, όχι;" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save me " "the expense of having another one made. Well, let me see, what could I give " "you as a fee for finding it? Hmmm, what have we here... ah, yes! This cap was " "left in one of the rooms by a magician who stayed here some time ago. Perhaps " "it may be of some value to you." msgstr "" "Ω, δεν χρειάστηκε να φέρεις πίσω την πινακίδα μου, αλλά υποθέτω ότι με " "γλιτώνεις από τα έξοδα να φτιάξω άλλη μία. Λοιπόν, για να δω, τι θα μπορούσα " "να σου δώσω ως αμοιβή που την βρήκες; Χμμμ, τι έχουμε εδώ... αχ, ναι! Αυτός ο " "σκούφος ξεχάστηκε σε ένα από τα δωμάτια μου από έναν μάγο που έμεινε εδώ πριν " "από λίγο καιρό. Ίσως έχει κάποια αξία για σένα." #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes - " "is nothing sacred? I hope that Ogden and Garda are all right. I suppose that " "they would come to see me if they were hurt..." msgstr "" "Θεέ μου, δαίμονες τρέχουν στο χωριό τη νύχτα, λεηλατούν τα σπίτια μας - δεν " "είναι τίποτα ιερό; Ελπίζω ότι ο Όγκντεν και η Γκάρντα είναι καλά. Υποθέτω ότι " "θα είχαν έρθει να με δουν αν είχαν πληγωθεί..." #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept right " "through the whole thing. Thank the Light that those monsters didn't attack the " "inn." msgstr "" "Ω θεε μου! Εκεί βρέθηκε η ταμπέλα; Η γιαγιά μου και εγώ πρέπει να κοιμηθήκαμε " "χωρίς να το πάρουμε χαμπάρι. Ευχαριστώ το Φως που αυτά τα τέρατα δεν " "επιτέθηκαν στο πανδοχείο." #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "Λες ότι οι δαίμονες έκλεψαν την πινακίδα του Όγκντεν; Αυτό δεν μοιάζει καθόλου " "με τις φρικαλεότητες που έχω ακούσει - ή έχω δει.\n" " \n" "Οι δαίμονες ενδιαφέρονται να ξεσκίσουν την καρδιά σου, όχι την ταμπέλα σου." #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece of " "cheese..." msgstr "" "Ξέρεις τι σκέφτομαι; Κάποιος πήρε την ταμπέλα και θα θέλει πολλά χρήματα για " "αυτήν. Αν ήμουν ο Όγκντεν... και δεν είμαι, αλλά αν ήμουν... θα αγόραζα απλώς " "μια νέα πινακίδα με ένα όμορφο σχέδιο πάνω της. Ίσως μια ωραία κούπα μπύρας ή " "ένα κομμάτι τυρί..." #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Κανένας θνητός δεν μπορεί να καταλάβει πραγματικά το μυαλό του δαίμονα.\n" " \n" "Μην αφήσετε ποτέ τις ακανόνιστες ενέργειές τους να σε μπερδέψουν, καθώς αυτό " "μπορεί να είναι και το σχέδιό τους." #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on a " "piece of wood." msgstr "" "Τι; - λέει ότι το πήρα εγώ; Μάλλον και ο Γκρίσγουαλντ είναι μαζί του.\n" " \n" "Κοίτα, ξεπέρασα την απλή κλοπή πριν από μήνες. Δεν μπορείς να αποφέρεις κέρδος " "με ένα κομμάτι ξύλο." #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You no " "leave with life! You kill big uglies and give back Magic. Go past corner and " "door, find uglies. You give, you go!" msgstr "" "Ε, εσύ - εσύ που σκοτώνεις όλα! Μου δίνεις Μαγικό Πανό ή κάνουμε επίθεση! Δεν " "φεύγεις με ζωή σου! Σκοτώνεις μεγάλους άσχημους και δίνεις πίσω Μαγικό. Πέρασε " "από γωνία δεξιά και στην πόρτα, βρες άσχημους. Μου δίνεις, περνάς!" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "Σκοτώνεις άσχημους, παίρνεις πανό. Θα το φέρεις, αλλιώς..." #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" "Δώστε μου! Ναι, όμορφα! Πήγαινε τώρα, είμαστε δυνατοί. Σκοτώνουμε όλους με " "μεγάλη Μαγεία!" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of them " "escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden him " "within the Labyrinth. I do not understand why the Archbishop turned to the " "darkness, or what his interest is in the child, unless he means to sacrifice " "him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say that " "Lazarus was last seen running into the deepest bowels of the labyrinth. You " "must hurry and save the prince from the sacrificial blade of this demented " "fiend!" msgstr "" "Αυτό δεν προμηνύεται καλό, γιατί επιβεβαιώνει τους πιο σκοτεινούς φόβους μου. " "Ενώ δεν επέτρεψα στον εαυτό μου να πιστέψει τους αρχαίους θρύλους, δεν μπορώ " "να τους αρνηθώ τώρα. Ίσως ήρθε η ώρα να αποκαλύψω ποιος είμαι.\n" " \n" "Το αληθινό μου όνομα είναι Ντέκαρντ Κάιν ο Πρεσβύτερος και είμαι ο τελευταίος " "απόγονος μιας αρχαίας Αδελφότητας που ήταν αφιερωμένη στη διαφύλαξη των " "μυστικών ενός διαχρονικού κακού. Ένα κακό που προφανώς έχει πλέον " "απελευθερωθεί.\n" " \n" "Ο Αρχιεπίσκοπος Λάζαρος, κάποτε ο πιο έμπιστος σύμβουλος του βασιλιά Λέορικ, " "οδήγησε μια ομάδα απλών αστών στον Λαβύρινθο για να βρει τον εξαφανισμένο γιο " "του Βασιλιά, τον Άλμπρεχτ. Πέρασε αρκετός καιρός μέχρι να επιστρέψουν, και " "μόνο λίγοι από αυτούς γλίτωσαν με τη ζωή τους.\n" " \n" "Ανάθεμα με τον ανόητο! Θα έπρεπε να είχα υποψιαστεί την καλυμμένη προδοσία του " "τότε. Πρέπει να ήταν ο ίδιος ο Λάζαρος που απήγαγε τον Άλμπρεχτ και έκτοτε τον " "έκρυψε μέσα στον Λαβύρινθο. Δεν καταλαβαίνω γιατί ο Αρχιεπίσκοπος στράφηκε στο " "σκοτάδι, ή ποιο είναι το ενδιαφέρον του για το παιδί, εκτός κι αν θέλει να το " "θυσιάσει στα σκοτεινά αφεντικά του!\n" " \n" "Αυτό πρέπει να είναι το σχεδόν του! Οι επιζώντες της \"ομάδας διάσωσης\" του, " "λένε ότι ο Λάζαρος εθεάθη τελευταία φορά να τρέχει στα βαθύτερα σπλάχνα του " "λαβύρινθου. Πρέπει να βιαστείς και να σώσεις τον πρίγκιπα από τη θυσιαστική " "λεπίδα αυτού του παραφρονημένου κακούργου!" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Πρέπει να βιαστείς και να σώσεις τον Άλμπρεχτ από τα χέρια του Λάζαρου. Ο " "πρίγκιπας και οι άνθρωποι αυτού του βασιλείου βασίζονται σε σένα!" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for his " "horrific deed. The boy that you describe is not our prince, but I believe that " "Albrecht may yet be in danger. The symbol of power that you speak of must be a " "portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once again " "sow chaos in the realm of mankind. You must venture through the portal and " "destroy Diablo before it is too late!" msgstr "" "Η ιστορία σου είναι πολύ ζοφερή φίλε/η μου. Ο Λάζαρος σίγουρα θα καεί στην " "Κόλαση για τη φρικτή πράξη του. Το αγόρι που περιγράφεις δεν είναι ο πρίγκιπας " "μας, αλλά πιστεύω ότι ο Άλμπρεχτ μπορεί ακόμα να βρίσκεται σε κίνδυνο. Το " "σύμβολο της δύναμης για το οποίο μιλάς πρέπει να είναι μια πύλη που οδηγεί " "στην ίδια την καρδιά του λαβύρινθου.\n" " \n" "Μάθε αυτό, φίλε/η μου - Το κακό στο οποίο εναντιώνεσαι είναι ο σκοτεινός " "Άρχοντας του Τρόμου. Είναι γνωστός στους θνητούς άντρες ως Ντιάμπλο. Αυτός " "ήταν που φυλακίστηκε μέσα στον Λαβύρινθο πριν από πολλούς αιώνες και φοβάμαι " "ότι επιδιώκει να σπείρει ξανά το χάος στον κόσμο της ανθρωπότητας. Πρέπει να " "περάσεις από την πύλη και να καταστρέψεις τον Ντιάμπλο πριν να είναι πολύ αργά!" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the labyrinth. " "I lost many good friends that day, and Lazarus never returned. I suppose he " "was killed along with most of the others. If you would do me a favor, good " "master - please do not talk to Farnham about that day." msgstr "" "Ο Λάζαρος ήταν ο Αρχιεπίσκοπος που οδήγησε πολλούς από τους κατοίκους της " "πόλης στον λαβύρινθο. Έχασα πολλούς καλούς φίλους εκείνη την ημέρα και ο " "Λάζαρος δεν επέστρεψε ποτέ. Υποθέτω ότι σκοτώθηκε μαζί με τους άλλους. Κάνε " "μου τη χάρη, Άρχοντα/ίσσα - σε παρακαλώ μην μιλήσεις στον Φάρνχαμ για εκείνη " "την ημέρα." #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Σοκαρίστηκα όταν άκουσα τι σχεδίαζαν να κάνουν οι κάτοικοι της πόλης εκείνο το " "βράδυ. Νόμιζα ότι από όλους τους ανθρώπους, ο Λάζαρος θα είχε περισσότερη " "λογική από αυτό. Ήταν Αρχιεπίσκοπος και πάντα φαινόταν να νοιάζεται τόσο πολύ " "για τους κατοίκους της πόλης της Τρίστραμ. Τόσοι πολλοί τραυματίστηκαν, που " "δεν μπόρεσα να τους σώσω όλους..." #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Θυμάμαι τον Λάζαρο ως έναν πολύ ευγενικό και χαρισματικό άνθρωπο. Μίλησε στην " "κηδεία της μητέρας μου και στήριξε τη γιαγιά μου και εμένα σε μια πολύ " "ταραγμένη περίοδο. Προσεύχομαι κάθε βράδυ, με κάποιο τρόπο, να είναι ακόμα " "ζωντανός και ασφαλής." #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so much " "as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Ήμουν εκεί όταν ο Λάζαρος μας οδήγησε στον λαβύρινθο. Μίλησε για ιερή " "ανταπόδοση, αλλά όταν αρχίσαμε να πολεμάμε αυτά τα κολασμένα πλάσματα, δεν " "σήκωσε το μαχαίρι του εναντίον τους. Απλώς έτρεξε πιο βαθιά μέσα στους " "σκοτεινούς, ατελείωτους θαλάμους που ήταν γεμάτοι με τους υπηρέτες του σκότους!" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their blood " "spilling out all over the floor... all his fault..." msgstr "" "Μαχαιρώνουν, μετά δαγκώνουν και μετά είναι παντού γύρω σου. Ψεύτης! ΨΕΥΤΗΣ! " "Είναι όλοι νεκροί! Νεκροί! Με ακούς? Συνεχίζουν να πέφτουν και να πέφτουν... " "το αίμα τους χύνεται και γεμίζουν τα πατώματα... Αυτός φταίει..." #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great conflict " "within his being. He poses a great danger, and will stop at nothing to serve " "the powers of darkness which have claimed him as theirs." msgstr "" "Δεν ξέρω αυτόν τον Λάζαρο για τον οποίο μιλάς, αλλά αισθάνομαι μια μεγάλη " "σύγκρουση μέσα στην ύπαρξή του. Αποτελεί έναν μεγάλο κίνδυνο και δεν θα " "σταματήσει με τίποτα να υπηρετεί τις δυνάμεις του σκότους που τον έχουν " "διεκδικήσει ως δικό τους." #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters down " "there. Didn't help save my leg, did it? Look, I'll give you a free piece of " "advice. Ask Farnham, he was there." msgstr "" "Ναι, ο δίκαιος Λάζαρος, ο οποίος ήταν τόόόσο αποτελεσματικός εναντίον εκείνων " "των τεράτων εκεί κάτω. Δεν με βοήθησε να σώσω το πόδι μου, σωστά; Κοίτα, θα " "σου δώσω μια δωρεάν συμβουλή. Ρωτήστε τον Φάρνχαμ, ήταν εκεί." #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! You " "are too late to save the child. Now you will join him in Hell!" msgstr "" "Εγκαταλείψε την ανόητη αναζήτησή σου. Το μόνο που σε περιμένει είναι η οργή " "του Άρχοντα μου! Είναι πολύ αργά για να σώσεις το παιδί. Τώρα θα το " "συναντήσεις στην Κόλαση!" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I have " "heard of a tunnel that leads to a great lake - perhaps they are one and the " "same. Unfortunately, I do not know what would cause our water supply to be " "tainted." msgstr "" "Χμμ, δεν ξέρω τι μπορώ πραγματικά να σου πω για αυτό, που θα βοηθούσε. Το νερό " "που γεμίζει τα πηγάδια μας προέρχεται από μια υπόγεια πηγή. Έχω ακούσει για " "ένα τούνελ που οδηγεί σε μια μεγάλη λίμνη - ίσως είναι ένα και το αυτό. " "Δυστυχώς, δεν γνωρίζω τι θα μπορούσε να προκαλέσει τη μόλυνση του νερού μας." #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, even " "our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Πάντα προσπαθούσα να διατηρήσω μια μεγάλη προσφορά τροφίμων και ποτών στο " "κελάρι μας, αλλά καθώς ολόκληρη η πόλη δεν έχει πηγή γλυκού νερού, ακόμη και " "τα αποθέματα μου σύντομα θα τελειώσουν.\n" " \n" "Σε παρακαλώ, κάνε ό,τι μπορείς γιατί δεν ξέρω τι θα απογίνουμε." #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "Χαίρομαι που σε πρόλαβα εγκαίρως! Τα πηγάδια μας έχουν γίνει υφάλμυρα και " "στάσιμα και κάποιοι από τους κατοίκους της πόλης έχουν αρρωστήσει πίνοντας από " "αυτά. Τα αποθέματα πόσιμου νερού, μας τελειώνουν γρήγορα. Πιστεύω ότι υπάρχει " "ένα πέρασμα που οδηγεί στις πηγές που προμηθεύουν την πόλη μας. Παρακαλώ βρες " "τι προκάλεσε αυτή την καταστροφή, διαφορετικά θα χαθούμε όλοι." #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Σε παρακαλώ, πρέπει να βιαστείς. Κάθε ώρα που περνάει μας φέρνει πιο κοντά στο " "να μην έχουμε νερό να πιούμε.\n" " \n" "Δεν μπορούμε να επιβιώσουμε για πολύ χωρίς τη βοήθειά σου." #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water to " "become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "Τι είναι αυτό που λες - η απλή παρουσία των δαιμόνων είχε κάνει το νερό να " "μολυνθεί; Ω, πραγματικά ένα μεγάλο κακό κρύβεται κάτω από την πόλη μας, αλλά η " "επιμονή και το θάρρος σου μας δίνουν ελπίδα. Πάρε αυτό το δαχτυλίδι - ίσως θα " "σε βοηθήσει στην καταστροφή αυτών των άθλιων πλασμάτων." #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Η γιαγιά μου είναι πολύ αδύναμη και η Γκάρντα λέει ότι δεν μπορούμε να πιούμε " "το νερό από τα πηγάδια. Σε παρακαλώ, μπορείς να κάνεις κάτι για να μας " "βοηθήσεις;" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I have " "tried to clear one of the smaller wells, but it reeks of stagnant filth. It " "must be getting clogged at the source." msgstr "" "Ο Πεπίν σου είπε την αλήθεια. Θα χρειαστούμε πολύ πόσιμο νερό και μάλιστα " "σύντομα. Προσπάθησα να καθαρίσω ένα από τα μικρότερα πηγάδια, αλλά μυρίζει " "στάσιμη βρωμιά. Πρέπει να έχει βουλώσει και βρωμίσει στην πηγή." #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "Πίνεις νερό;" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "Οι κάτοικοι της Τρίστραμ θα πεθάνουν εάν δεν μπορέσεις να επαναφέρεις το " "πόσιμο νερό στα πηγάδια τους.\n" " \n" "Να ξέρεις - Δαίμονες βρίσκονται στο επίκεντρο αυτού του ζητήματος, αλλά είναι " "ανίδεοι για το τι έχουν προκαλέσει." #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Για πρώτη φορά είμαι μαζί σου. Η επιχείρησή μου στεγνώνει - να το πω έτσι - αν " "δεν έχω αγορά να πουλήσω. Καλύτερα να μάθεις τι συμβαίνει και σύντομα!" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in the " "attempt to steal that treasure would be forever bound to defend it. A twisted, " "but strangely fitting, end?" msgstr "" "Ένα βιβλίο που μιλάει για θάλαμο ανθρώπινων οστών; Λοιπόν, ένας θάλαμος των " "οστών αναφέρεται σε ορισμένες αρχαϊκές γραφές που μελέτησα στις βιβλιοθήκες " "της Ανατολής. Αυτοί οι τόμοι συμπέραναν ότι όταν οι Άρχοντες του κάτω κόσμου " "ήθελαν να προστατεύσουν μεγάλους θησαυρούς, θα δημιουργούσαν περιοχές όπου " "όσοι πέθαιναν, στην προσπάθεια να κλέψουν αυτόν τον θησαυρό, θα ήταν για πάντα " "δεσμευμένοι για να τον προστατέψουν. Ένα διεστραμμένο, αλλά παράξενα ταιριαστό " "τέλος;" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has many " "books that may be of some help." msgstr "" "Φοβάμαι ότι δεν ξέρω τίποτα για αυτό, καλέ/ή μου άρχοντα/ίσσα. Ο Κάιν έχει " "πολλά βιβλία που μπορεί να σε βοηθήσουν." #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "" "Αυτό μου ακούγεται σαν ένα πολύ επικίνδυνο μέρος. Εάν τολμήσεις να πας εκεί, " "σε παρακαλώ να είσαι πολύ προσεκτικός/ή." #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Φοβάμαι ότι δεν έχω ακούσει τίποτα για αυτό. Ίσως ο Κάιν ο Αφηγητής θα " "μπορούσε να βοηθήσει." #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about many " "things, and it would not surprise me if he had some answers to your question." msgstr "" "Δεν ξέρω τίποτα για αυτό το μέρος, αλλά μπορείς να δοκιμάσεις να ρωτήσεις τον " "Κάιν. Μιλάει για πολλά πράγματα και δεν θα με εξέπληξε αν είχε κάποιες " "απαντήσεις στην ερώτησή σου." #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Εντάξει, άκου λοιπόν. Υπάρχει αυτός ο θάλαμος από ξύλο, βλέπεις. Και η γυναίκα " "του, ξέρεις - αυτή - λέει στο δέντρο... γιατί, πρέπει να περιμένεις. Μετά λέω, " "αυτό μπορεί να λειτουργήσει εναντίον του, αλλά αν νομίζεις ότι θα ΠΛΗΡΩΣΩ γι' " "αυτό... εσύ... ε... ναι." #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish within " "this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Θα γίνεις αιώνιος υπηρέτης των σκοτεινών αρχόντων αν πέσεις μέσα σε αυτόν τον " "καταραμένο μέρος.\n" " \n" "Μπες στον Θάλαμο των Οστών με δική σου ευθύνη." #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "Ένας τεράστιος και μυστηριώδης θησαυρός, λες; Ίσως θα με ενδιέφερε να πάρω " "μερικά πράγματα από σένα... ή καλύτερα, δεν χρειάζεσαι σπάνιες και ακριβές " "προμήθειες εξοπλισμού για να ξεπεράσεις αυτή τη δοκιμασία;" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - left " "in the clutches of unspeakable horrors - to die." msgstr "" "Φαίνεται ότι ο Αρχιεπίσκοπος Λάζαρος παρότρυνε πολλούς από τους κατοίκους της " "πόλης να τολμήσουν να μπουν στον Λαβύρινθο για να βρουν τον εξαφανισμένο γιο " "του Βασιλιά. Έπαιξε πάνω στους φόβους τους και τους μετάτρεψε σε έναν ξέφρενο " "όχλο. Κανείς τους δεν ήταν προετοιμασμένος για αυτό που βρισκόταν μέσα στην " "κρύα γη... Ο Λάζαρος τους εγκατέλειψε εκεί κάτω - αφημένους στα νύχια " "ανείπωτων τρόμων - για να πεθάνουν." #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a fierce " "weapon. I believe he called him a butcher." msgstr "" "Ναι, ο Φάρνχαμ μουρμούρισε κάτι για ένα ογκώδες θηρίο που κρατούσε ένα άγριο " "όπλο. Πιστεύω ότι τον ονόμασε χασάπη." #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "Μα το Φως, γνωρίζω αυτόν τον ποταπό δαίμονα. Υπήρχαν πολλοί που έφεραν τα " "σημάδια της οργής του στα σώματά τους, όταν οι λίγοι επιζώντες της επίθεσης με " "επικεφαλής τον Λάζαρο, σύρθηκαν από τον Καθεδρικό Ναό. Δεν ξέρω με τι συνήθιζε " "να τεμαχίζει τα θύματά του, αλλά δεν θα μπορούσε να ήταν όπλο αυτού του " "κόσμου. Άφηνε πληγές που γεμίζαν από ασθένειες και ακόμη και εγώ, τις έβρισκα " "σχεδόν αδύνατο να τις θεραπεύσω. Προσοχή αν σκοπεύεις να πολεμήσεις αυτό το " "κτήνος..." #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Όταν ο Φάρνχαμ είπε κάτι για έναν χασάπη που σκότωσε ανθρώπους, το αγνόησα " "αμέσως. Αλλά μιας και το ανέφερες εσύ, ίσως είναι αλήθεια." #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and cutting " "down brave men where they stood. I was separated from the fray by a host of " "small screeching demons and somehow found the stairway leading out. I never " "saw that hideous beast again, but his blood-stained visage haunts me to this " "day." msgstr "" "Είδα αυτό που ο Φάρνχαμ αποκαλεί χασάπη, καθώς διέσχιζε ένα μονοπάτι ανάμεσα " "από τα πτώματα των φίλων μου. Κουνούσε ένα μαχαίρι μεγάλο όσο ένα τσεκούρι, " "κόβοντας μέλη και κόβοντας γενναίους άνδρες εκεί που στέκονταν. Με χώρισαν από " "τη μάχη μια σειρά από μικρούς δαίμονες που ουρλιάζουν και με κάποιο τρόπο " "βρήκα τη σκάλα που οδηγεί έξω. Δεν είδα ποτέ ξανά αυτό το αποτρόπαιο θηρίο, " "αλλά το αιματοβαμμένο πρόσωπό του με στοιχειώνει μέχρι σήμερα." #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "Μεγάλος! Μεγάλος μπαλτάς σκοτώνει όλους τους φίλους μου. Δεν μπορώ να τον " "σταματήσω, έπρεπε να τρέξω μακριά, δεν μπορώ να τους σώσω. Παγιδευμένος σε ένα " "δωμάτιο, με τόσα κορμιά... τόσους φίλους... ΟΧΙΙΙΙΙΙΙΙΙ!" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "Ο χασάπης είναι ένα σαδιστικό πλάσμα που απολαμβάνει τα βασανιστήρια και τον " "πόνο των άλλων. Έχεις δει τη \"δουλειά\" του στον μεθυσμένο Φάρνχαμ. Η " "καταστροφή του θα κάνει πολλά για να διασφαλίσει την ασφάλεια αυτού του χωριού." #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got a " "hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to his " "collection." msgstr "" "Ξέρω περισσότερα από όσα θα φανταζόσουν για αυτό το φρικτό Θηρίο. Οι μικροί " "του \"φίλοι\" με έπιασαν και κατάφεραν να μου κόψουν το πόδι, πριν με βγάλει ο " "Γκρίσγουαλντ από εκείνη την τρύπα.\n" " \n" "Θα το πω ευθέως - σκότωσε το πριν σε σκοτώσει και προσθέσει το πτώμα σου στη " "συλλογή του." #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find the " "lost prince. The bastard led us into a trap! Now everyone is dead... killed by " "a demon he called the Butcher. Avenge us! Find this Butcher and slay him so " "that our souls may finally rest..." msgstr "" "Παρακαλώ, άκουσε με. Ο Αρχιεπίσκοπος Λάζαρος, μας οδήγησε εδώ κάτω για να " "βρούμε τον χαμένο πρίγκιπα. Το κάθαρμα μας οδήγησε σε παγίδα! Τώρα όλοι είναι " "νεκροί... σκοτωμένοι από έναν δαίμονα που τον ανέφερε ως Χασάπη. Εκδικήσου " "μας! Βρες αυτόν τον Χασάπη και σκότωσε τον για να αναπαυθούν επιτέλους οι " "ψυχές μας..." #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the Halls " "of the Blind..." msgstr "" "Απαγγέλλεις ένα ενδιαφέρον στιχάκι γραμμένο σε ύφος που μου θυμίζει άλλα έργα. " "Κάτσε να σκεφτώ τώρα - πώς ήταν;\n" " \n" "...Το σκοτάδι σκεπάζει το Κρυφό. Μάτια που λάμπουν αόρατα μόνο με τους ήχους " "των κοφτερών νυχιών που ξύνουν στιγμιαία για να βασανίσουν εκείνες τις φτωχές " "ψυχές που έχουν μείνει αόματες για όλη την αιωνιότητα. Η φυλακή για αυτούς που " "είναι τόσο καταραμένοι ονομάζεται η Αίθουσα των Τυφλών..." #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Ποτέ δεν ασχολήθηκα πολύ με την ποίηση. Περιστασιακά, είχα λόγο να προσλάβω " "βάρδους όταν το πανδοχείο πήγαινε καλά, αλλά αυτό μοιάζει πλέον ότι έγινε εδώ " "και πολύ καιρό πριν.\n" " \n" "Τι? Α, ναι... ε, καλά, υποθέτω ότι θα μπορούσες να δεις τι ξέρει κάποιος άλλος." #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very much " "like that poem while researching the history of demonic afflictions. It spoke " "of a place of great evil that... wait - you're not going there are you?" msgstr "" "Αυτό κατά κάποιο τρόπο μου ακούγεται γνωστό. Φαίνεται να θυμάμαι ότι διάβασα " "κάτι πολύ παρόμοιο με αυτό το ποίημα ενώ ερευνούσα την ιστορία των δαιμονικών " "δεινών. Μίλησε για ένα μέρος μεγάλου κακού που... περίμενε - δεν σκοπεύεις να " "πας εκεί;" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he can " "help you, too." msgstr "" "Εάν έχεις ερωτήσεις σχετικά με την τύφλωση, θα πρέπει να μιλήσετε με τον " "Πέπιν. Ξέρω ότι έδωσε στη γιαγιά μου ένα φίλτρο που της βοήθησε να καθαρίσει " "την όρασή της, οπότε ίσως μπορεί να βοηθήσει και εσένα." #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your vivid " "description, my friend. Perhaps Cain the Storyteller could be of some help." msgstr "" "Φοβάμαι ότι ούτε έχω ακούσει ούτε έχω δει ένα μέρος που να ταιριάζει με τη " "ζωντανή περιγραφή σου, φίλε μου. Ίσως ο Κάιν ο αφηγητής ιστοριών θα μπορούσε " "να βοηθήσει." #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "Κοίτα εδώ... είναι πολύ αστείο, ε; Το έπιασες; Τυφλός - κοιτά εδώ;" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Αυτό είναι ένα μέρος μεγάλης αγωνίας και τρόμου, και έτσι εξυπηρετεί καλά τον " "Άρχοντα του.\n" " \n" "Περπάτα προσεκτικά, διαφορετικά μπορεί να μείνεις πολύ περισσότερο από όσο " "περίμενες." #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell you " "about this? No. Are you now leaving and going to talk to the storyteller who " "lives for this kind of thing? Yes." msgstr "" "Για να δούμε, σου πουλάω κάτι; Όχι. Μου δίνεις χρήματα για να σου πω γι' αυτό; " "Όχι. Φεύγεις τώρα, για να μιλήσεις με τον αφηγητή ιστοριών που ζει για κάτι " "τέτοιο; Ναι." #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his life. " "Lachdanan was an honorable and just man who served his King faithfully for " "years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan would " "be the least likely to submit to the darkness without a fight, so I suppose " "that your story could be true. If I were in your place, my friend, I would " "find a way to release him from his torture." msgstr "" "Υποστηρίζεις ότι έχεις μιλήσει με τον Λακντάναν; Υπήρξε μεγάλος ήρωας όταν " "ζούσε. Ο Λακντάναν ήταν ένας έντιμος και δίκαιος άνθρωπος που υπηρέτησε πιστά " "τον Βασιλιά του για χρόνια. Αλλά φυσικά, το γνωρίζεις ήδη αυτό.\n" " \n" "Από αυτούς που πιάστηκαν στην Κατάρα του Βασιλιά, ο Λακντάναν θα ήταν λιγότερο " "πιθανό να υποταχθεί στο σκοτάδι χωρίς μάχη, οπότε υποθέτω ότι η ιστορία σου θα " "μπορούσε να είναι αληθινή. Αν ήμουν στη θέση σου, φίλε μου, θα έβρισκα τρόπο " "να τον απαλλάξω από το μαρτύριο του." #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "Μιλάς για έναν γενναίο πολεμιστή που είναι νεκρός εδώ και καιρό! Δεν θα κάνω " "κουβέντα για ψυχές που έχουν φύγει, στην αυλή του πανδοχείου μου, σε ευχαριστώ " "πολύ." #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Χρυσό ελιξίριο, λες. Δεν έχω φτιάξει ποτέ ένα φίλτρο αυτού του χρώματος, οπότε " "δεν μπορώ να σου πω πώς θα σε επηρέαζε αν προσπαθούσες να το πιείς. Ως " "θεραπευτής σου, σε συμβουλεύω ανεπιφύλακτα ότι εάν βρείτε ένα τέτοιο ελιξίριο, " "να κάνετε ό,τι σας ζητήσει ο Λακντάναν και ΜΗΝ προσπαθήσετε να το " "χρησιμοποιήσεις." #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Δεν έχω ξανακούσει για κάποιον Λακντάναν. Λυπάμαι, αλλά δεν νομίζω ότι μπορώ " "να σε βοηθήσω." #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest and " "loyal in nature. The curse that fell upon the followers of King Leoric would " "fall especially hard upon him." msgstr "" "Εάν όντως συνάντησες τον Λακντάναν, τότε θα σε συμβούλευα να τον βοηθήσεις. " "Ασχολήθηκα μαζί του πολλές φορές και είναι ειλικρινής και πιστός. Η κατάρα που " "έπεσε πάνω στους ακόλουθους του βασιλιά Λεόρικ θα είναι ιδιαίτερα σκληρή πάνω " "του." #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into thinking " "any other way. You can't talk to the dead. I know!" msgstr "" " Ο Λακντάναν είναι νεκρός. Όλοι το ξέρουν αυτό και δεν μπορείς να με " "ξεγελάσεις να σκεφτώ αλλιώς. Δεν μπορείς να μιλήσεις στους νεκρούς. Το ξέρω!" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Μπορεί να συναντήσεις άτομα που είναι παγιδευμένα μέσα στον Λαβύρινθο, όπως " "τον Λακντάναν.\n" " \n" "Νιώθω μέσα του μεγάλη τιμή και μεγάλη ενοχή. Βοήθησέ τον, και θα βοήθησες όλο " "το Τρίστραμ." #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be buying " "something or you'll be on your way." msgstr "" "Περίμενε, άσε με να μαντέψω. Ο Κάιν καταβροχθίστηκε από μια γιγαντιαία σχισμή " "που άνοιξε κάτω από τα πόδια του. Αποτεφρώθηκε μέσα σε μια μπάλα κολασμένης " "φωτιάς και δεν μπορεί πλέον να απαντήσει στις ερωτήσεις σου. Α, δεν έγινε " "αυτό; Τότε υποθέτω ότι θα αγοράσεις κάτι ή διαφορετικά σου εύχομαι καλό δρόμο." #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his dark " "Curse fell upon us for the role we played in his tragic death. As my fellow " "Knights succumbed to their twisted fate, I fled from the King's burial " "chamber, searching for some way to free myself from the Curse. I failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul to " "rest, but I have been unable to find it. My strength now wanes, and with it " "the last of my humanity as well. Please aid me and find the Elixir. I will " "repay your efforts - I swear upon my honor." msgstr "" "Σε παρακαλώ, μη με σκοτώσεις, απλώς άκουσέ με. Ήμουν κάποτε Λοχαγός των " "Ιπποτών του Βασιλιά Λέορικ, και υπηρετούσα τους νόμους αυτής της χώρας με " "δικαιοσύνη και τιμή. Τότε η σκοτεινή του κατάρα έπεσε πάνω μας για τον ρόλο " "που παίξαμε στον τραγικό θάνατό του. Καθώς οι συνάδελφοί μου Ιππότες υπέκυψαν " "στη διεστραμμένη μοίρα τους, έφυγα από τον ταφικό θάλαμο του Βασιλιά, " "αναζητώντας κάποιον τρόπο να ελευθερωθώ από την Κατάρα. Απέτυχα...\n" " \n" "Έχω ακούσει για ένα Χρυσό Ελιξίριο που θα μπορούσε να σηκώσει την Κατάρα και " "να αφήσει την ψυχή μου να ξεκουραστεί, αλλά δεν κατάφερα να το βρω. Η δύναμή " "μου τώρα μειώνεται, και μαζί της και η τελευταία ανθρωπιά μου. Παρακαλώ " "βοήθησε με και βρες το Ελιξίριο. Θα σε ανταμείψω για την προσπάθεια σου - " "ορκίζομαι στην τιμή μου." #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "Δεν έχεις βρει το Χρυσό Ελιξίριο. Φοβάμαι ότι είμαι καταδικασμένος στην " "αιωνιότητα. Σε παρακαλώ, συνέχισε να προσπαθείς..." #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find it, " "but for now - take my helm. On the journey I am about to take I will have " "little use for it. May it protect you against the dark powers below. Go with " "the Light, my friend..." msgstr "" "Έσωσες την ψυχή μου από την καταδίκη, και γι' αυτό είμαι υπόχρεος σου. Αν " "υπάρχει ποτέ τρόπος να σου το ανταποδώσω πέρα από τον τάφο, θα τον βρω, αλλά " "προς το παρόν - πάρε το κράνος μου. Στο ταξίδι που πρόκειται να κάνω δεν θα το " "χρειαστώ καθόλου. Είθε να σε προστατεύσει από τις σκοτεινές δυνάμεις του κάτω " "κόσμου. Περπάτα με το φως φίλε μου..." #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched for, " "but never found. Crafted from the metallic bones of the Razor Pit demons, the " "Anvil of Fury was smelt around the skulls of the five most powerful magi of " "the underworld. Carved with runes of power and chaos, any weapon or armor " "forged upon this Anvil will be immersed into the realm of Chaos, imbedding it " "with magical properties. It is said that the unpredictable nature of Chaos " "makes it difficult to know what the outcome of this smithing will be..." msgstr "" "Ο Γκρίσγουαλντ μιλάει για το Αμόνι της Οργής- ένα θρυλικό τεχνούργημα που το " "αναζητούσαν εδώ και πολύ καιρό, αλλά δεν βρέθηκε ποτέ. Φτιαγμένο από τα " "μεταλλικά οστά των δαιμόνων του λάκκου του ξυραφιού, το Αμόνι της Οργής " "χυτεύθηκε γύρω από τα κρανία των πέντε πιο ισχυρών μάγων του κάτω κόσμου. " "Σκαλισμένο με ρούνους δύναμης και χάους, οποιοδήποτε όπλο ή πανοπλία " "σφυρηλατηθεί πάνω σε αυτό το Αμόνι θα βυθιστεί στο βασίλειο του Χάους, " "εμβαπτίζοντάς το με μαγικές ιδιότητες. Λέγεται ότι η απρόβλεπτη φύση του Χάους " "καθιστά δύσκολο να γνωρίζουμε ποιο θα είναι το αποτέλεσμα αυτής της " "σφυρηλάτησης..." #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? He's " "quite handy, you know." msgstr "" "Δεν πιστεύεις ότι ο Γκρίσγουαλντ θα ήταν το καλύτερο άτομο να ρωτήσεις γι' " "αυτό; Είναι πολύ χρήσιμος, ξέρεις." #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the Silver " "Chalice of Purification, I could have assisted you, my friend. However, in " "this matter, you would be better served to speak to either Griswold or Cain." msgstr "" "Αν έψαχνες για πληροφορίες για το Γουδοχέρι της Θεραπείας ή το Ασημένιο " "Δισκοπότηρο της Κάθαρσης, θα μπορούσα να σε βοηθήσω, φίλε μου. Ωστόσο, σε αυτό " "το θέμα, θα ήταν καλύτερα να μιλήσεις είτε με τον Γκρίσγουαλντ είτε με τον " "Κάιν." #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. Whenever " "the earth moves, I always remember that story." msgstr "" "Ο πατέρας του Γκρισγουάλντ συνήθιζε να λέει σε μερικούς από εμάς όταν " "μεγαλώναμε μικροί για ένα γιγάντιο αμόνι που χρησιμοποιήθηκε για την κατασκευή " "πανίσχυρων όπλων. Είπε ότι όταν χτυπούσαν ένα σφυρί σε αυτό το αμόνι, το " "έδαφος ταρακουνούσε με μεγάλη οργή. Όποτε ταρακουνάει η γη, πάντα θυμάμαι αυτή " "την ιστορία." #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know that " "you have been venturing deeper into the Labyrinth, and there is a story I was " "told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of legends " "I had heard in my youth about the burning Hellforge where powerful weapons of " "magic are crafted. The legend had it that deep within the Hellforge rested the " "Anvil of Fury! This Anvil contained within it the very essence of the demonic " "underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with great " "power. If this anvil is indeed the Anvil of Fury, I may be able to make you a " "weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "Χαιρετισμούς! Είναι πάντα χαρά να βλέπω τον καλύτερο μου πελάτη! Ξέρω ότι " "έχεις μπει πιο βαθιά στον Λαβύρινθο και υπάρχει μια ιστορία που μου είχαν πει, " "που μπορεί να αξίζει τον χρόνο να την ακούσεις...\n" " \n" "Ένας από τους άντρες που επέστρεψαν από τον Λαβύρινθο μου είπε για ένα " "μυστικιστικό αμόνι που είδε κατά την απόδρασή του. Η περιγραφή του μου θύμισε " "θρύλους που είχα ακούσει στα νιάτα μου για το φλεγόμενο σιδηρουργείο της " "κολάσεως όπου κατασκευάζονται ισχυρά όπλα μαγείας. Ο θρύλος έλεγε ότι βαθιά " "μέσα στο σιδηρουργείο βρισκόταν το Αμόνι της Οργής! Αυτό το Αμόνι περιείχε " "μέσα του την ίδια την ουσία του δαιμονικού κάτω κόσμου...\n" " \n" "Λέγεται ότι κάθε όπλο που κατασκευάζεται πάνω στο φλεγόμενο αμόνι είναι " "εμποτισμένο με μεγάλη δύναμη. Αν αυτό το αμόνι είναι πράγματι το Αμόνι της " "Οργής, ίσως μπορέσω να σου φτιάξω ένα όπλο ικανό να νικήσει ακόμα και τον πιο " "σκοτεινό Άρχοντα της Κόλασης!\n" " \n" "Βρές μου το Αμόνι και θα πιάσω δουλειά!" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could be " "your best hope, and I am sure that I can make you one of legendary proportions." msgstr "" "Τίποτα ακόμα, ε; Λοιπόν, συνέχισε να ψάχνεις. Ένα όπλο σφυρηλατημένο στο Αμόνι " "θα μπορούσε να είναι η καλύτερη ελπίδα σου, και είμαι σίγουρος ότι μπορώ να " "σου σφυρηλατήσω ένα, θρυλικών διαστάσεων." #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. Now " "we'll show those bastards that there are no weapons in Hell more deadly than " "those made by men! Take this and may Light protect you." msgstr "" "Δεν μπορώ να το πιστέψω! Αυτό είναι το Αμόνι της Οργής - καλή δουλειά, φίλε " "μου. Τώρα θα δείξουμε σε αυτά τα καθάρματα ότι δεν υπάρχουν όπλα στην Κόλαση " "πιο θανατηφόρα από αυτά που φτιάχνουν οι άνθρωποι! Πάρε αυτό και εύχομαι το " "Φως να σε προστατεύσει." #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Ο Γκρίσγουαλντ δεν μπορεί να πουλήσει το αμόνι του. Τι θα κάνει τότε; Και εγώ " "θα θύμωνα και εγώ αν κάποιος μου έπαιρνε το αμόνι μου!" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be used " "by either the Light or the Darkness. Securing the Anvil from below could shift " "the course of the Sin War towards the Light." msgstr "" "Υπάρχουν πολλά τεχνουργήματα μέσα στον Λαβύρινθο που έχουν δυνάμεις πέρα από " "την κατανόηση των θνητών. Μερικά από αυτά έχουν φανταστική δύναμη που μπορεί " "να χρησιμοποιηθεί είτε από το Φως είτε από το Σκοτάδι. Η εξασφάλιση του " "αμονιού από τον κάτω κόσμο θα μπορούσε να μετατοπίσει την πορεία του Πολέμου " "της Αμαρτίας προς το Φως." #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious damper " "on my business here. Awwww, you'll never find it." msgstr "" "Εάν έβρισκες αυτό το τεχνούργημα για τον Γκρίσγουαλντ, θα μπορούσε να θέσει " "σοβαρά προβλήματα στην επιχείρησή μου εδώ. Ωχ τι λέω, δεν θα το βρεις ποτέ." #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a pool " "of boiling blood atop its bone encrusted surface. There are also allusions to " "Stones of Blood that will open a door that guards an ancient treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and chase " "the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don Valor " "once more. Perhaps you are that hero..." msgstr "" "Η Πύλη του Αίματος και οι Αίθουσες της Φωτιάς είναι ορόσημα μυστικιστικής " "προέλευσης. Από όπου κι αν προέρχεται αυτό το βιβλίο που διάβασες, είναι " "σίγουρα ένα μέρος με μεγάλη δύναμη.\n" " \n" "Οι θρύλοι μιλούν για ένα βάθρο που είναι λαξευμένο από πέτρα οψιανού και έχει " "μια λίμνη από βραστό αίμα στην επιφανειακή επιφάνειά του που είναι από " "κόκκαλα. Υπάρχουν επίσης νύξεις για Πέτρες από Αίμα που θα ανοίξουν μια πόρτα " "που φυλάει έναν αρχαίο θησαυρό...\n" " \n" "Η φύση αυτού του θησαυρού καλύπτεται από εικασίες, φίλε μου, αλλά λέγεται ότι " "ο αρχαίος ήρωας Αρκάϊν τοποθέτησε την Ιερή Πανοπλία της Τιμής σε ένα μυστικό " "θησαυροφυλάκιο. Ο Αρκάϊν ήταν ο πρώτος θνητός που αντέστρεψε την ροή του " "Πολέμου της Αμαρτίας και κυνήγησε τις λεγεώνες του σκότους πίσω στις " "Φλεγόμενες Κολάσεις.\n" " \n" "Λίγο πριν πεθάνει ο Αρκάϊν, η πανοπλία του κρύφθηκε σε ένα μυστικό " "θησαυροφυλάκιο. Λέγεται ότι όταν αυτή η ιερή πανοπλία θα χρειαστεί ξανά, ένας " "ήρωας θα αναδυθεί για να φορέσει την Τιμή για άλλη μια φορά. Ίσως εσύ να είσαι " "αυτός ο ήρωας..." #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor known " "as Valor. If you could find its resting place, you would be well protected " "against the evil in the Labyrinth." msgstr "" "Κάθε παιδί ακούει την ιστορία του πολεμιστή Αρκάϊν και της μυστικιστικής " "πανοπλίας του, γνωστής ως Τιμή. Αν μπορούσες να βρεις τον τόπο ανάπαυσής της, " "θα είσαι καλά προστατευμένος/η από το κακό στον Λαβύρινθο." #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Χμ... ακούγεται σαν κάτι που πρέπει να θυμάμαι, αλλά ήμουν τόσο απασχολημένος " "με το να μαθαίνω νέες θεραπείες και να δημιουργώ καλύτερα ελιξίρια που πρέπει " "να το έχω ξεχάσει. Συγνώμη..." #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the boys " "talk about. You had better ask one of the men in the village." msgstr "" "Η ιστορία της μαγικής πανοπλίας που ονομάζεται Τιμή είναι κάτι για το οποίο " "άκουσα συχνά τα αγόρια να μιλάνε. Καλύτερα να ρωτήσεις έναν από τους άντρες " "του χωριού." #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it well, " "my friend, and it will take more than a bit of luck to unlock the secrets that " "have kept it concealed oh, lo these many years." msgstr "" "Η πανοπλία γνωστή ως Τιμή θα μπορούσε να είναι αυτό που γείρει τη ζυγαριά υπέρ " "σου. Θα σου πω ότι πολλοί το έχουν ψάξει - συμπεριλαμβανομένου και του εαυτού " "μου. Ο Αρκάϊν την έκρυψε καλά, φίλε μου, και θα χρειαστεί περισσότερο από λίγη " "τύχη για να ξεκλειδώσεις τα μυστικά που την κράτησαν κρυμμένη, ω, τόσα χρόνια." #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Ζζζζζζζζζζζζζζζζζζζζζζζζζζζζζζζ..." #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self trust." msgstr "" "Εάν βρεις αυτές τις Πέτρες του Αίματος, χρησιμοποίησέ τες προσεκτικά.\n" " \n" "Ο δρόμος είναι γεμάτος κινδύνους και η μόνη σου ελπίδα βρίσκεται στην " "εμπιστοσύνη του εαυτού σου." #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "Σκοπεύεις να βρεις την πανοπλία γνωστή ως Τιμή;\n" " \n" "Κανείς δεν έχει καταλάβει ποτέ πού είχε κρύψει ο Αρκάϊν το πράμα, και αφού οι " "επαφές μου δεν μπορούσαν να την βρουν, αμφιβάλλω σοβαρά ότι θα την βρεις εσύ." #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a black " "curse to the living; a tortured invitation to any who would stand before this " "Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his insatiable " "hunger for blood." msgstr "" "Ξέρω μόνο έναν θρύλο που μιλά για έναν τέτοιο πολεμιστή όπως περιγράφεις. Η " "ιστορία του βρίσκεται στα αρχαία χρονικά του Πολέμου της Αμαρτίας...\n" " \n" "Λεκιασμένος από χίλια χρόνια πολέμου, αίματος και θανάτου, ο Πολέμαρχος του " "Αίματος στέκεται πάνω σε ένα βουνό με τα κουρελιασμένα θύματά του. Η σκοτεινή " "του λεπίδα φωνάζει μια μαύρη κατάρα στους ζωντανούς, μια βασανισμένη πρόσκληση " "σε όποιον θα σταθεί μπροστά σε αυτόν τον Δήμιο της Κόλασης.\n" " \n" "Γράφεται επίσης ότι αν και κάποτε ήταν θνητός που πολέμησε δίπλα στη Λεγεώνα " "του Σκότους κατά τη διάρκεια του Πολέμου της Αμαρτίας, έχασε την ανθρωπιά του " "από την ακόρεστη πείνα του για αίμα." #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Φοβάμαι ότι δεν έχω ακούσει τίποτα για έναν τόσο μοχθηρό πολεμιστή, καλέ/ή μου " "Άρχοντα/ίσσα. Ελπίζω να μην χρειαστεί να τον πολεμήσεις, γιατί ακούγεται " "εξαιρετικά επικίνδυνος." #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" "Ο Κάιν θα μπορούσε να σου πει πολύ περισσότερα για κάτι τέτοιο, από όσα θα " "ήθελα ποτέ να μάθω εγώ." #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and your " "defender. I will keep you in my thoughts." msgstr "" "Εάν πρόκειται να πολεμήσετε έναν τόσο σκληρό αντίπαλο, είθε το Φως να είναι ο " "οδηγός και ο υπερασπιστής σας. Θα σε έχω στις σκέψεις μου." #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well prepared, " "my friend, for he shows no mercy or quarter." msgstr "" "Σκοτεινοί και πονηροί θρύλοι περιβάλλουν τον Πολέμαρχο του Αίματος. Να είσαι " "καλά προετοιμασμένος, φίλε/η μου, γιατί δεν δείχνει έλεος και δεν χαρίζεται." #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and that " "pretty girl that brings the drinks. Listen here, friend - you're obsessive, " "you know that?" msgstr "" "Πρέπει πάντα να μιλάς για το αίμα; Τι έχεις να πεις για τα λουλούδια, τον " "ήλιο, και αυτό το όμορφο κορίτσι που φέρνει τα ποτά. Άκου εδώ, φίλε - είσαι " "εμμονικός, το ξέρεις;" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of years " "knowing only warfare. I am sorry... I can not see if you will defeat him." msgstr "" "Η ικανότητα του με τη λεπίδα είναι φοβερή και έχει ζήσει για χιλιάδες χρόνια " "γνωρίζοντας μόνο τον πόλεμο. Λυπάμαι... Δεν μπορώ να προβλέψω αν θα τον " "νικήσεις." #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Δεν έχω ασχοληθεί ποτέ με αυτόν τον Πολέμαρχο για τον οποίο μιλάς, αλλά " "ακούγεται σαν να έχει χρησιμοποιήσει πολλά ξίφη. Δεν θα με πείραζε να " "τροφοδοτήσω τον στρατό του..." #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not be " "denied." msgstr "" "Η λεπίδα μου τραγουδά για το αίμα σου, θνητέ, και μα τους σκοτεινούς αφέντες " "μου, δεν θα της το αρνηθώ." #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave located " "in the east. It was being taken there for further study. This stone glowed " "with an energy that somehow granted vision beyond that which a normal man " "could possess. I do not know what secrets it holds, my friend, but finding " "this stone would certainly prove most valuable." msgstr "" "Ο Γκρίσγουαλντ μιλά για την Ουράνια Πέτρα που προοριζόταν για το ενκλάβιο που " "βρίσκεται στα ανατολικά. Μεταφέρθηκε εκεί για περαιτέρω μελέτη. Αυτή η πέτρα " "έλαμψε με μια ενέργεια που με κάποιον τρόπο προσέφερε όραση πέρα από αυτό που " "μπορούσε να έχει ένας κανονικός άνθρωπος. Δεν ξέρω τι μυστικά κρύβει, φίλε/η " "μου, αλλά η εύρεση αυτής της πέτρας σίγουρα θα αποδειχτεί πολύτιμη." #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to them..." msgstr "" "Το καραβάνι σταμάτησε εδώ για να πάρει μερικές προμήθειες για το ταξίδι του " "προς τα ανατολικά. Τους πούλησα μια σειρά από φρέσκα φρούτα και μερικά " "εξαιρετικά γλυκά που μόλις τελείωσε η Γκάρντα να ψήνει. Κρίμα αυτό που " "πάθανε..." #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but I " "will say this. If rocks are falling from the sky, you had better be careful!" msgstr "" "Δεν ξέρω τι είναι αυτό που νόμιζαν ότι μπορούσαν να δουν με αυτόν τον βράχο, " "αλλά θα πω αυτό: Αν πέφτουν βράχοι από τον ουρανό, καλύτερα να προσέχεις!" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Λοιπόν, ένα καραβάνι μερικών πολύ σημαντικών ανθρώπων σταμάτησε εδώ, αλλά αυτό " "έγινε πριν από αρκετό καιρό. Είχαν περίεργες προφορές και ξεκινούσαν ένα μακρύ " "ταξίδι, όπως θυμάμαι.\n" " \n" "Δεν καταλαβαίνω πώς θα μπορούσες να ελπίζεις ότι θα βρεις κάτι που κουβαλούσαν." #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan that " "was bound for the eastern kingdoms passed through here some time ago. It was " "supposedly carrying a piece of the heavens that had fallen to earth! The " "caravan was ambushed by cloaked riders just north of here along the roadway. I " "searched the wreckage for this sky rock, but it was nowhere to be found. If " "you should find it, I believe that I can fashion something useful from it." msgstr "" "Κάτσε για λίγο - Έχω μια ιστορία που μπορεί να σου φανεί ενδιαφέρουσα. Από εδώ " "πέρασε πριν λίγο καιρό ένα καραβάνι που είχε προορισμό τα ανατολικά βασίλεια. " "Υποτίθεται ότι κουβαλούσε ένα κομμάτι του ουρανού που είχε πέσει στη γη! Το " "καραβάνι έπεσε σε ενέδρα από καβαλάρηδες που φορούσαν μανδύες ακριβώς βόρεια " "από εδώ κατά μήκος του δρόμου. Έψαξα στα συντρίμμια για αυτόν τον ουράνιο " "βράχο, αλλά δεν τον βρήκα πουθενά. Αν το βρεις, πιστεύω ότι μπορώ να φτιάξω " "κάτι χρήσιμο από αυτό." #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Ακόμα περιμένω να μου φέρεις εκείνη την πέτρα από τον ουρανό. Ξέρω ότι μπορώ " "να φτιάξω κάτι δυνατό από αυτό." #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that my " "father left me. I hope it serves you well." msgstr "" "Επίτρεψε μου να το δω - ναι... ναι, είναι όπως πίστευα. Δώσε μου ένα λεπτό...\n" " \n" "Α, ορίστε. Διαρρύθμισα κομμάτια της πέτρας μέσα σε ένα ασημένιο δαχτυλίδι που " "μου άφησε ο πατέρας μου. Ελπίζω ότι θα σε υπηρετήσει καλά." #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and green " "and red and silver. Don't remember what happened to it, though. I really miss " "that ring..." msgstr "" "Είχα ένα ωραίο δαχτυλίδι. ήταν πολύ ακριβό, με μπλε και πράσινο και κόκκινο " "και ασημί. Δεν θυμάμαι τι του συνέβη, όμως. Μου λείπει πολύ αυτό το " "δαχτυλίδι..." #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "Η Ουράνια Πέτρα είναι πολύ ισχυρή, και αν ήταν κάποιος εκτός από τον " "Γκρίσγουαλντι που σου πρότεινε να την βρεις, θα σε απέτρεπα. Θα αξιοποιήσει " "τις δυνάμεις της και η χρησιμότητα της θα είναι για το καλό όλων μας." #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what he " "is doing, and as much as I try to steal his customers, I respect the quality " "of his work." msgstr "" "Αν κάποιος μπορεί να φτιάξει κάτι από αυτήν την πέτρα, ο Γκρισγουάλντ μπορεί. " "Ξέρει τι κάνει και όσο και αν προσπαθώ να κλέψω τους πελάτες του, σέβομαι την " "ποιότητα της δουλειάς του." #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, but " "this is something that cannot be found in any of my stories or books." msgstr "" "Η μάγισσα η Άντρια αναζητά ένα μαύρο μανιτάρι; Ξέρω τόσα για τα Μαύρα " "Μανιτάρια όσο και για τις Κόκκινες Ρέγγες. Ίσως ο Πέπιν ο Θεραπευτής θα " "μπορούσε να σου πει περισσότερα, αλλά αυτό δεν είναι κάτι που μπορεί να βρεθεί " "σε κάποια από τις ιστορίες ή στα βιβλία μου." #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black mushrooms " "to our honored guests. If Adria wants some mushrooms in her stew, then that is " "her business, but I can't help you find any. Black mushrooms... disgusting!" msgstr "" "Επίτρεψε μου να πω μόνο αυτό. Τόσο η Γκάρντα όσο και εγώ δεν θα σερβίραμε ποτέ " "μαύρα μανιτάρια στους τιμημένους καλεσμένους μας. Αν η Άντρια θέλει μερικά " "μανιτάρια στο στιφάδο της, τότε είναι δική της δουλειά, αλλά δεν μπορώ να σε " "βοηθήσω να τα βρεις. Μαύρα μανιτάρια... αηδιαστικό!" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect that " "its alchemy holds. If you can remove the brain of a demon when you kill it, I " "would be grateful if you could bring it to me." msgstr "" "Η μάγισσα μου είπε ότι έψαχνες για τον εγκέφαλο ενός δαίμονα για να με " "βοηθήσει να δημιουργήσω το ελιξίριο μου. Θα πρέπει να έχει μεγάλη αξία για " "τους πολλούς που τραυματίζονται από αυτά τα αποκρουστικά θηρία, αν μπορούσα " "απλώς να ξεκλειδώσω τα μυστικά που υποπτεύομαι ότι κρύβει η αλχημεία του. Εάν " "μπορείς να αφαιρέσεις τον εγκέφαλο ενός δαίμονα όταν τον σκοτώσεις και να μου " "τον φέρεις, θα σου ήμουν ευγνώμων." #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please carry " "this to the witch? I believe that she is expecting it." msgstr "" "Εξαιρετικό, αυτό ακριβώς είχα στο μυαλό μου. Μπόρεσα να τελειώσω το ελιξίριο " "χωρίς αυτό, αλλά δεν είναι κακό να το μελετήσω. Θα το μεταφέρεις σε παρακαλώ " "στη μάγισσα; Πιστεύω ότι το περιμένει." #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" "Νομίζω ότι ο Όγκντεν μπορεί να έχει μερικά μανιτάρια στην αποθήκη του στο " "κελάρι. Γιατί δεν τον ρωτάς;" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. I " "can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Εάν η Άντρια δεν έχει ένα από αυτό, μπορείς να στοιχηματίσεις ότι είναι " "πράγματι σπάνιο. Δεν μπορώ να σου προσφέρω περισσότερη βοήθεια από αυτή, αλλά " "ακούγεται σαν... ένα τεράστιο, γιγαντιαίο, πρησμένο, φουσκωμένο μανιτάρι! Καλό " "κυνήγι, υποθέτω." #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Ο Όγκντεν ανακατεύει ένα ΚΑΚΟ μαύρο μανιτάρι, αλλά θα αρρωστήσω αν το πιώ. " "Άκου, άκου... εδώ είναι το μυστικό - το να έχεις μέτρο είναι το κλειδί!" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep your " "eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "Τι έχουμε εδώ? Είναι ενδιαφέρον, μοιάζει με ένα βιβλίο συστατικών. Κράτα τα " "μάτια σου ανοιχτά για ένα μαύρο μανιτάρι. Θα πρέπει να είναι αρκετά μεγάλο και " "να αναγνωρίζεται εύκολα. Αν το βρεις, φέρε το μου, έτσι;" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so that " "I can use it for a special concoction that I am working on." msgstr "" "Είναι ένα μεγάλο, μαύρο μανιτάρι που χρειάζομαι. Τώρα τρέξε και βρες το για " "μένα, ώστε να το χρησιμοποιήσω σε ένα ειδικό παρασκεύασμα που δουλεύω." #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat those " "who have been afflicted by their poisonous venom. I believe that he intends to " "make an elixir from it. If you help him find what he needs, please see if you " "can get a sample of the elixir for me." msgstr "" "Ναι, αυτό είναι τέλειο για ένα παρασκεύασμα που δημιουργώ. Παρεμπιπτόντως, ο " "θεραπευτής ψάχνει τον εγκέφαλο κάποιου δαίμονα ή κάτι άλλο για να μπορέσει να " "θεραπεύσει όσους έχουν πληγεί από το δηλητηριώδες φαρμάκι. Πιστεύω ότι " "σκοπεύει να φτιάξει ένα ελιξίριο με αυτό. Αν τον βοηθήσεις να βρει αυτό που " "χρειάζεται, δες αν μπορείς να πάρεις και ένα δείγμα από το ελιξίριο για μένα " "σε παρακαλώ." #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "Γιατί το έφερες αυτό εδώ; Δεν έχω ανάγκη για εγκέφαλο δαίμονα αυτή τη στιγμή. " "Χρειάζομαι λίγο από το ελιξίριο στο οποίο εργάζεται ο Θεραπευτής. Χρειάζεται " "αυτό το γκροτέσκο όργανο που κρατάς και μετά να μου φέρεις το ελιξίριο. Απλό " "όταν το σκεφτείς, έτσι δεν είναι;" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "Τι? Τώρα μου φέρνεις αυτό το ελιξίριο από τον θεραπευτή; Μπόρεσα να ολοκληρώσω " "την παρασκευή μου χωρίς αυτό. Γιατί δεν το κρατάς..." #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about something " "a bit more useful?" msgstr "" "Δεν έχω προς πώληση μανιτάρια οποιουδήποτε μεγέθους ή χρώματος. Τι θα έλεγες " "για κάτι λίγο πιο χρήσιμο;" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the secrets " "of a timeless evil. An evil that quite obviously has now been released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the time " "when Diablo would rise again from his imprisonment. When the two stars on that " "map align, Diablo will be at the height of his power. He will be all but " "invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of his " "evil again!" msgstr "" "Ώστε, ο θρύλος του Χάρτη είναι πραγματικός. Ακόμα κι εγώ δεν πίστεψα ποτέ " "τίποτα από αυτά! Υποθέτω ότι ήρθε η ώρα να σου πω την αλήθεια για το ποιος " "είμαι, φίλε/η μου. Βλέπεις, δεν είμαι μόνο όλο αυτό που φαίνομαι...\n" " \n" "Το αληθινό μου όνομα είναι Ντέκαρντ Κάιν ο Πρεσβύτερος και είμαι ο τελευταίος " "απόγονος μιας αρχαίας Αδελφότητας που ήταν αφιερωμένη στο να κρατά και να " "προστατεύει τα μυστικά ενός διαχρονικού κακού. Ένα κακό που προφανώς έχει " "πλέον απελευθερωθεί...\n" " \n" "Το κακό στο οποίο εναντιώνεσαι είναι ο σκοτεινός Άρχοντας του Τρόμου - γνωστός " "στους θνητούς ως Ντιάμπλο. Ήταν αυτός που φυλακίστηκε μέσα στον Λαβύρινθο πριν " "από πολλούς αιώνες. Ο Χάρτης που κρατάς τώρα δημιουργήθηκε πριν από πολλά " "χρόνια για να σηματοδοτήσει την εποχή που ο Ντιάμπλο θα σηκωθεί ξανά από τη " "φυλάκισή του. Όταν τα δύο αστέρια σε αυτόν τον χάρτη ευθυγραμμιστούν, ο " "Ντιάμπλο θα είναι στο απόγειο της δύναμής του. Θα είναι ανίκητος...\n" " \n" "Τώρα είσαι σε αγώνα δρόμου ενάντια στο χρόνο φίλε/η μου! Βρες τον Ντιάμπλο και " "καταστρέψτε τον πριν ευθυγραμμιστούν τα αστέρια, γιατί μπορεί να μην έχουμε " "ποτέ ξανά την ευκαιρία να απαλλάξουμε από τον κόσμο το κακό του!" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "Ο χρόνος μας είναι λίγος! Αισθάνομαι τη σκοτεινή ισχύ του και μόνο εσύ μπορείς " "να τον εμποδίσεις να αποκτήσει την πλήρη δύναμή του." #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, and " "you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Είμαι σίγουρος ότι έκανες το καλύτερο δυνατό, αλλά φοβάμαι ότι ακόμη και η " "δύναμη και η θέλησή σου μπορεί να μην είναι αρκετή. Ο Ντιάμπλο βρίσκεται τώρα " "στο απόγειο της γήινης ισχύος του και θα χρειαστείς όλο σας το θάρρος και τη " "δύναμή σου για να τον νικήσεις. Είθε το Φως να σε προστατεύει και να σε " "καθοδηγεί, φίλε/η μου. Θα βοηθήσω με όποιον τρόπο μπορώ." #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. You " "should hurry along and see the storyteller as Adria suggests." msgstr "" "Αν η μάγισσα δεν μπορεί να σε βοηθήσει και σου προτείνει να δεις τον Κάιν, τι " "σε κάνει να πιστεύεις ότι θα ήξερα τίποτα εγώ; Φαίνεται ότι αυτό είναι ένα " "πολύ σοβαρό θέμα. Θα πρέπει να βιαστείς και να δεις τον αφηγητή ιστοριών όπως " "προτείνει η Άντρια." #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain could " "help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that is " "beyond my talents." msgstr "" "Δεν μπορώ να καταλάβω από τα γραπτά σε αυτόν τον χάρτη, αλλά ίσως η Άντρια ή ο " "Κάιν θα μπορούσαν να σε βοηθήσουν να αποκρυπτογραφήσεις και να μάθεις σε τι " "αναφέρεται.\n" " \n" "Μπορώ να δω ότι είναι ένας χάρτης των αστεριών στον ουρανό μας, αλλά κάτι " "περισσότερο από αυτό είναι πέρα ​από τα ταλέντα μου." #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "Το καλύτερο άτομο για να ρωτήσεις για κάτι τέτοιο θα ήταν ο αφηγητής ιστοριών " "μας.\n" " \n" "Ο Κάιν γνωρίζει πολύ καλά τα αρχαία γραπτά, και αυτό είναι εύκολα το πιο παλιό " "χαρτί που έχω δει ποτέ." #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "Δεν έχω ξαναδεί τέτοιο χάρτη. Πού τον βρήκες; Αν και δεν έχω ιδέα πώς να τον " "διαβάσω, ο Κάιν ή η Άντρια μπορεί να είναι σε θέση να σου δώσουν τις " "απαντήσεις που αναζητάς." #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Άκου εδώ, έλα κοντά. Δεν ξέρω αν ξέρεις αυτό που ξέρω, αλλά έχεις κάτι εδώ " "πραγματικά. Αυτός είναι ένας χάρτης." #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars portends " "great disaster, but its secrets are not mine to tell. The time has come for " "you to have a very serious conversation with the Storyteller..." msgstr "" "Ω, φοβάμαι ότι αυτό δεν προμηνύεται καθόλου καλό. Αυτός ο χάρτης των αστεριών " "προμηνύει μεγάλη καταστροφή, αλλά τα μυστικά του δεν είναι δικά μου να τα πω. " "Ήρθε η ώρα να κάνετε μια πολύ σοβαρή συζήτηση με τον αφηγητή ιστοριών..." #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show that " "to Adria - she can probably tell you what it is. I'll say one thing; it looks " "old, and old usually means valuable." msgstr "" "Έψαχνα για έναν χάρτη, αλλά σίγουρα δεν είναι αυτός. Θα πρέπει να τον δείξεις " "στην Άντρια - πιθανότατα μπορεί να σου πει τι είναι. Θα πω ένα πράγμα. " "φαίνεται παλιός και παλιό συνήθως σημαίνει πολύτιμο." #: Source/translation_dummy.cpp:824 msgid "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" "Σε παρακαλωωωωώ, μην με πονέσεις. Όχι σκότωμα. Να είμαι ζωντανός και την " "επόμενη φορά το καλό πράμα θα σου φέρω." #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Κάτι για σένα φτιάχνω. Και πάλι, μην σκοτώσετε τον Γκάρμπαντ. Να ζήσω και σου " "δώσω καλό πράμα.\n" " \n" "Πάρε αυτό ως απόδειξη ότι κρατάω τη λέξη μου..." #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "Τίποτα ακόμη! Σχεδόν τελείωσε.\n" " \n" "Πολύ δυνατό, πολύ δυνατό. Να ζήσω! Ζήσω!\n" " \n" "Χωρίς πόνο και υπόσχεση κρατώ!" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Αυτό είναι πολύ καλό για σένα. Πολύ δυνατό! Το θέλεις - έλα πάρε το!" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "Τι?! Γιατί είσαι εδώ? Όλες αυτές οι διακοπές είναι αρκετές για να τρελαθεί " "κανείς. Ορίστε, πάρε αυτό και άφησε με στη δουλειά μου. Μην με ταλαιπωρείς " "άλλο!" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "Αρρρργκ! Η περιέργειά σου θα είναι ο θάνατος σου!!!" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Γειά σου, άνθρωπε μου. Ξαπόστασε για λίγο και άκου..." #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of great " "knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "Ενώ κατεβαίνεις βαθύτερα στον Λαβύρινθο, μπορεί να βρεις κρυμμένους τόμους με " "σπουδαία γνώση.\n" " \n" "Διάβασε τους προσεκτικά γιατί μπορούν να σου πουν πράγματα που ακόμη και εγώ " "δεν μπορώ." #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Γνωρίζω πολλούς μύθους και θρύλους που μπορεί να περιέχουν απαντήσεις σε " "ερωτήσεις που μπορεί να προκύψουν στα ταξίδια σου στον Λαβύρινθο. Αν " "συναντήσεις προκλήσεις και ερωτήσεις για τις οποίες αναζητάς γνώση, αναζητήστε " "με και θα σου πω ότι μπορώ." #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He is " "a skilled craftsman, and if he claims to be able to help you in any way, you " "can count on his honesty and his skill." msgstr "" "Ο Γκρίσγουαλντ - ένας άνθρωπος δράσης και με μεγάλο θάρρος. Βάζω στοίχημα ότι " "δεν σου είπε ποτέ για την εποχή που πήγε στον Λαβύρινθο για να σώσει τον " "Ουίρτ, σωστά; Ξέρει από τους κινδύνους που υπάρχουν εκεί, αλλά πάλι - το ίδιο " "και εσύ. Είναι επιδέξιος τεχνίτης και αν ισχυρίζεται ότι μπορεί να σε βοηθήσει " "με οποιονδήποτε τρόπο, μπορείς να βασιστείς στην ειλικρίνεια και την ικανότητά " "του." #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Ο Όγκντεν κατέχει και διευθύνει το πανδοχείο και ταβέρνα του Ανατέλλοντος " "Ηλίου εδώ και σχεδόν τέσσερα χρόνια. Το αγόρασε λίγους μήνες πριν όλα πάνε " "κατά διαόλου. Αυτός και η σύζυγός του η Γκάρντα δεν έχουν τα χρήματα να φύγουν " "καθώς επένδυσαν ό,τι είχαν για να φτιάξουν την ζωή τους εδώ. Είναι καλός " "άνθρωπος με βαθύ αίσθημα ευθύνης." #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that entered " "into the Cathedral with Lazarus on that dark day. He escaped with his life, " "but his courage and much of his sanity were left in some dark pit. He finds " "comfort only at the bottom of his tankard nowadays, but there are occasional " "bits of truth buried within his constant ramblings." msgstr "" "Ο καημένος ο Φάρνxαμ. Είναι μια ανησυχητική υπενθύμιση της καταδικασμένης " "ομάδας που μπήκε στον καθεδρικό ναό με τον Λάζαρο εκείνη τη σκοτεινή μέρα. " "Ξέφυγε με τη ζωή του, αλλά το θάρρος του και μεγαλύτερο μέρος της λογικής του " "έμειναν σε κάποιο σκοτεινό λάκκο. Βρίσκει παρηγοριά μόνο στον πάτο του " "μπουκαλιού του σήμερα, αλλά υπάρχουν περιστασιακά κομμάτια αλήθειας θαμμένα " "μέσα στις συνεχείς ασυναρτησίες του." #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has access " "to many strange and arcane artifacts and tomes of knowledge that even I have " "never seen before." msgstr "" "Η μάγισσα, Άντρια, είναι μια ιδιαιτερότητα εδώ στην Τρίστραμ. Έφτασε λίγο μετά " "τη βεβήλωση του καθεδρικού ναού, ενώ περίπου όλοι οι άλλοι τράπηκαν σε φυγή. " "Έχει χτίσει μια μικρή καλύβα στην άκρη της πόλης, σχεδόν μέσα σε μια νύχτα, " "και έχει πρόσβαση σε πολλά παράξενα και απόκρυφα τεχνουργήματα και τόμους " "γνώσης που ακόμη και εγώ δεν έχω ξαναδεί." #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the arms " "of his mother and dragged into the labyrinth by the small, foul demons that " "wield wicked spears. There were many other children taken that day, including " "the son of King Leoric. The Knights of the palace went below, but never " "returned. The Blacksmith found the boy, but only after the foul beasts had " "begun to torture him for their sadistic pleasures." msgstr "" "Η ιστορία του Ουίρτ είναι τρομακτική και τραγική. Τον πήραν από την αγκαλιά " "της μητέρας του και τον έσυραν στον λαβύρινθο οι μικροί, βρωμεροί δαίμονες που " "κρατούσαν άσχημες λόγχες. Πολλά άλλα παιδιά απήχθησαν εκείνη την ημέρα, " "συμπεριλαμβανομένου και του γιου του βασιλιά Λεόρικ. Οι Ιππότες του παλατιού " "πήγαν κάτω, αλλά δεν επέστρεψαν ποτέ. Ο Σιδηρουργός βρήκε το αγόρι, αλλά μόνο " "αφού τα θηρία είχαν αρχίσει να το βασανίζουν για τις σαδιστικές απολαύσεις " "τους." #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. He " "is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "Α, ο Πέπιν. Τον θεωρώ αληθινό φίλο μου - ίσως τον πιο κοντινό που έχω εδώ. " "Είναι λίγο μπερδεμένος κατά καιρούς, αλλά ποτέ δεν υπήρξε η πιο φροντιστική ή " "προσεκτική ψυχή. Οι γνώσεις και οι δεξιότητές του εξισώνονται με λίγους, και η " "πόρτα του είναι πάντα ανοιχτή." #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick laugh, " "she holds a special place in my heart. She stays on at the tavern to support " "her elderly grandmother who is too sick to travel. I sometimes fear for her " "safety, but I know that any man in the village would rather die than see her " "harmed." msgstr "" "Η Γκίλιαν είναι μια ωραία γυναίκα. Πολύ λατρεμένη για την καλή της διάθεση και " "το γρήγορο γέλιο της, κατέχει μια ξεχωριστή θέση στην καρδιά μου. Μένει στην " "ταβέρνα για να στηρίξει την ηλικιωμένη γιαγιά της που είναι πολύ άρρωστη για " "να ταξιδέψει. Μερικές φορές φοβάμαι για την ασφάλειά της, αλλά ξέρω ότι " "οποιοσδήποτε άντρας στο χωριό προτιμά να πεθάνει παρά να τη δει να παθαίνει " "κακό." #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "" "Χαιρετισμούς, καλέ/ή μου άρχοντα/ισσα Καλώς ήρθες στην Ταβέρνα του " "Ανατέλλοντος Ηλίου!" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard any " "of them agree on was this old axiom. Perhaps it will help you. You can cut the " "flesh, but you must crush the bone." msgstr "" "Πολλοί τυχοδιώκτες έχουν κοσμήσει τα τραπέζια της ταβέρνας μου και έχουν " "ειπωθεί δέκα φορές περισσότερες ιστορίες πάνω από μπίρες. Το μόνο πράγμα στο " "οποίο άκουσα ποτέ κάποιον από αυτούς να συμφωνεί ήταν αυτό το παλιό αξίωμα. " "Ίσως θα σε βοηθήσει. Μπορεί να κόψεις τη σάρκα, αλλά πρέπει να συνθλίψεις το " "κόκαλο." #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. If " "you ever need work done on your gear, he is definitely the man to see." msgstr "" "Ο Γκρίσγουαλντ ο σιδηρουργός είναι εξαιρετικός γνώστης των όπλων και της " "Θωράκισης. Αν χρειαστείς ποτέ δουλειά στον εξοπλισμό σου, είναι σίγουρα ο " "άνθρωπος που πρέπει να δεις." #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Ο Φάρνχαμ ξοδεύει πάρα πολύ χρόνο εδώ, πνίγοντας τις λύπες του σε φτηνή μπύρα. " "Θα τον έδιωχνα, αλλά υπέφερε τόσο πολύ όταν πήγε στον Λαβύρινθο." #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Η Άντρια είναι πολύ σοφή πέρα ​​από τα χρόνια της, αλλά πρέπει να ομολογήσω - με " "τρομάζει λίγο.\n" " \n" "Λοιπόν, δεν έχει σημασία. Αν ποτέ χρειαστεί να εμπορευθείς είδη μαγείας, " "διατηρεί μια περίεργα καλά εφοδιασμένη καλύβα ακριβώς απέναντι από το ποτάμι." #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Αν θέλετε να μάθεις περισσότερα για την ιστορία του χωριού μας, ο αφηγητής " "ιστοριών μας ο Κάιν γνωρίζει αρκετά για το παρελθόν." #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Ο Ουίρτ είναι ένα παλιόπαιδο και ένας μικρός απατεώνας. Πάντα έμπαινε σε " "μπελάδες και δεν αποτελεί έκπληξη αυτό που του συνέβη.\n" " \n" "Μάλλον έμπλεξε κάπου που δεν έπρεπε να ήταν. Λυπάμαι για το αγόρι, αλλά δεν " "αντέχω την παρέα του." #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Ο Πέπιν είναι καλός άνθρωπος - και σίγουρα ο πιο γενναιόδωρος στο χωριό. " "Φροντίζει πάντα τις ανάγκες των άλλων, αλλά κάποιου είδους προβλήματα φαίνεται " "να τον ακολουθούν όπου κι αν πάει..." #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after the " "old woman, but she is too sweet and caring to have done so." msgstr "" "Η Γκίλιαν, η σερβιτόρα μου; Αν δεν ήταν για την αίσθηση του καθήκοντός της " "προς τη γιαγιά της, θα είχε φύγει από εδώ, εδώ και πολύ καιρό.\n" " \n" "Μα το καλό μου, την παρακάλεσα να φύγει, λέγοντάς της ότι θα πρόσεχα τη " "ηλικιωμένη, αλλά είναι πολύ γλυκιά και περιποιητική για να έφευγε." #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "Τι σε ταλαιπωρεί άνθρωπε μου;" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you hurt " "one of the monsters, make sure it is dead or it very well may regenerate " "itself." msgstr "" "Έχω κάνει μια πολύ ενδιαφέρουσα ανακάλυψη. Σε αντίθεση με εμάς, τα πλάσματα " "στον Λαβύρινθο μπορούν να θεραπεύσουν τον εαυτό τους χωρίς τη βοήθεια φίλτρων " "ή μαγείας. Εάν πληγώσεις ένα από τα τέρατα, βεβαιώσου ότι είναι νεκρό " "διαφορετικά μπορεί να αναγεννήσει τις σάρκες του." #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Πριν καταληφθεί από... ό,τι κρύβεται από κάτω, ο καθεδρικός ναός ήταν ένας " "τόπος μεγάλης μάθησης. Υπάρχουν πολλά βιβλία που μπορείς να βρεις εκεί. Αν " "βρεις κάποια, σου προτείνω να τα διαβάσεις, γιατί μπορεί να κρατούν μυστικά " "για τον Λαβύρινθο." #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of healing. " "He is a shrewd merchant, but his work is second to none. Oh, I suppose that " "may be because he is the only blacksmith left here." msgstr "" "Ο Γκρίσγουαλντ γνωρίζει τόσα πολλά για την τέχνη του πολέμου όσο κι εγώ για " "την τέχνη της θεραπείας. Είναι οξυδερκής έμπορος, και η δουλειά του είναι " "πρώτης ποιότητας. Α, υποθέτω ότι αυτό μπορεί να οφείλεται στο ότι είναι ο " "μόνος σιδηρουργός που έχει απομείνει εδώ." #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has an " "innate ability to discern the true nature of many things. If you ever have any " "questions, he is the person to go to." msgstr "" "Ο Κάιν είναι αληθινός φίλος και σοφός γνώστης. Διατηρεί μια τεράστια " "βιβλιοθήκη και έχει μια έμφυτη ικανότητα να διακρίνει την αληθινή φύση πολλών " "πραγμάτων. Αν έχεις ποτέ απορίες, είναι το άτομο στο οποίο πρέπει να πας." #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able to " "mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Ακόμη και οι ικανότητές μου δεν μπόρεσαν να θεραπεύσουν πλήρως τον Φάρνχαμ. Ω, " "μπόρεσα να ιάσω το σώμα του, αλλά το μυαλό και το πνεύμα του είναι πέρα ​​από " "οτιδήποτε μπορώ να κάνω." #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may be " "much more than the hovel it appears to be, but I can never seem to get inside " "the place." msgstr "" "Ενώ εγώ χρησιμοποιώ μερικές περιορισμένες μορφές μαγείας για να δημιουργήσω τα " "φίλτρα και τα ελιξίρια που αποθηκεύω εδώ, η Άντρια είναι μια αληθινή μάγισσα. " "Δεν φαίνεται να κοιμάται ποτέ και έχει πάντα πρόσβαση σε πολλούς " "μυστικιστικούς τόμους και τεχνουργήματα. Πιστεύω ότι η καλύβα της μπορεί να " "είναι πολύ περισσότερο από την παράγκα που φαίνεται να είναι, αλλά δεν έχω " "καταφέρει ποτέ να μπω μέσα εκεί." #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Ο φτωχός Ουίρτ. Έκανα ό,τι ήταν δυνατό για το παιδί, αλλά ξέρω ότι περιφρονεί " "αυτό το ξύλινο παλούκι που αναγκάστηκα να βάλω στο πόδι του. Οι πληγές του " "ήταν φρικτές. Κανείς - και ειδικά ένα τόσο μικρό παιδί - δεν θα έπρεπε να " "υποφέρει όπως έπαθε." #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from a " "slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the many " "murders that happen in the surrounding countryside, or perhaps the wishes of " "his wife that keep him and his family where they are." msgstr "" "Πραγματικά δεν καταλαβαίνω γιατί ο Όγκντεν μένει εδώ στην Τρίστραμ. Πάσχει από " "μια ελαφριά νευρική πάθηση, αλλά είναι ένας έξυπνος και εργατικός άνθρωπος που " "θα τα πήγαινε πολύ καλά όπου κι αν πήγαινε. Υποθέτω ότι μπορεί να είναι ο " "φόβος των πολλών δολοφονιών που συμβαίνουν στη γύρω ύπαιθρο ή ίσως οι " "επιθυμίες της γυναίκας του που κρατούν αυτόν και την οικογένειά του εδώ." #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "Η σερβιτόρα του Όγκντεν είναι ένα γλυκό κορίτσι. Η γιαγιά της είναι αρκετά " "άρρωστη και υποφέρει από παραισθήσεις.\n" " \n" "Ισχυρίζεται ότι βλέπει οράματα, αλλά δεν υπάρχει καμία απόδειξη γι' αυτά με " "κανέναν τρόπο." #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "Καλημέρα! Πώς μπορώ να σε υπηρετήσω;" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Η γιαγιά μου είχε ένα όνειρο ότι θα έρθεις να μου μιλήσεις. Έχει οράματα " "ξέρεις, και μπορεί να δει στο μέλλον." #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her name, " "Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "Η γυναίκα στην άκρη της πόλης είναι μάγισσα! Φαίνεται αρκετά ωραία, και το " "όνομά της, Άντρια, είναι πολύ ευχάριστο στο αυτί, αλλά τη φοβάμαι πολύ.\n" " \n" "Θα χρειαζόταν κάποιος αρκετά γενναίος/α, όπως εσύ, για να δει τι κάνει εκεί " "έξω." #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he a " "master craftsman who has won many contests within his guild, but he received " "praises from our King Leoric himself - may his soul rest in peace. Griswold is " "also a great hero; just ask Cain." msgstr "" "Ο Σιδηρουργός μας είναι ορόσημο υπερηφάνειας για τους κατοίκους της Τρίστραμ. " "Όχι μόνο είναι ένας τεχνίτης που έχει κερδίσει πολλούς διαγωνισμούς στην " "συντεχνία του, αλλά έλαβε επαίνους από τον ίδιο τον Βασιλιά μας τον Λεόρικ - " "ας αναπαύεται η ψυχή του εν ειρήνη. Ο Γκρίσγουαλντ είναι επίσης μεγάλος ήρωας. " "απλά ρώτα τον Κάιν." #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Ο Κάιν είναι ο αφηγητής Ιστοριών της Τρίστραμ για όσο μπορώ να θυμηθώ. Ξέρει " "τόσα πολλά και μπορεί να σου πει σχεδόν τα πάντα για... σχεδόν τα πάντα." #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor every " "night." msgstr "" "Ο Φάρνχαμ είναι ένας μεθυσμένος που γεμίζει την κοιλιά του με μπύρα και τα " "αυτιά όλων μας με ανοησίες.\n" " \n" "Ξέρω ότι τόσο ο Πέπιν όσο και ο Όγκντεν αισθάνονται συμπάθεια γι' αυτόν, αλλά " "απογοητεύομαι τόσο πολύ βλέποντάς τον να γλιστράει όλο και πιο βαθιά σε ένα " "μπερδεμένο μεθύσι κάθε βράδυ." #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in need " "of healing, Pepin can help you." msgstr "" "Ο Πέπιν έσωσε τη ζωή της γιαγιάς μου και ξέρω ότι δεν μπορώ ποτέ να του το " "ανταποδώσω. Η ικανότητά του να θεραπεύει οποιαδήποτε ασθένεια είναι πιο ισχυρή " "από το ισχυρότερο σπαθί και πιο μυστηριώδης από οποιοδήποτε ξόρκι που μπορείς " "να ονομάσεις. Εάν ποτέ χρειαστείς θεραπεία, ο Πέπιν μπορεί να σε βοηθήσει." #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt when " "those hideous creatures stole him, she never recovered. I think she died of a " "broken heart. Wirt has become a mean-spirited youngster, looking only to " "profit from the sweat of others. I know that he suffered and has seen horrors " "that I cannot even imagine, but some of that darkness hangs over him still." msgstr "" "Μεγάλωσα με τη μητέρα του Ουίρτ, την Κάνας. Αν και αυτή μόνο τραυματίστηκε " "ελαφρά, όταν της έκλεψαν το γιό αυτά τα απαίσια πλάσματα, δεν συνήλθε ποτέ. " "Νομίζω ότι πέθανε από ραγισμένη καρδιά. Ο Ουίρτ έχει γίνει ένας κακότροπος " "νεαρός, που θέλει μόνο να επωφεληθεί από τον ιδρώτα των άλλων. Ξέρω ότι " "υπέφερε και έχει δει φρίκες που δεν μπορώ καν να φανταστώ, αλλά κάποιο από " "αυτό το σκοτάδι κρέμεται ακόμα από πάνω του." #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Ο Όγκντεν και η γυναίκα του πήραν εμένα και τη γιαγιά μου στο σπίτι τους και " "με άφησαν να κερδίσω μερικά χρυσά νομίσματα δουλεύοντας στο πανδοχείο. Τους " "χρωστάω τόσα πολλά και ελπίζω μια μέρα να φύγω από αυτό το μέρος και να τους " "βοηθήσω να ξεκινήσουν ένα μεγάλο ξενοδοχείο στα ανατολικά." #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "Λοιπόν, τι μπορώ να κάνω για σένα;" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your basic " "blunt weapon, such as a mace. Works like a charm against most of those undying " "horrors down there, and there's nothing better to shatter skinny little " "skeletons!" msgstr "" "Αν ψάχνεις για ένα καλό όπλο, επίτρεψε μου να σου δείξω αυτό. Πάρτε το βασικό " "σας αμβλύ όπλο, όπως τον κεφαλοθραύστη. Λειτουργεί τέλεια ενάντια στους " "περισσότερους από αυτούς τους αθάνατους τρόμους εκεί κάτω, και δεν υπάρχει " "τίποτα καλύτερο για να θρυμματίσεις τους μικρούς αδύνατους σκελετούς!" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy blow!" msgstr "" "Το τσεκούρι; Ναι, αυτό είναι ένα καλό όπλο, ισορροπημένο ενάντια σε κάθε " "εχθρό. Κοίταξε πώς κόβει τον αέρα και μετά φαντάσου ένα ωραίο χοντρό κεφάλι " "δαίμονα στο πέρασμά του. Λάβε υπόψη, ωστόσο, ότι αργεί στο κτύπημα - αλλά " "μιλάμε για ένα βαρύ και καίριο χτύπημα!" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Κοίτα αυτή την λεπίδα, αυτή την ισορροπία. Ένα ξίφος στα σωστά χέρια, και " "ενάντια στον σωστό εχθρό, είναι ο κύριος όλων των όπλων. Η κοφτερή λεπίδα του " "βρίσκει λίγα για να κόψει ή να διαπεράσει στους απέθαντους, αλλά ενάντια σε " "έναν ζωντανό εχθρό που αναπνέει, ένα σπαθί θα κόψει εύκολα τη σάρκα του!" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "Τα όπλα και η πανοπλία σου θα φέρουν τα σημάδια των αγώνων σου ενάντια στο " "Σκοτάδι. Αν μου τα φέρεις, με λίγη δουλειά στο ζεστό σιδηρουργείο μου, μπορώ " "να τα επαναφέρω στην κορυφαία αγωνιστική τους κατάσταση." #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how to " "harness magic as she did, I could make some truly incredible things." msgstr "" "Ενώ πρέπει κυριολεκτικά να μεταφέρω λαθραία τα μέταλλα και τα εργαλεία που " "χρειάζομαι από τα καραβάνια που περνούν απο τις άκρες της καταραμένης πόλης " "μας, αυτή η μάγισσα, η Άντρια, φαίνεται να παίρνει πάντα ό,τι χρειάζεται. Αν " "ήξερα έστω και το παραμικρό για το πώς να αξιοποιώ τη μαγεία όπως εκείνη, θα " "μπορούσα να φτιάξω μερικά πραγματικά απίστευτα πράγματα." #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Η Γκίλιαν είναι μια ωραία κοπέλα. Κρίμα που η ηλικιωμένη μάνα της είναι τόσο " "άρρωστη, διαφορετικά θα κανόνιζα να φύγουν και οι δύο από εδώ σε ένα από τα " "εμπορικά καραβάνια." #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling in " "life. If I could bend steel as well as he can bend your ear, I could make a " "suit of court plate good enough for an Emperor!" msgstr "" "Μερικές φορές νομίζω ότι ο Κάιν μιλάει πάρα πολύ, αλλά υποθέτω ότι αυτό είναι " "το κάλεσμα του στη ζωή. Αν μπορούσα να λυγίσω το ατσάλι όπως αυτός μπορεί να " "λυγίσει το αυτί σου, θα μπορούσα να φτιάξω μια αυλική ολομεταλλική πανοπλία " "αρκετά καλή για έναν Αυτοκράτορα!" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never saw " "the Archbishop again, and I may not have survived if Farnham was not at my " "side. I fear that the attack left his soul as crippled as, well, another did " "my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Ήμουν με τον Φάρνχαμ εκείνο το βράδυ που ο Λάζαρος μας οδήγησε στον Λαβύρινθο. " "Δεν είδα ποτέ ξανά τον Αρχιεπίσκοπο και μπορεί να μην είχα επιζήσει αν ο " "Φάρνχαμ δεν ήταν δίπλα μου. Φοβάμαι ότι η επίθεση άφησε την ψυχή του τόσο " "ανάπηρη όσο και το πόδι μου. Δεν μπορώ να δώσω αυτή τη μάχη για αυτόν τώρα, " "αλλά θα το έκανα αν μπορούσα." #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Ένας καλός άνθρωπος που βάζει τις ανάγκες των άλλων πάνω από τις δικές του. " "Δεν θα βρεις κανέναν στην Τρίστραμ - ή πουθενά αλλού για αυτό το θέμα - που να " "έχει κάτι κακό να πει για τον θεραπευτή." #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an honest " "trade, but he prefers the high profits of dealing in goods of dubious origin. " "I cannot hold that against him after what happened to him, but I do wish he " "would at least be careful." msgstr "" "Αυτό το παλικάρι θα βάλει τον εαυτό του σε σοβαρό μπελά... ή μάλλον πρέπει να " "πω θα ξαναβάλει. Προσπάθησα να τον κινήσω το ενδιαφέρον να δουλέψει εδώ και να " "μάθει ένα τίμιο επάγγελμα, αλλά προτιμά τα υψηλά κέρδη από την εμπορία " "εμπορευμάτων αμφιβόλου προέλευσης. Δεν μπορώ να τον κατηγορήσω για αυτό μετά " "από αυτό που του συνέβη, αλλά εύχομαι να ήταν τουλάχιστον προσεκτικός." #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of grains " "and dried meats he kept in his cellar, why, most of us would have starved " "during that first year when the entire countryside was overrun by demons." msgstr "" "Ο πανδοχέας έχει ελάχιστες δουλειές και δεν έχει πραγματικό τρόπο να βγάλει " "κέρδος. Καταφέρνει να τα βγάλει πέρα ​​παρέχοντας φαγητό και κατάλυμα σε όσους " "περιστασιακά περνούν στο χωριό, αλλά είναι τόσο πιθανό να φύγουν κρυφά τη " "νύχτα όπως και να τον πληρώσουν. Αν δεν υπήρχαν οι αποθήκες με σιτηρά και " "αποξηραμένα κρέατα που κρατούσε στο κελάρι του, ω, οι περισσότεροι από εμάς θα " "είχαμε πεινάσει κατά τη διάρκεια του πρώτου έτους, όταν ολόκληρη η ύπαιθρος " "είχε κατακλυστεί από δαίμονες." #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "Δεν μπορεί ένα φιλαράκι να πιεί με την ησυχία του;" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" "Το κορίτσι που φέρνει τα ποτά; Ω, ναι, τι όμορφη κυρία. Τόσο καλή επίσης." #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat or " "drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "Γιατί αυτή η παλιόγρια δεν κάνει κάτι για μια φορά. Σίγουρα, σίγουρα, έχει " "πράγματα, αλλά άκουσε με... είναι αφύσικη. Δεν την έχω δει ποτέ να τρώει ή να " "πίνει - και δεν μπορείς να εμπιστευτείς κάποιον που δεν πίνει τουλάχιστον λίγο." #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Ο Κάιν δεν είναι αυτό που λέει ότι είναι. Σίγουρα, σίγουρα, λέει μια καλή " "ιστορία... μερικές από αυτές είναι πραγματικά τρομακτικές ή αστείες... αλλά " "νομίζω ότι ξέρει περισσότερα από όσα ξέρει ότι ξέρει." #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Ο Γκρίσγουαλντ; Ο παλιός καλός Γκρίσγουαλντ. Τον αγαπώ σαν αδερφό! Μαζί " "πολεμήσαμε, ξέρεις τότε όταν... εμείς... Λάζαρε... Λάζαρε... Λάζαρε!!!" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should make " "sure you get to know him. Good fella like that with people always wantin' " "help. Hey, I guess that would be kinda like you, huh hero? I was a hero too..." msgstr "" "Χεχχε, μου αρέσει ο Πέπιν. Προσπαθεί πραγματικά, ξέρεις. Άκουσε με, θα πρέπει " "να φροντίσεις να τον γνωρίσεις. Καλό τυπάκι για ανθρώπους που πάντα θέλουν " "βοήθεια. Ε, υποθέτω ότι μοιάζει σαν εσένα, ε ήρωα/ίδα; Ήμουν κι εγώ ήρωας..." #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about problems. " "Listen here - that kid is gotta sweet deal, but he's been there, you know? " "Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "Ο Ουίρτ είναι ένα παιδί με περισσότερα προβλήματα ακόμα και από εμένα, και " "ξέρω τα πάντα για προβλήματα. Άκουσε εδώ - αυτό το παιδί έχει βρει μια καλή " "συμφωνία, αλλά ήταν εκεί, ξέρεις; Έχασε ένα πόδι! Πρέπει να περπατά με ένα " "κομμάτι ξύλο. Τόσο λυπηρό, τόσο λυπηρό..." #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Ο Όγκντεν είναι ο καλύτερος άντρας της πόλης. Δεν νομίζω ότι η γυναίκα του με " "συμπαθεί, αλλά όσο συνεχίζει να ανοίγει βαρέλια, θα μου αρέσει μια χαρά. " "Φαίνεται ότι περνάω περισσότερο χρόνο με τον Όγκντεν σε σχέση με τους άλλους, " "αλλά είναι τόσο καλός μαζί μου..." #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no good " "since those stupid dogs..." msgstr "" "Θέλω να σου πω κάτι τις, γιατί ξέρω τα πάντα για αυτό το πράγμα. Είναι η " "ειδικότητά μου. Αυτό εδώ είναι το καλύτερο... τοοοο καλύτερο! Αυτή η άλλη " "μπύρα δεν είναι καλή, αφού αυτά τα ανόητα σκυλιά..." #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but somewhere " "under the church is a whole pile o' gold. Gleamin' and shinin' and just " "waitin' for someone to get it." msgstr "" "Κανείς δεν με ακούει ποτέ. Κάπου - δεν είμαι και πολύ σίγουρος - αλλά κάπου " "κάτω από την εκκλησία είναι μια ολόκληρη στοίβα από χρυσάφι. Γυαλίζει και " "λάμπει και απλώς περιμένει κάποιος να την μαζέψει." #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, but " "that weapon you got there - it just ain't no good against those big brutes! " "Oh, I don't care what Griswold says, they can't make anything like they used " "to in the old days..." msgstr "" "Ξέρω ότι έχεις τις δικές σου ιδέες και ξέρω ότι δεν θα το πιστέψεις αυτό, αλλά " "αυτό το όπλο που έχεις εκεί - απλά δεν είναι καλό ενάντια σε αυτά τα μεγάλα " "βάναυσα θηρία! Α, δεν με νοιάζει τι λέει ο Γκρίσγουαλντ, δεν τα φτιάχνουν όπως " "τις παλιές μέρες πια..." #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, but " "you gotta give him some gold or he won't even show you what he's got." msgstr "" "Αν ήμουν στη θέση σου... και δεν είμαι... αλλά αν ήμουν, θα πουλούσα όλα αυτά " "που βρήκες και θα έφευγα από εδώ. Αυτό το αγόρι εκεί έξω... Έχει πάντα κάτι " "καλό, αλλά πρέπει να του δώσεις λίγο χρυσό αλλιώς δεν θα σου δείξει καν τι " "έχει." #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "Αισθάνομαι μια ψυχή που αναζητά απαντήσεις..." #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "Η σοφία κερδίζεται, δεν δίνεται. Εάν ανακαλύψεις ένα τόμο με γνώση, " "καταβροχθίστε τα λόγια του. Εάν έχεις ήδη γνώση των απόκρυφων μυστηρίων που " "είναι εγγεγραμμένα σε ένα βιβλίο, θυμήσου - αυτό το επίπεδο μαεστρίας μπορεί " "πάντα να αυξάνεται." #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words of " "power written upon scrolls of parchment. The strength of these scrolls lies in " "the ability of either apprentice or adept to cast them with equal ability. " "Their weakness is that they must first be read aloud and can never be kept at " "the ready in your mind. Know also that these scrolls can be read but once, so " "use them with care." msgstr "" "Η μεγαλύτερη δύναμη έχει συχνά την πιο σύντομη ζωή. Μπορείς να βρεις αρχαίες " "λέξεις δύναμης γραμμένες σε πάπυρο από περγαμηνή. Η δύναμη αυτών των πάπυρων " "έγκειται στην ικανότητα είτε του μαθητευόμενου είτε του έμπειρου να ρίχνει τα " "ξόρκια με την ίδια ικανότητα. Η αδυναμία τους είναι ότι πρέπει πρώτα να " "διαβαστούν δυνατά και δεν μπορούν ποτέ να διατηρηθούν σε ετοιμότητα στο μυαλό " "σου. Ξέρε επίσης ότι αυτοί οι πάπυροι μπορούν να διαβαστούν μόνο μία φορά, γι' " "αυτό χρησιμοποιήσε τους με προσοχή." #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is of " "greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with magical " "energies many times over. I have the ability to restore their power - but know " "that nothing is done without a price." msgstr "" "Αν και η θερμότητα του ήλιου είναι απεριόριστη, η απλή φλόγα ενός κεριού είναι " "μεγαλύτερος κίνδυνος. Καμία από τις ενέργειες, όσο μεγάλες κι αν είναι, δεν " "μπορεί να χρησιμοποιηθούν χωρίς την κατάλληλη εστίαση. Για πολλά ξόρκια, οι " "μαγικές Ράβδοι μπορούν να φορτίζονται με μαγικές ενέργειες πολλές φορές. Έχω " "τη δυνατότητα να αποκαταστήσω τη δύναμή τους - αλλά να ξέρεις ότι τίποτα δεν " "γίνεται χωρίς τίμημα." #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "Το άθροισμα της γνώσης μας βρίσκεται στο άθροισμα των ανθρώπων της. Αν βρεις " "ένα βιβλίο ή πάπυρο που δεν μπορείς να αποκρυπτογραφήσεις, μη διστάσεις να μου " "το φέρεις. Αν μπορώ να το κατανοήσω, θα μοιραστώ μαζί σου ό,τι βρω." #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "Για έναν άνθρωπο που γνωρίζει μόνο το Σίδηρο, δεν υπάρχει μεγαλύτερη μαγεία " "από το Ατσάλι. Ο σιδηρουργός ο Γκρίσγουαλντ είναι περισσότερο μάγος από όσο " "ξέρει. Η ικανότητά του να συγχωνεύει φωτιά και μέταλλο είναι απαράμιλλη σε " "αυτή τη χώρα." #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "Η διαφθορά έχει τη δύναμη της εξαπάτησης, αλλά η αθωότητα κατέχει τη δύναμη " "της αγνότητας. Η νεαρή Γκίλιαν έχει αγνή καρδιά, βάζοντας τις ανάγκες της " "μητέρας της πάνω από τις δικές της. Με φοβάται, αλλά είναι μόνο επειδή δεν με " "καταλαβαίνει." #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened in " "the light. The storyteller Cain is an enigma, but only to those who do not " "look. His knowledge of what lies beneath the cathedral is far greater than " "even he allows himself to realize." msgstr "" "Ένα σεντούκι που ανοίγει στο σκοτάδι δεν έχει μεγαλύτερο θησαυρό από όταν " "ανοίγει στο φως. Ο αφηγητής ιστοριών ο Κάιν είναι ένα αίνιγμα, αλλά μόνο για " "όσους δεν κοιτούν. Η γνώση του για το τι κρύβεται κάτω από τον καθεδρικό ναό " "είναι πολύ μεγαλύτερη από ό,τι αφήνει τον ίδιο τον εαυτό του να " "συνειδητοποιήσει." #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to be " "gleaned, but you must separate fact from fantasy." msgstr "" "Όσο πιο ψηλά βάζεις την πίστη σου σε έναν άνθρωπο, τόσο πιο μακριά πρέπει να " "πέσει. Ο Φάρνχαμ έχει χάσει την ψυχή του, αλλά όχι από κανένα δαίμονα. Χάθηκε " "όταν είδε τους συμπολίτες του να προδίδονται από τον Αρχιεπίσκοπο Λάζαρο. Έχει " "γνώσεις που πρέπει να σταχυολογηθούν, αλλά πρέπει να διαχωρίσεις τα γεγονότα " "από τη φαντασία του πρώτα." #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in perfect " "harmony. The healer Pepin sees into the body in a way that even I cannot. His " "ability to restore the sick and injured is magnified by his understanding of " "the creation of elixirs and potions. He is as great an ally as you have in " "Tristram." msgstr "" "Το χέρι, η καρδιά και το μυαλό μπορούν να κάνουν θαύματα όταν βρίσκονται σε " "τέλεια αρμονία. Ο θεραπευτής Πέπιν βλέπει μέσα στο σώμα με τρόπο που ούτε εγώ " "δεν μπορώ. Η ικανότητά του να αποκαθιστά τους άρρωστους και τους τραυματίες " "μεγεθύνεται από την κατανόησή του για τη δημιουργία ελιξιρίων και φίλτρων. " "Είναι τόσο σπουδαίος σύμμαχος όσο εσύ στην Τρίστραμ." #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be the " "children who wield it. The boy Wirt has a blackness upon his soul, but he " "poses no threat to the town or its people. His secretive dealings with the " "urchins and unspoken guilds of nearby towns gain him access to many devices " "that cannot be easily found in Tristram. While his methods may be reproachful, " "Wirt can provide assistance for your battle against the encroaching Darkness." msgstr "" "Υπάρχουν πολλά για το μέλλον που δεν μπορούμε να δούμε, αλλά όταν έρθει θα " "είναι τα παιδιά που θα το χειριστούν. Το αγόρι ο Ουίρτ έχει μια μαυρίλα στην " "ψυχή του, αλλά δεν αποτελεί απειλή για την πόλη ή τους ανθρώπους της. Οι " "μυστικοπαθείς συναλλαγές του με τα αλάνια και τις ανείπωτες συντεχνίες των " "κοντινών πόλεων, του δίνουν πρόσβαση σε πολλά μέσα που δεν μπορούν εύκολα να " "βρεθούν στην Τρίστραμ. Αν και οι μέθοδοί του μπορεί να είναι καταδικαστέες, ο " "Ουίρτ μπορεί να σε βοηθήσει στη μάχη σου ενάντια στο σκοτάδι που εισβάλει." #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left to " "him, and provides an anchor for all who are left in the town to what Tristram " "once was. His tavern, and the simple pleasures that can still be found there, " "provide a glimpse of a life that the people here remember. It is that memory " "that continues to feed their hopes for your success." msgstr "" "Οι πήλινοι τοίχοι με μία αχυρένια σκεπή δεν δημιουργούν ένα σπίτι. Ο ξενοδόχος " "Όγκντεν εξυπηρετεί μεγαλύτερο σκοπό σε αυτή την πόλη από ό,τι πολλοί " "καταλαβαίνουν. Παρέχει καταφύγιο για την Γκίλιαν και την μητέρα της, διατηρεί " "ότι ζωή έχει απομείνει στον Φάρνχαμ και παρέχει μια άγκυρα για όλους όσοι " "έχουν μείνει στο χωριό σε αυτό που ήταν κάποτε η Τρίστραμ. Η ταβέρνα του, και " "οι απλές απολαύσεις που μπορούν να βρεθούν ακόμα εκεί, παρέχουν μια ματιά από " "μια ζωή που θυμούνται οι άνθρωποι εδώ. Είναι αυτή η μνήμη που συνεχίζει να " "τρέφει τις ελπίδες τους για την επιτυχία σου." #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "Ψσστ… εδώ πέρα..." #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "Δεν έχουν όλοι στην Τρίστραμ μια χρησιμότητα - ή μια αγορά - για όλα όσα θα " "βρεις στον λαβύρινθο. Ούτε εγώ, όσο δύσκολο κι αν είναι να το πιστέψεις.\n" " \n" "Μερικές φορές, μόνο εσύ θα μπορείς να βρεις χρήση για κάποια πράγματα." #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "Μην εμπιστεύεσαι όλα όσα λέει ο μεθυσμένος. Πάρα πολλές μπίρες έχουν θολώσει " "την όρασή του και το μυαλό του." #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up whatever " "you can bring them..." msgstr "" "Σε περίπτωση που δεν το έχεις προσέξει, δεν αγοράζω τίποτα από την Τρίστραμ. " "Είμαι εισαγωγέας ποιοτικών προϊόντων. Αν θέλεις να πουλήσεις σκουπίδια, θα " "πρέπει να δεις τον Γκρίσγουαλντ, τον Πέπιν ή εκείνη τη μάγισσα, την Άντρια. " "Είμαι σίγουρος ότι θα αγοράσουν ό,τι μπορείς να τους φέρεις..." #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Υποθέτω ότι οφείλω τη ζωή μου στον σιδηρουργό - τι έχει απομείνει από αυτή. " "Σίγουρα, ο Γκρίσγουαλντ μου πρόσφερε μια μαθητεία στο σιδηρουργείο, και είναι " "αρκετά καλός τύπος, αλλά δεν θα πάρω ποτέ αρκετά χρήματα για να... καλά, ας " "πούμε απλώς ότι έχω συγκεκριμένα σχέδια που απαιτούν μεγάλη ποσότητα χρυσού." #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Αν ήμουν μερικά χρόνια μεγαλύτερος, θα την έλουζα με ό,τι πλούτη μπορούσα να " "συγκεντρώσω, και επίτρεψέ μου να σε διαβεβαιώσω ότι μπορώ να πάρω στα χέρια " "μου μερικά πολύ ωραία πράγματα. Η Γκίλιαν είναι ένα όμορφο κορίτσι που πρέπει " "να φύγει από το Τρίστραμ μόλις είναι ασφαλές. Χμμ... ίσως την πάρω μαζί μου " "όταν πάω..." #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that woman " "across the river. He keeps telling me about how lucky I am to be alive, and " "how my story is foretold in legend. I think he's off his crock." msgstr "" "Ο Κάιν ξέρει πάρα πολλά. Με τρομάζει πολύ - ακόμα περισσότερο από εκείνη τη " "γυναίκα απέναντι από το ποτάμι. Μου λέει συνέχεια για το πόσο τυχερός είμαι " "που είμαι ζωντανός και πώς η ιστορία μου προμηνύεται στο θρύλο. Νομίζω ότι δεν " "πάει καλά." #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, and " "Lazarus led him into the very jaws of death. Oh, I know what it's like down " "there, so don't even start telling me about your plans to destroy the evil " "that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Ο Φάρνχαμ - τώρα μιλάμε για έναν άνθρωπο με σοβαρά προβλήματα, και ξέρω τα " "πάντα για το πόσο σοβαρά μπορεί να είναι τα προβλήματα. Εμπιστευόταν πάρα πολύ " "την ακεραιότητα ενός ανθρώπου και ο Λάζαρος τον οδήγησε στα ίδια τα σαγόνια " "του θανάτου. Ω, ξέρω πώς είναι εκεί κάτω, οπότε μην αρχίσεις καν να μου μιλάς " "για τα σχέδιά σου να καταστρέψεις το κακό που κατοικεί σε αυτόν τον Λαβύρινθο. " "Απλά πρόσεχε τα πόδια σου..." #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Εφόσον δεν χρειάζεται να ξανακολλήσεις τίποτα, ο γερο-Πέπιν είναι το καλύτερο " "που θα σου τύχει.\n" " \n" "Αν είχα πάρει μερικά από αυτά τα φίλτρα που φτιάχνει, μπορεί να είχα ακόμα το " "πόδι μου..." #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about the " "past, but that witch can see into your past. She always has some way to get " "whatever she needs, too. Adria gets her hands on more merchandise than I've " "seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Η Άντρια με ενοχλεί πραγματικά. Σίγουρα, ο Κάιν είναι ανατριχιαστικός σε ό,τι " "μπορεί να σου πει για το παρελθόν, αλλά αυτή η μάγισσα μπορεί να δει το " "παρελθόν σου. Έχει πάντα τρόπο να πάρει ό,τι χρειάζεται. Η Άντρια παίρνει στα " "χέρια της περισσότερα εμπορεύματα από όσα έχω δει να περνούν από τις πύλες του " "Παζαριού του Βασιλιά κατά τη διάρκεια του Μεγάλου Πανηγυριού." #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that stupid " "tavern. I guess at the least he gives Gillian a place to work, and his wife " "Garda does make a superb Shepherd's pie..." msgstr "" "Ο Όγκντεν είναι ανόητος που μένει εδώ. Θα μπορούσα να τον βγάλω από την πόλη " "σε πολύ λογική τιμή, αλλά επιμένει να προσπαθεί να τα βγάλει πέρα ​​με αυτή την " "ηλίθια ταβέρνα. Υποθέτω ότι τουλάχιστον προσφέρει στην Γκίλιαν ένα μέρος να " "δουλέψει και η σύζυγός του η Γκάρντα φτιάχνει μια υπέροχη κρεατόπιτα..." #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks the " "Lord of Terror, and so it is written." msgstr "" "Πέρα από τον Θάλαμο των Ηρώων βρίσκεται ο Θάλαμος των Οστών. Ο αιώνιος θάνατος " "περιμένει όποιον θα ήθελε να κλέψει τους θησαυρούς που είναι ασφαλισμένοι σε " "αυτό το δωμάτιο. Έτσι λέει ο Άρχοντας του Τρόμου, και έτσι είναι γραμμένο." #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, Valor " "awaits for the Hero of Light to awaken..." msgstr "" "...και έτσι, κλειδωμένη πέρα ​​από την Πύλη του Αίματος και πέρα ​​από την Αίθουσα " "της Φωτιάς, η Τιμή περιμένει τον Ήρωα του Φωτός να ξυπνήσει..." #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Βλέπω εκεί, που δεν βλέπουν μάτια...\n" "Γαλακτώδης όραση αλλά σαπίζουν μάτια,\n" "Όταν γυρίσεις, θα έχουν φύγει...\n" "Ψιθυρίζοντας κρυφό τραγούδι, έχουν ξεφύγει,\n" "Τότε βλέπεις αυτό, που αδύνατον είναι...\n" "Σκιές κινούνται, όταν φως πρέπει να είναι,\n" "Έξω από το σκοτάδι, και πέρα της γνώσης των μυαλών,\n" "Εξόριστοι κάτω, μέσα στην Αίθουσες των Τυφλών." #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Τα οπλοστάσια της Κόλασης είναι το σπίτι του Πολέμαρχου του Αίματος. Στο " "πέρασμά του κείτονται τα ακρωτηριασμένα σώματα χιλιάδων. Άγγελοι και άνθρωποι " "έχουν σφαγιαστεί για να εκπληρώσουν τις ατελείωτες θυσίες του, στους " "Σκοτεινούς, που ουρλιάζουν για ένα πράγμα - αίμα." #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond the " "fields that we know - between the utopian kingdoms of the High Heavens and the " "chaotic pits of the Burning Hells. This war is known as the Great Conflict, " "and it has raged and burned longer than any of the stars in the sky. Neither " "side ever gains sway for long as the forces of Light and Darkness constantly " "vie for control over all creation." msgstr "" "Προσέξτε και δώστε μαρτυρία για τις αλήθειες που γράφονται εδώ, γιατί είναι η " "τελευταία κληρονομιά των Χοράντριμ. Υπάρχει ένας πόλεμος που μαίνεται ακόμη " "και τώρα, πέρα ​από τον κόσμο που γνωρίζουμε - μεταξύ των ουτοπικών βασιλείων " "των Υψηλών Ουρανών και των χαοτικών λάκκων των Φλεγόμενων Κολάσεων. Αυτός ο " "πόλεμος είναι γνωστός ως η Μεγάλη Σύγκρουση, και κρατάει και καίει, " "περισσότερο από οποιοδήποτε από τα αστέρια στον ουρανό. Καμία πλευρά δεν " "κερδίζει ποτέ επιρροή για μεγάλο χρονικό διάστημα, καθώς οι δυνάμεις του Φωτός " "και του Σκότους αγωνίζονται συνεχώς για τον έλεγχο όλης της δημιουργίας." #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in secret, " "away from the prying eyes of mortals. Some daring, powerful mortals have even " "allied themselves with either side, and helped to dictate the course of the " "Sin War." msgstr "" "Προσέξτε και δώστε μαρτυρία για τις αλήθειες που γράφονται εδώ, γιατί είναι η " "τελευταία κληρονομιά των Χοράντριμ. Όταν η Αιώνια Σύγκρουση μεταξύ των Υψηλών " "Ουρανών και των Φλεγόμενων Κολάσεων πέφτει στο θνητό έδαφος, ονομάζεται ως " "Πόλεμος της Αμαρτίας. Άγγελοι και Δαίμονες περπατούν ανάμεσα στην ανθρωπότητα " "μεταμφιεσμένοι, πολεμώντας κρυφά, μακριά από τα αδιάκριτα βλέμματα των θνητών. " "Μερικοί τολμηροί, ισχυροί θνητοί συμμάχησαν και με τις δύο πλευρές και " "βοήθησαν να υπαγορευτεί η πορεία του Πολέμου της Αμαρτίας." #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come to " "our world. The Three Brothers ravaged the lands of the east for decades, while " "humanity was left trembling in their wake. Our Order - the Horadrim - was " "founded by a group of secretive magi to hunt down and capture the Three Evils " "once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the Horadrim " "in pursuit. The Third Evil - known as Diablo, the Lord of Terror - was " "eventually captured, his essence set in a Soulstone and buried within this " "Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Προσέξτε και δώστε μαρτυρία για τις αλήθειες που γράφονται εδώ, γιατί είναι η " "τελευταία κληρονομιά των Χοράντριμ. Πριν από σχεδόν τριακόσια χρόνια, έγινε " "γνωστό ότι τα Τρία Πρωταρχικά Κακά των Φλεγόμενων Κολάσεων είχαν έρθει " "μυστηριωδώς στον κόσμο μας. Οι Τρεις Αδερφοί ρήμαξαν τα εδάφη της ανατολής για " "δεκαετίες, ενώ η ανθρωπότητα έμενε να τρέμει στο πέρασμά τους. Το Τάγμα μας - " "οι Χοράντριμ - ιδρύθηκε από μια ομάδα μυστικών μάγων για να κυνηγήσουν και να " "συλλάβουν τα Τρία Κακά μια για πάντα.\n" "Οι αρχικοί Χοράντριμ συνέλαβαν δύο από τα Τρία μέσα σε ισχυρά τεχνουργήματα " "γνωστά ως Λίθους των Ψυχών και τα έθαψαν βαθιά κάτω από την έρημη ανατολική " "έρημο. Το τρίτο Κακό διέφυγε τη σύλληψη και διέφυγε προς τα δυτικά με πολλούς " "από τους Χοράντριμ να το καταδιώκουν. Το Τρίτο Κακό - γνωστό ως ο Ντιάμπλο, ο " "Άρχοντας του Τρόμου - τελικά αιχμαλωτίστηκε, η ουσία του τοποθετήθηκε σε ένα " "Λίθο της Ψυχής και θάφτηκε σε αυτόν εδώ τον Λαβύρινθο.\n" "\n" "Σας προειδοποιώ ότι ο Λίθος της Ψυχής δεν πρέπει να ανακαλυφθεί από εκείνους " "που δεν είναι της πίστης μας. Αν ο Ντιάμπλο απελευθερωνόταν, θα αναζητούσε ένα " "σώμα που να ελέγχεται εύκολα καθώς θα ήταν πολύ αδύναμος - ίσως αυτό ενός " "γέρου ή ενός παιδιού." #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils and " "banished their spirit forms to the mortal realm. The demons Belial (the Lord " "of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of Hell " "during the absence of the Three Brothers. All of Hell polarized between the " "factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "Έτσι έγινε γνωστό ότι μια μεγάλη επανάσταση έλαβε χώρα μέσα στις Φλεγόμενες " "Κολάσεις, γνωστή ως Η Σκοτεινή Εξορία. Τα Μικρότερα Κακά ανέτρεψαν τα Τρία " "Πρωτεύοντα Κακά και εξόρισαν τις πνευματικές τους μορφές στον θνητό κόσμο. Οι " "δαίμονες Μπελίαλ (ο Άρχοντας του Ψέματος) και ο Άζμονταν (ο Άρχοντας της " "Αμαρτίας) πολέμησαν για να διεκδικήσουν την κυριαρχία της Κόλασης κατά την " "απουσία των Τριών Αδελφών. Όλη η Κόλαση πολώθηκε μεταξύ των φατριών του " "Μπελίαλ και του Άζμονταν ενώ οι δυνάμεις των Υψηλών Ουρανών μάχονταν και " "χτυπούσαν αδιάκοπα τις ίδιες τις Πύλες της Κόλασης." #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept at " "hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Πολλοί δαίμονες ταξίδεψαν στο βασίλειο των θνητών αναζητώντας τους Τρεις " "Αδερφούς. Αυτούς τους δαίμονες ακολούθησαν στην διάσταση των θνητών Άγγελοι, " "που τους κυνήγησαν σε όλες τις απέραντες πόλεις της Ανατολής. Οι Άγγελοι " "συμμάχησαν με ένα μυστικό Τάγμα θνητών μάγων που ονομάζονταν Χοράντριμ, οι " "οποίοι γρήγορα έγιναν έμπειροι στο κυνήγι δαιμόνων. Έκαναν επίσης πολλούς " "σκοτεινούς εχθρούς στους κάτω κόσμους." #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they were " "hunted down by the cursed Order of the mortal Horadrim. The Horadrim used " "artifacts called Soulstones to contain the essence of Mephisto, the Lord of " "Hatred and his brother Baal, the Lord of Destruction. The youngest brother - " "Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and buried " "him under an ancient, forgotten Cathedral. There, the Lord of Terror sleeps " "and awaits the time of his rebirth. Know ye that he will seek a body of youth " "and power to possess - one that is innocent and easily controlled. He will " "then arise to free his Brothers and once more fan the flames of the Sin War..." msgstr "" "Έτσι συνέβη ότι τα Τρία Πρωτεύοντα Κακά εκδιώχθηκαν με τη μορφή πνεύματων στο " "βασίλειο των θνητών και αφού έσπειραν το χάος σε όλη την Ανατολή για " "δεκαετίες, κυνηγήθηκαν από το καταραμένο Τάγμα των θνητών Χοράντριμ. Οι " "Χοράντριμ χρησιμοποίησαν τεχνουργήματα που ονομάστηκαν Λίθοι των Ψυχών για να " "αποθηκεύσουν την ουσία του Μεφίστο, του Άρχοντα του Μίσους και του αδελφού του " "Βαάλ, του Άρχοντα της Καταστροφής. Ο μικρότερος αδερφός - ο Ντιάμπλο, ο " "Άρχοντας του Τρόμου - διέφυγε στη δύση.\n" "\n" "Τελικά οι Χοράντριμ φυλάκισαν τον Ντιάμπλο μέσα σε μια Λίθο της Ψυχής επίσης, " "και τον έθαψαν κάτω από έναν αρχαίο, ξεχασμένο Καθεδρικό ναό. Εκεί κοιμάται ο " "Άρχοντας του Τρόμου και περιμένει την ώρα της αναγέννησής του. Να ξέρετε ότι " "θα αναζητήσει ένα σώμα νεότητας και δύναμης για να το καταβάλει - ένα που " "είναι αθώο και εύκολα ελεγχόμενο. Στη συνέχεια θα σηκωθεί για να ελευθερώσει " "τα αδέρφια του και να αναζωπυρώσει ξανά τις φλόγες του Πολέμου της Αμαρτίας..." #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When he " "awakened from his long slumber, my Lord and Master spoke to me of secrets that " "few mortals know. He told me the kingdoms of the High Heavens and the pits of " "the Burning Hells engage in an eternal war. He revealed the powers that have " "brought this discord to the realms of man. My lord has named the battle for " "this world and all who exist here the Sin War." msgstr "" "Όλοι οι έπαινοι για τον Ντιάμπλο - Άρχοντα του Τρόμου και επιζώντα της " "Σκοτεινής Εξορίας. Όταν ξύπνησε από τον μακρύ ύπνο του, ο Κύριος και ο " "Άρχοντας μου, μου μίλησε για μυστικά που λίγοι θνητοί γνωρίζουν. Μου είπε ότι " "τα βασίλεια των Υψηλών Ουρανών και οι λάκκοι των Φλεγόμενων Κολάσεων, " "εμπλέκονται σε έναν αιώνιο πόλεμο. Μου αποκάλυψε τις δυνάμεις που έφεραν αυτή " "τη διχόνοια στα βασίλεια του ανθρώπου. Ο Κύριός μου ονόμασε τη μάχη για τον " "κόσμο μας, και για όλους όσους υπάρχουν εδώ, ως τον Πόλεμο της Αμαρτίας." #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished to " "this world long ago. My Lord wishes to bide his time and harness his awesome " "power so that he may free his captive brothers from their tombs beneath the " "sands of the east. Once my Lord releases his Brothers, the Sin War will once " "again know the fury of the Three." msgstr "" "Δόξα και Επιδοκιμασία στον Ντιάμπλο - Άρχοντα του Τρόμου και Ηγέτη των Τριών. " "Ο Κύριός μου, μου μίλησε για τους δύο Αδερφούς του, τον Μεφίστο και τον Βαάλ, " "που εξορίστηκαν σε αυτόν τον κόσμο πριν από πολύ καιρό. Ο Κύριός μου θέλει να " "αφιερώσει το χρόνο του και να αξιοποιήσει τη φοβερή δύναμή του, ώστε να " "μπορέσει να ελευθερώσει τους αιχμάλωτους αδελφούς του από τους τάφους τους " "κάτω από την έρημο της ανατολής. Μόλις ο Κύριός μου απελευθερώσει τους " "Αδερφούς του, ο Πόλεμος της Αμαρτίας θα γνωρίσει ξανά την οργή των Τριών." #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too weak " "from his imprisonment. My Lord required a simple and innocent anchor to this " "world, and so found the boy Albrecht to be perfect for the task. While the " "good King Leoric was left maddened by Diablo's unsuccessful possession, I " "kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as the " "Lord of this world." msgstr "" "Χαιρετισμός και Θυσία για τον Ντιάμπλο - Άρχοντα του Τρόμου και Καταστροφέα " "των Ψυχών. Όταν ξύπνησα τον Δάσκαλό μου από τον ύπνο του, προσπάθησε να " "αποκτήσει τη μορφή ενός θνητού. Ο Ντιάμπλο προσπάθησε να διεκδικήσει το σώμα " "του βασιλιά Λεόρικ, αλλά ο Άρχοντας μου ήταν πολύ αδύναμος από τη φυλάκισή " "του. Ο Κύριός μου ζήτησε μια απλή και αθώα άγκυρα σε αυτόν τον κόσμο, και έτσι " "βρήκε το αγόρι τον Άλμπρεχτ τέλειο για το έργο αυτό. Ενώ ο καλός βασιλιάς " "Λεόρικ έμεινε τρελός από την ανεπιτυχή προσπάθεια κατοχής του Ντιάμπλο, " "απήγαγα τον γιο του τον Άλμπρεχτ και τον έφερα ενώπιον του Άρχοντα μου. Τώρα " "περιμένω την προσκλήση του Ντιάμπλο και προσεύχομαι να ανταμειφθώ όταν " "επιτέλους αναδειχθεί ως ο Κύριος αυτού του κόσμου." #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become slaves " "- or worse. The church at the edge of town has been desecrated and is being " "used for dark rituals. The screams that echo in the night are inhuman, but " "some of our townsfolk may yet survive. Follow the path that lies between my " "tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "Δόξα τω Θεώ που επέστρεψες!\n" "Πολλά έχουν αλλάξει από τότε που έμενες εδώ, φίλε/η μου. Όλα ήταν ήρεμα μέχρι " "που ήρθαν οι σκοτεινοί καβαλάρηδες και κατέστρεψαν το χωριό μας. Πολλοί " "σκοτώθηκαν εκεί που στέκονταν και όσοι άρπαξαν τα όπλα σφαγιάσθηκαν ή τους " "πήραν μαζί τους για σκλάβους -ή χειρότερα. Η εκκλησία στην άκρη της πόλης έχει " "βεβηλωθεί και χρησιμοποιείται για σκοτεινές τελετουργίες. Οι κραυγές που " "αντηχούν τη νύχτα είναι απάνθρωπες, αλλά κάποιοι από τους κατοίκους μας μπορεί " "να έχουν επιβιώσει ακόμα. Ακολούθα το μονοπάτι που βρίσκεται ανάμεσα στην " "ταβέρνα μου και το σιδηρουργείο για να βρεις την εκκλησία και να σώσεις όποιον " "μπορείς.\n" "\n" "Ίσως Θα σου πω περισσότερα, αν μιλήσουμε ξανά αργότερα. Καλή τύχη." #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding a " "treasure that is hidden less so. I will leave you with this. Do not let the " "sands of time confuse your search." msgstr "" "Διατήρησε την αποστολή σου. Το να βρεις έναν θησαυρό που έχει χαθεί δεν είναι " "εύκολο. Το να βρεις έναν θησαυρό που είναι κρυμμένος είναι ακόμα πιο δύσκολο. " "Θα σε αφήσω με αυτό. Μην αφήσετε τις άμμους του χρόνου να μπερδέψουν την " "αναζήτησή σου." #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in Tristram. " "Let me see that!! Ah, Look these drawings are inaccurate. They don't match " "our town at all. I'd keep my mind on what lies below the cathedral and not " "what lies below our topsoil." msgstr "" "Ενα τι?! Αυτό είναι ανοησία. Δεν υπάρχει θησαυρός θαμμένος εδώ στην Τρίστραμ. " "Άσε με να το δω αυτό!! Α, κοίτα αυτά τα σχέδια είναι ανακριβή. Δεν ταιριάζουν " "καθόλου με την πόλη μας. Έχε στο μυαλό σου σε αυτά που βρίσκονται κάτω από τον " "καθεδρικό ναό και όχι σε αυτά που βρίσκονται άμεσα κάτω από τα χωράφια μας." #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have many " "sick people that require my help and yours as well." msgstr "" "Πραγματικά δεν έχω χρόνο να συζητήσω κάποιο χάρτη που ψάχνεις. Έχω πολλούς " "άρρωστους που χρειάζονται τη βοήθειά μου και τη δική σας επίσης." #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. His " "honor stripped and his visage altered. He is trapped in immortal torment. " "Charged to conceal the very thing that could free him." msgstr "" "O άλλοτε περήφανος Ίσγουαλ είναι παγιδευμένος βαθιά κάτω από την επιφάνεια " "αυτού του κόσμου. Απογυμνωμένος από την τιμή του, και με αλλαγμένη όψη. Είναι " "παγιδευμένος σε αθάνατο μαρτύριο. Είναι εξαναγκασμένος να κρύψει τον τρόπο που " "θα μπορούσε να τον απελευθερώσει." #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh at " "you later when you were running around the town with your nose in the dirt. " "I'd ignore it." msgstr "" "Βάζω στοίχημα ότι ο Ουίρτ σε είδε να έρχεσαι και σου κάνει φάρσα μόνο και μόνο " "για να μπορεί να γελάσει μαζί σου αργότερα, όταν θα τρέχεις στην πόλη με τη " "μύτη σου στο χώμα ψάχνοντας. Θα το αγνοούσα." #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far and " "wide. Much has changed since then. But hidden caves and buried treasure are " "common fantasies of any child. Wirt seldom indulges in youthful games. So it " "may just be his imagination." msgstr "" "Υπήρχε μια εποχή που αυτή η πόλη ήταν μια συχνή στάση για ταξιδιώτες από πολύ " "μακριά. Πολλά έχουν αλλάξει από τότε. Όμως οι κρυμμένες σπηλιές και οι " "θαμμένοι θησαυροί είναι κοινές φαντασιώσεις κάθε παιδιού. Ο Ουίρτ σπάνια " "επιδίδεται σε νεανικά παιχνίδια. Οπότε μπορεί να είναι απλώς η φαντασία του." #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Άκου εδώ. Έλα κοντά. Δεν ξέρω αν ξέρεις αυτά που ξέρω, αλλά έχεις πραγματικά " "κάτι εδώ. Αυτός είναι ένας χάρτης." #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear one " "of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Η γιαγιά μου, μου λέει συχνά ιστορίες για τις παράξενες δυνάμεις που κατοικούν " "στο νεκροταφείο έξω από την εκκλησία. Και μπορεί κάλλιστα να σε ενδιαφέρει να " "ακούσεις μια από αυτές. Είπε ότι αν άφηνες την κατάλληλη θυσία στο " "νεκροταφείο, και μπεις στον καθεδρικό ναό για να προσευχηθείς για τους νεκρούς " "και μετά επιστρέψεις, η θυσία θα αλλοιωνόταν με κάποιο περίεργο τρόπο. Δεν " "ξέρω αν αυτό είναι μόνο λόγια μιας ηλικιωμένης άρρωστης γυναίκας, αλλά όλα " "φαίνονται πιθανά αυτές τις μέρες." #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you need " "some rare and expensive supplies to get you through this ordeal?" msgstr "" "Χμμμ. Ένας τεράστιος και μυστηριώδης θησαυρός μου λες. Μμμμ. Ίσως θα μπορούσα " "να με ενδιαφέρει να πάρω μερικά πράγματα από σένα. Ή ακόμα καλύτερα, δεν " "χρειάζεσαι κάποιες σπάνιες και ακριβές προμήθειες για να ξεπεράσεις αυτή τη " "δοκιμασία;" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "Λοιπόν, είσαι ο ήρωας/ίδα για τον οποίο/α μιλούν όλοι. Ίσως θα μπορούσες να " "βοηθήσεις έναν φτωχό, απλό αγρότη να λύσει ένα τρομερό πρόβλημα; Στην άκρη του " "οπωρώνα μου, ακριβώς νότια από εδώ, υπάρχει ένα φρικτό πράγμα που φουσκώνει " "από το έδαφος! Δεν μπορώ να φτάσω στις καλλιέργειές μου ή στα σανοδέματα μου, " "και οι φτωχές αγελάδες μου θα πεινάσουν. Η μάγισσα μου έδωσε αυτό και μου είπε " "ότι θα έσκαγε, καταστρέφοντας αυτό το πράγμα από το χωράφι μου. Αν μπορούσες " "να το βγάλεις, θα σου ήμουν για πάντα ευγνώμων. Θα το έκανα μόνος μου, αλλά " "κάποιος πρέπει να μείνει εδώ με τις αγελάδες..." #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Ήξερα ότι δεν θα μπορούσε να είναι τόσο απλό όσο το έκανε να ακούγεται εκείνη " "η μάγισσα. Είναι ένας θλιβερός κόσμος όταν δεν μπορείς να εμπιστευτείς ούτε " "τους γείτονές σου." #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "Έφυγε; Το έστειλες πίσω στις σκοτεινές εσοχές του Άδη που το γέννησαν; Εσύ τι? " "Α, μη μου πεις ότι το έχασες! Αυτά τα πράγματα δεν είναι φτηνά, ξέρεις. Πρέπει " "να τον βρεις και μετά να τον διώξεις αυτόν τον τρόμο από την πόλη μας." #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What with " "all these things comin' out of the ground, monsters taking over the church, " "and so forth, these are trying times. I am but a poor farmer, but here -- take " "this with my great thanks." msgstr "" "Άκουσα την έκρηξη από εδώ! Ευχαριστώ πολύ, ευγενικέ ξένε. Με όλα αυτά τα " "πράγματα που βγαίνουν από τη γη, τα τέρατα που καταλαμβάνουν την εκκλησία, και " "ούτω καθεξής, είναι δύσκολοι καιροί. Δεν είμαι παρά ένας φτωχός αγρότης, αλλά " "εδώ -- πάρε αυτό με τις ευχαριστίες μου." #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a poor " "farmer?" msgstr "" "Ω, τέτοιο μπελά έχω...ίσως...Όχι, δεν θα μπορούσα να σου το επιβάλλω, τι με " "όλα τα άλλα δεινά. Ίσως αφού καθαρίσετε την εκκλησία από κάποια από αυτά τα " "πλάσματα, θα μπορούσες να επιστρέψεις... και να αφιερώσεις λίγο χρόνο για να " "βοηθήσεις έναν φτωχό αγρότη;" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Ουααα! (σνίφ) Ουααα! (σνίφ)" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away but " "Theo fell down and the bug GRABBED him and took him away!" msgstr "" "Έχασα τον Θέο! Έχασα τον καλύτερό μου φίλο! Παίζαμε δίπλα στο ποτάμι, και ο " "Θέο είπε ότι ήθελε να πάει να δει το μεγάλο πράσινο πράγμα. Είπα ότι δεν " "πρέπει, αλλά κρυφο-πήγαμε από εκεί, και ξαφνικά βγήκε αυτό το ΕΝΤΟΜΟ! Τρέξαμε " "μακριά αλλά ο Θέο έπεσε κάτω και το ζωύφιο τον ΑΡΠΑΞΕ και τον πήρε μακριά!" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He can't " "take care of himself! Please!" msgstr "" "Τον βρήκες; Πρέπει να βρεις τον Θεόδωρο, σε παρακαλώ! Είναι απλά μικρός. Δεν " "μπορεί να φροντίσει τον εαυτό του! Σε παρακαλώ!" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "Τον βρήκες! Τον βρήκες! Ευχαριστώ! Ω Θέο, σε τρόμαξαν αυτά τα άσχημα έντομα; " "Έιι! Ούφ! Κάτι έχει κολλήσει στη γούνα σου! μπλιάχ! Έλα, Θέο, πάμε σπίτι! " "Ευχαριστώ και πάλι, ήρωα άνθρωπε!" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Παραμείναμε σε λήθαργο και ήρθε η ώρα να ξυπνήσουμε. Μετά τον μακρύ ύπνο μας, " "έχουμε μεγάλη πείνα. Σύντομα, τώρα, θα φάμε..." #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "Διασκέδασες, μικρό θηλαστικό; Πόσο αξιολύπητο. Ο μικρός σας κόσμος δεν θα " "είναι πρόκληση για μας." #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that men " "call home. Our tendrils shall envelop this world, and we will feast on the " "flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Αυτά τα εδάφη θα μολυνθούν, και ο γόνος μας θα κατακλύσει τα χωράφια που οι " "άνθρωποι αποκαλούν σπίτι. Τα πλοκάμια μας θα τυλίξουν αυτόν τον κόσμο και θα " "γλεντήσουμε με τη σάρκα των κατοίκων του. Ο άνθρωπος θα γίνει δουλοκτησία και " "διατροφή μας." #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Αχ μπορώ να σε μυρίσω...είσαι κοντά! Κοντά! Σσσς...το άρωμα του αίματος και " "του φόβου...πόσο δελεαστικό..." #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great Cathedral " "be raised. The cornerstone of this holy place was to be carved from the " "translucent stone Antyrael, named for the Angel who shared his power with the " "Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "Και στο έτος του Χρυσού Φωτός, ορίστηκε με διάγγελμα να υψωθεί ένας μεγάλος " "Καθεδρικός Ναός. Ο ακρογωνιαίος λίθος αυτού του ιερού τόπου επρόκειτο να " "λαξευτεί από την ημιδιαφανή πέτρα Αντίραελ, που πήρε το όνομά του από τον " "Άγγελο που μοιραζόταν τη δύναμή του με τους Χοράντριμ.\n" " \n" "Στο Έτος του Ερχομού των Σκιών, το έδαφος σείστηκε και ο Καθεδρικός Ναός " "γκρεμίστηκε και έπεσε. Καθώς άρχισε η οικοδόμηση των κατακομβών και των " "κάστρων και ο άνθρωπος στάθηκε ενάντια στις καταστροφές του Πολέμου της " "Αμαρτίας, τα ερείπια είχαν καθαριστεί για τις πέτρες τους. Και έτσι ήταν που ο " "ακρογωνιαίος λίθος εξαφανίστηκε από τα μάτια του ανθρώπου.\n" " \n" "Η πέτρα ήταν αυτού του κόσμου -- και όλων των κόσμων -- καθώς το Φως είναι " "τόσο μέσα σε όλα τα πράγματα όσο και πέρα ​​από όλα τα πράγματα. Το φως και η " "ενότητα είναι τα προϊόντα αυτού του ιερού θεμελίου, μιας ενότητας σκοπού και " "μιας ενότητας της κατοχής." #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "Μοο." #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "Είπα, Μοο." #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "Κοίτα είμαι απλά μια αγελάδα, εντάξει;" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around like " "this; but, I was sitting at home minding my own business and all of a sudden " "these bugs & vines & bulbs & stuff started coming out of the floor... it was " "horrible! If only I had something normal to wear, it wouldn't be so bad. " "Hey! Could you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't want " "anyone seeing me like this. Here, take this, you might need it... to kill " "those things that have overgrown everything. You can't miss my house, it's " "just south of the fork in the river... you know... the one with the overgrown " "vegetable garden." msgstr "" "Εντάξει, εντάξει. Δεν είμαι πραγματικά αγελάδα. Συνήθως δεν κυκλοφορώ έτσι. " "αλλά, απλά καθόμουν στο σπίτι και κοιτούσα τη δουλειά μου όταν ξαφνικά αυτά τα " "ζωύφια, οι κληματσίδες, οι βολβοί και άλλα πράγματα άρχισαν να βγαίνουν από το " "έδαφος... ήταν φρικτό! Αν είχα μόνο κάτι κανονικό να φορέσω, δεν θα ήταν τόσο " "κακό. Έι! Θα μπορούσες να πας στο σπίτι μου και να μου φέρεις την στολή μου; " "Την καφέ, όχι την γκρι, είναι για βραδινό. Θα το έκανα μόνος μου, αλλά δεν " "θέλω να με βλέπει κανείς έτσι. Ορίστε, πάρε αυτό, μπορεί να το χρειαστείς... " "για να σκοτώσεις εκείνα τα πράγματα που έχουν κατακλύσει τα πάντα. Είναι " "αδύνατον να σου ξεφύγει το σπίτι μου, είναι νότια της διχάλας στο ποτάμι... " "ξέρεις... εκείνο με τον κατάφυτο λαχανόκηπο." #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "Για τι χάνεις χρόνο; Πήγαινε να πάρεις την στολή μου! Και βιάσου! Αυτή η " "αγελάδα Χόλστειν εκεί πέρα ​​μου κλείνει το μάτι!" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Έι, έχεις την στολή μου εκεί; Γρήγορα, δώστε την μου! Αυτά τα αυτιά μου " "φέρνουν μια απίστευτη φαγούρα!" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need the " "BROWN suit." msgstr "" "Οχι όχι όχι όχι! Αυτό είναι η ΓΚΡΙ στολή μου! Είναι για βραδινό ντύσιμο! " "Επίσημες περιστάσεις! Δεν μπορώ να το φορέσω ΑΥΤΟ. Τι είσαι, τρελαμένος; " "Χρειάζομαι τη ΚΑΦΕ στολή." #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as a " "gift; and, you know... a little fashion tip... you could use a little... you " "could use a new... yknowwhatImean? The whole adventurer motif is just so... " "retro. Just a word of advice, eh? Ciao." msgstr "" "Αχ, τώρα είμαι ΠΟΛΥ καλύτερο. Ούφ! Επιτέλους, λίγη αξιοπρέπεια! Είναι τα " "κέρατα μου σε ευθεία; Καλός. Κοίτα, ευχαριστώ πολύ που με βοήθησες. Εδώ, πάρτε " "αυτό ως δώρο. και, για να ξέρεις... μια μικρή συμβουλή μόδας... θα μπορούσατε " "να χρησιμοποιήσετε λίγο... θα μπορούσατε να χρησιμοποιήσετε μια νέα... " "ξερειςτιλέωτώρα; Το όλο μοτίβο του ήρωα είναι απλά τόσο... ρετρό. Απλά μια " "συμβουλή, ε; Τσάοοο." #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Κοίτα. Είμαι αγελάδα. Κι εσύ, είσαι δόλωμα τέρας. Αποκτήστε λίγη εμπειρία! Θα " "μιλήσουμε..." #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Πρέπει να είναι πραγματικά ένα τρομερό καθήκον που έχω θέσει πάνω σου. Αν " "υπήρχε κάποιος τρόπος με τον οποίο θα μπορούσα... θα βοηθούσε μια καράφα με " "ωραίο, φρέσκο ​​γάλα;" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "Ω, θα μπορούσα να είχα την βοήθειά σου, αλλά ίσως αφού πρώτα σώσεις τις " "κατακόμβες από τη βεβήλωση αυτών των θηρίων." #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Χρειάζομαι κάποιον να κάνει κάτι, αλλά δεν μπορούσα να επιβληθώ σε έναν " "εντελώς ξένο. Ίσως μετά από λίγο καιρό που είσαι εδώ, να νιώσω πιο άνετα και " "να ζητήσω μια χάρη." #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "Βλέπω σε σένα τη δυνατότητα για μεγαλεία. Ίσως κάποια στιγμή ενώ εκπληρώνεις " "το πεπρωμένο σου, θα μπορούσες να περάσεις και να κάνεις μια μικρή χάρη για " "μένα;" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to destroy " "the menace in the church!" msgstr "" "Νομίζω ότι θα μπορούσες πιθανώς να με βοηθήσεις, αλλά ίσως αφού γίνεις λίγο " "πιο δυνατός/ή. Δεν θα ήθελα να τραυματίσω τη μοναδική ευκαιρία του χωριού να " "καταστρέψει την απειλή στην εκκλησία!" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Εγώ, είμαι μια αυτοδημιούργητη αγελάδα. Κάνε τον εαυτό σου κάτι στον κόσμο, " "και μετά θα μιλήσουμε." #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you have " "some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "Δεν χρειάζεται να εξηγώ τον εαυτό μου σε κάθε τουρίστα που περνάει! Δεν έχεις " "μερικά τέρατα να σκοτώσεις; Ίσως τα πούμε αργότερα. Αν ζεις..." #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not it. " "I can't trust you, you're going to get eaten by monsters any day now... I need " "someone who's an experienced hero." msgstr "" "Σταμάτα να με ενοχλείς. Ψάχνω για κάποιον πραγματικά ηρωικό. Και δεν είσαι " "εσύ. Δεν μπορώ να σε εμπιστευτώ, θα σε φάνε τα τέρατα όπου να ναι τώρα... " "Χρειάζομαι κάποιον που να έχει εμπειρία σαν ήρωας/ίδα." #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened to " "be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, not " "the gray one, that's for evening wear. I'd do it myself, but I don't want " "anyone seeing me like this. Here, take this, you might need it... to kill " "those things that have overgrown everything. You can't miss my house, it's " "just south of the fork in the river... you know... the one with the overgrown " "vegetable garden." msgstr "" "Εντάξει, δεν μουλαρώνω. Δεν είχα σκοπό να σε σταβλίσω. Καθόμουν στο σπίτι, και " "μού-γκρισα, όταν τα πράγματα έγιναν πραγματικά ασταθή. μια ορδή από τέρατα " "βγήκε από το πάτωμα! Απλά γελάδεψα! Έτυχε να φοράω αυτή τη φανέλα όταν έτρεξα " "έξω, και τώρα φαίνομαι απίστευτα αγελαδινός-γελοίος, μα τα μαστάρια μου. Αν " "είχα μόνο κάτι κανονικό να φορέσω, δεν θα ήταν τόσο κακό. Έι! Θα μπορούσες να " "πας στο σπίτι μου και να μου φέρεις την στολή μου; Την καφέ, όχι την γκρι, " "είναι για βραδινό. Θα το έκανα μόνος μου, αλλά δεν θέλω να με βλέπει κανείς " "έτσι. Ορίστε, πάρε αυτό, μπορεί να το χρειαστείς... για να σκοτώσεις εκείνα τα " "πράγματα που έχουν κατακλύσει τα πάντα. Είναι αδύνατον να σου ξεφύγει το σπίτι " "μου, είναι νότια της διχάλας στο ποτάμι... ξέρεις... εκείνο με τον κατάφυτο " "λαχανόκηπο." #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "Έχω δοκιμάσει ξόρκια, απειλές, αφορισμούς και διαπραγματεύσεις με αυτό το " "αποκρουστικό πλάσμα -- χωρίς αποτέλεσμα. Οι μέθοδοι μου για την υποδούλωση των " "μικρότερων δαιμόνων δεν φαίνεται να έχουν καμία επίδραση σε αυτό το τρομερό " "θηρίο." #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners of " "my vision. The faint scrabble of claws dances at the edges of my hearing. " "They are searching, I think, for this journal." msgstr "" "Το σπίτι μου διαφθείρεται σιγά σιγά από την κακία αυτού του ανεπιθύμητου " "κρατούμενου. Οι κρύπτες είναι γεμάτες σκιές που κινούνται λίγο πιο πέρα ​​από " "τις γωνίες της οράσεως μου. Το αχνό ξύσιμο των νυχιών, χορεύει στις άκρες της " "ακοής μου. Ψάχνουν, νομίζω, για αυτό το ημερολόγιο." #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow destroyed " "my library. Na-Krul... The name fills me with a cold dread. I prefer to " "think of it only as The Creature rather than ponder its true name." msgstr "" "Μέσα στο παραλήρημα του, το πλάσμα άφησε να ξεφύγει το όνομά του -- Να-Κρούλ. " "Προσπάθησα να ερευνήσω το όνομα, αλλά οι μικρότεροι δαίμονες κατά κάποιο τρόπο " "κατέστρεψαν τη βιβλιοθήκη μου. Να-Κρούλ... Το όνομα με γεμίζει κρύο τρόμο. " "Προτιμώ να το σκέφτομαι μόνο ως Το Πλάσμα παρά να αναλογιστώ το πραγματικό του " "όνομα." #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, and " "yet I cannot block out its voice." msgstr "" "Τα ουρλιαχτά της οργής του παγιδευμένου πλάσματος με εμποδίζουν να αποκτήσω " "τον τόσο απαραίτητο ύπνο μου. Μαίνεται εναντίον αυτού που το έστειλε στο Κενό, " "και με βρίζει που το παγίδευσα εδώ. Τα λόγια του γεμίζουν την καρδιά μου με " "τρόμο, κι δεν μπορώ να αποκλείσω τη φωνή του από το μυαλό μου." #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my knowledge " "to free their lord. I hope that whoever finds this journal will seek the " "knowledge." msgstr "" "Ο χρόνος μου τελειώνει γρήγορα. Πρέπει να καταγράψω τους τρόπους για να " "αποδυναμώσω τον δαίμονα και μετά να κρύψω αυτό το κείμενο, μήπως οι δαιμονικοί " "υπηρέτες του βρουν κάποιο τρόπο να χρησιμοποιήσουν τις γνώσεις μου για να " "ελευθερώσουν τον κύριό τους. Ελπίζω ότι όποιος βρει αυτό το ημερολόγιο θα " "αναζητήσει τη γνώση." #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions claw " "at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great walls, " "but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance to " "his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Όποιος βρει αυτόν τον πάπυρο χρεώνεται με το βάρος να σταματήσει το δαιμονικό " "πλάσμα που βρίσκεται μέσα σε αυτά τα τείχη. Ο χρόνος μου τελείωσε. Ακόμα και " "τώρα, τα κολασμένα διαβολάκια του νυχιώνουν την εύθραυστη πόρτα πίσω από την " "οποία κρύβομαι.\n" " \n" "Έχω στριμώξει τον δαίμονα με απόκρυφη μαγεία και τον έχω εγκλωβίσει μέσα σε " "μεγάλα τείχη, αλλά φοβάμαι ότι δεν θα είναι αρκετό.\n" " \n" "Τα ξόρκια που βρίσκονται στα τρία Γραμματικά μου θα σας προσφέρουν " "προστατευμένη είσοδο στην φυλακή του, αλλά μόνο εάν τεθούν με τη σωστή σειρά. " "Οι μοχλοί στην είσοδο θα αφαιρέσουν τα μαγικά εμπόδια και θα απελευθερώσουν " "τον δαίμονα. μην τα αγγίζετε! Χρησιμοποιήστε μόνο αυτά τα ξόρκια για να " "αποκτήσετε είσοδο, διαφορετικά η δύναμή του μπορεί να είναι πολύ μεγάλη για να " "μπορέσεις να νικήσεις." #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "Praedictum Otium." #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "Αγριόχοιρος της Κολάσεως" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "Κεντρί" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "Ψυχόσφαιρος" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "Αραχνόμος" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "Έκπτωτος Δίδυμος" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "Γόνος Σαρκοδαίμονα" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "Δηλητηριώδες Ουρά" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "Νεκρόσφαιρος" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "Άρχοντας της Αράχνης" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "Μαστιγοσκώληκας" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "Πυρσοδαίμονας" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "Έντομο της Κολάσεως" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "Νεκροθάφτης" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "Αρουραίος Τάφων" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "Νυχτερίδα της Φωτιάς" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "Φτερωτό Κρανίο" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "Απέθαντος Μάγος" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "Δαίμονας της Κρύπτης" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "Κολασμένη Νυχτερίδα" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "Δαίμονας των Οστών" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "Απέθαντος Αρχιμάγος" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "Δικέφαλος Κύκλωπας" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "Πράγμα Σάρκας" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "Θεριστής" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "H Άρθρωση των Δακτύλων του Γίγαντα" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "Το Υδραργυρικό Δακτυλίδι" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "Το Δακτυλίδι του Ξορίνε" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "Το Δακτυλίδι του Κάρικ" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "Το Δακτυλίδι του Μάγματος" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "Το Δακτυλίδι των Μυστών" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "Το Δακτυλίδι του Κεραυνού" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "Το Φυλακτό της Προστασίας" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "Το Κεντρί της Σκνίπας" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "Η Φλεγόμενη Δάδα" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "Θωράκιση της Θλίψης" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "Κεραυνοβόλο Τόξο" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "Το Μπουμπουνητό" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "Σιροτάτσι" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "Ο Φάγος των Ψυχών" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "Η Άκρη του Διαμαντιού" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "Η Κοκάλινη Αλυσιδωτή Πανοπλία" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "Ο Δαιμονικός Ολομεταλλικός Θώρακας" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "Το Φυλακτό του Ακόλουθου" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "Το Δαχτυλίδι του Μονομάχου" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "του Γελωτοποιού" #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "του Κρυσταλλινικού" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "του Σωσία" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "της Καταστροφής" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "της Σήψης" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "του Κινδύνου" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "Μάνα" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "ο Μάγος" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "ο Γελωτοποιός" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "Τείχος Κεραυνού" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "Πυρπόληση" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "Αναδίπλωση" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "Αντανάκλαση" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "Μπερσέκερος" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "Κύκλος Φωτιάς" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "Αναζήτηση" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "Ρούνος Φωτιάς" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "Ρούνος Φωτός" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "Ρούνος Αστρικής Έκρηξης" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "Ρούνος Πυρπόλησης" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "Ρούνος Πέτρας" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "," #~ msgid "/help" #~ msgstr "/βοήθεια" #~ msgid "({command})" #~ msgstr "({command})" #~ msgid "/arena" #~ msgstr "/αρένα" #~ msgid "/arenapot" #~ msgstr "/φίλτρααρένας" #~ msgid "/inspect" #~ msgstr "/επιθεώρηση" #~ msgid "/seedinfo" #~ msgstr "/πληροφορίασπόρου" #~ msgid "Command \"" #~ msgstr "Εντολή \"" #~ msgid "Decrease Gamma" #~ msgstr "Μείωση συντελεστή φωτεινότητας Gamma" #~ msgid "Increase Gamma" #~ msgstr "Αύξηση συντελεστή φωτεινότητας Gamma" #~ msgid "No automap available in town" #~ msgstr "Ο χάρτης δεν είναι διαθέσιμος στην πόλη" #~ msgid "Restart In Town" #~ msgstr "Ανάσταση στην Πόλη" #~ msgid "Heart" #~ msgstr "Καρδιά" #~ msgid "Trying to drop a floor item?" #~ msgstr "Προσπαθείς να ρίξεις αντικείμενο που είναι ήδη στο πάτωμα;" #~ msgid "" #~ "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " #~ "frame. Disabling it can help with mouse lag on some systems." #~ msgstr "" #~ "Ενεργοποιεί την αναμονή για Οριζόντιο Συγχρονισμό. Αποτρέπει το σχίσιμο " #~ "οθόνης στην εμφάνιση του κάθε καρέ. Η απενεργοποίηση του μπορεί να βοηθήσει " #~ "με πιθανόν καθυστέρηση ποντικιού σε κάποια συστήματα." #~ msgid "FPS Limiter" #~ msgstr "Όριο KΑΔ(FPS)" #~ msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." #~ msgstr "" #~ "Τα ΚΑΔ(Καρέ ανά Δευτερόλεπτο) περιορίζονται για την αποφυγή υπερφόρτωσης " #~ "της ΚΜΕ. Το όριο λαμβάνει υπόψιν τον ρυθμό ανανέωσης της οθόνης." #~ msgid "To hit" #~ msgstr "% για χτύπημα" #~ msgid "Indestructible, " #~ msgstr "Άφθαρτο, " #~ msgid "No required attributes" #~ msgstr "Δεν απαιτούνται στατιστικά" #~ msgid "" #~ "Cloudy and cooler today. Casting the nets of necromancy across the void " #~ "landed two new subspecies of flying horror; a good day's work. Must " #~ "remember to order some more bat guano and black candles from Adria; I'm " #~ "running a bit low." #~ msgstr "" #~ "Συννεφιά και πιο δροσερά σήμερα. Ρίχνοντας τα δίχτυα της νεκρομαντείας στην " #~ "διάσταση του κενού έπιασα δύο νέα υποείδη ιπτάμενων τρόμων, μια καλή μέρα " #~ "στη δουλειά. Πρέπει να θυμηθώ να παραγγείλω επιπλέον κουτσουλιές νυχτερίδας " #~ "και μαύρα κεριά από την Άντρια. Μου τελειώνουν." #~ msgid "Options:" #~ msgstr "Επιλογές" #~ msgid "version {:s}" #~ msgstr "έκδοση {:s}" #~ msgid "recover life" #~ msgstr "ανάκτηση ζωής" #~ msgid "deadly heal" #~ msgstr "θανατηφόρα θεραπεία" #~ msgid "decrease strength" #~ msgstr "μειώνει την δύναμη" #~ msgid "decrease dexterity" #~ msgstr "μειώνει την επιδεξιότητα" #~ msgid "decrease vitality" #~ msgstr "μειώνει την ζωτικότητα" #~ msgid "you can't heal" #~ msgstr "δεν μπορείς να θεραπευθείς" #~ msgid "hit monster doesn't heal" #~ msgstr "ο κτύπος δεν επιτρέπει θεραπεία" #~ msgid "Faster attack swing" #~ msgstr "Γρηγορότερη κίνηση επίθεσης" #~ msgid "see with infravision" #~ msgstr "βλέπεις με υπέρυθρη όραση" #~ msgid "Failed to open player archive for writing." #~ msgstr "Αποτυχία ανοίγματος αρχείου παίκτη για εγγραφή." #~ msgid "Unable to read to save file archive" #~ msgstr "Αποτυχία ανάγνωσης από αρχείο παιχνιδιού" #~ msgid "Unable to write to save file archive" #~ msgstr "Αποτυχία εγγραφής σε αρχείο παιχνιδιού" #~ msgid "" #~ "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits " #~ "any who would seek to steal the treasures secured within this room. So " #~ "speaks the Lord of Terror, and so it is written." #~ msgstr "" #~ "Πέρα από τον Θάλαμο των Ηρώων βρίσκεται ο Θάλαμος των Οστών. Ο αιώνιος " #~ "θάνατος περιμένει όποιον θα ήθελε να κλέψει τους θησαυρούς που είναι " #~ "ασφαλισμένοι σε αυτό το δωμάτιο. Έτσι λέει ο Άρχοντας του Τρόμου, και έτσι " #~ "είναι γραμμένο." #~ msgid "" #~ "The armories of Hell are home to the Warlord of Blood. In his wake lay the " #~ "mutilated bodies of thousands. Angels and man alike have been cut down to " #~ "fulfill his endless sacrifices to the Dark ones who scream for one thing - " #~ "blood." #~ msgstr "" #~ "Τα οπλοστάσια της Κόλασης είναι το σπίτι του Πολέμαρχου του Αίματος. Στο " #~ "πέρασμά του κείτονται τα ακρωτηριασμένα σώματα χιλιάδων. Άγγελοι και " #~ "άνθρωποι έχουν σφαγιαστεί για να εκπληρώσουν τις ατελείωτες θυσίες του, " #~ "στους Σκοτεινούς, που ουρλιάζουν για ένα πράγμα - αίμα." ================================================ FILE: Translations/es.po ================================================ # Translation of DevilutionX to Spanish # Emiliano Augusto Gonzalez , 2021. # egonzalez, 2021. # Mad-Soft , 2021. # msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:19+0200\n" "PO-Revision-Date: 2025-10-02 15:19+0200\n" "Last-Translator: Oleksandr Kalko (tsunami_state) \n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: _;N_;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Diseño del Juego" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Diseñadores Senior" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Diseño adicional" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Programador Principal" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Programadores Senior" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Programación" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Programadores Invitados Especiales" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Programación Battle.net" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Programación de Comunicaciones en Serie" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Programación del Instalador" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Directores de Arte" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Arte" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Técnico de Arte" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Directores de Arte Cinematográfico" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "Arte Cinematográfico 3D" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Técnico de Arte Cinematográfico" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Productor Ejecutivo" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Productor" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Productor Asociado" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Strike Team" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Música" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Diseño de sonido" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Música y Sonido Cinematográfico" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Producción de Voz, Dirección y Casting" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Guion e historia" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Edición de voz" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Voces" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Ingeniero de Grabación" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Diseño y Maquetación del Manual" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Arte del Manual" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "Director Provisional de Control de Calidad (Probador Jefe)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "Escuadrón de Asalto del Control de Calidad (Probadores)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "" "Equipo de Operaciones Especiales del Control de Calidad (Probadores de " "Compatibilidad)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "Soporte de Artillería del Control de Calidad (probadores adicionales) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "Contrainteligencia del Control de Calidad" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Pedido de los Servicios de Información de la Red" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Atención al cliente" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Ventas" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Inútil" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Vocalistas de Fondo del Sr. Dabiri" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Relaciones públicas" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Márketing" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Ventas internacionales" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "Ventas en EEUU" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Fabricación" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Legal y Comercial" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Agradecimientos especiales a" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Gracias a" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "En memoria de" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Un agradecimiento muy especial a" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Gerente General" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Ingeniería de Software" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Director de Arte" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Artistas" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Diseño" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Diseño de Sonido, SFX e Ingeniería de Audio" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Responsable de la Garantía de Calidad" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Probadores" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Manual" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\tTrabajo Adicional" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Escritura de Texto de Búsqueda" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Gracias a" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tUn Agradecimiento Especial a Blizzard Entertainment" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Noroeste" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Gerente de Garantía de Calidad" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Probador Principal de Garantía de Calidad" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Probadores Principales" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Probadores Adicionales" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Gerente de Marketing del Producto" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Director de Relaciones Públicas" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Gerente de Producto Asociado" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "El Anillo de los Mil" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tNingún alma fue vendida para la realización de este juego." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "OK" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Elegir Clase" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Cancelar" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Nuevo Héroe Multijugador" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Nuevo Héroe para un Jugador" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Existe juego guardado" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Cargar" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Nueva Partida" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Personajes de un Jugador" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "El Ladrón y el Hechicero solo están disponibles en la versión comercial " "completa de Diablo. Visite https://www.gog.com/game/diablo para comprar." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Introduzca nombre" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Nombre inválido. El nombre no puede contener espacios, ni caracteres o " "palabras reservadas.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Imposible crear personaje." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Nivel:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Fuerza:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Magia:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Destreza:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Vitalidad:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Juego guardado:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Elije Héroe" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Nuevo Héroe" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Eliminar" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Personajes Multijugador" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Eliminar Héroe Multijugador" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Eliminar Héroe" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "¿Seguro que quieres eliminar el personaje \"{:s}\"?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Un Jugador" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Multijugador" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Ajustes" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Soporte" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Créditos" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Salir" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Salir" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Shareware" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Cliente-Servidor (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 msgid "Offline" msgstr "Fuera de línea" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Partida Multijugador" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Requisitos:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "sin puerta de enlace" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Conexión" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Cambiar puerta de enlace" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "" "Todas las computadoras deben estar conectadas a una red compatible con TCP." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Todas las computadoras deben estar conectadas a Internet." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Juegar solo sin exposición a la red." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Jugadores soportados: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Diablo Shareware" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Hellfire Shareware" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "El anfitrión está ejecutando un juego diferente al tuyo." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "" "El anfitrión está ejecutando un modo de juego ({:s}) diferente al tuyo." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "Su versión {:s} no coincide con el anfitrión {:d}.{:d}.{:d}." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Descripción:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Seleccione acción" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Crear Juego" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Crear Juego Público" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Unirse al Juego" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Juegos público" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Cargando..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "Ninguno" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "Cancelar" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Crear un nuevo juego con la dificultad que elijas." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" "Crear un nuevo juego público donde cualquiera puede unirse con la dificultad " "que elijas." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "Introduzca un ID de juego para unirse a un juego en curso." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "" "Introduzca una IP o un nombre de equipo para unirse a un juego en curso." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "Unirse a un juego público que ya esté en curso." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Normal" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Pesadilla" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Infierno" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Dificultad {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Velocidad: Normal" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Velocidad: Rápido" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Velocidad: Más rápido" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Velocidad: Lo más rápido" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Jugadores: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Dificultad" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Unirse a los juegos {:s}" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Introduzca su ID de juego" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Introduzca la dirección" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Dificultad normal\n" "Aquí es donde un personaje inicial debe comenzar la búsqueda para derrotar a " "Diablo." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Dificultad de pesadilla\n" "Los habitantes del Laberinto se han reforzado y demostrarán ser un desafío " "mayor. Esto se recomienda solo para personajes experimentados." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Dificultad del infierno\n" "La más poderosa de las criaturas del inframundo acecha en la entrada al " "infierno. Solo los personajes más experimentados deberían aventurarse en " "este reino." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Su personaje debe alcanzar el nivel 20 antes de que pueda ingresar a un " "juego multijugador de dificultad Pesadilla." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Su personaje debe alcanzar el nivel 30 antes de que pueda ingresar a una " "partida multijugador de dificultad infernal." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Seleccionar velocidad de juego" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Rápido" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "Más rápido" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Lo más rápido" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Velocidad normal\n" "Aquí es donde un personaje inicial debe comenzar la búsqueda para derrotar a " "Diablo." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Velocidad Rápida\n" "Los habitantes del Laberinto se han acelerado y demostrarán ser un desafío " "mayor. Esto se recomienda solo para personajes experimentados." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "Velocidad más rápida\n" "La mayoría de los monstruos de la mazmorra te buscarán más rápido que nunca. " "Solo un campeón experimentado debería probar suerte a esta velocidad." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Velocidad más rápida\n" "Los esbirros del inframundo se apresurarán a atacar sin dudarlo. Solo un " "verdadero demonio de la velocidad debería entrar a este ritmo." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Introduzca la contraseña" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Entrar a Hellfire" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Cambiar a Diablo" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Si" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "No" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "Presiona los botones del gamepad para cambiar." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Enlazar tecla:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Presiona una tecla para cambiar." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Liberar tecla" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "Agrupar combinación de botones:" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "Desagrupar combinación de botones" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Menú Anterior" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Mantenemos un servidor en Discord.gg/devilutionx Sigue los enlaces para " "unirte a nuestra comunidad donde hablamos de cosas relacionadas con Diablo, " "y la expansión Hellfire." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX es mantenido por Diasurgical, los problemas y fallos pueden ser " "reportados en esta dirección: https://github.com/diasurgical/devilutionX " "Para ayudarnos a servirle mejor, por favor asegúrese de incluir el número de " "versión, el sistema operativo y la naturaleza del problema." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Descargo de responsabilidad:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX no está soportado ni mantenido por Blizzard Entertainment, ni " "por GOG.com. Ni Blizzard Entertainment ni GOG.com han probado o certificado " "la calidad o compatibilidad de DevilutionX. Todas las preguntas relacionadas " "con DevilutionX deben dirigirse a Diasurgical, no a Blizzard Entertainment " "ni a GOG.com." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tEsta adaptación hace uso de Charis SIL, New Athena Unicode, Unifont, y " "Noto que están licenciados bajo la SIL Open Font License, así como Twitmoji " "que está licenciado bajo CC-BY 4.0. También hace uso de SDL que está " "licenciado bajo la licencia zlib. Para más detalles, consulte el Léame." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Copyright © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Error" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "El error ocurrió en: {:s} línea {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Error de Archivo de Datos" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Imposible abrir el archivo de datos principal ({:s}).\n" "\n" "Asegúrese de que esté en la carpeta del juego." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Error de Directorio de Solo Lectura" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "No se puede escribir en la ubicación:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Juego: " #: Source/automap.cpp:1424 msgid "Offline Game" msgstr "Juego sin conexión" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Contraseña: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Juego público" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Nivel: Guarida {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Nivel: Cripta {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Pueblo" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Nivel: {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Tab" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Intro" #: Source/control.cpp:206 msgid "Character Information" msgstr "Información del Personaje" #: Source/control.cpp:207 msgid "Quests log" msgstr "Registro de Misiones" #: Source/control.cpp:208 msgid "Automap" msgstr "Automapa" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Menú Principal" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Inventario" #: Source/control.cpp:211 msgid "Spell book" msgstr "Libro de Hechizos" #: Source/control.cpp:212 msgid "Send Message" msgstr "Enviar Mensaje" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "Comandos Disponibles:" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "Comando " #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr " es desconocido." #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "Descripción: " #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" "\n" "Parametros No se necesitan parámetros adicionales." #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" "\n" "Parametros: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "Las Arenas solo están soportadas en multijugador." #: Source/control.cpp:653 msgid "What arena do you want to visit?" msgstr "¿Cuál arena quieres visitar?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "Número de Arena inválido. Los números válidos son:" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "Para entrar a una arena debes estar en el pueblo o en otra arena." #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "Inspección solo está soportado en multijugador." #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "Parar jugadores de inspectores." #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "No se encontraron jugadoras con tal nombre" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "Jugador inspector: " #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "" "Imprime una descripción general de la ayuda o ayuda para un comando " "específico." #: Source/control.cpp:800 msgid "[command]" msgstr "[comando]" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "Enttrar a una Arena PvP." #: Source/control.cpp:801 msgid "" msgstr "{arena-número}" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "Da pociones de arena." #: Source/control.cpp:802 msgid "" msgstr "" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "Inspecciona las estadísticas y el equipamiento de otro jugador." #: Source/control.cpp:803 msgid "" msgstr "" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "Mostrar información inicial para el nivel actual." #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Jugador amigable" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Ataque del jugador" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Tecla de acceso rápido: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Seleccionar botón de hechizo actual" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Tecla de acceso rápido: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "Habilidad {:s}" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "Hechizo {:s}" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Nivel 0 - Inutilizable" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Nivel {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "Pergamino de {:s}" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Pergamino" msgstr[1] "{:d} Pergaminos" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "Bastón de {:s}" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Carga" msgstr[1] "{:d} Cargas" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} pieza de oro" msgstr[1] "{:s} piezas de oro" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Requisitos no cumplidos" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Nivel: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Vida {:d} de {:d}" #: Source/control.cpp:1525 msgid "Right click to inspect" msgstr "Clic derecho para inspeccionar" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Subir Nivel" #: Source/control.cpp:1687 msgid "You have died" msgstr "Has muerto" #: Source/control.cpp:1695 msgid "ESC" msgstr "ESC" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "Botón de Menú" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "Presiona {} para cargar la última grabación." #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "Presiona {} para volver al Menú Principal." #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "Presiona {} para reiniciar en el pueblo." #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Tiene {:s} moneda de oro. ¿Cuanto quiere eliminar?" msgstr[1] "Tiene {:s} monedas de oro. ¿Cuantas quiere eliminar?" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Portal del pueblo" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "de {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Portal a" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "El Altar Impío" #: Source/cursor.cpp:636 msgid "level 15" msgstr "nivel 15" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, c++-format msgid "Unable to load data from file {0}" msgstr "No se pueden cargar datos del archivo {0}" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "{0} está incompleto, verifique el contenido del archivo." #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" "Su archivo {0} no tiene las columnas esperadas; asegúrese de que coincida " "con el formato documentado." #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "Valor no numérico {0} para {1} en {2} en la fila {3} y la columna {4}" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "" "Valor fuera de rango {0} para {1} en {2} en la fila {3} y la columna {4}" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "Valor no válido {0} para {1} en {2} en la fila {3} y la columna {4}" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Imprime este mensaje y sal" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Imprime la versión y sal" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Especifique la carpeta de diabdat.mpq" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Especifique la carpeta de archivos guardados" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Especifique la ubicación de diablo.ini" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "Especifique el código de idioma (ej. en ó pt_BR)" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Omitir videos de inicio" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Mostrar fotogramas por segundo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "Habilitar el registro detallado" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "Log en un archivo en lugar de stderr" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Grabar un archivo de demostración" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Jugar un archivo de demostración" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Desactivar la limitación de FPS durante la reproducción de la demo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Selección de juego:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Forzar modo shareware" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Forzar modo Diablo" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Forzar modo Hellfire" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Opciones de Hellfire:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "Reportar errores a https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "" "Por favor a\n" "ctualice devilutionx.mpq y fonts.mpq a la última versión" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "No se pudieron cargar los recursos de la IU.\n" "\n" "Asegúrese que Devilutionx.mpq se encuentra en la carpeta del juego y está " "actualizado." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "" "Por favor ac\n" "tualice fonts.mpq a la última versión" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Tiempo de espera de la red terminado --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Esperando jugadores --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "No hay ayuda disponible" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "mientras que en las tiendas" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Objeto del cinturón {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Usar objeto del cinturón." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Hechizo rápido {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Tecla rápida para habilidad o hechizo." #: Source/diablo.cpp:1809 msgid "Previous quick spell" msgstr "Hechizo rápido anterior" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "Selecciona el hechizo rápido anterior (ciclos)." #: Source/diablo.cpp:1817 msgid "Next quick spell" msgstr "Siguiente hechizo rápido" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "Selecciona el siguiente hechizo rápido (ciclos)." #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "Usar poción de curación" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "Usar pociones de salud del cinturón." #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "Usar poción de maná" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "Usar poción de maná del cinturón." #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Libro rápido" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Abrir libro rápido." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Grabación rápida" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Grabar el juego." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Carga rápida" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Cargar el Juego." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Salir" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Cerrar el juego." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Detener héroe" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Dejar de caminar y cancelar acciones pendientes." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Resaltado de elementos" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Mostrar/ocultar elementos en el suelo." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Alternar resaltado de elementos" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Mostrar/ocultar elementos permanentes en el suelo." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Alternar mapa automático" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Alterna si se muestra el mapa automático." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "Tipo de mapa de ciclo" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "Opaco -> Transparente -> Minimapa -> Ninguno" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Abrir la pantalla de Inventario." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Personaje" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Abrir pantalla de Personaje." #: Source/diablo.cpp:1928 msgid "Party" msgstr "Grupo" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "Abrir panel lateral de Grupo" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Registro de Misiones" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Abrir Registro de Misiones." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Libro de Hechizos" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Abrir Libro de Hechizos." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Mensaje rápido {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Usar Mensaje rápido en el chat." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Ocultar las pantallas de información" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Ocultar todas las pantallas de información." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Ampliar" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Ampliar de la pantalla del juego." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Pausar Juego" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Pausar el juego." #: Source/diablo.cpp:2004 msgid "Pause Game (Alternate)" msgstr "Pausar juego (alternativo)" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 msgid "Decrease Brightness" msgstr "Disminuir el brillo de la pantalla." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Reducir el brillo de la pantalla." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 msgid "Increase Brightness" msgstr "Aumentar el Brillo" #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Aumentar el brillo de la pantalla." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Ayuda" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Abrir pantalla de ayuda." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Captura de pantalla" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Toma una captura de pantalla." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Información del Juego" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Mostrar información del juego." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Log del Chat" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Mostrar log del Chat." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 msgid "Sort Inventory" msgstr "Ordenar Inventario" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "Ordenar el Inventario." #: Source/diablo.cpp:2072 msgid "Console" msgstr "Consola" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "Abrir consola de LUA." #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "Acción primaria" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" "Ataca a los monstruos, habla con los habitantes, levanta y coloca artículos " "de inventario." #: Source/diablo.cpp:2144 msgid "Secondary action" msgstr "Ación secundaria" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "Abre cofres, interactúa con las puertas, recoge objetos." #: Source/diablo.cpp:2159 msgid "Spell action" msgstr "Acción de hechizo" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "Lanzar hechizo activo." #: Source/diablo.cpp:2174 msgid "Cancel action" msgstr "Cancelar acción" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "Cerrar menús." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "Ascender" #: Source/diablo.cpp:2201 msgid "Moves the player character up." msgstr "Mueve el personaje del jugador hacia arriba." #: Source/diablo.cpp:2206 msgid "Move down" msgstr "Descender" #: Source/diablo.cpp:2207 msgid "Moves the player character down." msgstr "Mueve el personaje del jugador hacia abajo." #: Source/diablo.cpp:2212 msgid "Move left" msgstr "Mover a la izquierda" #: Source/diablo.cpp:2213 msgid "Moves the player character left." msgstr "Mueve el personaje del jugador a la izquierda." #: Source/diablo.cpp:2218 msgid "Move right" msgstr "Mover a la derecha" #: Source/diablo.cpp:2219 msgid "Moves the player character right." msgstr "Mueve el personaje del jugador a la derecha." #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "Estar en tierra" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "Mantén presionado para evitar que el jugador se mueva." #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "Alternar mantener en tierra" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "Alternar mientras el jugador se mueve." #: Source/diablo.cpp:2310 msgid "Automap Move Up" msgstr "Subir Automapa" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "Mueve el automapa hacia arriba cuando está activo." #: Source/diablo.cpp:2316 msgid "Automap Move Down" msgstr "Bajar Automapa" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "Mueve el automapa hacia abajo cuando está activo." #: Source/diablo.cpp:2322 msgid "Automap Move Left" msgstr "Mover Automapa a la izquierda" #: Source/diablo.cpp:2323 msgid "Moves the automap left when active." msgstr "Mueve el automapa hacia la izquierda cuando está activo." #: Source/diablo.cpp:2328 msgid "Automap Move Right" msgstr "Mover Automapa a la derecha" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "Mueve el automapa hacia la derecha cuando está activo." #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "Mover el mouse hacia arriba" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "Simula el movimiento ascendente del ratón." #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "Mover el ratón hacia abajo" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "Simula el movimiento del ratón hacia abajo." #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "Mover el mouse hacia la izquierda" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "Simula el movimiento del ratón hacia la izquierda." #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "Mover el ratón a la derecha" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "Simula el movimiento del ratón hacia la derecha." #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "Clic izquierdo del ratón" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "Simula el botón izquierdo del ratón." #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "Clic derecho del mouse" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "Simula el botón derecho del ratón." #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "Menú de hechizos de gamepad" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "" "Mantener presionado para configurar o usar las teclas de acceso rápido de " "hechizos." #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "Navegador de menú Gamepad" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "Mantén presionado para acceder a la navegación del menú del gamepad." #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 msgid "Toggle game menu" msgstr "Alternar el menú del juego" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 msgid "Opens the game menu." msgstr "Abre el menú del juego." #: Source/diablo_msg.cpp:63 msgid "Game saved" msgstr "Juego guardado" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "No hay funciones multi jugador en la demostración" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Falló la creación de Direct Sound" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "No disponible en la versión shareware" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "No hay suficiente espacio para grabar" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Sin Pausa en el pueblo" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Se recomienda copiar a un disco duro" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Problema de sincronización multi jugador" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Sin pausa en multi jugador" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Grabando..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Algunos se debilitan a medida que uno se hace más fuerte" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Nueva fuerza se forja a través de la destrucción" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Aquellos que defienden raras veces atacan" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "La espada de la justicia es rápida y afilada" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Mientras el espíritu está alerta, el cuerpo prospera" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Los poderes del maná reenfocado se renuevan" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "El tiempo no puede disminuir el poder del acero" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "La Magia no siempre es lo que parece" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Lo que una vez estuvo abierto ahora está cerrado" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "La intensidad viene a costa de la sabiduría" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "El poder arcano trae destrucción" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Lo que no se puede sujetar, no se puede dañar" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Crarmesí y Azur se vuelven como el sol" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Conocimiento y sabiduría a costa de uno mismo" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Bebe y refréscate" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Dondequiera que vayas, ahí estás" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "La energía viene a costa de la sabiduría" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Las riquezas abundan cuando menos se espera" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "Donde la avaricia falla, la paciencia gana recompensa" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "¡Bendecido por un compañero benevolente!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Las manos de los hombres pueden ser guiadas por el destino" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "La fuerza se ve reforzada por la fe celestial" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "La esencia de la vida fluye desde dentro" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "El camino se aclara cuando se ve desde arriba" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "La salvación viene a costa de la sabiduría" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Los misterios se revelan a la luz de la razón" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Los que son los últimos pueden ser los primeros" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "La generosidad trae sus propias recompensas" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Debes tener al menos el nivel 8 para usar esto." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Debes tener al menos el nivel 13 para usar esto." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Debes tener al menos el nivel 17 para usar esto." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "¡Conocimiento arcano adquirido!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "Eso que no te mata ..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "El conocimiento es poder." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Da y recibirás." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Se adquiere algo de experiencia mediante el tacto." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "No hay lugar como el hogar." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Se restaura la energía espiritual." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Te sientes más ágil." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Te sientes más fuerte." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Te sientes más sabio." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Te sientes renovado." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "Aquello que puede romper la voluntad." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Catedral" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Catacumbas" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Cuevas" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Nido" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Crípta" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Niv {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "{} dificultad" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "En Menú" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "loopback" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "No puede conectarse" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "error: leidos 0 bytes del servidor" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" "No se pudo abrir el archivo:\n" "{:s}\n" "\n" "{:s}\n" "\n" "Es posible que los archivos MPQ estén dañados. Verifique la integridad de " "los archivos." #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq o spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Faltan algunos MPQ de Hellfire" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "No se encontraron todos los MPQ de Hellfire.\n" "Copie todos los archivos hf * .mpq." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Resolución" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Correr en el pueblo" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "La misión de Theo" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "La misión de la vaca" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Auto recoger oro" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Auto recoger elixir" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 msgid "Auto Oil Pickup" msgstr "Recolección automática de aceite" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Auto recoger en el pueblo" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Adria recarga maná" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Auto equiparse con armas" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Auto equiparse con armadura" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Auto equiparse con yelmo" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Auto equiparse con escudos" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Auto equiparse con joyería" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Misiones aleatorias" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "Mostrar etiquetas de artículos" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Auto recargar cinturón" # La traduccion debe ser corta para que entre en el cuadro de dialogo #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Desact. santuarios paralizantes" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Recoger poción de salud" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Recoger poción de salud completa" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Recoger poción de maná" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Recoger poción de maná completo" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Recoger poción de rejuvenecimiento" # La traduccion debe ser corta para que entre en el cuadro de dialogo #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Recoger poción de rejuvenec. completo" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Opciones" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Guardar" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 msgid "Exit to Main Menu" msgstr "Salir al Menú Principal" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Salir" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Gama" # Mantener corto. Texto de menú #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Veloc." #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Música Desactivada" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Sonido" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Sonido Desactivado" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Pausa" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Atajos de Teclado:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Abrir Pantalla de Ayuda" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Mostrar Menú Principal" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Pestaña: Mostrar Mapa Automático" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Espacio: Ocultar todas las pantallas de información" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Abrir libro rápido" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Abrir Libro de Hechizos" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Abrir Pantalla de Inventario" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Abrir pantalla de Personaje" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "Q: Abrir Registro de Misiones" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Reducir el brillo de la pantalla" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "G: Aumentar el brillo de la pantalla" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Zoom de la pantalla del juego" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Zoom Automapa" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Usar elemento del Cinturón" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "" "F5, F6, F7, F8: Establecer tecla de acceso rápido para habilidad o " "hechizo" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Mayús + Clic izquierdo: Atacar sin moverse" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Mayús + Click Izquierdo (en la pantalla del personaje): Asignar todos los " "puntos de estadística" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Mayús + Click Izquierdo (en el inventario) Mover objeto al cinturón o " "equipar/desequipar objeto" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "Mayús + Click Izquierdo (en el cinturón)): Mover objeto al inventario" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Movimiento:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" "Si mantienes presionado el botón del mouse mientras se mueve, el personaje " "continuará moviéndose en esa dirección." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Combate:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Manteniendo presionado la tecla Mayús y luego hacer clic con el botón " "izquierdo permite que el personaje ataque sin moverse." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Automapa:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "Para acceder al auto-mapa, haga clic en el botón 'MAP' en la barra de " "información o presione 'TAB' en el teclado. Acercar y alejar el mapa se " "realiza con las teclas + y -. Para desplazarse por el mapa se utilizan las " "teclas de flecha." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Recoger Objetos:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Los elementos utilizables que son de tamaño pequeño, como pociones o " "pergaminos, se colocan automáticamente en su 'cinturón' ubicado en la parte " "superior de la barra de Interfaz. Cuando se coloca un objeto en el cinturón, " "aparece un pequeño número en ese recuadro. Los artículos se pueden usar " "presionando el número correspondiente o haciendo clic derecho en el objeto." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Oro:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Puedes seleccionar una cantidad específica de oro para soltar haciendo clic " "derecho en un montón de oro de su inventario." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Habilidades y Hechizos:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "Puedes acceder a tu lista de habilidades y hechizos haciendo clic izquierdo " "en el botón 'HECHIZOS' en la barra de la interfaz. Aquí se enumeran los " "hechizos memorizados y los disponibles a través de las varas. Al hacer clic " "con el botón izquierdo en el hechizo que desea lanzar, se preparará el " "hechizo. Se puede lanzar un hechizo preparado simplemente haciendo clic con " "el botón derecho en el área de juego." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Uso del libro rápido para Hechizos:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Al hacer clic con el botón izquierdo en el botón 'hechizo listo' se abrirá " "el 'libro rápido' que le permite seleccionar una habilidad o hechizo de uso " "inmediato. Para usar la habilidad o el hechizo, simplemente haga clic con " "el botón derecho en el área de juego principal." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Mayús + Click Izquierdo en el botón 'seleccionar hechizo actual' borrará el " "hechizo preparado." #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Configurar teclas de acceso rápido para Hechizos:" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Puede asignar hasta cuatro teclas de Acceso Rápido para habilidades, " "hechizos o pergaminos. Empiece por abrir el 'libro rápido' como se describe " "en la sección anterior. Presione las teclas F5, F6, F7 o F8 después de " "resaltar el hechizo que desea asignar." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Libros de hechizos:" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Leer más de un libro aumenta su conocimiento de ese hechizo, lo que le " "permite lanzar el hechizo de manera más efectiva." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Ayuda de Hellfire Shareware" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Ayuda de Hellfire" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Ayuda de Diablo Shareware" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Ayuda de Diablo" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Presione ESC para finalizar o las teclas de flecha para desplazarse." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "No se pudo crear la ventana principal" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "No hay espacio para el artículo" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Aceite de Precisión" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Aceite de Maestría" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Aceite de Nitidez" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Aceite de Muerte" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Aceite de Habilidad" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Aceite de Herrero" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Aceite de Entereza" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Aceite de Permanencia" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Aceite de Endurecimiento" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Aceite de Impermeabilidad" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} de {1}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{1} {0} de {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{1} {0} {2}" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{1} {0}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} {1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "en el arma aumenta" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "probabilidad de acertar" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "aumenta enormemente un" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "posibilidad de que el arma golpee" #: Source/items.cpp:1652 msgid "damage potential" msgstr "daño potencial" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "aumenta en gran medida la de un arma" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "daño potencial - no arcos" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "reduce los atributos necesarios" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "para usar armaduras o armas" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "restaura el 20% de la" #: Source/items.cpp:1664 msgid "item's durability" msgstr "durabilidad del artículo" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "aumenta la de un artículo" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "durabilidad actual y máxima" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "hace que un artículo sea indestructible" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "aumenta la clase de armadura" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "de armaduras y escudos" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "aumenta enormemente la armadura" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "clase de armaduras y escudos" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "pone trampa de fuego" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "establece trampa de rayos" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "establece trampa de petrificación" #: Source/items.cpp:1695 msgid "restore all life" msgstr "restaura toda la vida" #: Source/items.cpp:1698 msgid "restore some life" msgstr "restaura algo de vida" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "restaura algo de maná" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "restaura todo el maná" #: Source/items.cpp:1707 msgid "increase strength" msgstr "aumenta la fuerza" #: Source/items.cpp:1710 msgid "increase magic" msgstr "aumenta la magia" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "aumenta la destreza" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "aumenta la vitalidad" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "restaura algo de vida y maná" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "restaura toda la vida y maná" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "(funciona solo en arenas)" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Clic derecho para ver" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Clic derecho para usar" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Haga clic derecho para leer, luego\n" "clic izquierdo para apuntar" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Haga clic derecho para leer" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Activar para ver" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Abrir inventario para usar" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Activar para usar" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Seleccione del libro de hechizos, luego\n" "lance el hechizo para leer" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Activar para leer" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "{} para ver" #: Source/items.cpp:1806 #, c++-format msgid "{} to use" msgstr "{} para usar" #: Source/items.cpp:1809 #, c++-format msgid "" "Select from spell book,\n" "then {} to read" msgstr "" "Seleccione del libro de hechizos,\n" "luego {} para leer" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "{} para leer" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Nivel: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Duplica la capacidad de oro" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Requiere:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Fue" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Mag" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Des" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "Libro de {:s}" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "Oído de {:s}" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "probabilidad de acertar: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% daño" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "al golpear: {:+d}%, {:+d} daño" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% armadura" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "clase de armadura: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Resistencia al Fuego: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Resistencia al Fuego: {:+d}% MAX" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Resistencia a Relámpagos: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Resistencia a Relámpagos: {:+d}% MAX" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Resistencia a la Magia: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Resistencia a la Magia: {:+d}% MAX" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Resistencia a Todo: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Resistencia a Todo: {:+d}% MAX" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "los hechizos aumentan {:d} nivel" msgstr[1] "los hechizos aumentan {:d} niveles" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "los hechizos se reducen {:d} nivel" msgstr[1] "los hechizos se reducen {:d} niveles" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "niveles de hechizo sin cambios (?)" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Cargas extras" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} carga" msgstr[1] "{:d} {:s} cargas" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Daño por impacto de fuego: {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Daño por impacto de fuego: {:d}- {:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Daño por impacto de rayo: {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Daño por impacto de rayo: {:d}- {:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} a la fuerza" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} a la magia" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} a la destreza" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} a la vitalidad" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} a todos los atributos" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} de daño de enemigos" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Puntos de Vida: {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Maná: {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "alta durabilidad" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "durabilidad disminuida" #: Source/items.cpp:3960 msgid "indestructible" msgstr "indestructible" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% radio de luz" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% radio de luz" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "múltiples flechas por disparo" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "daño de las flechas de fuego: {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "daño de las flechas de fuego: {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "daño de las flechas de rayo {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "daño de las flechas de rayo {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "daño de bola de fuego: {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "daño de bola de fuego: {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "el atacante recibe 1-3 daños" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "el usuario pierde todo el maná" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "absorbe la mitad del daño de la trampa" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "hace retroceder al objetivo" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% de daño contra demonios" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Toda la Resistencia es igual a 0" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "el golpe roba 3% de maná" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "el golpe roba 5% de maná" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "el golpe roba el 3% de vida" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "el golpe roba 5% de vida" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "penetra la armadura del objetivo" #: Source/items.cpp:4010 msgid "quick attack" msgstr "ataque rápido" #: Source/items.cpp:4012 msgid "fast attack" msgstr "ataque rápido" #: Source/items.cpp:4014 msgid "faster attack" msgstr "ataque más rápido" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "ataque más rápido posible" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Otra habilidad (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "recuperación rápida de golpes" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "recuperación de golpes más rápida" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "recuperación de golpe más rápida posible" #: Source/items.cpp:4027 msgid "fast block" msgstr "bloqueo rapido" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "agrega {:d} punto al daño" msgstr[1] "agrega {:d} puntos al daño" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "dispara flechas de velocidad aleatoria" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "daño inusual del artículo" #: Source/items.cpp:4035 msgid "altered durability" msgstr "durabilidad alterada" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "espada de una mano" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "pierde puntos de vida constantemente" #: Source/items.cpp:4041 msgid "life stealing" msgstr "robo de vida" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "sin requisito de fuerza" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "daño por rayo: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "daño por rayo: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "rayos por cada golpe" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "triple daño ocasional" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "{:+d}% de daño de descomposición" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x dañ al mes, 1x a ti" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Daño aleatorio 0 - 600%" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "baja dur, {:+d}% de daño" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "extra CA contra demonios" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "extra CA contra muertos vivientes" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% de Maná se movió a Salud" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% de Salud se movió a Maná" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "daño: {:d} Indestructible" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "daño: {:d} Dur: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "daño: {:d}-{:d} Indestructible" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "daño: {:d}-{:d} Dur: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "defensa: {:d} Indestructible" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "defensa: {:d} Dur: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Cargas: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "artículo único" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "No Identificado" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Guarida del Rey Esqueleto" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Cámara de Hueso" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Laberinto" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Red de Agua Envenenada" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Guarida del Arzobispo Lazarus" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "Arena de la Iglesia" #: Source/levels/setmaps.cpp:33 msgid "Hell Arena" msgstr "Arena Hellfire" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "Arena Círculo de la Vida" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Bajar a la mazmorra" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Bajar a las catacumbas" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Bajar a las cuevas" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Bajar al infierno" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Bajar a la colmena" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Bajar a la Cripta" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Sube al nivel {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Sube al pueblo" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Baja al nivel {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Baja a Diablo" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Sube al nivel de la Colmena {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Sube al nivel de la Cripta {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Piedra Angular del Mundo" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Baja al nivel de la Cripta {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Volver al nivel {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "No se puede abrir el archivo guardado" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" "Versión del guardado no válida. Si intentas acceder a él se sobrescribirán " "los datos." #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" "El tamaño del guardado no es válido. Si intentas acceder a él, se " "sobrescribirán los datos." #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Archivo guardado no válido" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "El jugador está en un nivel único Hellfire" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Estado de juego no válido" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "No se puede mostrar el menú principal" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "Error al cargar los datos del monstruo" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" "No se pudo agregar un monstruo, ya que el número máximo de tipo de monstruo " "de {} ha sido alcanzado." #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "Ya existe un tipo de monstruo para el ID \"{}\"." #: Source/monster.cpp:2990 msgid "Animal" msgstr "Animal" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Demonio" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Muerto viviente" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Tipo: {:s} Muertes: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Muertes totales: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Puntos de Golpe: {:d}- {:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Sin resistencia mágica" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Resiste:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Magia" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Fuego" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Rayo" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Inmune:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Tipo: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Sin resistencias" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Sin Inmunidades" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Algunas Resistencias Mágicas" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Algunas Inmunidades Mágicas" #: Source/mpq/mpq_writer.cpp:174 msgid "Failed to open archive for writing." msgstr "No se pudo abrir el archivo para escribir." #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "{:s} ha lanzado un hechizo inválido." #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "{:s} ha lanzado un hechizo ilegal." #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Jugador ' {:s}' (nivel {:d}) acaba de unirse" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "El juego terminó" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "No se pueden obtener datos del nivel" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "El jugador ' {:s}' acaba de salir del juego" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "¡El jugador ' {:s}' mató a Diablo y abandonó el juego!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "El jugador ' {:s}' desconectado debido al tiempo de espera" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "El jugador ' {:s}' (nivel {:d}) ya está en el juego" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Misterioso" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Oculto" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Sombrío" # ** #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "misterioso" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Mágico" #: Source/objects.cpp:132 msgid "Stone" msgstr "Piedra" #: Source/objects.cpp:133 msgid "Religious" msgstr "Religioso" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Encantada" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Taumatúrgico" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Fascinante" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Críptico" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Espeluznante" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Inquietante" #: Source/objects.cpp:141 msgid "Divine" msgstr "Divino" # ** #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "sagrado" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Sagrado" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Espiritual" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Escalofriante" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Abandonado" #: Source/objects.cpp:147 msgid "Creepy" msgstr "Siniestro" #: Source/objects.cpp:148 msgid "Quiet" msgstr "Tranquilo" #: Source/objects.cpp:149 msgid "Secluded" msgstr "Aislado" #: Source/objects.cpp:150 msgid "Ornate" msgstr "Decorado" #: Source/objects.cpp:151 msgid "Glimmering" msgstr "Resplandeciente" #: Source/objects.cpp:152 msgid "Tainted" msgstr "Contaminado" #: Source/objects.cpp:153 msgid "Oily" msgstr "Aceitoso" #: Source/objects.cpp:154 msgid "Glowing" msgstr "Brillante" #: Source/objects.cpp:155 msgid "Mendicant's" msgstr "Del Mendicante" #: Source/objects.cpp:156 msgid "Sparkling" msgstr "Centelleante" #: Source/objects.cpp:158 msgid "Shimmering" msgstr "Reluciente" #: Source/objects.cpp:159 msgid "Solar" msgstr "Solar" #. TRANSLATORS: Shrine Name Block end #: Source/objects.cpp:161 msgid "Murphy's" msgstr "De Murphy" #. TRANSLATORS: Book Title #: Source/objects.cpp:214 msgid "The Great Conflict" msgstr "El Gran Conflicto" #. TRANSLATORS: Book Title #: Source/objects.cpp:215 msgid "The Wages of Sin are War" msgstr "La Paga del Pecado es la Guerra" #. TRANSLATORS: Book Title #: Source/objects.cpp:216 msgid "The Tale of the Horadrim" msgstr "El Cuento de los Horadrim" #. TRANSLATORS: Book Title #: Source/objects.cpp:217 msgid "The Dark Exile" msgstr "El Exilio Oscuro" #. TRANSLATORS: Book Title #: Source/objects.cpp:218 msgid "The Sin War" msgstr "La Guerra del Pecado" #. TRANSLATORS: Book Title #: Source/objects.cpp:219 msgid "The Binding of the Three" msgstr "La Unión de los Tres" #. TRANSLATORS: Book Title #: Source/objects.cpp:220 msgid "The Realms Beyond" msgstr "Los Reinos del Más Allá" #. TRANSLATORS: Book Title #: Source/objects.cpp:221 msgid "Tale of the Three" msgstr "Cuento de los Tres" #. TRANSLATORS: Book Title #: Source/objects.cpp:222 msgid "The Black King" msgstr "El Rey Negro" #. TRANSLATORS: Book Title #: Source/objects.cpp:223 msgid "Journal: The Ensorcellment" msgstr "Diario: El Hechizado" #. TRANSLATORS: Book Title #: Source/objects.cpp:224 msgid "Journal: The Meeting" msgstr "Diario: El Encuentro" #. TRANSLATORS: Book Title #: Source/objects.cpp:225 msgid "Journal: The Tirade" msgstr "Diario: La Diatriba" #. TRANSLATORS: Book Title #: Source/objects.cpp:226 msgid "Journal: His Power Grows" msgstr "Diario: Su Poder Crece" #. TRANSLATORS: Book Title #: Source/objects.cpp:227 msgid "Journal: NA-KRUL" msgstr "Diario: NA-KRUL" #. TRANSLATORS: Book Title #: Source/objects.cpp:228 msgid "Journal: The End" msgstr "Diario: El Fin" #. TRANSLATORS: Book Title #: Source/objects.cpp:229 msgid "A Spellbook" msgstr "Un libro de Hechizos" #: Source/objects.cpp:4795 msgid "Crucified Skeleton" msgstr "Esqueleto Crucificado" #: Source/objects.cpp:4799 msgid "Lever" msgstr "Palanca" #: Source/objects.cpp:4809 msgid "Open Door" msgstr "Puerta Abierta" #: Source/objects.cpp:4811 msgid "Closed Door" msgstr "Puerta Cerrada" #: Source/objects.cpp:4813 msgid "Blocked Door" msgstr "Puerta Bloqueada" #: Source/objects.cpp:4818 msgid "Ancient Tome" msgstr "Tomo Antiguo" #: Source/objects.cpp:4820 msgid "Book of Vileness" msgstr "Libro de la Vileza" #: Source/objects.cpp:4825 msgid "Skull Lever" msgstr "Palanca de Cráneo" #: Source/objects.cpp:4827 msgid "Mythical Book" msgstr "Libro Mítico" #: Source/objects.cpp:4830 msgid "Small Chest" msgstr "Cofre Pequeño" #: Source/objects.cpp:4833 msgid "Chest" msgstr "Cofre" #: Source/objects.cpp:4837 msgid "Large Chest" msgstr "Arcón" #: Source/objects.cpp:4840 msgid "Sarcophagus" msgstr "Sarcófago" #: Source/objects.cpp:4842 msgid "Bookshelf" msgstr "Estante para Libros" #: Source/objects.cpp:4845 msgid "Bookcase" msgstr "Estantería" #: Source/objects.cpp:4848 msgid "Barrel" msgstr "Barril" #: Source/objects.cpp:4851 msgid "Pod" msgstr "Vaina" #: Source/objects.cpp:4854 msgid "Urn" msgstr "Urna" #. TRANSLATORS: {:s} will be a name from the Shrine block above #: Source/objects.cpp:4857 #, c++-format msgid "{:s} Shrine" msgstr "Santuario {:s}" #: Source/objects.cpp:4859 msgid "Skeleton Tome" msgstr "Tomo de Esqueleto" #: Source/objects.cpp:4861 msgid "Library Book" msgstr "Libro de la Biblioteca" #: Source/objects.cpp:4863 msgid "Blood Fountain" msgstr "Fuente de Sangre" #: Source/objects.cpp:4865 msgid "Decapitated Body" msgstr "Cuerpo Decapitado" #: Source/objects.cpp:4867 msgid "Book of the Blind" msgstr "Libro de los Ciegos" #: Source/objects.cpp:4869 msgid "Book of Blood" msgstr "Libro de Sangre" #: Source/objects.cpp:4871 msgid "Purifying Spring" msgstr "Manantial Purificante" #: Source/objects.cpp:4874 Source/translation_dummy.cpp:275 msgid "Armor" msgstr "Armadura" #: Source/objects.cpp:4876 Source/objects.cpp:4893 msgid "Weapon Rack" msgstr "Estante de Armas" #: Source/objects.cpp:4878 msgid "Goat Shrine" msgstr "Santuario de las Cabras" #: Source/objects.cpp:4880 msgid "Cauldron" msgstr "Caldero" #: Source/objects.cpp:4882 msgid "Murky Pool" msgstr "Piscina Turbia" #: Source/objects.cpp:4884 msgid "Fountain of Tears" msgstr "Fuente de las Lágrimas" #: Source/objects.cpp:4886 msgid "Steel Tome" msgstr "Tomo de Acero" #: Source/objects.cpp:4888 msgid "Pedestal of Blood" msgstr "Pedestal de Sangre" #: Source/objects.cpp:4895 msgid "Mushroom Patch" msgstr "Parche de Hongos" #: Source/objects.cpp:4897 msgid "Vile Stand" msgstr "Vil Estante" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Héroe Asesinado" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "{:s} Trampa" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (deshabilitado)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "ENCENDIDO" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "APAGADO" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Modo de juego" #: Source/options.cpp:422 msgid "Game Mode Settings" msgstr "Configuraciones del modo de juego" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Jugar Diablo o Hellfire." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Restringir a modo shareware" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Hace que el juego sea compatible con la demostración. Habilita el modo " "multijugador con amigos que no poseen una copia completa de Diablo." #: Source/options.cpp:442 msgid "Start Up" msgstr "Inicio" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Configuraciones de inicio" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Introducción" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Mostrar cinemática de introducción." #: Source/options.cpp:455 msgid "Splash" msgstr "Pantalla de bienvenida" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Mostrar pantalla de bienvenida." # La traducción no es exacta para reducir el largo. #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Logo y título" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Pantalla de título" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Configuraciones específicas de Diablo" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Configuraciones específicas de Hellfire" #: Source/options.cpp:501 msgid "Audio" msgstr "Audio" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Configuraciones de audio" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Sonido al caminar" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Los jugadores emiten sonido cuando caminan." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Sonido de auto equipo" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "" "Al equiparse automáticamente con un objeto se emite el sonido de " "equipamiento." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Sonido de recoger objeto" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Al recoger un objeto se emite el sonido de recoger." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Frecuencia de muestreo" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Frecuencia de muestreo de salida (Hz)." #: Source/options.cpp:508 msgid "Channels" msgstr "Canales" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Número de canales de salida." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Tamaño del buffer" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Tamaño del buffer (numero de cuadros por segundo)." #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Calidad de remuestreo" #: Source/options.cpp:510 msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "Calidad de remuestreo, desde 0 (el más bajo) a 5 (el más alto)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Afecta a la resolución interna del juego y determina su área de visión. " "Nota: Esto puede diferir de la resolución de la pantalla cuando se utiliza " "Aumento de escala, Escala de entera o Ajustar a la pantalla." #: Source/options.cpp:574 msgid "Resampler" msgstr "Remuestreador" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "Remuestreador de audio" #: Source/options.cpp:631 msgid "Device" msgstr "Dispositivo" #: Source/options.cpp:631 msgid "Audio device" msgstr "Dispositivo de audio" #: Source/options.cpp:688 msgid "Graphics" msgstr "Gráficos" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Configuración de gráficos" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Pantalla completa" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Muestra el juego en modo ventana o pantalla completa." #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "Ajustar a la pantalla" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Ajusta automáticamente la venta del juego a la relación de aspecto y " "resolución del escritorio actual." #: Source/options.cpp:700 msgid "Upscale" msgstr "Aumento de escala" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Permite escalar la imagen de la resolución del juego a la resolución de su " "monitor. Evita cambiar la resolución del monitor y permite cambiar el tamaño " "de la ventana." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Calidad de escalado" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "" "Habilita filtros opcionales para la imagen de salida al ampliar la escala." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Pixel más cercano" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Bilineal" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Anisotrópico" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Escalado entero" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Escala la imagen usando una proporción de píxeles de números enteros." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "Control de Velocidad de Cuadros" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" "Administra la velocidad de cuadros para equilibrar el rendimiento, reducir " "el desgarro o ahorrar energía." #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Sincronismo vertical" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "Limitar FPS" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "Acercar cuando está habilitado." #: Source/options.cpp:738 msgid "Per-pixel Lighting" msgstr " Iluminación por píxel" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "Iluminación de subtejas para gradientes de luz más suaves." #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Ciclo de color" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "" "Efecto de ciclo de color usado para la animación del agua, lava y ácido." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Alternar arte alternativa" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" "El juego usará una paleta alternativa para el juego de fichas alternativo de " "Hellfire." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Cursor por hardware" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Usar cursor HW" # la traduccion debe ser corta para que entre en el cuado de menu #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Cursor HW para objetos" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Usa un cursor HW para los objetos." # Mantener corto. Menu de configuración #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Tamaño máximo del cursor HW" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Máximo ancho/alto del cursor por hardware. Cursores muy grandes se " "sustituyen por software." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Mostrar FPS" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Muestra los FPS en la esquina superior izquierda de la pantalla." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Juego" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Configuraciones durante el juego" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Habillita trotar/caminar veloz en el pueblo para Diablo y Hellfire. Esta " "opción fue introducida en la expansión." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Capturar entrada" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "Cuando está habilitado el ratón se captura en la ventana de juego." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "Pausar el juego cuando la ventana pierde el foco" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "Cuando está habilitado, el juego se detendrá cuando se pierda el foco." #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Habilita la misión de la Niñita." #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "" "Habilita la misión de Jersey. El granjero Lester es reemplazado por el Loco " "de Remate." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Fuego amigo" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Permite que las flechas/hechizos produzcan daño entre los jugadores en un " "juego multijugador aun cuando el modo amigo está habilitado." #: Source/options.cpp:790 msgid "Full quests in Multiplayer" msgstr "Misiones completas en multijugador" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "" "Habilita la versión completa/sin cortes de las misiones para un jugador." #: Source/options.cpp:791 msgid "Test Bard" msgstr "Probar Bardo" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "" "Fuerza al tipo de personaje Bardo a aparecer en el menú de selección del " "personaje." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Probar Bárbaro" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "" "Fuerza al tipo de personaje Bárbaro a aparecer en el menú de selección del " "personaje." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Barra de experiencia" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "" "Se agrega la barra de experiencia a la UI en la parte inferior de la " "pantalla." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "Mostrar gráficos de artículos en tiendas" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "" "Mostrar gráficos de artículos a la izquierda de las descripciones de los " "artículos en los menús de la tienda." #: Source/options.cpp:795 msgid "Show health values" msgstr "Muestra valores de salud" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "Muestra el valor de salud actual/máximo en el globo de salud." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Muestra valores de maná" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "Muestra el valor de maná actual/máximo en el globo de maná." #: Source/options.cpp:797 msgid "Show Party Information" msgstr "Mostrar Información del Grupo" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" "Muestra la salud y el maná de todos los miembros del grupo multijugador " "conectados." #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Barra de salud del enemigo" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "" "Se muestra la barra de salud del enemigo en la parte superior de la pantalla." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "Cuadro de información de elemento flotante" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" "Muestra la información del artículo en un cuadro flotante al pasar el cursor " "sobre un artículo." #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "" "El oro es automáticamente recogido cuando se encuentra en proximidad del " "jugador." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "" "Los elixires son automáticamente recogidos cuando se encuentran en " "proximidad del jugador." #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "Los aceites se recogen automáticamente cuando está cerca del jugador." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Automáticamente recoge los objetos en el pueblo." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Adria recargará tu maná cuando la visites en su tienda." #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" "Si está habilitado las armas serán automáticamente equipadas al recogerse o " "comprarse." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" "Si está habilitado las armaduras serán automáticamente equipadas al " "recogerse o comprarse." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" "Si está habilitado los yelmos serán automáticamente equipadas al recogerse o " "comprarse." #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" "Si está habilitado los escudos serán automáticamente equipadas al recogerse " "o comprarse." #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" "Si está habilitado las joyerías serán automáticamente equipadas al recogerse " "o comprarse." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "" "Selecciona aleatoriamente las misiones disponibles en las nuevas partidas." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Mostrar el tipo de monstruo" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Al pasar el cursor sobre un monstruo, se mostrará el tipo de monstruo en el " "cuadro de descripción de la interfaz de usuario." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "Mostrar etiquetas para artículos en el suelo cuando está habilitado." #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "" "Recarga el cinturón desde el inventario cuando el objeto ha sido consumido." #: Source/options.cpp:814 msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" "Cuando están habilitados, no se puede hacer clic en Calderos, Santuarios " "fascinantes, Santuarios de cabras, Santuarios ornamentados, Santuarios " "sagrados y Santuarios de Murphy y etiquetarlos como deshabilitados." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Lanzamiento rápido" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Las teclas rápidas de hechizos los lanzan inmediatamente en vez de cambiar a " "hechizo leído." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Número de pociones de salud que se recogerán automáticamente." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "Número de pociones de salud completa que se recogerán automáticamente." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Número de pociones de maná que se recogerán automáticamente." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "Número de pociones de maná completo que se recogerán automáticamente." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "" "Número de pociones de rejuvenecimiento que se recogerán automáticamente." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "" "Número de pociones de rejuvenecimiento completo que se recogerán " "automáticamente." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "Habilitar números flotantes" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "Habilita números flotantes al ganar XP/infligir daño, etc." #: Source/options.cpp:824 msgid "Off" msgstr "Apagado" #: Source/options.cpp:825 msgid "Random Angles" msgstr "Ángulos aleatorios" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "Solo vertical" #: Source/options.cpp:880 msgid "Controller" msgstr "Controlador" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Configuraciones del controlador" #: Source/options.cpp:889 msgid "Network" msgstr "Red" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Configuraciones de red" #: Source/options.cpp:901 msgid "Chat" msgstr "Chat" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Configuraciones del chat" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Idioma" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Define el idioma que se utilizará en el juego." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Configuración de idioma" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Mapeo de teclas" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Configuración de mapeo de teclas" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "Mapeo del pad" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "Configuración de mapeo del pad" #: Source/options.cpp:1512 msgid "Mods" msgstr "Mods" #: Source/options.cpp:1512 msgid "Mod Settings" msgstr "Configuraciones de Mods" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Nivel" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Experiencia" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Siguiente Nivel" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "Base" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Ahora" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Fuerza" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Magia" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Destreza" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Vitalidad" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Puntos a distribuir" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Oro" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Clase armadura" #: Source/panels/charpanel.cpp:176 msgid "Chance To Hit" msgstr "Probabilidad de acertar" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Daño" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Vida" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Maná" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "Resist magia" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Resist fuego" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Resist rayos" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "personaje" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "misiones" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "mapa" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "menú" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "inv" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "hechizos" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "voz" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "silenciar" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Inutilizable" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Daño: 1/3 tgt hp" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Sana: {:d} - {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Daño: {:d} - {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Habilidad" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Bastón ({:d} carga)" msgstr[1] "Bastón ({:d} cargas)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Nivel: {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Maná: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Hechizo" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Solo daña muertos vivientes" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Pergamino" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Bastón" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Tecla de acceso rápido de Hechizo {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "No se puede abrir el archivo" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "No se puede cargar el personaje" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "Error al cargar datos de la clase" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" "No se pudo agregar una clase, ya que se alcanzó el número máximo de clases " "de {}." #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (nivel {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "Historia del Chat (Mensajes: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} oro" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "¿Cuántas piezas de oro quieres retirar?" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Nivel: {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Experiencia: {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Nivel Máximo" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Siguiente Nivel: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} al Nivel {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "Tumba del Rey Leoric" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "La Cámara de Hueso" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Un Pasadizo Oscuro" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Altar Impío" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "A {:s}" #: Source/quick_messages.cpp:10 msgid "I need help! Come here!" msgstr "¡Necesito ayuda! ¡Ven aquí!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Sígueme." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Aquí hay algo para ti." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "¡Ahora MUERES!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "¡Cúrate a ti mismo!" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "¡Cuidado!" #: Source/quick_messages.cpp:16 msgid "Thanks." msgstr "Gracias ." #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "¡Retírate!" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "Lo siento." #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "Estpy esperando." #: Source/stores.cpp:131 msgid "Griswold" msgstr "Griswold" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Pepin" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Ogden" #: Source/stores.cpp:135 msgid "Cain" msgstr "Cain" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Farnham" #: Source/stores.cpp:137 msgid "Adria" msgstr "Adria" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Gillian" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Wirt" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Atrás" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Daño: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Defensa: {:d} " #: Source/stores.cpp:315 #, c++-format msgid "Dur: {:d}/{:d}" msgstr "Dur: {:d}/{:d}" #: Source/stores.cpp:317 msgid "Indestructible" msgstr "Indestructible" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Bienvenido a la" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Herrería" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Te gustaría:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Hablar" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Comprar" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Comprar mercancía selecta" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Vender" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Reparar" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Dejar la herrería" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Tengo esto a la venta:" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Ésta es mi mercancía de primera calidad:" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "No tienes nada que me interese." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "¿Qué tienes a la venta?" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "No tienes nada que reparar." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "¿Qué objeto quieres reparar?" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Choza de la Bruja" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Hablar" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Comprar" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Recargar bastones" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Dejar la choza" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "No tienes nada que recargar." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "¿Qué objeto quieres recargar?" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "No tienes oro suficiente" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "No tienes espacio suficiente en el inventario" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "¿Tenemos un trato?" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "¿Seguro que quieres identificar esto?" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "¿Seguro que quieres comprar esto?" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "¿Seguro que quieres recargar esto?" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "¿Seguro que quieres vender esto?" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "¿Seguro que quieres reparar esto?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Wirt el Patapalo" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Hablar" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Tengo algo en venta," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "te costará 50 de oro" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "sólo echar un vistazo. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "¿Qué es lo que tienes?" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Despedirte" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Esto es lo que tengo:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Irse" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Botica" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Hablar" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Dejar la Botica" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "El Sabio del Pueblo" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Hablar" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Identificar objetos" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "No tienes nada que identificar." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "¿Qué objeto quieres identificar?" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "Este objeto es:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Hecho" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Habla con {:s}" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Hablando con {:s}" #: Source/stores.cpp:1217 msgid "is not available" msgstr "no está disponible" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "en el shareware" #: Source/stores.cpp:1219 msgid "version" msgstr "versión" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Chismorrear" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Taberna del Sol Naciente" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Hablar" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Dejar la taberna" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Hablar" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Examinar Alijo" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Farnham el Borracho" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Hablar" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Despedirte" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Tu oro: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "Error al cargar datos de texto" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "Ya existe una entrada de datos de texto para el ID \"{}\"." #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Aldeano Asesinado" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Griswold el Herrero" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Pepin el Curandero" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Aldeano Herido" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Ogden el Tabernero" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Caín el Sabio" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Adria la Bruja" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Gillian la Camarera" #: Source/towners.cpp:786 msgid "Cow" msgstr "Vaca" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Lester el granjero" #: Source/towners.cpp:788 msgid "Celia" msgstr "Celia" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Loco de Remate" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Guerrero" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Arpía" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Hechicero" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Monje" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Bardo" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Bárbaro" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Zombi" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Necrófago" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Cádaver Podrido" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Muerte Negra" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "El Caído" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Trinchante" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Pariente del Demonio" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "El Oscuro" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Esqueleto" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Hacha Cadáver" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Muerte Ardiente" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Horror" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Carroñero" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Devorador de la Plaga" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Bestia de las Sombras" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Desgarrador de Huesos" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Arquero Cadáver" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Capitán Esqueleto" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Capitán Cadáver" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Capitán Muerte Ardiente" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Capitán del Horror" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Señor Invisible" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Oculto" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Acechador" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Invisible" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Tejedor de Ilusiones" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Señor Sátiro" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Clan de Carne" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Clan de Piedra" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Clan de Fuego" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Clan Nocturno" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Maligno" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Murciélago destellante" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Murciélago oscuro" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Familiar" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Bestia Ácida" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Escupidor de Veneno" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Bestia del Foso" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Fauces de Lava" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Rey Esqueleto" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "El Carnicero" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Cacique" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Hombre de Barro" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Demonio Sapo" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "El Desollado" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Pequeño Dragón" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Babosa de la Cueva" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Pequeño Dragón Demoníaco" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Devorador" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Demonio de Magma" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Piedra de Sangre" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Piedra del Infierno" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Señor de la Lava" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Demonio Cornudo" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Corredor de Barro" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Cargador de Escarcha" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Señor de Obsidiana" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "envejecido" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Muerte Roja" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Demonio Exánime" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Balrog Zombi" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Incinerador" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Señor de las Llamas" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Fuego de Destrucción" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Quemador del Infierno" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Tormenta Roja" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Jinete de Tormenta" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Señor de las Tormentas" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Remolino" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Pariente del Demonio Bruto" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Demonio Alado" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Gárgola" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Garra de Sangre" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Ala de la Muerte" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Asesino" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Guardián" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Señor del Vórtice" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Balrog" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Víbora de Cueva" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Dragón de Fuego" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Víbora de Oro" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Dragón Azur" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Caballero Negro" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Guardia de la Destrucción" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Señor de Acero" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Caballero de Sangre" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "El Destrozado" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "El Hueco" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Maestro del Dolor" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Tejedora de Realidad" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Súcubo" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Bruja de la Nieve" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Engendro del Infierno" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Quemador de Almas" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Consejero" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Magistrado" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Cabalista" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Defensor" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Gólem" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "El Señor Oscuro" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "El Archi Exánime Maligno" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Gharbad el Débil" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Zhar el Loco" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Derrame de Mocos" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Arzobispo Lazarus" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Molestia Roja" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Jade Negro" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Señor de la Guerra de Sangre" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Demonio Hork" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "El Profanador" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Hacha Afilada de Cabeza de Hueso" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Bladeskin el Descuartizador" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Pus del Alma" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Pukerat el Inmundo" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Desgarrador de Huesos" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Rotfeast el Hambriento" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Gutshank el Veloz" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Brokenhead Bangshield" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Bongo" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Carnicería de Putrefacción" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Mordedura de Sombra" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Vigota" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Madeye el Muerto" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "El Chupacabras" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Calavera de Fuego" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Warpskull" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Lengua Sangrienta" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Rastreador de Pulsos" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Moonbender" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Cuervo de la Ira" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Comedor de Columna" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Blackash el Quemado" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Cuervo de Sombra" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Blightstone el Débil" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Bilefroth el Maestro del Foso" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Arco oscuro Piel de Sangre" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "Foulwing" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Bebedor de Sombras" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Cambiador de Neblina" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Escupidor de Muerte" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Bloodgutter" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Deathshade Fleshmaul" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Warmaggot el Loco" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Glasskull el Dentado" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Añublo" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Nightwing el Frio" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Piedra de Sangre" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Piedra de Fuego Puño de Bronce" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Wrathfire el Condenado" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Firewound el Siniestro" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Barón Lodo" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Maza de acero Cuerno de Plaga" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Aullador del Caos" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Doomgrin el Podrido" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Quemador Loco" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Bonesaw el Litch" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Rompe Columna" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Hueso Afilado del Cráneo del Diablo" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Tormenta Rota" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Stormbane" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Exuda Baba" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Dorado de la Llama" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Tormenta Negra" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Ira de la Peste" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "El Desollador" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Cuerno Azul" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Engendro del Infierno del Fuego de la Disformidad" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Colmillo" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Festerskull" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Lionskull el Doblado" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Lengua Negra" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Toque Vil" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Vivora Llamenate" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Fangskin" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Witchfire el Profano" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Calavera Negra" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Corte de Alma" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Engendro del Viento" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Señor del Pozo" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Tejedor de Óxido" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Howlingire la Sombra" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Nube de la Fatalidad" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Fuego del Alma de la Luna de Sangre" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Luna de la Bruja" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Fiesta de Sangre" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Graywar el Asesino" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Juez Aterrador" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Stareye la Bruja" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Steelskull el Cazador" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Sir Gorash" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "El Visir" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Zamphir" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Bloodlust" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Webwidow" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Bailarín de la Carne" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "Pico Sombrío" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Cerradura de la Condenación" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Espada Corta" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Rodela" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Porra" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Arco Corto" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Bastón Corto de Maná" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Cuchilla de carnicero" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "La Corona de los Muertos Vivientes" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Banda Empírea" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Roca Mágica" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Amuleto Óptico" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Anillo de la Verdad" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Cartel de la Taberna" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Cresta del Arlequín" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Velo de Acero" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Elixir Dorado" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Yunque de Furia" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Hongo Negro" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Cerebro" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Tomo de Hongos" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Elixir Espectral" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Piedra de Sangre" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Mapa de la Catedral" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "Oreja" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Poción Curativa" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Poción de Maná" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Pergamino de la Identidad" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Pergamino del Portal de la Ciudad" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Valor de Arkaine" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Poción Curativa Completa" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Poción de Maná Completa" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Hoja de Griswold" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Armadura de Bovino" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Bastón de Lazarus" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Pergamino de Resurrección" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Bastón Corto" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Espada" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Daga" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Bomba Rúnica" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Teodoro" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Amuleto Áurico" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Nota Rasgada 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Nota Rasgada 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Nota Rasgada 3" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Nota Reconstruida" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Traje Marrón" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Traje Gris" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Gorro" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Gorro Metálico" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Yelmo" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Yelmo Completo" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Corona" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Gran Yelmo" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Capa" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Andrajo" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Manto" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Túnica" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Armadura Acolchada" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Armadura de Cuero" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Armadura de Cuero Duro" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Cuero Tachonado" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Cota de Anillos" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Cota" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Cota de Malla" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Cota de Escamas" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Coraza Blindada" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Coraza" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Cota de Láminas" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Cota de Placas" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Coraza de Campaña" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Coraza Gótica" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Cota de Placas Completa" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Escudo" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Escudo Pequeño" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Escudo Grande" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Escudo de Vértices" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Escudo de la Torre" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Escudo Gótico" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Poción Rejuvenecedora" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Poción Rejuvenecedora Completa" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Aceite" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Elixir de Fuerza" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Elixir de Magia" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Elixir de Destreza" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Elixir de Vitalidad" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Pergamino de curación" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Pergamino de Búsqueda" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Pergamino de Relámpago" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Pergamino de Muro de Fuego" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Pergamino de Infierno" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Pergamino de Flash" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Pergamino de Infravisión" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Pergamino de Fase" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Pergamino de Escudo de Maná" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Pergamino de Ola de Llamas" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Pergamino de Bola de Fuego" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Pergamino de Maldición de Piedra" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Pergamino de Cadena de Relámpagos" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Pergamino de Guardián" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Pergamino de Nova" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Pergamino de Golem" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Pergamino de Teletransporte" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Pergamino de Apocalipsis" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Chafarote" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Cimitarra" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Claymore" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Hoja" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Sable" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Espada Larga" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Espada Ancha" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Espada Bastarda" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Mandoble" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Gran Espada" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Hacha Pequeña" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Hacha" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Hacha Grande" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Hacha Ancha" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Hacha de Batalla" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Gran Hacha" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Maza" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Estrella del Alba" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Martillo de Guerra" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Martillo" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Porra con puntas" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Rompecabezas" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Almádena" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Arco" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Arco de Cazador" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Arco Largo" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Arco Compuesto" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Arco Corto de Batalla" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Arco Largo de Batalla" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Arco Corto de Guerra" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Arco Largo de Guerra" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Bastón Largo" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Bastón Plegable" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Bastón de Mando" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Bastón de Guerra" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Anillo" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Amuleto" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Runa de Fuego" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Runa" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Runa de Relámpago" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Gran Runa de Fuego" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Gran Runa de Relámpago" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Runa de Piedra" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Bastón Corto de la Centella" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "Poción de arena" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "La Cuchilla del Carnicero" #: Source/translation_dummy.cpp:370 msgid "Lightforge" msgstr "Forja de luz" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "El Arco de la Desaveniencia" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "El Insoportable" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "El Arco Celestial" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Cazador Mortal" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Arco de la Muerte" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "El Arco de Roble negro" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Dardo de Fuego" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Aguijoneador de Carne" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Fuerza del Viento" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Cuerno de Águila" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Puñal de Gonnagal" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "El Defensor" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Garra del Grifo" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Navaja Negra" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Luna Gibosa" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Mango de Hielo" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "La Espada del Verdugo" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "La Sierra de Hueso" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Halcón de las Sombras" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Pico de Mago" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Sable de Luz" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "La Garra del Halcón" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Infierno" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "Portador de Destrucción" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "El Grizzly" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "El Abuelo" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "El Destrozador" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Pico Afilado" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Asesino de Sangre" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "El Hacha Celestial" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "Hacha Malvada" #: Source/translation_dummy.cpp:402 msgid "Stonecleaver" msgstr "Cuchilla de Piedra" #: Source/translation_dummy.cpp:403 msgid "Aguinara's Hatchet" msgstr "Destral de Aguinara" #: Source/translation_dummy.cpp:404 msgid "Hellslayer" msgstr "Asesino del Infierno" #: Source/translation_dummy.cpp:405 msgid "Messerschmidt's Reaver" msgstr "Atracador de Messerschmidt" #: Source/translation_dummy.cpp:406 msgid "Crackrust" msgstr "Crackrust" #: Source/translation_dummy.cpp:407 msgid "Hammer of Jholm" msgstr "Martillo de Jholm" #: Source/translation_dummy.cpp:408 msgid "Civerb's Cudgel" msgstr "Garrote de Civerb" #: Source/translation_dummy.cpp:409 msgid "The Celestial Star" msgstr "La Estrella Celestial" #: Source/translation_dummy.cpp:410 msgid "Baranar's Star" msgstr "Estrella de Baranar" #: Source/translation_dummy.cpp:411 msgid "Gnarled Root" msgstr "Raíz Nudosa" #: Source/translation_dummy.cpp:412 msgid "The Cranium Basher" msgstr "El Aplastador de Cráneo" #: Source/translation_dummy.cpp:413 msgid "Schaefer's Hammer" msgstr "Martillo de Schaefer" #: Source/translation_dummy.cpp:414 msgid "Dreamflange" msgstr "Brida de Ensueño" #: Source/translation_dummy.cpp:415 msgid "Staff of Shadows" msgstr "Bastón de las Sombras" #: Source/translation_dummy.cpp:416 msgid "Immolator" msgstr "Inmolador" #: Source/translation_dummy.cpp:417 msgid "Storm Spire" msgstr "Aguja de la Tormenta" #: Source/translation_dummy.cpp:418 msgid "Gleamsong" msgstr "Canción de Brillo" #: Source/translation_dummy.cpp:419 msgid "Thundercall" msgstr "Llamada de Trueno" #: Source/translation_dummy.cpp:420 msgid "The Protector" msgstr "El Protector" #: Source/translation_dummy.cpp:421 msgid "Naj's Puzzler" msgstr "Rompecabezas de Naj" #: Source/translation_dummy.cpp:422 msgid "Mindcry" msgstr "Mindcry" #: Source/translation_dummy.cpp:423 msgid "Rod of Onan" msgstr "Vara de Onan" #: Source/translation_dummy.cpp:424 msgid "Helm of Spirits" msgstr "Yelmo de los Espíritus" #: Source/translation_dummy.cpp:425 msgid "Thinking Cap" msgstr "Gorro del Pensamiento" #: Source/translation_dummy.cpp:426 msgid "OverLord's Helm" msgstr "Yelmo del Señor Supremo" #: Source/translation_dummy.cpp:427 msgid "Fool's Crest" msgstr "Cresta del Tonto" #: Source/translation_dummy.cpp:428 msgid "Gotterdamerung" msgstr "El Ocaso de los Dioses" #: Source/translation_dummy.cpp:429 msgid "Royal Circlet" msgstr "Aro Real" #: Source/translation_dummy.cpp:430 msgid "Torn Flesh of Souls" msgstr "Carne de Almas Desgarradas" #: Source/translation_dummy.cpp:431 msgid "The Gladiator's Bane" msgstr "El Flagelo del Gladiador" #: Source/translation_dummy.cpp:432 msgid "The Rainbow Cloak" msgstr "La Capa Arcoíris" #: Source/translation_dummy.cpp:433 msgid "Leather of Aut" msgstr "Armadura de Cuero" #: Source/translation_dummy.cpp:434 msgid "Wisdom's Wrap" msgstr "Manto de la Sabiduría" #: Source/translation_dummy.cpp:435 msgid "Sparking Mail" msgstr "Coraza Brillante" #: Source/translation_dummy.cpp:436 msgid "Scavenger Carapace" msgstr "Caparazón de Carroñero" #: Source/translation_dummy.cpp:437 msgid "Nightscape" msgstr "Paisaje Nocturno" #: Source/translation_dummy.cpp:438 msgid "Naj's Light Plate" msgstr "Armadura Liviana de Naj" #: Source/translation_dummy.cpp:439 msgid "Demonspike Coat" msgstr "Manto de Demonspike" #: Source/translation_dummy.cpp:440 msgid "The Deflector" msgstr "El Deflector" #: Source/translation_dummy.cpp:441 msgid "Split Skull Shield" msgstr "Escudo de Cráneo Dividido" #: Source/translation_dummy.cpp:442 msgid "Dragon's Breach" msgstr "Brecha del Dragón" #: Source/translation_dummy.cpp:443 msgid "Blackoak Shield" msgstr "Escudo de Roble Negro" #: Source/translation_dummy.cpp:444 msgid "Holy Defender" msgstr "Santo Defensor" #: Source/translation_dummy.cpp:445 msgid "Stormshield" msgstr "Escudo de Tormenta" #: Source/translation_dummy.cpp:446 msgid "Bramble" msgstr "Zarza" #: Source/translation_dummy.cpp:447 msgid "Ring of Regha" msgstr "Anillo de Regha" #: Source/translation_dummy.cpp:448 msgid "The Bleeder" msgstr "El Sangrante" #: Source/translation_dummy.cpp:449 msgid "Constricting Ring" msgstr "Anillo de Constricción" #: Source/translation_dummy.cpp:450 msgid "Ring of Engagement" msgstr "Anillo de Compromiso" # ** Adjetivo sensible a cambio de género, terminaciones o/a #: Source/translation_dummy.cpp:451 msgid "Tin" msgstr "de estaño" #: Source/translation_dummy.cpp:452 msgid "Brass" msgstr "de latón" #: Source/translation_dummy.cpp:453 msgid "Bronze" msgstr "de bronce" #: Source/translation_dummy.cpp:454 msgid "Iron" msgstr "de hierro" #: Source/translation_dummy.cpp:455 msgid "Steel" msgstr "de acero" #: Source/translation_dummy.cpp:456 msgid "Silver" msgstr "de plata" #: Source/translation_dummy.cpp:457 msgid "Platinum" msgstr "de platino" #: Source/translation_dummy.cpp:458 msgid "Mithril" msgstr "de mithril" # ** #: Source/translation_dummy.cpp:459 msgid "Meteoric" msgstr "meteórico" # ** #: Source/translation_dummy.cpp:461 msgid "Strange" msgstr "extraño" # ** #: Source/translation_dummy.cpp:462 msgid "Useless" msgstr "estropeado" # ** #: Source/translation_dummy.cpp:463 msgid "Bent" msgstr "mellado" # ** #: Source/translation_dummy.cpp:464 msgid "Weak" msgstr "desgastado" # ** #: Source/translation_dummy.cpp:465 msgid "Jagged" msgstr "dentado" #: Source/translation_dummy.cpp:466 msgid "Deadly" msgstr "mortal" # ** #: Source/translation_dummy.cpp:467 msgid "Heavy" msgstr "pesado" #: Source/translation_dummy.cpp:468 msgid "Vicious" msgstr "atroz" #: Source/translation_dummy.cpp:469 msgid "Brutal" msgstr "brutal" #: Source/translation_dummy.cpp:470 msgid "Massive" msgstr "descomunal" #: Source/translation_dummy.cpp:471 msgid "Savage" msgstr "cruel" #: Source/translation_dummy.cpp:472 msgid "Ruthless" msgstr "implacable" # ** #: Source/translation_dummy.cpp:473 msgid "Merciless" msgstr "despiadado" # ** #: Source/translation_dummy.cpp:474 msgid "Clumsy" msgstr "roñoso" # ** #: Source/translation_dummy.cpp:475 msgid "Dull" msgstr "mohoso" # ** #: Source/translation_dummy.cpp:476 msgid "Sharp" msgstr "afilado" # ** #: Source/translation_dummy.cpp:477 msgid "Fine" msgstr "fino" #: Source/translation_dummy.cpp:478 msgid "Warrior's" msgstr "de guerrero" #: Source/translation_dummy.cpp:479 msgid "Soldier's" msgstr "de soldado" #: Source/translation_dummy.cpp:480 msgid "Lord's" msgstr "del señor" #: Source/translation_dummy.cpp:481 msgid "Knight's" msgstr "de caballero" #: Source/translation_dummy.cpp:482 msgid "Master's" msgstr "de maestro" #: Source/translation_dummy.cpp:483 msgid "Champion's" msgstr "de campeón" #: Source/translation_dummy.cpp:484 msgid "King's" msgstr "real" #: Source/translation_dummy.cpp:485 msgid "Vulnerable" msgstr "frágil" # ** #: Source/translation_dummy.cpp:486 msgid "Rusted" msgstr "oxidado" #: Source/translation_dummy.cpp:487 msgid "Strong" msgstr "fuerte" # ** #: Source/translation_dummy.cpp:488 msgid "Grand" msgstr "grandioso" #: Source/translation_dummy.cpp:489 msgid "Valiant" msgstr "valiente" # ** #: Source/translation_dummy.cpp:490 msgid "Glorious" msgstr "glorioso" # ** #: Source/translation_dummy.cpp:491 msgid "Blessed" msgstr "bendito" # ** #: Source/translation_dummy.cpp:492 msgid "Saintly" msgstr "santo" # ** #: Source/translation_dummy.cpp:493 msgid "Awesome" msgstr "asombroso" # ** #: Source/translation_dummy.cpp:495 msgid "Godly" msgstr "piadoso" # ** #: Source/translation_dummy.cpp:496 msgid "Red" msgstr "rojo" #: Source/translation_dummy.cpp:497 msgid "Crimson" msgstr "carmesí" #: Source/translation_dummy.cpp:498 msgid "Garnet" msgstr "granate" #: Source/translation_dummy.cpp:499 msgid "Ruby" msgstr "rubí" #: Source/translation_dummy.cpp:500 msgid "Blue" msgstr "azul" #: Source/translation_dummy.cpp:501 msgid "Azure" msgstr "celeste" #: Source/translation_dummy.cpp:502 msgid "Lapis" msgstr "lapislázuli" #: Source/translation_dummy.cpp:503 msgid "Cobalt" msgstr "cobalto" #: Source/translation_dummy.cpp:504 msgid "Sapphire" msgstr "zafiro" # ** #: Source/translation_dummy.cpp:505 msgid "White" msgstr "blanco" #: Source/translation_dummy.cpp:506 msgid "Pearl" msgstr "perla" #: Source/translation_dummy.cpp:507 msgid "Ivory" msgstr "marfil" #: Source/translation_dummy.cpp:508 msgid "Crystal" msgstr "cristal" #: Source/translation_dummy.cpp:509 msgid "Diamond" msgstr "diamante" #: Source/translation_dummy.cpp:510 msgid "Topaz" msgstr "topacio" #: Source/translation_dummy.cpp:511 msgid "Amber" msgstr "ámbar" #: Source/translation_dummy.cpp:512 msgid "Jade" msgstr "jade" #: Source/translation_dummy.cpp:513 msgid "Obsidian" msgstr "obsidiana" #: Source/translation_dummy.cpp:514 msgid "Emerald" msgstr "esmeralda" #: Source/translation_dummy.cpp:515 msgid "Hyena's" msgstr "de la hiena" #: Source/translation_dummy.cpp:516 msgid "Frog's" msgstr "de la rana" #: Source/translation_dummy.cpp:517 msgid "Spider's" msgstr "de la araña" #: Source/translation_dummy.cpp:518 msgid "Raven's" msgstr "del cuervo" #: Source/translation_dummy.cpp:519 msgid "Snake's" msgstr "de la culebra" #: Source/translation_dummy.cpp:520 msgid "Serpent's" msgstr "de la serpiente" #: Source/translation_dummy.cpp:521 msgid "Drake's" msgstr "del pequeño dragón" #: Source/translation_dummy.cpp:522 msgid "Dragon's" msgstr "del dragón" #: Source/translation_dummy.cpp:523 msgid "Wyrm's" msgstr "del gran dragón" #: Source/translation_dummy.cpp:524 msgid "Hydra's" msgstr "de la hidra" #: Source/translation_dummy.cpp:525 msgid "Angel's" msgstr "del ángel" #: Source/translation_dummy.cpp:526 msgid "Arch-Angel's" msgstr "del arcángel" #: Source/translation_dummy.cpp:527 msgid "Plentiful" msgstr "cuantioso" #: Source/translation_dummy.cpp:528 msgid "Bountiful" msgstr "colmado" #: Source/translation_dummy.cpp:529 msgid "Flaming" msgstr "llameante" #: Source/translation_dummy.cpp:530 msgid "Lightning" msgstr "centelleante" #: Source/translation_dummy.cpp:531 msgid "quality" msgstr "de la calidad" #: Source/translation_dummy.cpp:532 msgid "maiming" msgstr "de la mutilación" #: Source/translation_dummy.cpp:533 msgid "slaying" msgstr "del asesinato" #: Source/translation_dummy.cpp:534 msgid "gore" msgstr "de sangre" #: Source/translation_dummy.cpp:535 msgid "carnage" msgstr "de la matanza" #: Source/translation_dummy.cpp:536 msgid "slaughter" msgstr "de la tortura" #: Source/translation_dummy.cpp:537 msgid "pain" msgstr "del dolor" #: Source/translation_dummy.cpp:538 msgid "tears" msgstr "del llanto" #: Source/translation_dummy.cpp:539 msgid "health" msgstr "de salud" #: Source/translation_dummy.cpp:540 msgid "protection" msgstr "de protección" #: Source/translation_dummy.cpp:541 msgid "absorption" msgstr "de absorción" #: Source/translation_dummy.cpp:542 msgid "deflection" msgstr "de desvío" #: Source/translation_dummy.cpp:543 msgid "osmosis" msgstr "de ósmosis" #: Source/translation_dummy.cpp:544 msgid "frailty" msgstr "de la endeblez" #: Source/translation_dummy.cpp:545 msgid "weakness" msgstr "de la debilidad" #: Source/translation_dummy.cpp:546 msgid "strength" msgstr "de fuerza" #: Source/translation_dummy.cpp:547 msgid "might" msgstr "del poder" #: Source/translation_dummy.cpp:548 msgid "power" msgstr "del buey" #: Source/translation_dummy.cpp:549 msgid "giants" msgstr "del gigante" #: Source/translation_dummy.cpp:550 msgid "titans" msgstr "del titán" #: Source/translation_dummy.cpp:551 msgid "paralysis" msgstr "de la parálisis" #: Source/translation_dummy.cpp:552 msgid "atrophy" msgstr "de la atrofia" #: Source/translation_dummy.cpp:553 msgid "dexterity" msgstr "de la destreza" #: Source/translation_dummy.cpp:554 msgid "skill" msgstr "de la habilidad" #: Source/translation_dummy.cpp:555 msgid "accuracy" msgstr "de la exactitud" #: Source/translation_dummy.cpp:556 msgid "precision" msgstr "de la precisión" #: Source/translation_dummy.cpp:557 msgid "perfection" msgstr "de la perfección" #: Source/translation_dummy.cpp:558 msgid "the fool" msgstr "del mentecato" #: Source/translation_dummy.cpp:559 msgid "dyslexia" msgstr "de la dislexia" #: Source/translation_dummy.cpp:560 msgid "magic" msgstr "de la energía" #: Source/translation_dummy.cpp:561 msgid "the mind" msgstr "de la mente" #: Source/translation_dummy.cpp:562 msgid "brilliance" msgstr "de la brillantez" #: Source/translation_dummy.cpp:563 msgid "sorcery" msgstr "de la brujería" #: Source/translation_dummy.cpp:564 msgid "wizardry" msgstr "de la hechicería" #: Source/translation_dummy.cpp:565 msgid "illness" msgstr "de la dolencia" #: Source/translation_dummy.cpp:566 msgid "disease" msgstr "de la enfermedad" #: Source/translation_dummy.cpp:567 msgid "vitality" msgstr "de la vitalidad" #: Source/translation_dummy.cpp:568 msgid "zest" msgstr "de la vivacidad" #: Source/translation_dummy.cpp:569 msgid "vim" msgstr "del brío" #: Source/translation_dummy.cpp:570 msgid "vigor" msgstr "del vigor" #: Source/translation_dummy.cpp:571 msgid "life" msgstr "de la vida" #: Source/translation_dummy.cpp:572 msgid "trouble" msgstr "del problema" #: Source/translation_dummy.cpp:573 msgid "the pit" msgstr "del hoyo" #: Source/translation_dummy.cpp:574 msgid "the sky" msgstr "del cielo" #: Source/translation_dummy.cpp:575 msgid "the moon" msgstr "de la luna" #: Source/translation_dummy.cpp:576 msgid "the stars" msgstr "de las estrellas" #: Source/translation_dummy.cpp:577 msgid "the heavens" msgstr "de los cielos" #: Source/translation_dummy.cpp:578 msgid "the zodiac" msgstr "del zodiaco" #: Source/translation_dummy.cpp:579 msgid "the vulture" msgstr "del buitre" #: Source/translation_dummy.cpp:580 msgid "the jackal" msgstr "del chacal" #: Source/translation_dummy.cpp:581 msgid "the fox" msgstr "del zorro" #: Source/translation_dummy.cpp:582 msgid "the jaguar" msgstr "del jaguar" #: Source/translation_dummy.cpp:583 msgid "the eagle" msgstr "del águila" #: Source/translation_dummy.cpp:584 msgid "the wolf" msgstr "del lobo" #: Source/translation_dummy.cpp:585 msgid "the tiger" msgstr "del tigre" #: Source/translation_dummy.cpp:586 msgid "the lion" msgstr "del león" #: Source/translation_dummy.cpp:587 msgid "the mammoth" msgstr "del mamut" #: Source/translation_dummy.cpp:588 msgid "the whale" msgstr "de la ballena" #: Source/translation_dummy.cpp:589 msgid "fragility" msgstr "de la fragilidad" #: Source/translation_dummy.cpp:590 msgid "brittleness" msgstr "de la inconsistencia" #: Source/translation_dummy.cpp:591 msgid "sturdiness" msgstr "de la robustez" #: Source/translation_dummy.cpp:592 msgid "craftsmanship" msgstr "de la artesanía" #: Source/translation_dummy.cpp:593 msgid "structure" msgstr "de la solidez" #: Source/translation_dummy.cpp:594 msgid "the ages" msgstr "de las eras" #: Source/translation_dummy.cpp:595 msgid "the dark" msgstr "de la oscuridad" #: Source/translation_dummy.cpp:596 msgid "the night" msgstr "de la noche" #: Source/translation_dummy.cpp:597 msgid "light" msgstr "de la luz" #: Source/translation_dummy.cpp:598 msgid "radiance" msgstr "del resplandor" #: Source/translation_dummy.cpp:599 msgid "flame" msgstr "de la llama" #: Source/translation_dummy.cpp:600 msgid "fire" msgstr "del fuego" #: Source/translation_dummy.cpp:601 msgid "burning" msgstr "de la combustión" #: Source/translation_dummy.cpp:602 msgid "shock" msgstr "de la conmoción" #: Source/translation_dummy.cpp:603 msgid "lightning" msgstr "del relámpago" #: Source/translation_dummy.cpp:604 msgid "thunder" msgstr "del trueno" #: Source/translation_dummy.cpp:605 msgid "many" msgstr "de la abundancia" #: Source/translation_dummy.cpp:606 msgid "plenty" msgstr "de la infinidad" #: Source/translation_dummy.cpp:607 msgid "thorns" msgstr "de espinas" #: Source/translation_dummy.cpp:608 msgid "corruption" msgstr "de la corrupción" #: Source/translation_dummy.cpp:609 msgid "thieves" msgstr "del ladrón" #: Source/translation_dummy.cpp:610 msgid "the bear" msgstr "del oso" #: Source/translation_dummy.cpp:611 msgid "the bat" msgstr "del murciélago" #: Source/translation_dummy.cpp:612 msgid "vampires" msgstr "del vampiro" #: Source/translation_dummy.cpp:613 msgid "the leech" msgstr "de la sanguijuela" #: Source/translation_dummy.cpp:614 msgid "blood" msgstr "de la langosta" #: Source/translation_dummy.cpp:615 msgid "piercing" msgstr "de la perforación" #: Source/translation_dummy.cpp:616 msgid "puncturing" msgstr "del pinchazo" #: Source/translation_dummy.cpp:617 msgid "bashing" msgstr "del golpe" #: Source/translation_dummy.cpp:618 msgid "readiness" msgstr "de la buena voluntad" #: Source/translation_dummy.cpp:619 msgid "swiftness" msgstr "de la presteza" #: Source/translation_dummy.cpp:620 msgid "speed" msgstr "de la velocidad" #: Source/translation_dummy.cpp:621 msgid "haste" msgstr "de la rapidez" #: Source/translation_dummy.cpp:622 msgid "balance" msgstr "del equilibrio" #: Source/translation_dummy.cpp:623 msgid "stability" msgstr "de la estabilidad" #: Source/translation_dummy.cpp:624 msgid "harmony" msgstr "de la armonía" #: Source/translation_dummy.cpp:625 msgid "blocking" msgstr "de bloqueo" #: Source/translation_dummy.cpp:626 msgid "The Magic Rock" msgstr "La Roca Mágica" #: Source/translation_dummy.cpp:627 msgid "Gharbad The Weak" msgstr "Gharbad el Débil" #: Source/translation_dummy.cpp:628 msgid "Zhar the Mad" msgstr "Zhar el Loco" #: Source/translation_dummy.cpp:629 msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:631 msgid "The Butcher" msgstr "El Carnicero" #: Source/translation_dummy.cpp:632 msgid "Ogden's Sign" msgstr "Signo de Ogden" #: Source/translation_dummy.cpp:633 msgid "Halls of the Blind" msgstr "Pasillos de los Ciegos" #: Source/translation_dummy.cpp:634 msgid "Valor" msgstr "Valor" #: Source/translation_dummy.cpp:635 msgid "Warlord of Blood" msgstr "Señor de la Guerra de Sangre" #: Source/translation_dummy.cpp:636 msgid "The Curse of King Leoric" msgstr "La Maldición del Rey Leoric" #: Source/translation_dummy.cpp:639 msgid "Archbishop Lazarus" msgstr "Arzobispo Lazarus" #: Source/translation_dummy.cpp:640 msgid "Grave Matters" msgstr "Asuntos de Tumbas" #: Source/translation_dummy.cpp:641 msgid "Farmer's Orchard" msgstr "Huerto del Granjero" #: Source/translation_dummy.cpp:642 msgid "Little Girl" msgstr "Niñita" #: Source/translation_dummy.cpp:643 msgid "Wandering Trader" msgstr "Comerciante Errante" #: Source/translation_dummy.cpp:644 msgid "The Defiler" msgstr "El Profanador" #: Source/translation_dummy.cpp:645 msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:647 msgid "The Jersey's Jersey" msgstr "El Jersey de Jersey" #: Source/translation_dummy.cpp:648 msgctxt "spell" msgid "Firebolt" msgstr "Flecha de fuego" #: Source/translation_dummy.cpp:649 msgctxt "spell" msgid "Healing" msgstr "Curación" #: Source/translation_dummy.cpp:650 msgctxt "spell" msgid "Lightning" msgstr "Rayo" #: Source/translation_dummy.cpp:651 msgctxt "spell" msgid "Flash" msgstr "Destello" #: Source/translation_dummy.cpp:652 msgctxt "spell" msgid "Identify" msgstr "Identificar" #: Source/translation_dummy.cpp:653 msgctxt "spell" msgid "Fire Wall" msgstr "Muro de Fuego" #: Source/translation_dummy.cpp:654 msgctxt "spell" msgid "Town Portal" msgstr "Portal al pueblo" #: Source/translation_dummy.cpp:655 msgctxt "spell" msgid "Stone Curse" msgstr "Maldición de Piedra" #: Source/translation_dummy.cpp:656 msgctxt "spell" msgid "Infravision" msgstr "Infravisión" #: Source/translation_dummy.cpp:657 msgctxt "spell" msgid "Phasing" msgstr "Ajuste de Fase" #: Source/translation_dummy.cpp:658 msgctxt "spell" msgid "Mana Shield" msgstr "Escudo de Maná" #: Source/translation_dummy.cpp:659 msgctxt "spell" msgid "Fireball" msgstr "Bola de Fuego" #: Source/translation_dummy.cpp:660 msgctxt "spell" msgid "Guardian" msgstr "Guardián" #: Source/translation_dummy.cpp:661 msgctxt "spell" msgid "Chain Lightning" msgstr "Cadena de Relámpagos" #: Source/translation_dummy.cpp:662 msgctxt "spell" msgid "Flame Wave" msgstr "Ola de Llamas" #: Source/translation_dummy.cpp:663 msgctxt "spell" msgid "Doom Serpents" msgstr "Serpientes de la Condenación" #: Source/translation_dummy.cpp:664 msgctxt "spell" msgid "Blood Ritual" msgstr "Ritual de Sangre" #: Source/translation_dummy.cpp:665 msgctxt "spell" msgid "Nova" msgstr "Nova" #: Source/translation_dummy.cpp:666 msgctxt "spell" msgid "Invisibility" msgstr "Invisibilidad" #: Source/translation_dummy.cpp:667 msgctxt "spell" msgid "Inferno" msgstr "Infierno" #: Source/translation_dummy.cpp:668 msgctxt "spell" msgid "Golem" msgstr "Gólem" #: Source/translation_dummy.cpp:669 msgctxt "spell" msgid "Rage" msgstr "Furia" #: Source/translation_dummy.cpp:670 msgctxt "spell" msgid "Teleport" msgstr "Teletransporte" #: Source/translation_dummy.cpp:671 msgctxt "spell" msgid "Apocalypse" msgstr "Apocalipsis" #: Source/translation_dummy.cpp:672 msgctxt "spell" msgid "Etherealize" msgstr "Etéreo" #: Source/translation_dummy.cpp:673 msgctxt "spell" msgid "Item Repair" msgstr "Reparación de Artículo" #: Source/translation_dummy.cpp:674 msgctxt "spell" msgid "Staff Recharge" msgstr "Recarga de Bastón" #: Source/translation_dummy.cpp:675 msgctxt "spell" msgid "Trap Disarm" msgstr "Desarmar Trampa" #: Source/translation_dummy.cpp:676 msgctxt "spell" msgid "Elemental" msgstr "Elemental" #: Source/translation_dummy.cpp:677 msgctxt "spell" msgid "Charged Bolt" msgstr "Rayo Cargado" #: Source/translation_dummy.cpp:678 msgctxt "spell" msgid "Holy Bolt" msgstr "Rayo Santo" #: Source/translation_dummy.cpp:679 msgctxt "spell" msgid "Resurrect" msgstr "Resucitar" #: Source/translation_dummy.cpp:680 msgctxt "spell" msgid "Telekinesis" msgstr "Telequinesis" #: Source/translation_dummy.cpp:681 msgctxt "spell" msgid "Heal Other" msgstr "Sanar a Otros" #: Source/translation_dummy.cpp:682 msgctxt "spell" msgid "Blood Star" msgstr "Estrella de Sangre" #: Source/translation_dummy.cpp:683 msgctxt "spell" msgid "Bone Spirit" msgstr "Espíritu de Hueso" #: Source/translation_dummy.cpp:684 msgid "" " Ahh, the story of our King, is it? The tragic fall of Leoric was a harsh " "blow to this land. The people always loved the King, and now they live in " "mortal fear of him. The question that I keep asking myself is how he could " "have fallen so far from the Light, as Leoric had always been the holiest of " "men. Only the vilest powers of Hell could so utterly destroy a man from " "within..." msgstr "" " Ah, la historia de nuestro Rey, ¿verdad? La trágica caída de Leoric fue un " "duro golpe para esta tierra. La gente siempre amó al Rey, y ahora vive con " "un miedo mortal hacia él. La pregunta que me sigo haciendo es cómo pudo " "haber caído tan lejos de la Luz, ya que Leoric siempre había sido el más " "santo de los hombres. Sólo los poderes más viles del Infierno podrían " "destruir tan completamente a un hombre desde dentro ..." #: Source/translation_dummy.cpp:685 msgid "" "The village needs your help, good master! Some months ago King Leoric's son, " "Prince Albrecht, was kidnapped. The King went into a rage and scoured the " "village for his missing child. With each passing day, Leoric seemed to slip " "deeper into madness. He sought to blame innocent townsfolk for the boy's " "disappearance and had them brutally executed. Less than half of us survived " "his insanity...\n" " \n" "The King's Knights and Priests tried to placate him, but he turned against " "them and sadly, they were forced to kill him. With his dying breath the King " "called down a terrible curse upon his former followers. He vowed that they " "would serve him in darkness forever...\n" " \n" "This is where things take an even darker twist than I thought possible! Our " "former King has risen from his eternal sleep and now commands a legion of " "undead minions within the Labyrinth. His body was buried in a tomb three " "levels beneath the Cathedral. Please, good master, put his soul at ease by " "destroying his now cursed form..." msgstr "" "¡El pueblo necesita tu ayuda, buen maestro! Hace algunos meses, el hijo del " "rey Leoric, el Príncipe Albrecht, fue secuestrado. El rey se enfureció y " "recorrió el pueblo en busca de su hijo desaparecido. Con cada día que " "pasaba, Leoric parecía hundirse cada vez más en la locura. Trató de culpar a " "los habitantes inocentes de la desaparición del niño y los ejecutó " "brutalmente. Menos de la mitad de nosotros sobrevivimos a su locura ...\n" " \n" "Los Caballeros y Sacerdotes del Rey intentaron aplacarlo, pero él se volvió " "contra ellos y, lamentablemente, se vieron obligados a matarlo. Con su " "último aliento, el Rey lanzó una terrible maldición sobre sus antiguos " "seguidores. Juró que lo servirían en la oscuridad para siempre ...\n" " \n" "¡Aquí es donde las cosas toman un giro aún más oscuro de lo que creía " "posible! Nuestro antiguo Rey se ha levantado de su sueño eterno y ahora " "comanda una legión de esbirros de muertos vivientes dentro del Laberinto. Su " "cuerpo fue enterrado en una tumba tres niveles debajo de la Catedral. Por " "favor, buen maestro, tranquilice su alma destruyendo su forma ahora " "maldita ..." #: Source/translation_dummy.cpp:686 msgid "" "As I told you, good master, the King was entombed three levels below. He's " "down there, waiting in the putrid darkness for his chance to destroy this " "land..." msgstr "" "Como le dije, buen maestro, el Rey fue sepultado tres niveles más abajo. " "Está ahí abajo, esperando en la pútrida oscuridad su oportunidad de destruir " "esta tierra ..." #: Source/translation_dummy.cpp:687 msgid "" "The curse of our King has passed, but I fear that it was only part of a " "greater evil at work. However, we may yet be saved from the darkness that " "consumes our land, for your victory is a good omen. May Light guide you on " "your way, good master." msgstr "" "La maldición de nuestro Rey ha terminado, pero me temo que fue solo una " "parte de un mal mayor en acción. Sin embargo, aún podemos salvarnos de la " "oscuridad que consume nuestra tierra, porque tu victoria es un buen augurio. " "Que la Luz te guíe en tu camino, buen maestro." #: Source/translation_dummy.cpp:688 msgid "" "The loss of his son was too much for King Leoric. I did what I could to ease " "his madness, but in the end it overcame him. A black curse has hung over " "this kingdom from that day forward, but perhaps if you were to free his " "spirit from his earthly prison, the curse would be lifted..." msgstr "" "La pérdida de su hijo fue demasiado para el Rey Leoric. Hice lo que pude " "para aliviar su locura, pero al final lo superó. Una maldición negra se " "cierne sobre este reino desde ese día, pero tal vez si liberaras su espíritu " "de su prisión terrenal, la maldición se levantaría ..." #: Source/translation_dummy.cpp:689 msgid "" "I don't like to think about how the King died. I like to remember him for " "the kind and just ruler that he was. His death was so sad and seemed very " "wrong, somehow." msgstr "" "No me gusta pensar en cómo murió el Rey. Me gusta recordarlo como el " "gobernante amable y justo que era. Su muerte fue tan triste y parecía muy " "mal, de alguna manera." #: Source/translation_dummy.cpp:690 msgid "" "I made many of the weapons and most of the armor that King Leoric used to " "outfit his knights. I even crafted a huge two-handed sword of the finest " "mithril for him, as well as a field crown to match. I still cannot believe " "how he died, but it must have been some sinister force that drove him insane!" msgstr "" "Hice muchas de las armas y la mayor parte de las armaduras que el rey Leoric " "usó para equipar a sus caballeros. Incluso le elaboré una enorme espada a " "dos manos del mejor mithril, así como una corona de campo a juego. Todavía " "no puedo creer cómo murió ¡Pero debe haber sido alguna fuerza siniestra lo " "que lo volvió loco!" #: Source/translation_dummy.cpp:691 msgid "" "I don't care about that. Listen, no skeleton is gonna be MY king. Leoric is " "King. King, so you hear me? HAIL TO THE KING!" msgstr "" "Eso no me importa. Escucha, ningún esqueleto será MI rey. Leoric es el Rey. " "Rey, ¿me escuchas? ¡VIVA EL REY!" #: Source/translation_dummy.cpp:692 msgid "" "The dead who walk among the living follow the cursed King. He holds the " "power to raise yet more warriors for an ever growing army of the undead. If " "you do not stop his reign, he will surely march across this land and slay " "all who still live here." msgstr "" "Los muertos que caminan entre los vivos siguen al Rey maldito. Tiene el " "poder de crear aún más guerreros para un ejército de muertos vivientes en " "constante crecimiento. Si no detienes su reinado, seguramente marchará a " "través de esta tierra y matará a todos los que todavía viven aquí." #: Source/translation_dummy.cpp:693 msgid "" "Look, I'm running a business here. I don't sell information, and I don't " "care about some King that's been dead longer than I've been alive. If you " "need something to use against this King of the undead, then I can help you " "out..." msgstr "" "Mira, tengo un negocio aquí. No vendo información, y no me importa un Rey " "que ha estado muerto más tiempo que yo vivo. Si necesitas algo para usar " "contra este Rey de los muertos vivientes, entonces puedo ayudarte ..." #: Source/translation_dummy.cpp:694 msgid "" "The warmth of life has entered my tomb. Prepare yourself, mortal, to serve " "my Master for eternity!" msgstr "" "El calor de la vida ha entrado en mi tumba. ¡Prepárate, mortal, para servir " "a mi Maestro por la eternidad!" #: Source/translation_dummy.cpp:695 msgid "" "I see that this strange behavior puzzles you as well. I would surmise that " "since many demons fear the light of the sun and believe that it holds great " "power, it may be that the rising sun depicted on the sign you speak of has " "led them to believe that it too holds some arcane powers. Hmm, perhaps they " "are not all as smart as we had feared..." msgstr "" "Veo que este comportamiento extraño también te desconcierta. Supongo que, " "dado que muchos demonios temen la luz del sol y creen que tienen un gran " "poder, es posible que el sol naciente representado en el letrero del que " "hablas les haya llevado a creer que también tiene algunos poderes arcanos. " "Mmm, quizás no todos sean tan inteligentes como nos temíamos ..." #: Source/translation_dummy.cpp:696 msgid "" "Master, I have a strange experience to relate. I know that you have a great " "knowledge of those monstrosities that inhabit the labyrinth, and this is " "something that I cannot understand for the very life of me... I was awakened " "during the night by a scraping sound just outside of my tavern. When I " "looked out from my bedroom, I saw the shapes of small demon-like creatures " "in the inn yard. After a short time, they ran off, but not before stealing " "the sign to my inn. I don't know why the demons would steal my sign but " "leave my family in peace... 'tis strange, no?" msgstr "" "Maestro, tengo una experiencia extraña que contarle. Sé que tiene un gran " "conocimiento de esas monstruosidades que habitan el laberinto, y esto es " "algo que no puedo entender, por mi vida ... Me desperté durante la noche por " "un sonido de rascado justo afuera de mi taberna. Cuando miré desde mi " "habitación, vi las formas de pequeñas criaturas parecidas a demonios en el " "patio de la posada. Poco tiempo después, se fueron corriendo, no sin antes " "robar el cartel de mi posada. No sé por qué los demonios robarían mi cartel " "y dejarían a mi familia en paz ... es extraño, ¿no?" #: Source/translation_dummy.cpp:697 msgid "" "Oh, you didn't have to bring back my sign, but I suppose that it does save " "me the expense of having another one made. Well, let me see, what could I " "give you as a fee for finding it? Hmmm, what have we here... ah, yes! This " "cap was left in one of the rooms by a magician who stayed here some time " "ago. Perhaps it may be of some value to you." msgstr "" "Oh, no tenías que traer mi letrero, pero supongo que me ahorra el gasto de " "hacer otro. Bueno, déjame ver, ¿qué puedo darte como tarifa por encontrarlo? " "Hmmm, qué tenemos aquí ... ¡ah, sí! Este gorro fue dejado en una de las " "habitaciones por un mago que se quedó aquí hace algún tiempo. Quizás pueda " "tener algún valor para ti." #: Source/translation_dummy.cpp:698 msgid "" "My goodness, demons running about the village at night, pillaging our homes " "- is nothing sacred? I hope that Ogden and Garda are all right. I suppose " "that they would come to see me if they were hurt..." msgstr "" "Dios mío, los demonios que corren por la aldea de noche, saquean nuestras " "casas, ¿no es nada sagrado? Espero que Ogden y Garda estén bien. Supongo que " "vendrían a verme si les hubieran hecho daño ..." #: Source/translation_dummy.cpp:699 msgid "" "Oh my! Is that where the sign went? My Grandmother and I must have slept " "right through the whole thing. Thank the Light that those monsters didn't " "attack the inn." msgstr "" "¡Oh Dios! ¿Es ahí donde fue el letrero? Mi Abuela y yo debimos haber dormido " "todo el rato. Gracias a la Luz que esos monstruos no atacaron la posada." #: Source/translation_dummy.cpp:700 msgid "" "Demons stole Ogden's sign, you say? That doesn't sound much like the " "atrocities I've heard of - or seen. \n" " \n" "Demons are concerned with ripping out your heart, not your signpost." msgstr "" "¿Dices que los demonios robaron el letrero de Ogden? Eso no se parece mucho " "a las atrocidades de las que he oído, o visto. \n" " \n" "A los demonios les preocupa arrancarte el corazón, no tu letrero." #: Source/translation_dummy.cpp:701 msgid "" "You know what I think? Somebody took that sign, and they gonna want lots of " "money for it. If I was Ogden... and I'm not, but if I was... I'd just buy a " "new sign with some pretty drawing on it. Maybe a nice mug of ale or a piece " "of cheese..." msgstr "" "¿Sabes lo que pienso? Alguien tomó ese letrero y querrán mucho dinero por " "él. Si yo fuera Ogden ... y no lo soy, pero si fuera ... compraría un nuevo " "cartel con un bonito dibujo. Quizás una buena jarra de cerveza o un trozo de " "queso ..." #: Source/translation_dummy.cpp:702 msgid "" "No mortal can truly understand the mind of the demon. \n" " \n" "Never let their erratic actions confuse you, as that too may be their plan." msgstr "" "Ningún mortal puede comprender verdaderamente la mente del demonio. \n" " \n" "Nunca dejes que sus acciones erráticas te confundan, ya que ese también " "puede ser su plan." #: Source/translation_dummy.cpp:703 msgid "" "What - is he saying I took that? I suppose that Griswold is on his side, " "too. \n" " \n" "Look, I got over simple sign stealing months ago. You can't turn a profit on " "a piece of wood." msgstr "" "¿Qué? ¿Está diciendo que me llevé eso? Supongo que Griswold también está de " "su lado. \n" " \n" "Mira, superé el simple robo de letreros hace meses. No puede obtener " "ganancias con un trozo de madera." #: Source/translation_dummy.cpp:704 msgid "" "Hey - You that one that kill all! You get me Magic Banner or we attack! You " "no leave with life! You kill big uglies and give back Magic. Go past corner " "and door, find uglies. You give, you go!" msgstr "" "¡Oye, tú eres el que mata a todos! ¡Consígueme el Estandarte Mágico o " "atacamos! ¡No dejes con vida! Matas a los grandes feos y devuelves la magia. " "Pasa la esquina y la puerta, encuentra a los feos. ¡Das, te vas!" #: Source/translation_dummy.cpp:705 msgid "You kill uglies, get banner. You bring to me, or else..." msgstr "Matas a los feos, obtienes estandarte. Me lo traes, o si no ..." #: Source/translation_dummy.cpp:706 msgid "You give! Yes, good! Go now, we strong. We kill all with big Magic!" msgstr "" "¡Das! ¡Si, bien! Vete ahora, somos fuertes. ¡Matamos a todos con gran Magia!" #: Source/translation_dummy.cpp:707 msgid "" "This does not bode well, for it confirms my darkest fears. While I did not " "allow myself to believe the ancient legends, I cannot deny them now. Perhaps " "the time has come to reveal who I am.\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to safeguarding the secrets of a " "timeless evil. An evil that quite obviously has now been released.\n" " \n" "The Archbishop Lazarus, once King Leoric's most trusted advisor, led a party " "of simple townsfolk into the Labyrinth to find the King's missing son, " "Albrecht. Quite some time passed before they returned, and only a few of " "them escaped with their lives.\n" " \n" "Curse me for a fool! I should have suspected his veiled treachery then. It " "must have been Lazarus himself who kidnapped Albrecht and has since hidden " "him within the Labyrinth. I do not understand why the Archbishop turned to " "the darkness, or what his interest is in the child, unless he means to " "sacrifice him to his dark masters!\n" " \n" "That must be what he has planned! The survivors of his 'rescue party' say " "that Lazarus was last seen running into the deepest bowels of the labyrinth. " "You must hurry and save the prince from the sacrificial blade of this " "demented fiend!" msgstr "" "Esto no augura nada bueno, ya que confirma mis temores más oscuros. Si bien " "no me permití creer las leyendas antiguas, no puedo negarlas ahora. Quizás " "ha llegado el momento de revelar quién soy.\n" " \n" "Mi verdadero nombre es Deckard Cain el Sabio, y soy el último descendiente " "de una antigua Hermandad que se dedicó a salvaguardar los secretos de un mal " "atemporal. Un mal que obviamente ahora se ha liberado.\n" " \n" "El arzobispo Lazarus, una vez el consejero más confiable del Rey Leoric, " "condujo a un grupo de simples habitantes del pueblo al Laberinto para " "encontrar al hijo desaparecido del Rey, Albrecht. Pasó bastante tiempo antes " "de que regresaran, y solo unos pocos escaparon con vida.\n" " \n" "¡Maldito sea por tonto! Entonces debería haber sospechado su velada " "traición. Debe haber sido el mismo Lazarus quien secuestró a Albrecht y " "desde entonces lo ha escondido dentro del Laberinto. No entiendo por qué el " "Arzobispo se volvió hacia la oscuridad, ni cuál es su interés en el niño. ¡a " "menos que tenga la intención de sacrificarlo a sus amos oscuros!\n" " \n" "¡Eso debe ser lo que ha planeado! Los sobrevivientes de su 'grupo de " "rescate' dicen que Lazarus fue visto por última vez corriendo hacia las " "entrañas más profundas del laberinto. ¡Debes darte prisa y salvar al " "príncipe de la espada sacrifical de este demonio demente!" #: Source/translation_dummy.cpp:708 msgid "" "You must hurry and rescue Albrecht from the hands of Lazarus. The prince and " "the people of this kingdom are counting on you!" msgstr "" "Debes darte prisa y rescatar a Albrecht de las manos de Lazarus. ¡El " "príncipe y la gente de este reino cuentan contigo!" #: Source/translation_dummy.cpp:709 msgid "" "Your story is quite grim, my friend. Lazarus will surely burn in Hell for " "his horrific deed. The boy that you describe is not our prince, but I " "believe that Albrecht may yet be in danger. The symbol of power that you " "speak of must be a portal in the very heart of the labyrinth.\n" " \n" "Know this, my friend - The evil that you move against is the dark Lord of " "Terror. He is known to mortal men as Diablo. It was he who was imprisoned " "within the Labyrinth many centuries ago and I fear that he seeks to once " "again sow chaos in the realm of mankind. You must venture through the portal " "and destroy Diablo before it is too late!" msgstr "" "Tu historia es bastante sombría, amigo. Lazarus seguramente arderá en el " "Infierno por su horrible acto. El chico que describe no es nuestro príncipe, " "pero creo que Albrecht aún puede estar en peligro. El símbolo de poder del " "que hablas debe ser un portal en el corazón mismo del laberinto.\n" " \n" "Debes saber esto, amigo mío: el mal contra el que te mueves es el oscuro " "Señor del Terror. Es conocido por los hombres mortales como Diablo. Fue él " "quien fue encarcelado dentro del Laberinto hace muchos siglos y me temo que " "busca sembrar una vez más el caos en el reino de la humanidad. ¡Debes " "aventurarte a través del portal y destruir a Diablo antes de que sea " "demasiado tarde!" #: Source/translation_dummy.cpp:710 msgid "" "Lazarus was the Archbishop who led many of the townspeople into the " "labyrinth. I lost many good friends that day, and Lazarus never returned. I " "suppose he was killed along with most of the others. If you would do me a " "favor, good master - please do not talk to Farnham about that day." msgstr "" "Lazarus fue el Arzobispo que condujo a muchos de los habitantes del pueblo " "al laberinto. Ese día perdí muchos buenos amigos y Lazarus nunca regresó. " "Supongo que lo mataron junto con la mayoría de los demás. Si quiere hacerme " "un favor, buen maestro, por favor no hable con Farnham sobre ese día." #: Source/translation_dummy.cpp:711 msgid "" "I was shocked when I heard of what the townspeople were planning to do that " "night. I thought that of all people, Lazarus would have had more sense than " "that. He was an Archbishop, and always seemed to care so much for the " "townsfolk of Tristram. So many were injured, I could not save them all..." msgstr "" "Me sorprendió cuando me enteré de lo que la gente del pueblo planeaba hacer " "esa noche. Pensé que, de todas las personas, Lazarus habría tenido más " "sentido común. Era Arzobispo y siempre pareció preocuparse mucho por la " "gente del pueblo de Tristram. Tantos resultaron heridos, no pude salvarlos a " "todos ..." #: Source/translation_dummy.cpp:712 msgid "" "I remember Lazarus as being a very kind and giving man. He spoke at my " "mother's funeral, and was supportive of my grandmother and myself in a very " "troubled time. I pray every night that somehow, he is still alive and safe." msgstr "" "Recuerdo a Lazarus como un hombre muy amable y generoso. Habló en el funeral " "de mi madre y nos apoyó a mi abuela y a mí en un momento muy difícil. Rezo " "todas las noches para que, de alguna manera, todavía esté vivo y a salvo." #: Source/translation_dummy.cpp:713 msgid "" "I was there when Lazarus led us into the labyrinth. He spoke of holy " "retribution, but when we started fighting those hellspawn, he did not so " "much as lift his mace against them. He just ran deeper into the dim, endless " "chambers that were filled with the servants of darkness!" msgstr "" "Estaba allí cuando Lazarus nos condujo al laberinto. Habló de la santa " "retribución, pero cuando empezamos a luchar contra esos engendros del " "infierno, ni siquiera levantó su maza contra ellos. ¡Simplemente corrió más " "profundamente en las oscuras e interminables cámaras que estaban llenas de " "los sirvientes de la oscuridad!" #: Source/translation_dummy.cpp:714 msgid "" "They stab, then bite, then they're all around you. Liar! LIAR! They're all " "dead! Dead! Do you hear me? They just keep falling and falling... their " "blood spilling out all over the floor... all his fault..." msgstr "" "Te apuñalan, luego muerden y luego te rodean. ¡Mentiroso! ¡MENTIROSO! ¡Están " "todos muertos! ¡Muertos! ¿Me escuchas? Siguen cayendo y cayendo ... su " "sangre se derrama por todo el suelo ... todo es culpa suya ..." #: Source/translation_dummy.cpp:715 msgid "" "I did not know this Lazarus of whom you speak, but I do sense a great " "conflict within his being. He poses a great danger, and will stop at nothing " "to serve the powers of darkness which have claimed him as theirs." msgstr "" "No conocía a este Lazarus de quien hablas, pero siento un gran conflicto " "dentro de su ser. Representa un gran peligro y no se detendrá ante nada para " "servir a los poderes de las tinieblas que lo han reclamado como suyo." #: Source/translation_dummy.cpp:716 msgid "" "Yes, the righteous Lazarus, who was sooo effective against those monsters " "down there. Didn't help save my leg, did it? Look, I'll give you a free " "piece of advice. Ask Farnham, he was there." msgstr "" "Sí, el justo Lazarus, que fue taaan eficaz contra esos monstruos de allí. No " "ayudó a salvar mi pierna, ¿verdad? Mira, te daré un consejo gratis. " "Pregúntale a Farnham, él estaba allí." #: Source/translation_dummy.cpp:717 msgid "" "Abandon your foolish quest. All that awaits you is the wrath of my Master! " "You are too late to save the child. Now you will join him in Hell!" msgstr "" "Abandona tu tonta búsqueda. ¡Todo lo que les espera es la ira de mi Maestro! " "Es demasiado tarde para salvar al niño. ¡Ahora te unirás a él en el Infierno!" #: Source/translation_dummy.cpp:718 msgid "" "Hmm, I don't know what I can really tell you about this that will be of any " "help. The water that fills our wells comes from an underground spring. I " "have heard of a tunnel that leads to a great lake - perhaps they are one and " "the same. Unfortunately, I do not know what would cause our water supply to " "be tainted." msgstr "" "Hmm, no sé qué puedo decirte realmente que sea de alguna ayuda. El agua que " "llena nuestros pozos proviene de un manantial subterráneo. He oído hablar de " "un túnel que conduce a un gran lago - tal vez sean lo mismo. " "Desafortunadamente, no sé qué pasaría si nuestro suministro de agua " "estuviera contaminado." #: Source/translation_dummy.cpp:719 msgid "" "I have always tried to keep a large supply of foodstuffs and drink in our " "storage cellar, but with the entire town having no source of fresh water, " "even our stores will soon run dry. \n" " \n" "Please, do what you can or I don't know what we will do." msgstr "" "Siempre he tratado de mantener una gran cantidad de alimentos y bebidas en " "nuestro sótano de almacenamiento, pero como todo el pueblo no tiene una " "fuente de agua dulce, incluso nuestras tiendas se secarán pronto. \n" " \n" "Por favor, haz lo que puedas o no sé qué haremos." #: Source/translation_dummy.cpp:720 msgid "" "I'm glad I caught up to you in time! Our wells have become brackish and " "stagnant and some of the townspeople have become ill drinking from them. Our " "reserves of fresh water are quickly running dry. I believe that there is a " "passage that leads to the springs that serve our town. Please find what has " "caused this calamity, or we all will surely perish." msgstr "" "¡Me alegro de haberte encontrado a tiempo! Nuestros pozos se han vuelto " "salobres, estancados y algunos de los habitantes del pueblo han enfermado al " "beber de ellos. Nuestras reservas de agua dulce se están secando " "rápidamente. Creo que hay un pasadizo que conduce a los manantiales que " "sirven a nuestro pueblo. Por favor, averigua qué ha causado esta calamidad, " "o seguramente todos moriremos." #: Source/translation_dummy.cpp:721 msgid "" "Please, you must hurry. Every hour that passes brings us closer to having no " "water to drink. \n" " \n" "We cannot survive for long without your help." msgstr "" "Por favor, debes darse prisa. Cada hora que pasa nos acerca a no tener agua " "para beber. \n" " \n" "No podemos sobrevivir por mucho tiempo sin tu ayuda." #: Source/translation_dummy.cpp:722 msgid "" "What's that you say - the mere presence of the demons had caused the water " "to become tainted? Oh, truly a great evil lurks beneath our town, but your " "perseverance and courage gives us hope. Please take this ring - perhaps it " "will aid you in the destruction of such vile creatures." msgstr "" "¿Qué dices? ¿La mera presencia de los demonios provocó que el agua se " "contaminara? Oh, verdaderamente un gran mal acecha debajo de nuestro pueblo, " "pero tu perseverancia y coraje nos dan esperanza. Por favor, toma este " "anillo, tal vez te ayude a destruir a esas criaturas tan viles." #: Source/translation_dummy.cpp:723 msgid "" "My grandmother is very weak, and Garda says that we cannot drink the water " "from the wells. Please, can you do something to help us?" msgstr "" "Mi abuela está muy débil y Garda dice que no podemos beber el agua de los " "pozos. Por favor, ¿puedes hacer algo para ayudarnos?" #: Source/translation_dummy.cpp:724 msgid "" "Pepin has told you the truth. We will need fresh water badly, and soon. I " "have tried to clear one of the smaller wells, but it reeks of stagnant " "filth. It must be getting clogged at the source." msgstr "" "Pepin te ha dicho la verdad. Necesitaremos agua dulce con urgencia, y " "pronto. He intentado limpiar uno de los pozos más pequeños, pero huele a " "suciedad estancada. Debe estar obstruido en la fuente." #: Source/translation_dummy.cpp:725 msgid "You drink water?" msgstr "¿Tu bebes agua?" #: Source/translation_dummy.cpp:726 msgid "" "The people of Tristram will die if you cannot restore fresh water to their " "wells. \n" " \n" "Know this - demons are at the heart of this matter, but they remain ignorant " "of what they have spawned." msgstr "" "La gente de Tristram morirá si no puedes devolver agua fresca a sus pozos. \n" " \n" "Sepa esto: los demonios están en el centro de este asunto, pero siguen " "ignorando lo que han engendrado." #: Source/translation_dummy.cpp:727 msgid "" "For once, I'm with you. My business runs dry - so to speak - if I have no " "market to sell to. You better find out what is going on, and soon!" msgstr "" "Por una vez, estoy contigo. Mi negocio se seca, por así decirlo, si no tengo " "un mercado al que vender. ¡Será mejor que averigüe lo que está pasando, y " "pronto!" #: Source/translation_dummy.cpp:728 msgid "" "A book that speaks of a chamber of human bones? Well, a Chamber of Bone is " "mentioned in certain archaic writings that I studied in the libraries of the " "East. These tomes inferred that when the Lords of the underworld desired to " "protect great treasures, they would create domains where those who died in " "the attempt to steal that treasure would be forever bound to defend it. A " "twisted, but strangely fitting, end?" msgstr "" "¿Un libro que habla de una cámara de huesos humanos? Bueno, una Cámara de " "Hueso se menciona en ciertos escritos arcaicos que estudié en las " "bibliotecas del Este. Estos tomos inferían que cuando los Señores del " "inframundo desearan proteger grandes tesoros, crearían dominios donde " "aquellos que murieran en el intento de robar ese tesoro estarían obligados a " "defenderlo para siempre. ¿Un final retorcido, pero extrañamente apropiado?" #: Source/translation_dummy.cpp:729 msgid "" "I am afraid that I don't know anything about that, good master. Cain has " "many books that may be of some help." msgstr "" "Me temo que no sé nada de eso, buen maestro. Caín tiene muchos libros que " "pueden ser de alguna ayuda." #: Source/translation_dummy.cpp:730 msgid "" "This sounds like a very dangerous place. If you venture there, please take " "great care." msgstr "Parece un lugar muy peligroso. Si te aventuras allí ten mucho cuidado." #: Source/translation_dummy.cpp:731 msgid "" "I am afraid that I haven't heard anything about that. Perhaps Cain the " "Storyteller could be of some help." msgstr "" "Me temo que no he escuchado nada al respecto. Quizás Caín el Narrador podría " "ser de alguna ayuda." #: Source/translation_dummy.cpp:732 msgid "" "I know nothing of this place, but you may try asking Cain. He talks about " "many things, and it would not surprise me if he had some answers to your " "question." msgstr "" "No sé nada de este lugar, pero puedes intentar preguntarle a Caín. Habla de " "muchas cosas y no me sorprendería que tuviera algunas respuestas a tu " "pregunta." #: Source/translation_dummy.cpp:733 msgid "" "Okay, so listen. There's this chamber of wood, see. And his wife, you know - " "her - tells the tree... cause you gotta wait. Then I says, that might work " "against him, but if you think I'm gonna PAY for this... you... uh... yeah." msgstr "" "Bien, escucha. Ahí está esta cámara de madera ¿ves? Y su esposa, ya sabes, " "ella, le dice al árbol ... porque tienes que esperar. Entonces digo, eso " "podría funcionar en su contra, pero si crees que voy a PAGAR por esto ... " "tú ... eh ... sí." #: Source/translation_dummy.cpp:734 msgid "" "You will become an eternal servant of the dark lords should you perish " "within this cursed domain. \n" " \n" "Enter the Chamber of Bone at your own peril." msgstr "" "Te convertirás en un sirviente eterno de los señores oscuros si pereces " "dentro de este dominio maldito. \n" " \n" "Ingresa a la Cámara de Hueso bajo tu propio riesgo." #: Source/translation_dummy.cpp:735 msgid "" "A vast and mysterious treasure, you say? Maybe I could be interested in " "picking up a few things from you... or better yet, don't you need some rare " "and expensive supplies to get you through this ordeal?" msgstr "" "¿Un tesoro vasto y misterioso, dices? Tal vez podría estar interesado en " "recoger algunas cosas de ti ... o mejor aún, ¿no necesita algunos " "suministros raros y costosos para superar este calvario?" #: Source/translation_dummy.cpp:736 msgid "" "It seems that the Archbishop Lazarus goaded many of the townsmen into " "venturing into the Labyrinth to find the King's missing son. He played upon " "their fears and whipped them into a frenzied mob. None of them were prepared " "for what lay within the cold earth... Lazarus abandoned them down there - " "left in the clutches of unspeakable horrors - to die." msgstr "" "Parece que el Arzobispo Lazarus incitó a muchos de los habitantes del pueblo " "a aventurarse en el Laberinto para encontrar al hijo desaparecido del Rey. " "Jugó con sus miedos y los convirtió en una multitud frenética. Ninguno de " "ellos estaba preparado para lo que había dentro de la tierra fría ... " "Lazarus los abandonó allí - dejados en las garras de indescriptibles " "horrores - para morir." #: Source/translation_dummy.cpp:737 msgid "" "Yes, Farnham has mumbled something about a hulking brute who wielded a " "fierce weapon. I believe he called him a butcher." msgstr "" "Sí, Farnham ha murmurado algo sobre un enorme bruto que empuñaba un arma " "feroz. Creo que lo llamó carnicero." #: Source/translation_dummy.cpp:738 msgid "" "By the Light, I know of this vile demon. There were many that bore the scars " "of his wrath upon their bodies when the few survivors of the charge led by " "Lazarus crawled from the Cathedral. I don't know what he used to slice open " "his victims, but it could not have been of this world. It left wounds " "festering with disease and even I found them almost impossible to treat. " "Beware if you plan to battle this fiend..." msgstr "" "Por la Luz, conozco a este vil demonio. Hubo muchos que llevaron las " "cicatrices de su ira en sus cuerpos cuando los pocos supervivientes de la " "carga encabezada por Lazarus salieron arrastrándose de la Catedral. No sé " "qué utilizó para cortar a sus víctimas, pero no pudo haber sido de este " "mundo. Dejó heridas infectadas por la enfermedad e incluso yo las encontré " "casi imposibles de tratar. Cuidado si planeas luchar contra este demonio ..." #: Source/translation_dummy.cpp:739 msgid "" "When Farnham said something about a butcher killing people, I immediately " "discounted it. But since you brought it up, maybe it is true." msgstr "" "Cuando Farnham dijo algo sobre un carnicero matando gente, inmediatamente lo " "descarté. Pero ya que lo mencionaste, tal vez sea cierto." #: Source/translation_dummy.cpp:740 msgid "" "I saw what Farnham calls the Butcher as it swathed a path through the bodies " "of my friends. He swung a cleaver as large as an axe, hewing limbs and " "cutting down brave men where they stood. I was separated from the fray by a " "host of small screeching demons and somehow found the stairway leading out. " "I never saw that hideous beast again, but his blood-stained visage haunts me " "to this day." msgstr "" "Vi lo que Farnham llama el Carnicero mientras se abría camino a través de " "los cuerpos de mis amigos. Blandió una cuchilla del tamaño de un hacha, " "cortando miembros y cortando a hombres valientes donde estaban. Me separaron " "de la refriega una multitud de pequeños demonios que chillaban y, de alguna " "manera, encontré la escalera que conducía hacia afuera. Nunca volví a ver a " "esa horrible bestia, pero su rostro manchado de sangre me persigue hasta el " "día de hoy." #: Source/translation_dummy.cpp:741 msgid "" "Big! Big cleaver killing all my friends. Couldn't stop him, had to run away, " "couldn't save them. Trapped in a room with so many bodies... so many " "friends... NOOOOOOOOOO!" msgstr "" "¡Grande! Una cuchilla grande matando a todos mis amigos. No pude detenerlo, " "tuve que huir, no pude salvarlos. Atrapado en una habitación con tantos " "cuerpos ... tantos amigos ... ¡NOOOOOOOOOO!" #: Source/translation_dummy.cpp:742 msgid "" "The Butcher is a sadistic creature that delights in the torture and pain of " "others. You have seen his handiwork in the drunkard Farnham. His destruction " "will do much to ensure the safety of this village." msgstr "" "El Carnicero es una criatura sádica que se deleita con la tortura y el dolor " "de los demás. Has visto su obra en el borracho Farnham. Su destrucción hará " "mucho para garantizar la seguridad de esta aldea." #: Source/translation_dummy.cpp:743 msgid "" "I know more than you'd think about that grisly fiend. His little friends got " "a hold of me and managed to get my leg before Griswold pulled me out of that " "hole. \n" " \n" "I'll put it bluntly - kill him before he kills you and adds your corpse to " "his collection." msgstr "" "Sé más de lo que piensas sobre ese demonio espantoso. Sus amiguitos me " "agarraron y lograron sacarme la pierna antes de que Griswold me sacara de " "ese agujero. \n" " \n" "Lo diré sin rodeos: mátalo antes de que te mate y agregue tu cadáver a su " "colección." #: Source/translation_dummy.cpp:744 msgid "" "Please, listen to me. The Archbishop Lazarus, he led us down here to find " "the lost prince. The bastard led us into a trap! Now everyone is dead... " "killed by a demon he called the Butcher. Avenge us! Find this Butcher and " "slay him so that our souls may finally rest..." msgstr "" "Por favor escúchame. El arzobispo Lazarus, nos llevó hasta aquí para " "encontrar al príncipe perdido. ¡El bastardo nos llevó a una trampa! Ahora " "todo el mundo está muerto ... asesinados por un demonio que llamó el " "Carnicero. ¡Vénganos! Encuentra a este Carnicero y mátalo para que nuestras " "almas finalmente descansen ..." #: Source/translation_dummy.cpp:745 msgid "" "You recite an interesting rhyme written in a style that reminds me of other " "works. Let me think now - what was it?\n" " \n" "...Darkness shrouds the Hidden. Eyes glowing unseen with only the sounds of " "razor claws briefly scraping to torment those poor souls who have been made " "sightless for all eternity. The prison for those so damned is named the " "Halls of the Blind..." msgstr "" "Recitas una rima interesante escrita en un estilo que me recuerda a otras " "obras. Déjame pensar ahora, ¿qué fue?\n" " \n" "... La oscuridad envuelve lo Oculto. Ojos que brillan sin ser vistos con " "solo el sonido de las garras como navajas raspando brevemente para " "atormentar a esas pobres almas que han quedado ciegas por toda la eternidad. " "La prisión para los condenados se llama las Cámaras de los Ciegos ..." #: Source/translation_dummy.cpp:746 msgid "" "I never much cared for poetry. Occasionally, I had cause to hire minstrels " "when the inn was doing well, but that seems like such a long time ago now. \n" " \n" "What? Oh, yes... uh, well, I suppose you could see what someone else knows." msgstr "" "Nunca me interesó mucho la poesía. De vez en cuando, tenía motivos para " "contratar juglares cuando la posada iba bien, pero eso parece que fue hace " "mucho tiempo. \n" " \n" "¿Qué? Oh, sí ... eh, bueno, supongo que podrías ver lo que alguien más sabe." #: Source/translation_dummy.cpp:747 msgid "" "This does seem familiar, somehow. I seem to recall reading something very " "much like that poem while researching the history of demonic afflictions. It " "spoke of a place of great evil that... wait - you're not going there are you?" msgstr "" "Esto parece familiar, de alguna manera. Me parece recordar haber leído algo " "muy parecido a ese poema mientras investigaba la historia de las aflicciones " "demoníacas. Hablaba de un lugar de gran maldad que ... espera, no vas a ir " "allí, ¿verdad?" #: Source/translation_dummy.cpp:748 msgid "" "If you have questions about blindness, you should talk to Pepin. I know that " "he gave my grandmother a potion that helped clear her vision, so maybe he " "can help you, too." msgstr "" "Si tienes preguntas sobre la ceguera, debes hablar con Pepin. Sé que le dio " "a mi abuela una poción que ayudó a aclarar su visión, así que tal vez él " "también pueda ayudarte." #: Source/translation_dummy.cpp:749 msgid "" "I am afraid that I have neither heard nor seen a place that matches your " "vivid description, my friend. Perhaps Cain the Storyteller could be of some " "help." msgstr "" "Me temo que no he escuchado ni visto un lugar que coincida con su vívida " "descripción, amigo mío. Quizás Caín el Narrador podría ser de alguna ayuda." #: Source/translation_dummy.cpp:750 msgid "Look here... that's pretty funny, huh? Get it? Blind - look here?" msgstr "" "Mira aquí ... eso es muy gracioso, ¿eh? ¿Conseguirlo? Ciego, ¿mira aquí?" #: Source/translation_dummy.cpp:751 msgid "" "This is a place of great anguish and terror, and so serves its master " "well. \n" " \n" "Tread carefully or you may yourself be staying much longer than you had " "anticipated." msgstr "" "Este es un lugar de gran angustia y terror, y por eso sirve bien a su amo. \n" " \n" "Pisa con cuidado o puede que tu mismo te quedes mucho más tiempo de lo que " "habías anticipado." #: Source/translation_dummy.cpp:752 msgid "" "Lets see, am I selling you something? No. Are you giving me money to tell " "you about this? No. Are you now leaving and going to talk to the storyteller " "who lives for this kind of thing? Yes." msgstr "" "Veamos, ¿te estoy vendiendo algo? No. ¿Me estás dando dinero para contarte " "esto? No. ¿Ahora te vas y vas a hablar con el narrador que vive para este " "tipo de cosas? Si." #: Source/translation_dummy.cpp:753 msgid "" "You claim to have spoken with Lachdanan? He was a great hero during his " "life. Lachdanan was an honorable and just man who served his King faithfully " "for years. But of course, you already know that.\n" " \n" "Of those who were caught within the grasp of the King's Curse, Lachdanan " "would be the least likely to submit to the darkness without a fight, so I " "suppose that your story could be true. If I were in your place, my friend, I " "would find a way to release him from his torture." msgstr "" "¿Afirmas haber hablado con Lachdanan? Fue un gran héroe durante su vida. " "Lachdanan fue un hombre honorable y justo que sirvió fielmente a su Rey " "durante años. Pero claro, eso ya lo sabes.\n" " \n" "De aquellos que quedaron atrapados en las garras de la Maldición del Rey, " "Lachdanan sería el menos propenso a someterse a la oscuridad sin luchar, así " "que supongo que tu historia podría ser cierta. Si estuviera en tu lugar, " "amigo mío, encontraría la manera de liberarlo de su tortura." #: Source/translation_dummy.cpp:754 msgid "" "You speak of a brave warrior long dead! I'll have no such talk of speaking " "with departed souls in my inn yard, thank you very much." msgstr "" "¡Hablas de un valiente guerrero muerto hace mucho tiempo! No diré nada de " "hablar con los difuntos en el patio de mi posada, muchas gracias." #: Source/translation_dummy.cpp:755 msgid "" "A golden elixir, you say. I have never concocted a potion of that color " "before, so I can't tell you how it would effect you if you were to try to " "drink it. As your healer, I strongly advise that should you find such an " "elixir, do as Lachdanan asks and DO NOT try to use it." msgstr "" "Un elixir dorado, dices. Nunca antes había preparado una poción de ese " "color, así que no puedo decirte cómo te afectaría si intentaras beberla. " "Como su sanador, le recomiendo encarecidamente que, si encuentra un elixir " "de este tipo, haga lo que le pide Lachdanan y NO trate de usarlo." #: Source/translation_dummy.cpp:756 msgid "" "I've never heard of a Lachdanan before. I'm sorry, but I don't think that I " "can be of much help to you." msgstr "" "Nunca antes había oído hablar de un Lachdanan. Lo siento, pero no creo que " "pueda ser de mucha ayuda." #: Source/translation_dummy.cpp:757 msgid "" "If it is actually Lachdanan that you have met, then I would advise that you " "aid him. I dealt with him on several occasions and found him to be honest " "and loyal in nature. The curse that fell upon the followers of King Leoric " "would fall especially hard upon him." msgstr "" "Si realmente es Lachdanan a quien encontraste, te aconsejo que lo ayudes. " "Traté con él en varias ocasiones y descubrí que era honesto y leal por " "naturaleza. La maldición que cayó sobre los seguidores del Rey Leoric caería " "especialmente sobre él." #: Source/translation_dummy.cpp:758 msgid "" " Lachdanan is dead. Everybody knows that, and you can't fool me into " "thinking any other way. You can't talk to the dead. I know!" msgstr "" " Lachdanan está muerto. Todo el mundo lo sabe, y no puedes engañarme para " "que piense de otra manera. No puedes hablar con los muertos. ¡Lo sé!" #: Source/translation_dummy.cpp:759 msgid "" "You may meet people who are trapped within the Labyrinth, such as " "Lachdanan. \n" " \n" "I sense in him honor and great guilt. Aid him, and you aid all of Tristram." msgstr "" "Es posible que encuentres a personas atrapadas dentro del Laberinto, como " "Lachdanan. \n" " \n" "Siento en él honor y una gran culpa. Ayúdale y ayudarás a todo Tristram." #: Source/translation_dummy.cpp:760 msgid "" "Wait, let me guess. Cain was swallowed up in a gigantic fissure that opened " "beneath him. He was incinerated in a ball of hellfire, and can't answer your " "questions anymore. Oh, that isn't what happened? Then I guess you'll be " "buying something or you'll be on your way." msgstr "" "Espera, déjame adivinar. Cain fue tragado por una gigantesca fisura que se " "abrió debajo de él. Fue incinerado en una bola de fuego del infierno y ya no " "puede responder a tus preguntas. Oh, ¿no es eso lo que pasó? Entonces " "supongo que comprarás algo o seguirás tu camino." #: Source/translation_dummy.cpp:761 msgid "" "Please, don't kill me, just hear me out. I was once Captain of King Leoric's " "Knights, upholding the laws of this land with justice and honor. Then his " "dark Curse fell upon us for the role we played in his tragic death. As my " "fellow Knights succumbed to their twisted fate, I fled from the King's " "burial chamber, searching for some way to free myself from the Curse. I " "failed...\n" " \n" "I have heard of a Golden Elixir that could lift the Curse and allow my soul " "to rest, but I have been unable to find it. My strength now wanes, and with " "it the last of my humanity as well. Please aid me and find the Elixir. I " "will repay your efforts - I swear upon my honor." msgstr "" "Por favor, no me mates, solo escúchame. Una vez fui Capitán de los " "Caballeros del Rey Leoric, defendiendo las leyes de esta tierra con justicia " "y honor. Entonces su oscura Maldición cayó sobre nosotros por el papel que " "jugamos en su trágica muerte. Mientras mis compañeros Caballeros sucumbían a " "su retorcido destino, huí de la cámara funeraria del Rey, buscando alguna " "forma de liberarme de la Maldición. Fallé...\n" " \n" "He oído hablar de un Elixir Dorado que podría levantar la Maldición y " "permitir que mi alma descanse, pero no he podido encontrarlo. Mi fuerza " "ahora se desvanece, y con ella también lo último de mi humanidad. Por favor " "ayúdame y encuentra el Elixir. Te pagaré tus esfuerzos, lo juro por mi honor." #: Source/translation_dummy.cpp:762 msgid "" "You have not found the Golden Elixir. I fear that I am doomed for eternity. " "Please, keep trying..." msgstr "" "No has encontrado el Elixir Dorado. Temo estar condenado por la eternidad. " "Por favor, sigue intentándolo ..." #: Source/translation_dummy.cpp:763 msgid "" "You have saved my soul from damnation, and for that I am in your debt. If " "there is ever a way that I can repay you from beyond the grave I will find " "it, but for now - take my helm. On the journey I am about to take I will " "have little use for it. May it protect you against the dark powers below. Go " "with the Light, my friend..." msgstr "" "Has salvado mi alma de la condenación, y por eso estoy en deuda contigo. Si " "alguna vez hay una forma de recompensarte desde más allá de la tumba, la " "encontraré, pero por ahora, toma mi yelmo. En el viaje que estoy a punto de " "emprender, lo utilizaré poco. Que te proteja contra los poderes oscuros de " "abajo. Ve con la Luz, amigo mío ..." #: Source/translation_dummy.cpp:764 msgid "" "Griswold speaks of The Anvil of Fury - a legendary artifact long searched " "for, but never found. Crafted from the metallic bones of the Razor Pit " "demons, the Anvil of Fury was smelt around the skulls of the five most " "powerful magi of the underworld. Carved with runes of power and chaos, any " "weapon or armor forged upon this Anvil will be immersed into the realm of " "Chaos, imbedding it with magical properties. It is said that the " "unpredictable nature of Chaos makes it difficult to know what the outcome of " "this smithing will be..." msgstr "" "Griswold habla de El yunque de la furia, un artefacto legendario buscado " "durante mucho tiempo, pero nunca encontrado. Elaborado a partir de los " "huesos metálicos de los demonios del Pozo de la Navaja, el Yunque de la " "Furia se fundió alrededor de los cráneos de los cinco magos más poderosos " "del inframundo. Tallado con runas de poder y caos, cualquier arma o armadura " "forjada en este Yunque se sumergirá en el reino del Caos, dándole " "propiedades mágicas. Se dice que la naturaleza impredecible del Caos " "dificulta saber cuál será el resultado de esta herrería ..." #: Source/translation_dummy.cpp:765 msgid "" "Don't you think that Griswold would be a better person to ask about this? " "He's quite handy, you know." msgstr "" "¿No crees que Griswold sería una mejor persona para preguntarle sobre esto? " "Es bastante hábil, ¿sabes?." #: Source/translation_dummy.cpp:766 msgid "" "If you had been looking for information on the Pestle of Curing or the " "Silver Chalice of Purification, I could have assisted you, my friend. " "However, in this matter, you would be better served to speak to either " "Griswold or Cain." msgstr "" "Si hubiera estado buscando información sobre el Mortero de Curación o el " "Cáliz de Plata de la Purificación, podría haberte ayudado, amigo mío. Sin " "embargo, en este asunto, sería mejor que hablaras con Griswold o Caín." #: Source/translation_dummy.cpp:767 msgid "" "Griswold's father used to tell some of us when we were growing up about a " "giant anvil that was used to make mighty weapons. He said that when a hammer " "was struck upon this anvil, the ground would shake with a great fury. " "Whenever the earth moves, I always remember that story." msgstr "" "El padre de Griswold solía contarnos a algunos de nosotros, cuando éramos " "pequeños, acerca de un yunque gigante que se usaba para fabricar armas " "poderosas. Dijo que cuando se golpeaba un martillo en este yunque, el suelo " "temblaba con gran furia. Cada vez que la tierra se mueve, siempre recuerdo " "esa historia." #: Source/translation_dummy.cpp:768 msgid "" "Greetings! It's always a pleasure to see one of my best customers! I know " "that you have been venturing deeper into the Labyrinth, and there is a story " "I was told that you may find worth the time to listen to...\n" " \n" "One of the men who returned from the Labyrinth told me about a mystic anvil " "that he came across during his escape. His description reminded me of " "legends I had heard in my youth about the burning Hellforge where powerful " "weapons of magic are crafted. The legend had it that deep within the " "Hellforge rested the Anvil of Fury! This Anvil contained within it the very " "essence of the demonic underworld...\n" " \n" "It is said that any weapon crafted upon the burning Anvil is imbued with " "great power. If this anvil is indeed the Anvil of Fury, I may be able to " "make you a weapon capable of defeating even the darkest lord of Hell! \n" " \n" "Find the Anvil for me, and I'll get to work!" msgstr "" "¡Saludos! ¡Siempre es un placer ver a uno de mis mejores clientes! Sé que te " "has estado aventurando más profundamente en el Laberinto, y hay una historia " "que me contaron que puede que valga la pena escucharla ...\n" " \n" "Uno de los hombres que regresó del Laberinto me habló de un yunque místico " "que encontró durante su fuga. Su descripción me recordó las leyendas que " "había escuchado en mi juventud sobre la ardiente Forja Infernal donde se " "fabrican poderosas armas mágicas. ¡La leyenda decía que en lo profundo de la " "Forja Infernal descansaba el Yunque de la Furia! Este Yunque contenía la " "esencia misma del inframundo demoníaco ...\n" " \n" "Se dice que cualquier arma fabricada sobre el Yunque en llamas está imbuida " "de un gran poder. Si este yunque es de hecho el Yunque de la Furia, ¡podría " "crearte en un arma capaz de derrotar incluso al señor más oscuro del " "infierno! \n" " \n" "¡Encuentra el Yunque para mí y me pondré manos a la obra!" #: Source/translation_dummy.cpp:769 msgid "" "Nothing yet, eh? Well, keep searching. A weapon forged upon the Anvil could " "be your best hope, and I am sure that I can make you one of legendary " "proportions." msgstr "" "Nada todavía, ¿eh? Bueno, sigue buscando. Un arma forjada en el Yunque " "podría ser tu mejor esperanza, y estoy seguro de que puedo crearte una de " "proporciones legendarias." #: Source/translation_dummy.cpp:770 msgid "" "I can hardly believe it! This is the Anvil of Fury - good work, my friend. " "Now we'll show those bastards that there are no weapons in Hell more deadly " "than those made by men! Take this and may Light protect you." msgstr "" "¡Casi no puedo creerlo! Este es el Yunque de la Furia. Buen trabajo, amigo. " "¡Ahora les mostraremos a esos bastardos que no hay armas en el infierno más " "mortíferas que las fabricadas por los hombres! Toma esto y que la Luz te " "proteja." #: Source/translation_dummy.cpp:771 msgid "" "Griswold can't sell his anvil. What will he do then? And I'd be angry too if " "someone took my anvil!" msgstr "" "Griswold no puede vender su yunque. ¿Qué hará entonces? ¡Y también me " "enojaría si alguien me quitara el yunque!" #: Source/translation_dummy.cpp:772 msgid "" "There are many artifacts within the Labyrinth that hold powers beyond the " "comprehension of mortals. Some of these hold fantastic power that can be " "used by either the Light or the Darkness. Securing the Anvil from below " "could shift the course of the Sin War towards the Light." msgstr "" "Hay muchos artefactos dentro del Laberinto que tienen poderes más allá de la " "comprensión de los mortales. Algunos de estos tienen un poder fantástico que " "puede ser utilizado tanto por la Luz como por la Oscuridad. Asegurar el " "Yunque desde abajo podría cambiar el curso de la Guerra del Pecado hacia la " "Luz." #: Source/translation_dummy.cpp:773 msgid "" "If you were to find this artifact for Griswold, it could put a serious " "damper on my business here. Awwww, you'll never find it." msgstr "" "Si encuentras este artefacto para Griswold, podrías poner en serios " "problemas a mi negocio. Awwww, nunca lo encontrarás." #: Source/translation_dummy.cpp:774 msgid "" "The Gateway of Blood and the Halls of Fire are landmarks of mystic origin. " "Wherever this book you read from resides it is surely a place of great " "power.\n" " \n" "Legends speak of a pedestal that is carved from obsidian stone and has a " "pool of boiling blood atop its bone encrusted surface. There are also " "allusions to Stones of Blood that will open a door that guards an ancient " "treasure...\n" " \n" "The nature of this treasure is shrouded in speculation, my friend, but it is " "said that the ancient hero Arkaine placed the holy armor Valor in a secret " "vault. Arkaine was the first mortal to turn the tide of the Sin War and " "chase the legions of darkness back to the Burning Hells.\n" " \n" "Just before Arkaine died, his armor was hidden away in a secret vault. It is " "said that when this holy armor is again needed, a hero will arise to don " "Valor once more. Perhaps you are that hero..." msgstr "" "La Puerta de la Sangre y las Cámaras del Fuego son hitos de origen místico. " "Dondequiera que resida este libro que lea, seguramente es un lugar de gran " "poder.\n" " \n" "Las leyendas hablan de un pedestal que está tallado en piedra de obsidiana y " "tiene un charco de sangre hirviendo sobre su superficie incrustada de " "huesos. También hay alusiones a unas Piedras de sangre que abrirán una " "puerta que guarda un antiguo tesoro ...\n" " \n" "La naturaleza de este tesoro está envuelta en especulaciones, amigo mío, " "pero se dice que el antiguo héroe Arkaine colocó la armadura sagrada Valor " "en una bóveda secreta. Arkaine fue el primer mortal en cambiar el rumbo de " "la Guerra del Pecado y perseguir a las legiones de la oscuridad de regreso a " "los Infiernos Ardientes.\n" " \n" "Justo antes de que Arkaine muriera, su armadura estaba escondida en una " "bóveda secreta. Se dice que cuando se necesite de nuevo esta armadura " "sagrada, un héroe se levantará para don Valor una vez más. Quizás eres ese " "héroe ..." #: Source/translation_dummy.cpp:775 msgid "" "Every child hears the story of the warrior Arkaine and his mystic armor " "known as Valor. If you could find its resting place, you would be well " "protected against the evil in the Labyrinth." msgstr "" "Todos los niños escuchan la historia del guerrero Arkaine y su armadura " "mística conocida como Valor. Si pudieras encontrar su lugar de descanso, " "estarías bien protegido contra el mal en el Laberinto." #: Source/translation_dummy.cpp:776 msgid "" "Hmm... it sounds like something I should remember, but I've been so busy " "learning new cures and creating better elixirs that I must have forgotten. " "Sorry..." msgstr "" "Hmm ... suena como algo que debería recordar, pero he estado tan ocupado " "aprendiendo nuevas curas y creando mejores elixires que debí haberlo " "olvidado. Lo siento ..." #: Source/translation_dummy.cpp:777 msgid "" "The story of the magic armor called Valor is something I often heard the " "boys talk about. You had better ask one of the men in the village." msgstr "" "La historia de la armadura mágica llamada Valor es algo de lo que a menudo " "escuché decir a los chicos. Será mejor que pregunte a uno de los hombres del " "pueblo." #: Source/translation_dummy.cpp:778 msgid "" "The armor known as Valor could be what tips the scales in your favor. I will " "tell you that many have looked for it - including myself. Arkaine hid it " "well, my friend, and it will take more than a bit of luck to unlock the " "secrets that have kept it concealed oh, lo these many years." msgstr "" "La armadura conocida como Valor podría ser la que incline la balanza a tu " "favor. Les diré que muchos lo han buscado, incluyéndome a mí. Arkaine lo " "escondió bien, amigo mío, y se necesitará más que un poco de suerte para " "descubrir los secretos que lo han mantenido oculto, oh, he aquí hace muchos " "años." #: Source/translation_dummy.cpp:779 msgid "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..." msgstr "Zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz ..." #: Source/translation_dummy.cpp:780 msgid "" "Should you find these Stones of Blood, use them carefully. \n" " \n" "The way is fraught with danger and your only hope rests within your self " "trust." msgstr "" "Si encuentras estas Piedras de Sangre, úsalas con cuidado. \n" " \n" "El camino está plagado de peligros y tu única esperanza reside en la " "confianza en ti mismo." #: Source/translation_dummy.cpp:781 msgid "" "You intend to find the armor known as Valor? \n" " \n" "No one has ever figured out where Arkaine stashed the stuff, and if my " "contacts couldn't find it, I seriously doubt you ever will either." msgstr "" "¿Tienes la intención de encontrar la armadura conocida como Valor? \n" " \n" "Nadie ha descubierto nunca dónde escondió Arkaine las cosas, y si mis " "contactos no pudieron encontrarlo, dudo seriamente que tú tampoco lo hagas." #: Source/translation_dummy.cpp:782 msgid "" "I know of only one legend that speaks of such a warrior as you describe. His " "story is found within the ancient chronicles of the Sin War...\n" " \n" "Stained by a thousand years of war, blood and death, the Warlord of Blood " "stands upon a mountain of his tattered victims. His dark blade screams a " "black curse to the living; a tortured invitation to any who would stand " "before this Executioner of Hell.\n" " \n" "It is also written that although he was once a mortal who fought beside the " "Legion of Darkness during the Sin War, he lost his humanity to his " "insatiable hunger for blood." msgstr "" "Solo conozco una leyenda que habla de un guerrero como el que usted " "describe. Su historia se encuentra dentro de las antiguas crónicas de la " "Guerra del Pecado ...\n" " \n" "Manchado por mil años de guerra, sangre y muerte, el Señor de la Guerra de " "la Sangre se alza sobre una montaña de sus destrozadas víctimas. Su espada " "oscura grita una maldición negra a los vivos; una torturada invitación a " "cualquiera que se presente ante este Verdugo del Infierno.\n" " \n" "También está escrito que, aunque una vez fue un mortal que luchó junto a la " "Legión de la Oscuridad durante la Guerra del Pecado, perdió su humanidad " "debido a su insaciable hambre de sangre." #: Source/translation_dummy.cpp:783 msgid "" "I am afraid that I haven't heard anything about such a vicious warrior, good " "master. I hope that you do not have to fight him, for he sounds extremely " "dangerous." msgstr "" "Me temo que no he oído nada sobre un guerrero tan despiadado, buen maestro. " "Espero que no tengas que pelear con él, porque parece extremadamente " "peligroso." #: Source/translation_dummy.cpp:784 msgid "" "Cain would be able to tell you much more about something like this than I " "would ever wish to know." msgstr "" "Cain podría contarte mucho más sobre algo como esto de lo que yo jamás " "desearía saber." #: Source/translation_dummy.cpp:785 msgid "" "If you are to battle such a fierce opponent, may Light be your guide and " "your defender. I will keep you in my thoughts." msgstr "" "Si vas a luchar contra un oponente tan feroz, que la Luz sea tu guía y tu " "defensora. Te llevaré en mis pensamientos." #: Source/translation_dummy.cpp:786 msgid "" "Dark and wicked legends surrounds the one Warlord of Blood. Be well " "prepared, my friend, for he shows no mercy or quarter." msgstr "" "Leyendas oscuras y malvadas rodean al único Señor de la Guerra de la Sangre. " "Debes estar bien preparado, amigo mío, porque no muestra piedad ni cuartel." #: Source/translation_dummy.cpp:787 msgid "" "Always you gotta talk about Blood? What about flowers, and sunshine, and " "that pretty girl that brings the drinks. Listen here, friend - you're " "obsessive, you know that?" msgstr "" "¿Siempre tienes que hablar de Sangre? ¿Qué pasa con las flores, el sol y esa " "chica bonita que trae las bebidas? Escucha, amigo, eres obsesivo, ¿lo sabías?" #: Source/translation_dummy.cpp:788 msgid "" "His prowess with the blade is awesome, and he has lived for thousands of " "years knowing only warfare. I am sorry... I can not see if you will defeat " "him." msgstr "" "Su destreza con la espada es asombrosa, y ha vivido durante miles de años " "conociendo solo la guerra. Lo siento ... no puedo ver si lo derrotarás." #: Source/translation_dummy.cpp:789 msgid "" "I haven't ever dealt with this Warlord you speak of, but he sounds like he's " "going through a lot of swords. Wouldn't mind supplying his armies..." msgstr "" "Nunca he tratado con este Señor de la Guerra del que hablas, pero parece que " "necesite muchas espadas. No me importaría abastecer a sus ejércitos ..." #: Source/translation_dummy.cpp:790 msgid "" "My blade sings for your blood, mortal, and by my dark masters it shall not " "be denied." msgstr "" "Mi espada canta por tu sangre, mortal, y mis oscuros maestros no la negarán." #: Source/translation_dummy.cpp:791 msgid "" "Griswold speaks of the Heaven Stone that was destined for the enclave " "located in the east. It was being taken there for further study. This stone " "glowed with an energy that somehow granted vision beyond that which a normal " "man could possess. I do not know what secrets it holds, my friend, but " "finding this stone would certainly prove most valuable." msgstr "" "Griswold habla de la Piedra del Cielo que estaba destinada al enclave " "ubicado en el este. Se estaba llevando allí para estudiarla más a fondo. " "Esta piedra brillaba con una energía que de alguna manera otorgaba una " "visión más allá de la que un hombre normal podría poseer. No sé qué secretos " "encierra, amigo mío, pero encontrar esta piedra sin duda resultaría de lo " "más valioso." #: Source/translation_dummy.cpp:792 msgid "" "The caravan stopped here to take on some supplies for their journey to the " "east. I sold them quite an array of fresh fruits and some excellent " "sweetbreads that Garda has just finished baking. Shame what happened to " "them..." msgstr "" "La caravana se detuvo aquí para hacerse con algunos suministros para su " "viaje hacia el este. Les vendí una gran variedad de frutas frescas y " "excelentes mollejas que Garda acababa de hornear. Es una lástima lo que les " "pasó ..." #: Source/translation_dummy.cpp:793 msgid "" "I don't know what it is that they thought they could see with that rock, but " "I will say this. If rocks are falling from the sky, you had better be " "careful!" msgstr "" "No sé qué es lo que pensaron que podían ver con esa piedra, pero diré esto: " "Si caen rocas del cielo, ¡es mejor que tengas cuidado!" #: Source/translation_dummy.cpp:794 msgid "" "Well, a caravan of some very important people did stop here, but that was " "quite a while ago. They had strange accents and were starting on a long " "journey, as I recall. \n" " \n" "I don't see how you could hope to find anything that they would have been " "carrying." msgstr "" "Bueno, una caravana de personas muy importantes se detuvo aquí, pero eso fue " "hace bastante tiempo. Tenían acentos extraños y estaban iniciando un largo " "viaje, según recuerdo. \n" " \n" "No veo cómo podrías esperar encontrar algo que hubieran estado cargando." #: Source/translation_dummy.cpp:795 msgid "" "Stay for a moment - I have a story you might find interesting. A caravan " "that was bound for the eastern kingdoms passed through here some time ago. " "It was supposedly carrying a piece of the heavens that had fallen to earth! " "The caravan was ambushed by cloaked riders just north of here along the " "roadway. I searched the wreckage for this sky rock, but it was nowhere to be " "found. If you should find it, I believe that I can fashion something useful " "from it." msgstr "" "Quédate un momento, tengo una historia que puede resultarte interesante. Una " "caravana que se dirigía a los reinos del este pasó por aquí hace algún " "tiempo. ¡Supuestamente llevaba un pedazo de los cielos que había caído a la " "tierra! La caravana fue emboscada por jinetes encapuchados justo al norte de " "aquí a lo largo de la carretera. Busqué entre los escombros esta roca " "celeste, pero no la encontré por ningún lado. Si la encuentra, creo que " "puedo crear algo útil a partir de ella." #: Source/translation_dummy.cpp:796 msgid "" "I am still waiting for you to bring me that stone from the heavens. I know " "that I can make something powerful out of it." msgstr "" "Todavía estoy esperando que me traigas esa piedra del cielo. Sé que puedo " "hacer algo poderoso con eso." #: Source/translation_dummy.cpp:797 msgid "" "Let me see that - aye... aye, it is as I believed. Give me a moment...\n" " \n" "Ah, Here you are. I arranged pieces of the stone within a silver ring that " "my father left me. I hope it serves you well." msgstr "" "Déjame ver eso - sí ... sí, es como yo creía. Dame un momento...\n" " \n" "Ah, aquí tienes. Acomodé pedazos de la piedra dentro de un anillo de plata " "que me dejó mi padre. Espero que te sirva bien." #: Source/translation_dummy.cpp:798 msgid "" "I used to have a nice ring; it was a really expensive one, with blue and " "green and red and silver. Don't remember what happened to it, though. I " "really miss that ring..." msgstr "" "Solía tener un bonito anillo; era muy caro, con azul, verde, rojo y " "plateado. Sin embargo, no recuerdo qué le pasó. Realmente extraño ese " "anillo ..." #: Source/translation_dummy.cpp:799 msgid "" "The Heaven Stone is very powerful, and were it any but Griswold who bid you " "find it, I would prevent it. He will harness its powers and its use will be " "for the good of us all." msgstr "" "La Piedra del Cielo es muy poderosa, y si alguien que no sea Griswold te " "pidiera que la encontraras, yo lo evitaría. El aprovechará sus poderes y su " "uso será para el bien de todos nosotros." #: Source/translation_dummy.cpp:800 msgid "" "If anyone can make something out of that rock, Griswold can. He knows what " "he is doing, and as much as I try to steal his customers, I respect the " "quality of his work." msgstr "" "Si alguien puede hacer algo con esa roca, ese es Griswold. Él sabe lo que " "hace y, por mucho que trato de robar a sus clientes, respeto la calidad de " "su trabajo." #: Source/translation_dummy.cpp:801 msgid "" "The witch Adria seeks a black mushroom? I know as much about Black Mushrooms " "as I do about Red Herrings. Perhaps Pepin the Healer could tell you more, " "but this is something that cannot be found in any of my stories or books." msgstr "" "¿La bruja Adria busca un hongo negro? Sé tanto sobre Hongos Negros como " "sobre Arenques Rojos. Quizás Pepin el Curandero podría contarte más, pero " "esto es algo que no se puede encontrar en ninguna de mis historias o libros." #: Source/translation_dummy.cpp:802 msgid "" "Let me just say this. Both Garda and I would never, EVER serve black " "mushrooms to our honored guests. If Adria wants some mushrooms in her stew, " "then that is her business, but I can't help you find any. Black mushrooms... " "disgusting!" msgstr "" "Déjame decirte esto. Tanto Garda como yo nunca, NUNCA serviríamos hongos " "negros a nuestros invitados de honor. Si Adria quiere hongos en su estofado, " "entonces es asunto suyo, pero no puedo ayudarte a encontrar ninguno. Hongos " "negros ... ¡repugnantes!" #: Source/translation_dummy.cpp:803 msgid "" "The witch told me that you were searching for the brain of a demon to assist " "me in creating my elixir. It should be of great value to the many who are " "injured by those foul beasts, if I can just unlock the secrets I suspect " "that its alchemy holds. If you can remove the brain of a demon when you kill " "it, I would be grateful if you could bring it to me." msgstr "" "La bruja me dijo que estabas buscando el cerebro de un demonio para ayudarme " "a crear mi elixir. Debería ser de gran valor para los muchos que resultan " "heridos por esas horribles bestias, si pudiera descubrir los secretos que " "sospecho guarda su alquimia. Si puedes quitarle el cerebro a un demonio " "cuando lo mates, te agradecería que me lo trajeras." #: Source/translation_dummy.cpp:804 msgid "" "Excellent, this is just what I had in mind. I was able to finish the elixir " "without this, but it can't hurt to have this to study. Would you please " "carry this to the witch? I believe that she is expecting it." msgstr "" "Excelente, esto es justo lo que tenía en mente. Pude terminar el elixir sin " "esto, pero no está de más tener esto para estudiar. ¿Podrías llevarle esto a " "la bruja? Creo que lo está esperando." #: Source/translation_dummy.cpp:805 msgid "" "I think Ogden might have some mushrooms in the storage cellar. Why don't you " "ask him?" msgstr "" "Creo que Ogden podría tener algunos hongos en el sótano de almacenamiento. " "¿Por qué no le preguntas?" #: Source/translation_dummy.cpp:806 msgid "" "If Adria doesn't have one of these, you can bet that's a rare thing indeed. " "I can offer you no more help than that, but it sounds like... a huge, " "gargantuan, swollen, bloated mushroom! Well, good hunting, I suppose." msgstr "" "Si Adria no tiene uno de estos, puedes apostar que es algo raro. No puedo " "ofrecerte más ayuda que esa, pero suena como ... ¡un hongo enorme, " "gigantesco, hinchado y desmesurado! Bueno, buena caza, supongo." #: Source/translation_dummy.cpp:807 msgid "" "Ogden mixes a MEAN black mushroom, but I get sick if I drink that. Listen, " "listen... here's the secret - moderation is the key!" msgstr "" "Ogden mezcla un INFAME hongo negro, pero me enfermaré si bebo eso. Escucha, " "escucha ... aquí está el secreto: ¡La moderación es la clave!" #: Source/translation_dummy.cpp:808 msgid "" "What do we have here? Interesting, it looks like a book of reagents. Keep " "your eyes open for a black mushroom. It should be fairly large and easy to " "identify. If you find it, bring it to me, won't you?" msgstr "" "¿Qué tenemos aquí? Interesante, parece un libro de reactivos. Mantén los " "ojos abiertos para un hongo negro. Debe ser bastante grande y fácil de " "identificar. Si lo encuentras, tráemelo, ¿quieres?" #: Source/translation_dummy.cpp:809 msgid "" "It's a big, black mushroom that I need. Now run off and get it for me so " "that I can use it for a special concoction that I am working on." msgstr "" "Es un hongo negro grande el que necesito. Ahora corre y consíguelo para que " "pueda usarlo en un brebaje especial en el que estoy trabajando." #: Source/translation_dummy.cpp:810 msgid "" "Yes, this will be perfect for a brew that I am creating. By the way, the " "healer is looking for the brain of some demon or another so he can treat " "those who have been afflicted by their poisonous venom. I believe that he " "intends to make an elixir from it. If you help him find what he needs, " "please see if you can get a sample of the elixir for me." msgstr "" "Sí, será perfecto para una infusión que estoy creando. Por cierto, el " "sanador está buscando el cerebro de algún demonio para poder tratar a los " "que han sido afectados por su ponzoña venenosa. Creo que tiene la intención " "de hacer un elixir con eso. Si lo ayudas a encontrar lo que necesita, fíjate " "si puedes conseguirme una muestra del elixir." #: Source/translation_dummy.cpp:811 msgid "" "Why have you brought that here? I have no need for a demon's brain at this " "time. I do need some of the elixir that the Healer is working on. He needs " "that grotesque organ that you are holding, and then bring me the elixir. " "Simple when you think about it, isn't it?" msgstr "" "¿Por qué has traído eso aquí? No necesito el cerebro de un demonio en este " "momento. Necesito algo del elixir en el que está trabajando el Curandero. " "Necesita ese órgano grotesco que estás sosteniendo, y luego tráeme el " "elixir. Simple cuando lo piensas, ¿no?" #: Source/translation_dummy.cpp:812 msgid "" "What? Now you bring me that elixir from the healer? I was able to finish my " "brew without it. Why don't you just keep it..." msgstr "" "¿Qué? ¿Ahora me traes ese elixir del sanador? Pude terminar mi infusión sin " "él. ¿Por qué no te lo quedas? ..." #: Source/translation_dummy.cpp:813 msgid "" "I don't have any mushrooms of any size or color for sale. How about " "something a bit more useful?" msgstr "" "No tengo hongos de ningún tamaño o color a la venta. ¿Qué tal algo un poco " "más útil?" #: Source/translation_dummy.cpp:814 msgid "" "So, the legend of the Map is real. Even I never truly believed any of it! I " "suppose it is time that I told you the truth about who I am, my friend. You " "see, I am not all that I seem...\n" " \n" "My true name is Deckard Cain the Elder, and I am the last descendant of an " "ancient Brotherhood that was dedicated to keeping and safeguarding the " "secrets of a timeless evil. An evil that quite obviously has now been " "released...\n" " \n" "The evil that you move against is the dark Lord of Terror - known to mortal " "men as Diablo. It was he who was imprisoned within the Labyrinth many " "centuries ago. The Map that you hold now was created ages ago to mark the " "time when Diablo would rise again from his imprisonment. When the two stars " "on that map align, Diablo will be at the height of his power. He will be all " "but invincible...\n" " \n" "You are now in a race against time, my friend! Find Diablo and destroy him " "before the stars align, for we may never have a chance to rid the world of " "his evil again!" msgstr "" "Entonces, la leyenda del Mapa es real. ¡Incluso yo nunca creí realmente nada " "de eso! Supongo que es hora de que te diga la verdad sobre quién soy, amigo " "mío. Verás, no soy todo lo que parezco ...\n" " \n" "Mi verdadero nombre es Deckard Cain el Sabio, y soy el último descendiente " "de una antigua Hermandad que se dedicó a conservar y salvaguardar los " "secretos de un mal atemporal. Un mal que obviamente ahora ha sido " "liberado ...\n" " \n" "El mal contra el que te mueves es el oscuro Señor del Terror, conocido por " "los mortales como Diablo. Fue él quien fue encarcelado dentro del Laberinto " "hace muchos siglos. El Mapa que tienes ahora fue creado hace siglos para " "marcar el momento en que Diablo se levantaría nuevamente de su " "encarcelamiento. Cuando las dos estrellas en ese mapa se alineen, Diablo " "estará en el apogeo de su poder. Será casi invencible ...\n" " \n" "¡Ahora estás en una carrera contra el tiempo, amigo! Encuentra a Diablo y " "destrúyelo antes de que las estrellas se alineen, ¡porque es posible que " "nunca más tengamos la oportunidad de librar al mundo de su maldad!" #: Source/translation_dummy.cpp:815 msgid "" "Our time is running short! I sense his dark power building and only you can " "stop him from attaining his full might." msgstr "" "¡Nuestro tiempo se está acabando! Siento que su poder oscuro se está " "acumulando y solo tú puedes evitar que logre todo su poder." #: Source/translation_dummy.cpp:816 msgid "" "I am sure that you tried your best, but I fear that even your strength and " "will may not be enough. Diablo is now at the height of his earthly power, " "and you will need all your courage and strength to defeat him. May the Light " "protect and guide you, my friend. I will help in any way that I am able." msgstr "" "Estoy seguro de que hiciste todo lo posible, pero me temo que ni siquiera tu " "fuerza y voluntad serán suficientes. Diablo está ahora en el apogeo de su " "poder terrenal, y necesitarás todo tu coraje y fuerza para derrotarlo. Que " "la Luz te proteja y te guíe, amigo mío. Ayudaré en todo lo que pueda." #: Source/translation_dummy.cpp:817 msgid "" "If the witch can't help you and suggests you see Cain, what makes you think " "that I would know anything? It sounds like this is a very serious matter. " "You should hurry along and see the storyteller as Adria suggests." msgstr "" "Si la bruja no puede ayudarte y te sugiere que veas a Caín, ¿qué te hace " "pensar que yo sabría algo? Parece que este es un asunto muy serio. Debes " "darte prisa y ver al narrador como sugiere Adria." #: Source/translation_dummy.cpp:818 msgid "" "I can't make much of the writing on this map, but perhaps Adria or Cain " "could help you decipher what this refers to. \n" " \n" "I can see that it is a map of the stars in our sky, but any more than that " "is beyond my talents." msgstr "" "No puedo hacer mucho de lo escrito en este mapa, pero tal vez Adria o Caín " "podrían ayudarlo a descifrar a qué se refiere esto. \n" " \n" "Puedo ver que es un mapa de las estrellas en nuestro cielo, pero está más " "allá de mis talentos." #: Source/translation_dummy.cpp:819 msgid "" "The best person to ask about that sort of thing would be our storyteller. \n" " \n" "Cain is very knowledgeable about ancient writings, and that is easily the " "oldest looking piece of paper that I have ever seen." msgstr "" "La mejor persona para preguntar sobre ese tipo de cosas sería nuestro " "narrador. \n" " \n" "Caín está muy bien informado sobre los escritos antiguos, y ese es " "fácilmente el pedazo de papel más antiguo que he visto en mi vida." #: Source/translation_dummy.cpp:820 msgid "" "I have never seen a map of this sort before. Where'd you get it? Although I " "have no idea how to read this, Cain or Adria may be able to provide the " "answers that you seek." msgstr "" "Nunca antes había visto un mapa de este tipo. ¿Dónde lo conseguiste? Aunque " "no tengo ni idea de cómo leer esto, Caín o Adria pueden darte las respuestas " "que buscas." #: Source/translation_dummy.cpp:821 msgid "" "Listen here, come close. I don't know if you know what I know, but you have " "really got somethin' here. That's a map." msgstr "" "Escúchame, acércate. No sé si sabes lo que yo sé, pero realmente tienes algo " "aquí. Eso es un mapa." #: Source/translation_dummy.cpp:822 msgid "" "Oh, I'm afraid this does not bode well at all. This map of the stars " "portends great disaster, but its secrets are not mine to tell. The time has " "come for you to have a very serious conversation with the Storyteller..." msgstr "" "¡Oh! Me temo que esto no augura nada bueno. Este mapa de las estrellas " "presagia un gran desastre, pero sus secretos no son míos como para " "contarlos. Ha llegado el momento de que tengas una conversación muy seria " "con el Narrador ..." #: Source/translation_dummy.cpp:823 msgid "" "I've been looking for a map, but that certainly isn't it. You should show " "that to Adria - she can probably tell you what it is. I'll say one thing; it " "looks old, and old usually means valuable." msgstr "" "He estado buscando un mapa, pero ciertamente no es así. Deberías mostrárselo " "a Adria; probablemente ella pueda decirte lo que es. Diré una cosa; parece " "viejo, y viejo por lo general significa valioso." #: Source/translation_dummy.cpp:824 msgid "" "Pleeeease, no hurt. No Kill. Keep alive and next time good bring to you." msgstr "" "Por favooor, no herir. No matar. Mantener con vida y la próxima vez te " "ayudaré." #: Source/translation_dummy.cpp:825 msgid "" "Something for you I am making. Again, not kill Gharbad. Live and give " "good. \n" " \n" "You take this as proof I keep word..." msgstr "" "Estoy haciendo algo nuevo para ti. Nuevamente, no mates a Gharbad. Vive y " "haz el bien. \n" " \n" "Toma esto como prueba de que cumplo la palabra ..." #: Source/translation_dummy.cpp:826 msgid "" "Nothing yet! Almost done. \n" " \n" "Very powerful, very strong. Live! Live! \n" " \n" "No pain and promise I keep!" msgstr "" "¡Nada aún! Casi termino. \n" " \n" "Muy poderoso, muy fuerte. ¡Vivir! ¡Vivir! \n" " \n" "¡Sin dolor y la promesa mantengo!" #: Source/translation_dummy.cpp:827 msgid "This too good for you. Very Powerful! You want - you take!" msgstr "Esto es demasiado bueno para ti. ¡Muy poderoso! Tu quieres - tu tomas!" #: Source/translation_dummy.cpp:828 msgid "" "What?! Why are you here? All these interruptions are enough to make one " "insane. Here, take this and leave me to my work. Trouble me no more!" msgstr "" "¿Qué? ¿Por qué estás aquí? Todas estas interrupciones son suficientes para " "volverse loco. Toma, toma esto y déjame con mi trabajo. ¡No me molestes más!" #: Source/translation_dummy.cpp:829 msgid "Arrrrgh! Your curiosity will be the death of you!!!" msgstr "¡Arrrrgh! ¡Tu curiosidad te matará !!!" #: Source/translation_dummy.cpp:830 msgid "Hello, my friend. Stay awhile and listen..." msgstr "Hola mi amigo. Quédate un rato y escucha ..." #: Source/translation_dummy.cpp:831 msgid "" "While you are venturing deeper into the Labyrinth you may find tomes of " "great knowledge hidden there. \n" " \n" "Read them carefully for they can tell you things that even I cannot." msgstr "" "Mientras te adentres más profundo en el Laberinto, es posible que encuentres " "tomos de gran conocimiento escondidos allí. \n" " \n" "Léelos atentamente porque pueden decirte cosas que incluso yo no puedo." #: Source/translation_dummy.cpp:832 msgid "" "I know of many myths and legends that may contain answers to questions that " "may arise in your journeys into the Labyrinth. If you come across challenges " "and questions to which you seek knowledge, seek me out and I will tell you " "what I can." msgstr "" "Conozco muchos mitos y leyendas que pueden contener respuestas a preguntas " "que puedan surgir en tus viajes al Laberinto. Si te encuentras con desafíos " "y preguntas sobre las que buscas conocimiento, búscame y te diré lo que " "pueda." #: Source/translation_dummy.cpp:833 msgid "" "Griswold - a man of great action and great courage. I bet he never told you " "about the time he went into the Labyrinth to save Wirt, did he? He knows his " "fair share of the dangers to be found there, but then again - so do you. He " "is a skilled craftsman, and if he claims to be able to help you in any way, " "you can count on his honesty and his skill." msgstr "" "Griswold: un hombre de gran acción y gran coraje. Apuesto a que nunca te " "contó de la vez que entró en el Laberinto para salvar a Wirt, ¿verdad? Él " "conocía los peligros que se encuentran allí, pero, de nuevo, tu también. Es " "un hábil artesano, y si dice poder ayudarlo de alguna manera, puede contar " "con su honestidad y habilidad." #: Source/translation_dummy.cpp:834 msgid "" "Ogden has owned and run the Rising Sun Inn and Tavern for almost four years " "now. He purchased it just a few short months before everything here went to " "hell. He and his wife Garda do not have the money to leave as they invested " "all they had in making a life for themselves here. He is a good man with a " "deep sense of responsibility." msgstr "" "Ogden es propietario y dirige la Posada y Taberna del Sol Naciente desde " "hace casi cuatro años. Lo compró unos pocos meses antes de que todo se fuera " "al diablo. Él y su esposa Garda no tienen dinero para irse, ya que " "invirtieron todo lo que tenían para ganarse la vida aquí. Es un buen hombre " "con un profundo sentido de responsabilidad." #: Source/translation_dummy.cpp:835 msgid "" "Poor Farnham. He is a disquieting reminder of the doomed assembly that " "entered into the Cathedral with Lazarus on that dark day. He escaped with " "his life, but his courage and much of his sanity were left in some dark pit. " "He finds comfort only at the bottom of his tankard nowadays, but there are " "occasional bits of truth buried within his constant ramblings." msgstr "" "Pobre Farnham. Es un inquietante recordatorio de la condenada asamblea que " "entró en la Catedral con Lazarus en ese día oscuro. Escapó con vida, pero su " "coraje y gran parte de su cordura quedaron en algún pozo oscuro. Hoy en día " "encuentra consuelo solo en el fondo de su jarra, pero hay fragmentos " "ocasionales de verdad enterrados en sus constantes divagaciones." #: Source/translation_dummy.cpp:836 msgid "" "The witch, Adria, is an anomaly here in Tristram. She arrived shortly after " "the Cathedral was desecrated while most everyone else was fleeing. She had a " "small hut constructed at the edge of town, seemingly overnight, and has " "access to many strange and arcane artifacts and tomes of knowledge that even " "I have never seen before." msgstr "" "La bruja, Adria, es una anomalía aquí en Tristram. Llegó poco después de que " "la Catedral fuera profanada mientras la mayoría de los demás huían. Ella " "hizo construir una pequeña cabaña en las afueras del pueblo, aparentemente " "de la noche a la mañana, y tiene acceso a muchos artefactos extraños y " "arcanos y tomos de conocimiento que ni siquiera yo había visto antes." #: Source/translation_dummy.cpp:837 msgid "" "The story of Wirt is a frightening and tragic one. He was taken from the " "arms of his mother and dragged into the labyrinth by the small, foul demons " "that wield wicked spears. There were many other children taken that day, " "including the son of King Leoric. The Knights of the palace went below, but " "never returned. The Blacksmith found the boy, but only after the foul beasts " "had begun to torture him for their sadistic pleasures." msgstr "" "La historia de Wirt es aterradora y trágica. Fue arrancado de los brazos de " "su madre y arrastrado al laberinto por los pequeños e inmundos demonios que " "empuñan malvadas lanzas. Se llevaron a muchos otros niños ese día, incluido " "el hijo del Rey Leoric. Los Caballeros del palacio bajaron, pero nunca " "regresaron. El herrero encontró al niño, pero solo después de que las " "horribles bestias comenzaran a torturarlo para sus sádicos placeres." #: Source/translation_dummy.cpp:838 msgid "" "Ah, Pepin. I count him as a true friend - perhaps the closest I have here. " "He is a bit addled at times, but never a more caring or considerate soul has " "existed. His knowledge and skills are equaled by few, and his door is always " "open." msgstr "" "Ah, Pepin. Lo considero un verdadero amigo, quizás el más cercano que tengo " "aquí. A veces está un poco confundido, pero nunca ha existido un alma más " "cariñosa o considerada. Sus conocimientos y habilidades son igualados por " "pocos, y su puerta siempre está abierta." #: Source/translation_dummy.cpp:839 msgid "" "Gillian is a fine woman. Much adored for her high spirits and her quick " "laugh, she holds a special place in my heart. She stays on at the tavern to " "support her elderly grandmother who is too sick to travel. I sometimes fear " "for her safety, but I know that any man in the village would rather die than " "see her harmed." msgstr "" "Gillian es una buena mujer. Muy querida por su buen humor y su risa rápida, " "ocupa un lugar especial en mi corazón. Se queda en la taberna para ayudar a " "su abuela anciana, que está demasiado enferma para viajar. A veces temo por " "su seguridad, pero sé que cualquier hombre de la aldea preferiría morir " "antes que verla lastimada." #: Source/translation_dummy.cpp:840 msgid "Greetings, good master. Welcome to the Tavern of the Rising Sun!" msgstr "Saludos, buen maestro. ¡Bienvenido a la Taberna del Sol Naciente!" #: Source/translation_dummy.cpp:841 msgid "" "Many adventurers have graced the tables of my tavern, and ten times as many " "stories have been told over as much ale. The only thing that I ever heard " "any of them agree on was this old axiom. Perhaps it will help you. You can " "cut the flesh, but you must crush the bone." msgstr "" "Muchos aventureros han honrado las mesas de mi taberna, y diez veces se han " "contado tantas historias como bebido cerveza. Lo único que escuché de todos " "ellos fue el estar de acuerdo con este viejo axioma. Quizás te ayude. Puedes " "cortar la carne, pero debes triturar el hueso." #: Source/translation_dummy.cpp:842 msgid "" "Griswold the blacksmith is extremely knowledgeable about weapons and armor. " "If you ever need work done on your gear, he is definitely the man to see." msgstr "" "Griswold, el herrero, está muy bien informado sobre armas y armaduras. Si " "alguna vez necesitas trabajar en tu equipo, definitivamente es el hombre " "para ver." #: Source/translation_dummy.cpp:843 msgid "" "Farnham spends far too much time here, drowning his sorrows in cheap ale. I " "would make him leave, but he did suffer so during his time in the Labyrinth." msgstr "" "Farnham pasa demasiado tiempo aquí, ahogando sus penas en cerveza barata. Lo " "echaría, pero sufrió tanto durante su tiempo en el Laberinto." #: Source/translation_dummy.cpp:844 msgid "" "Adria is wise beyond her years, but I must admit - she frightens me a " "little. \n" " \n" "Well, no matter. If you ever have need to trade in items of sorcery, she " "maintains a strangely well-stocked hut just across the river." msgstr "" "Adria es sabia para su edad, pero debo admitirlo: Ella me asusta un poco. \n" " \n" "Bueno, no importa. Si alguna vez necesitas intercambiar objetos de " "hechicería, ella mantiene una choza extrañamente bien abastecida al otro " "lado del río." #: Source/translation_dummy.cpp:845 msgid "" "If you want to know more about the history of our village, the storyteller " "Cain knows quite a bit about the past." msgstr "" "Si quieres saber más sobre la historia de nuestro pueblo, el narrador Cain " "sabe bastante sobre el pasado." #: Source/translation_dummy.cpp:846 msgid "" "Wirt is a rapscallion and a little scoundrel. He was always getting into " "trouble, and it's no surprise what happened to him. \n" " \n" "He probably went fooling about someplace that he shouldn't have been. I feel " "sorry for the boy, but I don't abide the company that he keeps." msgstr "" "Wirt es un canalla y un pequeño sinvergüenza. Siempre se estaba metiendo en " "problemas, y no es de extrañar lo que le sucedió. \n" " \n" "Probablemente se fue a hacer el tonto por ahí donde no debería haber estado. " "Lo siento por el chico, pero no acepto la compañía que tiene." #: Source/translation_dummy.cpp:847 msgid "" "Pepin is a good man - and certainly the most generous in the village. He is " "always attending to the needs of others, but trouble of some sort or another " "does seem to follow him wherever he goes..." msgstr "" "Pepin es un buen hombre y, sin duda, el más generoso del pueblo. Siempre " "está atendiendo las necesidades de los demás, pero los problemas de una u " "otra clase parecen seguirlo dondequiera que vaya ..." #: Source/translation_dummy.cpp:848 msgid "" "Gillian, my Barmaid? If it were not for her sense of duty to her grand-dam, " "she would have fled from here long ago. \n" " \n" "Goodness knows I begged her to leave, telling her that I would watch after " "the old woman, but she is too sweet and caring to have done so." msgstr "" "Gillian, mi Camarera? Si no fuera por su sentido del deber hacia su abuela, " "habría huido de aquí hace mucho tiempo. \n" " \n" "Dios sabe que le rogué que se fuera, diciéndole que cuidaría de la anciana, " "pero ella es demasiado dulce y cariñosa para haberlo hecho." #: Source/translation_dummy.cpp:849 msgid "What ails you, my friend?" msgstr "¿Qué te aflige, amigo mío?" #: Source/translation_dummy.cpp:850 msgid "" "I have made a very interesting discovery. Unlike us, the creatures in the " "Labyrinth can heal themselves without the aid of potions or magic. If you " "hurt one of the monsters, make sure it is dead or it very well may " "regenerate itself." msgstr "" "He hecho un descubrimiento muy interesante. A diferencia de nosotros, las " "criaturas del Laberinto pueden curarse a sí mismas sin la ayuda de pociones " "o magia. Si hieres a uno de los monstruos, asegúrate de que esté muerto o " "podría regenerarse." #: Source/translation_dummy.cpp:851 msgid "" "Before it was taken over by, well, whatever lurks below, the Cathedral was a " "place of great learning. There are many books to be found there. If you find " "any, you should read them all, for some may hold secrets to the workings of " "the Labyrinth." msgstr "" "Antes de que fuera tomada por, bueno, lo que sea que esté al acecho debajo, " "la Catedral era un lugar de gran aprendizaje. Allí se pueden encontrar " "muchos libros. Si encuentra alguno, debe leerlos todos, ya que algunos " "pueden tener secretos sobre el funcionamiento del Laberinto." #: Source/translation_dummy.cpp:852 msgid "" "Griswold knows as much about the art of war as I do about the art of " "healing. He is a shrewd merchant, but his work is second to none. Oh, I " "suppose that may be because he is the only blacksmith left here." msgstr "" "Griswold sabe tanto sobre el arte de la guerra como yo sobre el arte de " "curar. Es un comerciante astuto, pero su trabajo es insuperable. Oh, supongo " "que puede deberse a que es el único herrero que queda aquí." #: Source/translation_dummy.cpp:853 msgid "" "Cain is a true friend and a wise sage. He maintains a vast library and has " "an innate ability to discern the true nature of many things. If you ever " "have any questions, he is the person to go to." msgstr "" "Caín es un verdadero amigo y un sabio sensato. Mantiene una vasta biblioteca " "y tiene una habilidad innata para discernir la verdadera naturaleza de " "muchas cosas. Si alguna vez tienes alguna pregunta, él es la persona a la " "que debes dirigirse." #: Source/translation_dummy.cpp:854 msgid "" "Even my skills have been unable to fully heal Farnham. Oh, I have been able " "to mend his body, but his mind and spirit are beyond anything I can do." msgstr "" "Incluso mis habilidades no han podido curar completamente a Farnham. Oh, he " "podido reparar su cuerpo, pero su mente y su espíritu están más allá de " "cualquier cosa que pueda hacer." #: Source/translation_dummy.cpp:855 msgid "" "While I use some limited forms of magic to create the potions and elixirs I " "store here, Adria is a true sorceress. She never seems to sleep, and she " "always has access to many mystic tomes and artifacts. I believe her hut may " "be much more than the hovel it appears to be, but I can never seem to get " "inside the place." msgstr "" "Si bien utilizo algunas formas limitadas de magia para crear las pociones y " "elixires que guardo aquí, Adria es una verdadera hechicera. Parece que nunca " "duerme y siempre tiene acceso a muchos libros y artefactos místicos. Creo " "que su cabaña puede ser mucho más de lo que parece ser, pero parece que " "nunca puedo entrar al lugar." #: Source/translation_dummy.cpp:856 msgid "" "Poor Wirt. I did all that was possible for the child, but I know he despises " "that wooden peg that I was forced to attach to his leg. His wounds were " "hideous. No one - and especially such a young child - should have to suffer " "the way he did." msgstr "" "Pobre Wirt. Hice todo lo posible por el niño, pero sé que desprecia ese " "broche de madera que me vi obligado a sujetar a su pierna. Sus heridas eran " "horribles. Nadie, y especialmente un niño tan pequeño, debería tener que " "sufrir como él lo hizo." #: Source/translation_dummy.cpp:857 msgid "" "I really don't understand why Ogden stays here in Tristram. He suffers from " "a slight nervous condition, but he is an intelligent and industrious man who " "would do very well wherever he went. I suppose it may be the fear of the " "many murders that happen in the surrounding countryside, or perhaps the " "wishes of his wife that keep him and his family where they are." msgstr "" "Realmente no entiendo por qué Ogden se queda aquí en Tristram. Sufre de una " "leve condición nerviosa, pero es un hombre inteligente y trabajador que le " "iría muy bien donde quiera que fuera. Supongo que puede ser el miedo a los " "muchos asesinatos que ocurren en el campo circundante, o quizás los deseos " "de su esposa lo que lo mantiene a él y a su familia donde están." #: Source/translation_dummy.cpp:858 msgid "" "Ogden's barmaid is a sweet girl. Her grandmother is quite ill, and suffers " "from delusions. \n" " \n" "She claims that they are visions, but I have no proof of that one way or the " "other." msgstr "" "La camarera de Ogden es una chica dulce. Su abuela está bastante enferma y " "sufre delirios \n" " \n" "Ella afirma que son visiones, pero no tengo pruebas de eso de una forma u " "otra." #: Source/translation_dummy.cpp:859 msgid "Good day! How may I serve you?" msgstr "¡Buenos días! ¿Cómo puedo servirte?" #: Source/translation_dummy.cpp:860 msgid "" "My grandmother had a dream that you would come and talk to me. She has " "visions, you know and can see into the future." msgstr "" "Mi abuela soñó que vendrías a hablar conmigo. Ella tiene visiones, sabes, y " "puedes ver el futuro." #: Source/translation_dummy.cpp:861 msgid "" "The woman at the edge of town is a witch! She seems nice enough, and her " "name, Adria, is very pleasing to the ear, but I am very afraid of her. \n" " \n" "It would take someone quite brave, like you, to see what she is doing out " "there." msgstr "" "¡La mujer de las afueras del pueblo es una bruja! Parece bastante agradable " "y su nombre, Adria, es muy agradable al oído, pero le temo. \n" " \n" "Se necesitaría alguien bastante valiente, como tú, para ver lo que está " "haciendo ahí fuera." #: Source/translation_dummy.cpp:862 msgid "" "Our Blacksmith is a point of pride to the people of Tristram. Not only is he " "a master craftsman who has won many contests within his guild, but he " "received praises from our King Leoric himself - may his soul rest in peace. " "Griswold is also a great hero; just ask Cain." msgstr "" "Nuestro herrero es un motivo de orgullo para la gente de Tristram. No solo " "es un maestro artesano que ha ganado muchos concursos dentro de su gremio, " "sino que recibió elogios de nuestro Rey Leoric en persona, que su alma " "descanse en paz. Griswold también es un gran héroe; pregúntale a Caín." #: Source/translation_dummy.cpp:863 msgid "" "Cain has been the storyteller of Tristram for as long as I can remember. He " "knows so much, and can tell you just about anything about almost everything." msgstr "" "Cain ha sido el narrador de Tristram desde que tengo uso de razón. Él sabe " "mucho y puede decirte casi cualquier cosa sobre casi todo." #: Source/translation_dummy.cpp:864 msgid "" "Farnham is a drunkard who fills his belly with ale and everyone else's ears " "with nonsense. \n" " \n" "I know that both Pepin and Ogden feel sympathy for him, but I get so " "frustrated watching him slip farther and farther into a befuddled stupor " "every night." msgstr "" "Farnham es un borracho que llena su barriga de cerveza y los oídos de los " "demás de tonterías. \n" " \n" "Sé que tanto Pepin como Ogden sienten simpatía por él, pero me frustra tanto " "verlo caer cada vez más en un aturdido estupor cada noche." #: Source/translation_dummy.cpp:865 msgid "" "Pepin saved my grandmother's life, and I know that I can never repay him for " "that. His ability to heal any sickness is more powerful than the mightiest " "sword and more mysterious than any spell you can name. If you ever are in " "need of healing, Pepin can help you." msgstr "" "Pepin salvó la vida de mi abuela y sé que nunca podré pagarle por eso. Su " "habilidad para curar cualquier enfermedad es más poderosa que la espada más " "potente y más misteriosa que cualquier hechizo que puedas nombrar. Si alguna " "vez necesitas curarte, Pepin puede ayudarte." #: Source/translation_dummy.cpp:866 msgid "" "I grew up with Wirt's mother, Canace. Although she was only slightly hurt " "when those hideous creatures stole him, she never recovered. I think she " "died of a broken heart. Wirt has become a mean-spirited youngster, looking " "only to profit from the sweat of others. I know that he suffered and has " "seen horrors that I cannot even imagine, but some of that darkness hangs " "over him still." msgstr "" "Crecí con la madre de Wirt, Canace. Aunque solo se sintió levemente herida " "cuando esas horribles criaturas se lo robaron, nunca se recuperó. Creo que " "murió con el corazón roto. Wirt se ha convertido en un joven mezquino que " "solo busca sacar provecho del sudor de los demás. Sé que sufrió y ha visto " "horrores que ni siquiera puedo imaginar, pero algo de esa oscuridad aún se " "cierne sobre él." #: Source/translation_dummy.cpp:867 msgid "" "Ogden and his wife have taken me and my grandmother into their home and have " "even let me earn a few gold pieces by working at the inn. I owe so much to " "them, and hope one day to leave this place and help them start a grand hotel " "in the east." msgstr "" "Ogden y su esposa nos han llevado a mi abuela y a mí a su casa e incluso me " "han dejado ganar algunas piezas de oro trabajando en la posada. Les debo " "mucho y espero algún día dejar este lugar y ayudarlos a comenzar un gran " "hotel en el este." #: Source/translation_dummy.cpp:868 msgid "Well, what can I do for ya?" msgstr "Bueno, ¿qué puedo hacer por ti?" #: Source/translation_dummy.cpp:869 msgid "" "If you're looking for a good weapon, let me show this to you. Take your " "basic blunt weapon, such as a mace. Works like a charm against most of those " "undying horrors down there, and there's nothing better to shatter skinny " "little skeletons!" msgstr "" "Si estás buscando un buen arma, déjame mostrarte esto. Toma tu arma básica " "contundente, como una maza. Funciona como un encanto contra la mayoría de " "esos horrores eternos que hay allí, ¡y no hay nada mejor para destrozar " "pequeños esqueletos delgados!" #: Source/translation_dummy.cpp:870 msgid "" "The axe? Aye, that's a good weapon, balanced against any foe. Look how it " "cleaves the air, and then imagine a nice fat demon head in its path. Keep in " "mind, however, that it is slow to swing - but talk about dealing a heavy " "blow!" msgstr "" "¿El hacha? Sí, esa es un buen arma, equilibrada contra cualquier enemigo. " "Mira cómo corta el aire y luego imagina una bonita y gorda cabeza de demonio " "en su camino. Sin embargo, ten en cuenta que el giro es lento, ¡pero habla " "de asestar un golpe fuerte!" #: Source/translation_dummy.cpp:871 msgid "" "Look at that edge, that balance. A sword in the right hands, and against the " "right foe, is the master of all weapons. Its keen blade finds little to hack " "or pierce on the undead, but against a living, breathing enemy, a sword will " "better slice their flesh!" msgstr "" "Mira ese filo, ese equilibrio. Una espada en la mano derecha, y contra el " "enemigo correcto, es el amo de todas las armas. Su hoja afilada encuentra " "poco para cortar o perforar en los muertos vivientes, pero contra un enemigo " "vivo que respira, ¡una espada cortará su carne mucho mejor!" #: Source/translation_dummy.cpp:872 msgid "" "Your weapons and armor will show the signs of your struggles against the " "Darkness. If you bring them to me, with a bit of work and a hot forge, I can " "restore them to top fighting form." msgstr "" "Tus armas y armaduras mostrarán los signos de tus luchas contra la " "Oscuridad. Si me las traes, con un poco de trabajo y una fragua en caliente, " "puedo restaurarlas a su mejor forma de lucha." #: Source/translation_dummy.cpp:873 msgid "" "While I have to practically smuggle in the metals and tools I need from " "caravans that skirt the edges of our damned town, that witch, Adria, always " "seems to get whatever she needs. If I knew even the smallest bit about how " "to harness magic as she did, I could make some truly incredible things." msgstr "" "Mientras prácticamente tengo que pasar de contrabando los metales y " "herramientas que necesito de las caravanas que bordean los límites de " "nuestro maldito pueblo, esa bruja, Adria, siempre parece conseguir lo que " "necesita. Si supiera lo más mínimo sobre cómo aprovechar la magia como ella " "lo hizo, podría hacer algunas cosas realmente increíbles." #: Source/translation_dummy.cpp:874 msgid "" "Gillian is a nice lass. Shame that her gammer is in such poor health or I " "would arrange to get both of them out of here on one of the trading caravans." msgstr "" "Gillian es una buena chica. Es una pena que su abuela tenga tan mala salud o " "haría los arreglos para sacarlos a ambos de aquí en una de las caravanas " "comerciales." #: Source/translation_dummy.cpp:875 msgid "" "Sometimes I think that Cain talks too much, but I guess that is his calling " "in life. If I could bend steel as well as he can bend your ear, I could make " "a suit of court plate good enough for an Emperor!" msgstr "" "A veces pienso que Caín habla demasiado, pero supongo que esa es su vocación " "en la vida. ¡Si pudiera doblar el acero tan bien como puede doblar tu oreja, " "podría hacer una armadura de placas lo suficientemente buena para un " "Emperador!" #: Source/translation_dummy.cpp:876 msgid "" "I was with Farnham that night that Lazarus led us into Labyrinth. I never " "saw the Archbishop again, and I may not have survived if Farnham was not at " "my side. I fear that the attack left his soul as crippled as, well, another " "did my leg. I cannot fight this battle for him now, but I would if I could." msgstr "" "Estaba con Farnham esa noche que Lazarus nos condujo al Laberinto. Nunca " "volví a ver al Arzobispo y es posible que no hubiera sobrevivido si Farnham " "no hubiera estado a mi lado. Me temo que el ataque dejó su alma tan lisiada " "como, bueno, otro dejó mi pierna. No puedo pelear esta batalla por él ahora, " "pero lo haría si pudiera." #: Source/translation_dummy.cpp:877 msgid "" "A good man who puts the needs of others above his own. You won't find anyone " "left in Tristram - or anywhere else for that matter - who has a bad thing to " "say about the healer." msgstr "" "Un buen hombre que antepone las necesidades de los demás a las suyas. No " "encontraras a nadie en Tristram, ni en ningún otro lugar, que tenga algo " "malo que decir sobre el sanador." #: Source/translation_dummy.cpp:878 msgid "" "That lad is going to get himself into serious trouble... or I guess I should " "say, again. I've tried to interest him in working here and learning an " "honest trade, but he prefers the high profits of dealing in goods of dubious " "origin. I cannot hold that against him after what happened to him, but I do " "wish he would at least be careful." msgstr "" "Ese chico se va a meter en serios problemas ... o creo que debería decirlo, " "de nuevo. He tratado de interesarle en trabajar aquí y aprender un oficio " "honesto, pero prefiere las altas ganancias de comerciar con bienes de origen " "dudoso. No puedo reprocharle eso después de lo que le sucedió, pero desearía " "que al menos tuviera cuidado." #: Source/translation_dummy.cpp:879 msgid "" "The Innkeeper has little business and no real way of turning a profit. He " "manages to make ends meet by providing food and lodging for those who " "occasionally drift through the village, but they are as likely to sneak off " "into the night as they are to pay him. If it weren't for the stores of " "grains and dried meats he kept in his cellar, why, most of us would have " "starved during that first year when the entire countryside was overrun by " "demons." msgstr "" "El Posadero tiene pocos negocios y ninguna forma real de obtener ganancias. " "Se las arregla para llegar a fin de mes proporcionando comida y alojamiento " "a aquellos que, de vez en cuando, deambulan por la aldea, pero es tan " "probable que se escapen por noche como que le paguen. Si no fuera por las " "reservas de cereales y carnes secas que guarda en su bodega, la mayoría de " "nosotros habríamos muerto de hambre durante ese primer año en que todo el " "campo fue invadido por demonios." #: Source/translation_dummy.cpp:880 msgid "Can't a fella drink in peace?" msgstr "¿No puede un colega beber en paz?" #: Source/translation_dummy.cpp:881 msgid "" "The gal who brings the drinks? Oh, yeah, what a pretty lady. So nice, too." msgstr "" "¿La chica que trae las bebidas? Oh, sí, qué hermosa dama. Muy agradable " "también." #: Source/translation_dummy.cpp:882 msgid "" "Why don't that old crone do somethin' for a change. Sure, sure, she's got " "stuff, but you listen to me... she's unnatural. I ain't never seen her eat " "or drink - and you can't trust somebody who doesn't drink at least a little." msgstr "" "¿Por qué esa vieja bruja no hace algo para variar? Claro, claro, tiene sus " "cosas, pero escúchame ... es antinatural. Nunca la he visto comer o beber, y " "no puedes confiar en alguien que no bebe al menos un poco." #: Source/translation_dummy.cpp:883 msgid "" "Cain isn't what he says he is. Sure, sure, he talks a good story... some of " "'em are real scary or funny... but I think he knows more than he knows he " "knows." msgstr "" "Caín no es lo que dice ser. Claro, claro, él cuenta una buena historia ... " "algunas de ellas son realmente aterradoras o divertidas ... pero creo que él " "sabe más de lo que sabe." #: Source/translation_dummy.cpp:884 msgid "" "Griswold? Good old Griswold. I love him like a brother! We fought together, " "you know, back when... we... Lazarus... Lazarus... Lazarus!!!" msgstr "" "Griswold? El bueno de Griswold. ¡Lo quiero como a un hermano! Luchamos " "juntos, ya sabes, cuando ... nosotros ... Lazarus ... Lazarus ... Lazarus!!!" #: Source/translation_dummy.cpp:885 msgid "" "Hehehe, I like Pepin. He really tries, you know. Listen here, you should " "make sure you get to know him. Good fella like that with people always " "wantin' help. Hey, I guess that would be kinda like you, huh hero? I was a " "hero too..." msgstr "" "Jejeje, me gusta Pepin. Realmente lo intenta, ya sabes. Escucha, debes " "asegurarte de conocerlo. Buen tipo como la gente que siempre quiere ayudar. " "Oye, supongo que sería un poco propio de ti, ¿eh héroe? Yo también fui un " "héroe ..." #: Source/translation_dummy.cpp:886 msgid "" "Wirt is a kid with more problems than even me, and I know all about " "problems. Listen here - that kid is gotta sweet deal, but he's been there, " "you know? Lost a leg! Gotta walk around on a piece of wood. So sad, so sad..." msgstr "" "Wirt es un niño que tiene incluso más problemas que yo, y yo sé todo sobre " "los problemas. Escucha, ese chico es afable, pero ha estado allí, ¿sabes? " "¡Perdió una pierna! Tiene que caminar sobre un trozo de madera. Es tan " "triste, tan triste ..." #: Source/translation_dummy.cpp:887 msgid "" "Ogden is the best man in town. I don't think his wife likes me much, but as " "long as she keeps tappin' kegs, I'll like her just fine. Seems like I been " "spendin' more time with Ogden than most, but he's so good to me..." msgstr "" "Ogden es el mejor hombre del pueblo. No creo que yo le guste mucho a su " "esposa, pero mientras ella siga golpeando barriles, supongo que estará bien. " "Parece que he pasado más tiempo con Ogden que la mayoría, pero es tan bueno " "conmigo ..." #: Source/translation_dummy.cpp:888 msgid "" "I wanna tell ya sumthin', 'cause I know all about this stuff. It's my " "specialty. This here is the best... theeeee best! That other ale ain't no " "good since those stupid dogs..." msgstr "" "Quiero decirte algo, porque sé todo sobre estas cosas. Es mi especialidad. " "Esto de aquí es lo mejor ... ¡looo mejor! Esa otra cerveza no es buena ya " "que esos estúpidos perros ..." #: Source/translation_dummy.cpp:889 msgid "" "No one ever lis... listens to me. Somewhere - I ain't too sure - but " "somewhere under the church is a whole pile o' gold. Gleamin' and shinin' and " "just waitin' for someone to get it." msgstr "" "Nunca nadie me esc... escucha. En algún lugar, no estoy muy seguro, pero en " "algún lugar debajo de la iglesia hay un gran montón de oro. Reluciendo y " "brillando y esperando a que alguien lo consiga." #: Source/translation_dummy.cpp:890 msgid "" "I know you gots your own ideas, and I know you're not gonna believe this, " "but that weapon you got there - it just ain't no good against those big " "brutes! Oh, I don't care what Griswold says, they can't make anything like " "they used to in the old days..." msgstr "" "Sé que tienes tus propias ideas, y sé que no vas a creer esto, pero ese arma " "que tienes allí, ¡simplemente no es buena contra esos grandes brutos! Oh, no " "me importa lo que diga Griswold, no pueden hacer nada como solían hacer en " "los viejos tiempos ..." #: Source/translation_dummy.cpp:891 msgid "" "If I was you... and I ain't... but if I was, I'd sell all that stuff you got " "and get out of here. That boy out there... He's always got somethin' good, " "but you gotta give him some gold or he won't even show you what he's got." msgstr "" "Si yo fuera tú ... y no lo soy ... pero si lo fuera, vendería todas esas " "cosas que tienes y me largaría de aquí. Ese chico de ahí fuera ... Siempre " "tiene algo bueno, pero tienes que darle algo de oro o ni siquiera te " "mostrará lo que tiene." #: Source/translation_dummy.cpp:892 msgid "I sense a soul in search of answers..." msgstr "Siento un alma en busca de respuestas ..." #: Source/translation_dummy.cpp:893 msgid "" "Wisdom is earned, not given. If you discover a tome of knowledge, devour its " "words. Should you already have knowledge of the arcane mysteries scribed " "within a book, remember - that level of mastery can always increase." msgstr "" "La sabiduría se gana, no se da. Si descubres un tomo de conocimiento, devora " "sus palabras. Si ya tienes conocimiento de los misterios arcanos escritos en " "un libro, recuerda, ese nivel de maestría siempre puede aumentar." #: Source/translation_dummy.cpp:894 msgid "" "The greatest power is often the shortest lived. You may find ancient words " "of power written upon scrolls of parchment. The strength of these scrolls " "lies in the ability of either apprentice or adept to cast them with equal " "ability. Their weakness is that they must first be read aloud and can never " "be kept at the ready in your mind. Know also that these scrolls can be read " "but once, so use them with care." msgstr "" "El mayor poder es a menudo el de menor duración. Puede encontrar antiguas " "palabras de poder escritas en rollos de pergamino. La fuerza de estos " "pergaminos radica en la capacidad del aprendiz o del adepto para lanzarlos " "con igual habilidad. Su debilidad es que primero se deben leer en voz alta y " "nunca se pueden tener listos en la mente. Debes saber también que estos " "pergaminos se pueden leer una sola vez, así que utilízalos con cuidado." #: Source/translation_dummy.cpp:895 msgid "" "Though the heat of the sun is beyond measure, the mere flame of a candle is " "of greater danger. No energies, no matter how great, can be used without the " "proper focus. For many spells, ensorcelled Staves may be charged with " "magical energies many times over. I have the ability to restore their power " "- but know that nothing is done without a price." msgstr "" "Aunque el calor del sol es inconmensurable, la mera llama de una vela es de " "mayor peligro. Ninguna energía, por grande que sea, puede usarse sin el " "enfoque adecuado. Para muchos hechizos, los Bastones encantados pueden " "cargarse muchas veces con energías mágicas. Tengo la capacidad de restaurar " "su poder, pero sé que nada se hace sin un precio." #: Source/translation_dummy.cpp:896 msgid "" "The sum of our knowledge is in the sum of its people. Should you find a book " "or scroll that you cannot decipher, do not hesitate to bring it to me. If I " "can make sense of it I will share what I find." msgstr "" "La suma de nuestro conocimiento está en la suma de su gente. Si encuentras " "un libro o un pergamino que no puedas descifrar, no dude en traérmelo. Si " "puedo encontrarle sentido, compartiré lo que encuentre." #: Source/translation_dummy.cpp:897 msgid "" "To a man who only knows Iron, there is no greater magic than Steel. The " "blacksmith Griswold is more of a sorcerer than he knows. His ability to meld " "fire and metal is unequaled in this land." msgstr "" "Para un hombre que solo conoce el Hierro, no hay mayor magia que el Acero. " "El herrero Griswold es más hechicero de lo que él cree. Su habilidad para " "fusionar el fuego y el metal es inigualable en esta tierra." #: Source/translation_dummy.cpp:898 msgid "" "Corruption has the strength of deceit, but innocence holds the power of " "purity. The young woman Gillian has a pure heart, placing the needs of her " "matriarch over her own. She fears me, but it is only because she does not " "understand me." msgstr "" "La corrupción tiene la fuerza del engaño, pero la inocencia tiene el poder " "de la pureza. La joven Gillian tiene un corazón puro, anteponiendo las " "necesidades de su matriarca sobre las suyas. Ella me teme, pero es solo " "porque no me comprende." #: Source/translation_dummy.cpp:899 msgid "" "A chest opened in darkness holds no greater treasure than when it is opened " "in the light. The storyteller Cain is an enigma, but only to those who do " "not look. His knowledge of what lies beneath the cathedral is far greater " "than even he allows himself to realize." msgstr "" "Un cofre abierto en la oscuridad no guarda mayor tesoro que cuando se abre a " "la luz. El narrador Caín es un enigma, pero solo para quienes no miran. Su " "conocimiento de lo que hay debajo de la catedral es mucho mayor de lo que él " "mismo se permite darse cuenta." #: Source/translation_dummy.cpp:900 msgid "" "The higher you place your faith in one man, the farther it has to fall. " "Farnham has lost his soul, but not to any demon. It was lost when he saw his " "fellow townspeople betrayed by the Archbishop Lazarus. He has knowledge to " "be gleaned, but you must separate fact from fantasy." msgstr "" "Cuanto más alto pongas tu fe en un hombre, más debe caer. Farnham ha perdido " "su alma, pero no ante ningún demonio. Se perdió cuando vio a la gente del " "pueblo traicionados por el Arzobispo Lazarus. Él tiene conocimientos para " "cosechar, pero debes separar los hechos de las fantasías." #: Source/translation_dummy.cpp:901 msgid "" "The hand, the heart and the mind can perform miracles when they are in " "perfect harmony. The healer Pepin sees into the body in a way that even I " "cannot. His ability to restore the sick and injured is magnified by his " "understanding of the creation of elixirs and potions. He is as great an ally " "as you have in Tristram." msgstr "" "La mano, el corazón y la mente pueden realizar milagros cuando están en " "perfecta armonía. El sanador Pepin ve el interior del cuerpo de una manera " "que ni siquiera yo puedo. Su capacidad para restaurar a los enfermos y " "heridos se ve magnificada por su comprensión en la creación de elixires y " "pociones. Es un aliado tan grande como tú en Tristram." #: Source/translation_dummy.cpp:902 msgid "" "There is much about the future we cannot see, but when it comes it will be " "the children who wield it. The boy Wirt has a blackness upon his soul, but " "he poses no threat to the town or its people. His secretive dealings with " "the urchins and unspoken guilds of nearby towns gain him access to many " "devices that cannot be easily found in Tristram. While his methods may be " "reproachful, Wirt can provide assistance for your battle against the " "encroaching Darkness." msgstr "" "Hay mucho sobre el futuro que no podemos ver, pero cuando llegue serán los " "niños quienes lo manejen. El niño Wirt tiene algo oscuro en el alma, pero no " "representa una amenaza para el pueblo o su gente. Sus tratos secretos con " "los rufianes y los inconfesables gremios de los pueblos cercanos le permiten " "acceder a muchos dispositivos que no se pueden encontrar fácilmente en " "Tristram. Si bien sus métodos pueden ser reprochables, Wirt puede ayudarte " "en tu batalla contra la Oscuridad que nos invade." #: Source/translation_dummy.cpp:903 msgid "" "Earthen walls and thatched canopy do not a home create. The innkeeper Ogden " "serves more of a purpose in this town than many understand. He provides " "shelter for Gillian and her matriarch, maintains what life Farnham has left " "to him, and provides an anchor for all who are left in the town to what " "Tristram once was. His tavern, and the simple pleasures that can still be " "found there, provide a glimpse of a life that the people here remember. It " "is that memory that continues to feed their hopes for your success." msgstr "" "Las paredes de tierra y el dosel de paja no crean una casa. El posadero " "Ogden tiene un mayor propósito en este pueblo de lo que muchos creen. " "Proporciona refugio para Gillian y su matriarca, mantiene la vida que le " "dejó Farnham y proporciona un ancla para todos los que quedan en el pueblo a " "lo que Tristram fue una vez. Su taberna, y los placeres sencillos que aún se " "pueden encontrar allí, dan una idea de una vida que la gente de aquí " "recuerda. Es ese recuerdo el que sigue alimentando sus esperanzas de éxito." #: Source/translation_dummy.cpp:904 msgid "Pssst... over here..." msgstr "Pssst ... por aquí ..." #: Source/translation_dummy.cpp:905 msgid "" "Not everyone in Tristram has a use - or a market - for everything you will " "find in the labyrinth. Not even me, as hard as that is to believe. \n" " \n" "Sometimes, only you will be able to find a purpose for some things." msgstr "" "No todo el mundo en Tristram tiene un uso, o un mercado, para todo lo que " "encontrará en el laberinto. Ni siquiera yo, por más difícil que sea de " "creer. \n" " \n" "A veces, solo tú podrás encontrar un propósito para algunas cosas." #: Source/translation_dummy.cpp:906 msgid "" "Don't trust everything the drunk says. Too many ales have fogged his vision " "and his good sense." msgstr "" "No te fíes de todo lo que dice el borracho. Demasiadas cervezas han empañado " "su visión y su sentido común." #: Source/translation_dummy.cpp:907 msgid "" "In case you haven't noticed, I don't buy anything from Tristram. I am an " "importer of quality goods. If you want to peddle junk, you'll have to see " "Griswold, Pepin or that witch, Adria. I'm sure that they will snap up " "whatever you can bring them..." msgstr "" "Por si no lo has notado, no compro nada de Tristram. Soy un importador de " "productos de calidad. Si quieres vender basura, tendrás que ver a Griswold, " "Pepin o esa bruja, Adria. Estoy seguro de que te sacarán de las manos todo " "lo que les puedas llevar ..." #: Source/translation_dummy.cpp:908 msgid "" "I guess I owe the blacksmith my life - what there is of it. Sure, Griswold " "offered me an apprenticeship at the smithy, and he is a nice enough guy, but " "I'll never get enough money to... well, let's just say that I have definite " "plans that require a large amount of gold." msgstr "" "Supongo que le debo la vida al herrero, lo que queda de ella. Claro, " "Griswold me ofreció un aprendizaje en la herrería, y es un tipo bastante " "agradable, pero nunca obtendré suficiente dinero para ... bueno, digamos que " "tengo planes definidos que requieren una gran cantidad de oro." #: Source/translation_dummy.cpp:909 msgid "" "If I were a few years older, I would shower her with whatever riches I could " "muster, and let me assure you I can get my hands on some very nice stuff. " "Gillian is a beautiful girl who should get out of Tristram as soon as it is " "safe. Hmmm... maybe I'll take her with me when I go..." msgstr "" "Si tuviera unos años más, la colmaría con todas las riquezas que pudiera " "reunir, y déjame asegurarte que puedo conseguir algunas cosas muy buenas. " "Gillian es una hermosa chica que debería salir de Tristram tan pronto como " "sea seguro. Hmmm ... tal vez me la lleve conmigo cuando me vaya ..." #: Source/translation_dummy.cpp:910 msgid "" "Cain knows too much. He scares the life out of me - even more than that " "woman across the river. He keeps telling me about how lucky I am to be " "alive, and how my story is foretold in legend. I think he's off his crock." msgstr "" "Caín sabe demasiado. Me asusta de muerte, incluso más que esa mujer al otro " "lado del río. No deja de contarme lo afortunado que soy de estar vivo y cómo " "mi historia está predicha en la leyenda. Creo que está loco." #: Source/translation_dummy.cpp:911 msgid "" "Farnham - now there is a man with serious problems, and I know all about how " "serious problems can be. He trusted too much in the integrity of one man, " "and Lazarus led him into the very jaws of death. Oh, I know what it's like " "down there, so don't even start telling me about your plans to destroy the " "evil that dwells in that Labyrinth. Just watch your legs..." msgstr "" "Farnham: ahora hay un hombre con problemas graves y sé todo acerca de lo " "graves que pueden ser los problemas. Confió demasiado en la integridad de un " "hombre, y Lazarus lo llevó a las mismas fauces de la muerte. Oh, sé lo que " "es ahí abajo, así que ni siquiera empieces a contarme tus planes para " "destruir el mal que habita en ese Laberinto. Cuida tus piernas ..." #: Source/translation_dummy.cpp:912 msgid "" "As long as you don't need anything reattached, old Pepin is as good as they " "come. \n" " \n" "If I'd have had some of those potions he brews, I might still have my leg..." msgstr "" "Siempre que no necesites volver a colocar nada, el viejo Pepin es tan bueno " "como pocos. \n" " \n" "Si hubiera tenido algunas de esas pociones que él prepara, aún podría tener " "mi pierna ..." #: Source/translation_dummy.cpp:913 msgid "" "Adria truly bothers me. Sure, Cain is creepy in what he can tell you about " "the past, but that witch can see into your past. She always has some way to " "get whatever she needs, too. Adria gets her hands on more merchandise than " "I've seen pass through the gates of the King's Bazaar during High Festival." msgstr "" "Adria realmente me molesta. Claro, Cain es espeluznante en lo que puede " "contarte sobre el pasado, pero esa bruja puede ver tu pasado. Ella siempre " "tiene alguna forma de conseguir lo que necesita también. Adria consigue más " "mercadería de la que he visto pasar por las puertas del Bazar del Rey " "durante el Festival Mayor." #: Source/translation_dummy.cpp:914 msgid "" "Ogden is a fool for staying here. I could get him out of town for a very " "reasonable price, but he insists on trying to make a go of it with that " "stupid tavern. I guess at the least he gives Gillian a place to work, and " "his wife Garda does make a superb Shepherd's pie..." msgstr "" "Ogden es un tonto por quedarse aquí. Podría sacarlo del pueblo por un precio " "muy razonable, pero él insiste en intentar salir adelante con esa estúpida " "taberna. Supongo que al menos le da a Gillian un lugar para trabajar, y su " "esposa Garda hace un excelente Pastel de cordero ..." #: Source/translation_dummy.cpp:915 msgid "" "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits any " "who would seek to steal the treasures secured within this room. So speaks " "the Lord of Terror, and so it is written." msgstr "" "Más allá del Salón de los Héroes se encuentra la Cámara de Hueso. La muerte " "eterna aguarda a cualquiera que busque robar los tesoros guardados dentro de " "esta habitación. Así habla el Señor del Terror, y así está escrito." #: Source/translation_dummy.cpp:916 msgid "" "...and so, locked beyond the Gateway of Blood and past the Hall of Fire, " "Valor awaits for the Hero of Light to awaken..." msgstr "" "... y así, encerrado más allá de la Puerta de la Sangre y más allá del Salón " "del Fuego, Valor espera a que el Héroe de la Luz despierte ..." #: Source/translation_dummy.cpp:917 msgid "" "I can see what you see not.\n" "Vision milky then eyes rot.\n" "When you turn they will be gone,\n" "Whispering their hidden song.\n" "Then you see what cannot be,\n" "Shadows move where light should be.\n" "Out of darkness, out of mind,\n" "Cast down into the Halls of the Blind." msgstr "" "Puedo ver lo que tú no ves.\n" "Visión lechosa, luego los ojos se pudren.\n" "Cuando te vuelvas se habrán ido,\n" "Susurrando su canción oculta.\n" "Entonces ves lo que no puede ser,\n" "Las sombras se mueven donde debería estar la luz.\n" "Fuera de la oscuridad, fuera de la mente,\n" "Arrojados a los Pasillos del Ciego." #: Source/translation_dummy.cpp:918 msgid "" "The armories of Hell are home to the Warlord of Blood. In his wake lay the " "mutilated bodies of thousands. Angels and men alike have been cut down to " "fulfill his endless sacrifices to the Dark ones who scream for one thing - " "blood." msgstr "" "Las armerías del Infierno son el hogar del Señor de la Guerra de la Sangre. " "A su paso yacían los cuerpos mutilados de miles. Tanto los ángeles como los " "hombres han sido cortados para cumplir con sus sacrificios interminables a " "los Oscuros que gritan por una cosa: sangre." #: Source/translation_dummy.cpp:919 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. There is a war that rages on even now, beyond " "the fields that we know - between the utopian kingdoms of the High Heavens " "and the chaotic pits of the Burning Hells. This war is known as the Great " "Conflict, and it has raged and burned longer than any of the stars in the " "sky. Neither side ever gains sway for long as the forces of Light and " "Darkness constantly vie for control over all creation." msgstr "" "Presta atención y da testimonio de las verdades que se encuentran aquí, " "porque son el último legado de los Horadrim. Hay una guerra que continúa " "incluso ahora, más allá de los campos que conocemos, entre los reinos " "utópicos de los Altos Cielos y los caóticos pozos de los Infiernos " "Ardientes. Esta guerra se conoce como el Gran Conflicto, y ha durado y " "ardido durante más tiempo que cualquiera de las estrellas del cielo. Ninguno " "de los bandos logrará dominar mientras las fuerzas de la Luz y la Oscuridad " "compitan constantemente por el control de toda la creación." #: Source/translation_dummy.cpp:920 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. When the Eternal Conflict between the High " "Heavens and the Burning Hells falls upon mortal soil, it is called the Sin " "War. Angels and Demons walk amongst humanity in disguise, fighting in " "secret, away from the prying eyes of mortals. Some daring, powerful mortals " "have even allied themselves with either side, and helped to dictate the " "course of the Sin War." msgstr "" "Presta atención y da testimonio de las verdades que se encuentran aquí, " "porque son el último legado de los Horadrim. Cuando el Conflicto Eterno " "entre los Altos Cielos y los Infiernos Ardientes cae sobre suelo mortal, se " "llama la Guerra del Pecado. Ángeles y demonios caminan entre la humanidad " "disfrazados, luchando en secreto, lejos de las miradas indiscretas de los " "mortales. Algunos mortales atrevidos y poderosos incluso se han aliado con " "ambos bandos y han ayudado a dictar el curso de la Guerra del Pecado." #: Source/translation_dummy.cpp:921 msgid "" "Take heed and bear witness to the truths that lie herein, for they are the " "last legacy of the Horadrim. Nearly three hundred years ago, it came to be " "known that the Three Prime Evils of the Burning Hells had mysteriously come " "to our world. The Three Brothers ravaged the lands of the east for decades, " "while humanity was left trembling in their wake. Our Order - the Horadrim - " "was founded by a group of secretive magi to hunt down and capture the Three " "Evils once and for all.\n" " \n" "The original Horadrim captured two of the Three within powerful artifacts " "known as Soulstones and buried them deep beneath the desolate eastern sands. " "The third Evil escaped capture and fled to the west with many of the " "Horadrim in pursuit. The Third Evil - known as Diablo, the Lord of Terror - " "was eventually captured, his essence set in a Soulstone and buried within " "this Labyrinth.\n" " \n" "Be warned that the soulstone must be kept from discovery by those not of the " "faith. If Diablo were to be released, he would seek a body that is easily " "controlled as he would be very weak - perhaps that of an old man or a child." msgstr "" "Presta atención y da testimonio de las verdades que se encuentran aquí, " "porque son el último legado de los Horadrim. Hace casi trescientos años, se " "supo que los Tres Malignos Principales de los Infiernos Ardientes habían " "llegado misteriosamente a nuestro mundo. Los Tres Hermanos asolaron las " "tierras del este durante décadas, mientras que la humanidad quedó temblando " "a su paso. Nuestra Orden, los Horadrim, fue fundada por un grupo de magos " "secretos para perseguir y capturar a los Tres Malignos de una vez por " "todas.\n" " \n" "El Horadrim original capturó a dos de los Tres dentro de poderosos " "artefactos conocidos como Piedras del Alma y los enterró profundamente bajo " "las desoladas arenas del este. El tercer Maligno escapó de la captura y huyó " "hacia el oeste con muchos de los Horadrim persiguiéndolo. El Tercer Mal, " "conocido como Diablo, el Señor del Terror, finalmente fue capturado, su " "esencia colocada en una Piedra del Alma y enterrada dentro de este " "Laberinto.\n" " \n" "Se advierte que debe evitarse que la Piedra del Alma sea descubierta por " "aquellos que no son de la fe. Si Diablo fuera liberado, buscaría un cuerpo " "que fuera fácil de controlar, y que fuera muy débil, tal vez el de un " "anciano o un niño." #: Source/translation_dummy.cpp:922 msgid "" "So it came to be that there was a great revolution within the Burning Hells " "known as The Dark Exile. The Lesser Evils overthrew the Three Prime Evils " "and banished their spirit forms to the mortal realm. The demons Belial (the " "Lord of Lies) and Azmodan (the Lord of Sin) fought to claim rulership of " "Hell during the absence of the Three Brothers. All of Hell polarized between " "the factions of Belial and Azmodan while the forces of the High Heavens " "continually battered upon the very Gates of Hell." msgstr "" "Así sucedió que hubo una gran revolución dentro de los Infiernos Ardientes " "conocida como El Exilio Oscuro. Los Malignos Menores derrocaron a los Tres " "Malignos Principales y desterraron sus formas espirituales al reino de los " "mortales. Los demonios Belial (el Señor de las Mentiras) y Azmodan (el Señor " "del Pecado) lucharon para reclamar el poder del Infierno durante la ausencia " "de los Tres Hermanos. Todo el Infierno se polarizó entre las facciones de " "Belial y Azmodan mientras las fuerzas de los Altos Cielos golpeaban " "continuamente las mismas Puertas del Infierno." #: Source/translation_dummy.cpp:923 msgid "" "Many demons traveled to the mortal realm in search of the Three Brothers. " "These demons were followed to the mortal plane by Angels who hunted them " "throughout the vast cities of the East. The Angels allied themselves with a " "secretive Order of mortal magi named the Horadrim, who quickly became adept " "at hunting demons. They also made many dark enemies in the underworlds." msgstr "" "Muchos demonios viajaron al reino de los mortales en busca de los Tres " "Hermanos. Estos demonios fueron seguidos al plano mortal por Ángeles que los " "cazaron por las vastas pueblos del Este. Los Ángeles se aliaron con una " "Orden secreta de magos mortales llamada Los Horadrim, quienes rápidamente se " "volvieron expertos en cazar demonios. Estos también se hicieron muchos " "enemigos oscuros en los inframundos." #: Source/translation_dummy.cpp:924 msgid "" "So it came to be that the Three Prime Evils were banished in spirit form to " "the mortal realm and after sewing chaos across the East for decades, they " "were hunted down by the cursed Order of the mortal Horadrim. The Horadrim " "used artifacts called Soulstones to contain the essence of Mephisto, the " "Lord of Hatred and his brother Baal, the Lord of Destruction. The youngest " "brother - Diablo, the Lord of Terror - escaped to the west.\n" " \n" "Eventually the Horadrim captured Diablo within a Soulstone as well, and " "buried him under an ancient, forgotten Cathedral. There, the Lord of Terror " "sleeps and awaits the time of his rebirth. Know ye that he will seek a body " "of youth and power to possess - one that is innocent and easily controlled. " "He will then arise to free his Brothers and once more fan the flames of the " "Sin War..." msgstr "" "Así sucedió que los Tres Malignos Principales fueron desterrados en forma " "espiritual al reino de los mortales y después de sembrar el caos en el Este " "durante décadas, fueron perseguidos por la Orden maldita de los mortales " "Horadrim. Los Horadrim usaron artefactos llamados Piedras del Alma para " "contener la esencia de Mephisto, el Señor del Odio y su hermano Baal, el " "Señor de la Destrucción. El hermano menor, Diablo, el Señor del Terror, " "escapó hacia el oeste.\n" " \n" "Finalmente, los Horadrim capturaron a Diablo dentro de una Piedra del Alma y " "lo enterraron bajo una antigua y olvidada Catedral. Allí, el Señor del " "Terror duerme y espera el momento de su renacimiento. Sepan que buscará un " "cuerpo de juventud y poder para poseer, uno que sea inocente y fácil de " "controlar. Luego se levantará para liberar a sus Hermanos y avivar una vez " "más las llamas de la Guerra del Pecado ..." #: Source/translation_dummy.cpp:925 msgid "" "All praises to Diablo - Lord of Terror and Survivor of The Dark Exile. When " "he awakened from his long slumber, my Lord and Master spoke to me of secrets " "that few mortals know. He told me the kingdoms of the High Heavens and the " "pits of the Burning Hells engage in an eternal war. He revealed the powers " "that have brought this discord to the realms of man. My lord has named the " "battle for this world and all who exist here the Sin War." msgstr "" "Todas las alabanzas a Diablo, Señor del Terror y Sobreviviente del Exilio " "Oscuro. Cuando despertó de su largo letargo, mi Señor y Maestro me habló de " "secretos que pocos mortales conocen. Me dijo que los reinos de los Altos " "Cielos y los pozos de los Infiernos Abrasadores se enzarzan en una guerra " "eterna. Él reveló los poderes que han traído esta discordia a los reinos del " "hombre. Mi señor ha llamado a la batalla por este mundo, y a todos los que " "existen aquí, la Guerra del Pecado." #: Source/translation_dummy.cpp:926 msgid "" "Glory and Approbation to Diablo - Lord of Terror and Leader of the Three. My " "Lord spoke to me of his two Brothers, Mephisto and Baal, who were banished " "to this world long ago. My Lord wishes to bide his time and harness his " "awesome power so that he may free his captive brothers from their tombs " "beneath the sands of the east. Once my Lord releases his Brothers, the Sin " "War will once again know the fury of the Three." msgstr "" "Gloria y Aprobación a Diablo, Señor del Terror y Líder de los Tres. Mi Señor " "me habló de sus dos hermanos, Mefisto y Baal, que fueron desterrados a este " "mundo hace mucho tiempo. Mi Señor desea esperar el momento oportuno y " "aprovechar su asombroso poder para liberar a sus hermanos cautivos de sus " "tumbas bajo las arenas del este. Una vez que mi Señor libere a sus Hermanos, " "la Guerra del Pecado volverá a conocer la furia de los Tres." #: Source/translation_dummy.cpp:927 msgid "" "Hail and Sacrifice to Diablo - Lord of Terror and Destroyer of Souls. When I " "awoke my Master from his sleep, he attempted to possess a mortal's form. " "Diablo attempted to claim the body of King Leoric, but my Master was too " "weak from his imprisonment. My Lord required a simple and innocent anchor to " "this world, and so found the boy Albrecht to be perfect for the task. While " "the good King Leoric was left maddened by Diablo's unsuccessful possession, " "I kidnapped his son Albrecht and brought him before my Master. I now await " "Diablo's call and pray that I will be rewarded when he at last emerges as " "the Lord of this world." msgstr "" "Aclamación y Sacrificio a Diablo, Señor del Terror y Destructor de Almas. " "Cuando desperté a mi Maestro de su sueño, intentó poseer la forma de un " "mortal. Diablo intentó reclamar el cuerpo del Rey Leoric, pero mi Maestro " "estaba demasiado débil por su encarcelamiento. Mi señor necesitaba un ancla " "simple e inocente a este mundo, y por eso encontró que el niño Albrecht era " "perfecto para la tarea. Mientras el buen Rey Leoric estaba enloquecido por " "la posesión fallida de Diablo, yo secuestré a su hijo Albrecht y lo llevé " "ante mi Maestro. Ahora espero la llamada de Diablo y rezo para ser " "recompensado cuando él finalmente emerja como el Señor de este mundo." #: Source/translation_dummy.cpp:928 msgid "" "Thank goodness you've returned!\n" "Much has changed since you lived here, my friend. All was peaceful until the " "dark riders came and destroyed our village. Many were cut down where they " "stood, and those who took up arms were slain or dragged away to become " "slaves - or worse. The church at the edge of town has been desecrated and is " "being used for dark rituals. The screams that echo in the night are inhuman, " "but some of our townsfolk may yet survive. Follow the path that lies between " "my tavern and the blacksmith shop to find the church and save who you can. \n" " \n" "Perhaps I can tell you more if we speak again. Good luck." msgstr "" "¡Gracias a Dios que has vuelto!\n" "Mucho ha cambiado desde que viviste aquí, amigo. Todo estaba en paz hasta " "que llegaron los jinetes oscuros y destruyeron nuestra aldea. Muchos fueron " "asesinados donde estaban, y los que tomaron las armas fueron asesinados o " "arrastrados para convertirse en esclavos, o algo peor. La iglesia en las " "afueras del pueblo ha sido profanada y se utiliza para rituales oscuros. Los " "gritos que resuenan en la noche son inhumanos, pero es posible que algunos " "de nuestros habitantes hayan sobrevivido. Sigue el camino que se encuentra " "entre mi taberna y la herrería para encontrar la iglesia y salvar a quien " "puedas. \n" " \n" "Quizás pueda contarte más si volvemos a hablar. Buena suerte." #: Source/translation_dummy.cpp:929 msgid "" "Maintain your quest. Finding a treasure that is lost is not easy. Finding " "a treasure that is hidden less so. I will leave you with this. Do not let " "the sands of time confuse your search." msgstr "" "Mantén tu búsqueda. Encontrar un tesoro perdido no es fácil. Encontrar un " "tesoro que está escondido, aún menos. Te dejaré con esto. No permitas que " "las arenas del tiempo confundan tu búsqueda." #: Source/translation_dummy.cpp:930 msgid "" "A what?! This is foolishness. There's no treasure buried here in " "Tristram. Let me see that!! Ah, Look these drawings are inaccurate. They " "don't match our town at all. I'd keep my mind on what lies below the " "cathedral and not what lies below our topsoil." msgstr "" "¿Un qué? ¡Esto es una tontería!. No hay ningún tesoro enterrado aquí en " "Tristram. ¡Déjame ver eso! Ah, mira, estos dibujos son inexactos. No " "coinciden en absoluto con nuestro pueblo. Me concentraría en lo que hay " "debajo de la catedral y no en lo que hay debajo de nuestra capa superficial " "del suelo." #: Source/translation_dummy.cpp:931 msgid "" "I really don't have time to discuss some map you are looking for. I have " "many sick people that require my help and yours as well." msgstr "" "Realmente no tengo tiempo para discutir sobre algún mapa que estás buscando. " "Tengo muchos enfermos que necesitan mi ayuda y la tuya también." #: Source/translation_dummy.cpp:932 msgid "" "The once proud Iswall is trapped deep beneath the surface of this world. " "His honor stripped and his visage altered. He is trapped in immortal " "torment. Charged to conceal the very thing that could free him." msgstr "" "El otrora orgulloso Iswall está atrapado en las profundidades de este mundo. " "Su honor fue despojado y su rostro alterado. Está atrapado en un tormento " "inmortal. Encargado de ocultar lo mismo que podría liberarlo." #: Source/translation_dummy.cpp:933 msgid "" "I'll bet that Wirt saw you coming and put on an act just so he could laugh " "at you later when you were running around the town with your nose in the " "dirt. I'd ignore it." msgstr "" "Apuesto a que Wirt te vio venir y fingió un acto para poder reírse de ti más " "tarde, cuando estabas corriendo por el pueblo con la nariz en la tierra. Yo " "lo ignoraría." #: Source/translation_dummy.cpp:934 msgid "" "There was a time when this town was a frequent stop for travelers from far " "and wide. Much has changed since then. But hidden caves and buried " "treasure are common fantasies of any child. Wirt seldom indulges in " "youthful games. So it may just be his imagination." msgstr "" "Hubo un tiempo en que este pueblo era una parada frecuente para los viajeros " "de todas partes. Mucho ha cambiado desde entonces. Pero las cuevas " "escondidas y los tesoros enterrados son fantasías comunes de cualquier niño. " "Wirt rara vez se entrega a juegos juveniles. Así que puede que sea sólo su " "imaginación." #: Source/translation_dummy.cpp:935 msgid "" "Listen here. Come close. I don't know if you know what I know, but you've " "have really got something here. That's a map." msgstr "" "Escucha. Acércate. No sé si sabes lo que yo sé, pero realmente tienes algo " "aquí. Eso es un mapa." #: Source/translation_dummy.cpp:936 msgid "" "My grandmother often tells me stories about the strange forces that inhabit " "the graveyard outside of the church. And it may well interest you to hear " "one of them. She said that if you were to leave the proper offering in the " "cemetery, enter the cathedral to pray for the dead, and then return, the " "offering would be altered in some strange way. I don't know if this is just " "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Mi abuela a menudo me cuenta historias sobre las extrañas fuerzas que " "habitan el cementerio fuera de la iglesia. Y puede que le interese escuchar " "uno de ellos. Dijo que si dejaba la ofrenda adecuada en el cementerio, " "entraba a la catedral para rezar por los muertos y luego regresaba, la " "ofrenda se alteraría de alguna manera extraña. No sé si esto es solo la " "charla de una anciana enferma, pero todo parece posible en estos días." #: Source/translation_dummy.cpp:937 msgid "" "Hmmm. A vast and mysterious treasure you say. Mmmm. Maybe I could be " "interested in picking up a few things from you. Or better yet, don't you " "need some rare and expensive supplies to get you through this ordeal?" msgstr "" "Mmmm ¿Un tesoro vasto y misterioso, dices? Mmmm. Tal vez podría estar " "interesado en adquirir algunas cosas tuyas... o mejor aún, ¿no necesitas " "algunos suministros raros y costosos para superar esta prueba?" #: Source/translation_dummy.cpp:938 msgid "" "So, you're the hero everyone's been talking about. Perhaps you could help a " "poor, simple farmer out of a terrible mess? At the edge of my orchard, just " "south of here, there's a horrible thing swelling out of the ground! I can't " "get to my crops or my bales of hay, and my poor cows will starve. The witch " "gave this to me and said that it would blast that thing out of my field. If " "you could destroy it, I would be forever grateful. I'd do it myself, but " "someone has to stay here with the cows..." msgstr "" "Entonces, eres el héroe del que todos han estado hablando. ¿Quizás podrías " "ayudar a un simple granjero pobre a salir de un lío terrible? En el borde de " "mi huerto, al sur de aquí, ¡hay una cosa horrible que se hincha en el suelo! " "No puedo llegar a mis cultivos ni a mis fardos de heno, y mis pobres vacas " "se morirán de hambre. La bruja me dio esto y dijo que volaría esa cosa fuera " "de mi campo. Si pudieras destruirlo, te estaré eternamente agradecido. Lo " "haría yo mismo, pero alguien tiene que quedarse aquí con las vacas ..." #: Source/translation_dummy.cpp:939 msgid "" "I knew that it couldn't be as simple as that witch made it sound. It's a sad " "world when you can't even trust your neighbors." msgstr "" "Sabía que no podía ser tan simple como lo decía la bruja. Es un mundo triste " "cuando ni siquiera puedes confiar en tus vecinos." #: Source/translation_dummy.cpp:940 msgid "" "Is it gone? Did you send it back to the dark recesses of Hades that spawned " "it? You what? Oh, don't tell me you lost it! Those things don't come cheap, " "you know. You've got to find it, and then blast that horror out of our town." msgstr "" "¿Se ha ido? ¿Lo enviaste de vuelta a los oscuros recovecos del Hades que lo " "engendraron? ¿Tu que? ¡Oh, no me digas que lo perdiste! Esas cosas no son " "baratas, ¿sabes? Tienes que encontrarlo y luego sacar ese horror de nuestro " "pueblo." #: Source/translation_dummy.cpp:941 msgid "" "I heard the explosion from here! Many thanks to you, kind stranger. What " "with all these things comin' out of the ground, monsters taking over the " "church, and so forth, these are trying times. I am but a poor farmer, but " "here -- take this with my great thanks." msgstr "" "¡Escuché la explosión desde aquí! Muchas gracias a ti, amable extraño. Con " "todas estas cosas que salen de la tierra, los monstruos que se apoderan de " "la iglesia, y esas cosas, estos son tiempos difíciles. No soy más que un " "agricultor pobre, pero toma esto con gran agradecimiento." #: Source/translation_dummy.cpp:942 msgid "" "Oh, such a trouble I have...maybe...No, I couldn't impose on you, what with " "all the other troubles. Maybe after you've cleansed the church of some of " "those creatures you could come back... and spare a little time to help a " "poor farmer?" msgstr "" "Oh, qué problema tengo ... tal vez ... No, no podría imponértelo, con todos " "los otros problemas. Quizás, después de haber limpiado la iglesia de algunas " "de esas criaturas, ¿podrías regresar ... y dedicar un poco de tiempo para " "ayudar a un pobre agricultor?" #: Source/translation_dummy.cpp:943 msgid "Waaaah! (sniff) Waaaah! (sniff)" msgstr "Waaaah! (sniff) Waaaah! (sniff)" #: Source/translation_dummy.cpp:944 msgid "" "I lost Theo! I lost my best friend! We were playing over by the river, and " "Theo said he wanted to go look at the big green thing. I said we shouldn't, " "but we snuck over there, and then suddenly this BUG came out! We ran away " "but Theo fell down and the bug GRABBED him and took him away!" msgstr "" "¡Perdí a Theo! ¡Perdí a mi mejor amigo! Estábamos jugando junto al río, y " "Theo dijo que quería ir a ver la gran cosa verde. Dije que no deberíamos, " "pero nos colamos allí, ¡y de repente salió este INSECTO! ¡Huimos, pero Theo " "se cayó y el insecto lo agarró y se lo llevó!" #: Source/translation_dummy.cpp:945 msgid "" "Didja find him? You gotta find Theodore, please! He's just little. He " "can't take care of himself! Please!" msgstr "" "¿Didja lo encontró? ¡Tienes que encontrar a Theodore, por favor! Es solo " "pequeño. ¡No puede cuidarse solo! ¡Por favor!" #: Source/translation_dummy.cpp:946 msgid "" "You found him! You found him! Thank you! Oh Theo, did those nasty bugs " "scare you? Hey! Ugh! There's something stuck to your fur! Ick! Come on, " "Theo, let's go home! Thanks again, hero person!" msgstr "" "¡Lo encontraste! ¡Lo encontraste! ¡Gracias! Oh Theo, ¿esos bichos " "desagradables te asustaron? ¡Oye! ¡Puaj! ¡Hay algo pegado a tu pelaje! ¡Agh! " "¡Vamos, Theo, vámonos a casa! ¡Gracias de nuevo, héroe!" #: Source/translation_dummy.cpp:947 msgid "" "We have long lain dormant, and the time to awaken has come. After our long " "sleep, we are filled with great hunger. Soon, now, we shall feed..." msgstr "" "Llevamos mucho tiempo dormidos y ha llegado el momento de despertar. Después " "de nuestro largo sueño, nos llena un gran hambre. Pronto, ahora, nos " "alimentaremos ..." #: Source/translation_dummy.cpp:948 msgid "" "Have you been enjoying yourself, little mammal? How pathetic. Your little " "world will be no challenge at all." msgstr "" "¿Te has estado divirtiendo, pequeño mamífero? Que patético. Tu pequeño mundo " "no será ningún desafío." #: Source/translation_dummy.cpp:949 msgid "" "These lands shall be defiled, and our brood shall overrun the fields that " "men call home. Our tendrils shall envelop this world, and we will feast on " "the flesh of its denizens. Man shall become our chattel and sustenance." msgstr "" "Estas tierras serán contaminadas y nuestra prole invadirá los campos que los " "hombres llaman hogar. Nuestros zarcillos envolverán este mundo y nos " "deleitaremos con la carne de sus habitantes. El hombre se convertirá en " "nuestro esclavos y sustento." #: Source/translation_dummy.cpp:950 msgid "" "Ah, I can smell you...you are close! Close! Ssss...the scent of blood and " "fear...how enticing..." msgstr "" "Ah, te puedo oler... ¡estás cerca! ¡Cerca! Ssss... el olor a sangre y " "miedo... qué tentador..." #: Source/translation_dummy.cpp:951 msgid "" "And in the year of the Golden Light, it was so decreed that a great " "Cathedral be raised. The cornerstone of this holy place was to be carved " "from the translucent stone Antyrael, named for the Angel who shared his " "power with the Horadrim. \n" " \n" "In the Year of Drawing Shadows, the ground shook and the Cathedral shattered " "and fell. As the building of catacombs and castles began and man stood " "against the ravages of the Sin War, the ruins were scavenged for their " "stones. And so it was that the cornerstone vanished from the eyes of man. \n" " \n" "The stone was of this world -- and of all worlds -- as the Light is both " "within all things and beyond all things. Light and unity are the products of " "this holy foundation, a unity of purpose and a unity of possession." msgstr "" "Y en el año de la Luz Dorada, se decretó que se levantara una gran Catedral. " "La piedra angular de este lugar sagrado debía ser tallada en la piedra " "translúcida Antyrael, llamada así por el Ángel que compartía su poder con " "los Horadrim.\n" " \n" "En el Año designado a las Sombras, el suelo tembló y la Catedral se hizo " "añicos y cayó. Cuando comenzó la construcción de catacumbas y castillos y el " "hombre se enfrentó a los estragos de la Guerra del Pecado, las ruinas fueron " "escarbadas en busca de sus piedras. Y así fue como la piedra angular " "desapareció de los ojos del hombre.\n" " \n" "La piedra era de este mundo , y de todos los mundos , ya que la Luz está " "tanto dentro de todas las cosas como más allá de todas las cosas. La luz y " "la unidad son los productos de este fundamento santo, una unidad de " "propósito y una unidad de posesión." #: Source/translation_dummy.cpp:952 msgid "Moo." msgstr "Muu." #: Source/translation_dummy.cpp:953 msgid "I said, Moo." msgstr "Dije, Muu." #: Source/translation_dummy.cpp:954 msgid "Look I'm just a cow, OK?" msgstr "Mira, solo soy una vaca, ¿de acuerdo?" #: Source/translation_dummy.cpp:955 msgid "" "All right, all right. I'm not really a cow. I don't normally go around " "like this; but, I was sitting at home minding my own business and all of a " "sudden these bugs & vines & bulbs & stuff started coming out of the floor... " "it was horrible! If only I had something normal to wear, it wouldn't be so " "bad. Hey! Could you go back to my place and get my suit for me? The brown " "one, not the gray one, that's for evening wear. I'd do it myself, but I " "don't want anyone seeing me like this. Here, take this, you might need " "it... to kill those things that have overgrown everything. You can't miss " "my house, it's just south of the fork in the river... you know... the one " "with the overgrown vegetable garden." msgstr "" "Bien, bien. Realmente no soy una vaca. Normalmente no ando así; pero, estaba " "sentado en casa ocupándome de mis propios asuntos y, de repente, estos " "insectos, enredaderas, bulbos y cosas empezaron a salir del suelo ... ¡fue " "horrible! Si tan solo tuviera algo normal para ponerme, no estaría tan mal. " "¡Oye! ¿Podrías volver a mi casa y traerme mi traje? El marrón, no el gris, " "es para la noche. Lo haría yo mismo, pero no quiero que nadie me vea así. " "Ten, toma esto, puede que lo necesites ... para matar a esas cosas que han " "crecido demasiado. No puedes perderte, mi casa está justo al sur de la " "bifurcación del río ... ya sabes ... la del huerto descuidado." #: Source/translation_dummy.cpp:956 msgid "" "What are you wasting time for? Go get my suit! And hurry! That Holstein " "over there keeps winking at me!" msgstr "" "¿En qué estás perdiendo el tiempo? ¡Ve por mi traje! ¡Y date prisa! ¡Ese " "Holstein de allí no deja de guiñarme el ojo!" #: Source/translation_dummy.cpp:957 msgid "" "Hey, have you got my suit there? Quick, pass it over! These ears itch like " "you wouldn't believe!" msgstr "" "Oye, ¿tienes mi traje ahí? ¡Rápido, pásalo! ¡Estos oídos pican como no lo " "creerías!" #: Source/translation_dummy.cpp:958 msgid "" "No no no no! This is my GRAY suit! It's for evening wear! Formal " "occasions! I can't wear THIS. What are you, some kind of weirdo? I need " "the BROWN suit." msgstr "" "¡No no no no! ¡Este es mi traje GRIS! ¡Es para la noche! ¡Ocasiones " "formales! No puedo usar ESTO. ¿Qué eres, una especie de bicho raro? Necesito " "el traje MARRÓN." #: Source/translation_dummy.cpp:959 msgid "" "Ahh, that's MUCH better. Whew! At last, some dignity! Are my antlers on " "straight? Good. Look, thanks a lot for helping me out. Here, take this as " "a gift; and, you know... a little fashion tip... you could use a little... " "you could use a new... yknowwhatImean? The whole adventurer motif is just " "so... retro. Just a word of advice, eh? Ciao." msgstr "" "Ahh, eso es MUCHO mejor. ¡Uf! ¡Por fin, algo de dignidad! ¿Mis astas están " "rectas? Bien. Mira, muchas gracias por ayudarme. Ten, toma esto como un " "regalo; y, ya sabes ... un pequeño consejo de moda ... te vendría bien un " "poco ... podrías usar un nuevo ... ¿sabes lo que quiero decir? Todo el " "motivo de los aventureros es tan ... retro. Solo un consejo, ¿eh? Chao." #: Source/translation_dummy.cpp:960 msgid "" "Look. I'm a cow. And you, you're monster bait. Get some experience under " "your belt! We'll talk..." msgstr "" "Mirar Soy una vaca. Y tú, eres un cebo para monstruos. ¡Consigue algo de " "experiencia debajo de tu cinturón! Hablaremos ..." #: Source/translation_dummy.cpp:961 msgid "" "It must truly be a fearsome task I've set before you. If there was just some " "way that I could... would a flagon of some nice, fresh milk help?" msgstr "" "Realmente debe ser una tarea temible la que te he propuesto. Si hubiera " "alguna manera de que pudiera ... ¿ayudaría una jarra de leche fresca?" #: Source/translation_dummy.cpp:962 msgid "" "Oh, I could use your help, but perhaps after you've saved the catacombs from " "the desecration of those beasts." msgstr "" "Oh, me vendría bien tu ayuda, pero quizás después de que hayas salvado las " "catacumbas de la profanación de esas bestias." #: Source/translation_dummy.cpp:963 msgid "" "I need something done, but I couldn't impose on a perfect stranger. Perhaps " "after you've been here a while I might feel more comfortable asking a favor." msgstr "" "Necesito que se haga algo, pero no podría obligar a un perfecto desconocido. " "Quizás después de que hayas estado aquí un tiempo me sienta más cómodo " "pidiendo un favor." #: Source/translation_dummy.cpp:964 msgid "" "I see in you the potential for greatness. Perhaps sometime while you are " "fulfilling your destiny, you could stop by and do a little favor for me?" msgstr "" "Veo en ti el potencial de grandeza. ¿Quizás en algún momento mientras estás " "cumpliendo tu destino, podrías pasar y hacerme un pequeño favor?" #: Source/translation_dummy.cpp:965 msgid "" "I think you could probably help me, but perhaps after you've gotten a little " "more powerful. I wouldn't want to injure the village's only chance to " "destroy the menace in the church!" msgstr "" "Creo que probablemente podrías ayudarme, pero quizás después de que te hayas " "vuelto un poco más poderoso. ¡No quisiera dañar la única oportunidad que " "tiene el pueblo de destruir la amenaza en la iglesia!" #: Source/translation_dummy.cpp:966 msgid "" "Me, I'm a self-made cow. Make something of yourself, and... then we'll talk." msgstr "" "Yo soy una vaca hecha a sí misma. Haz algo de ti mismo y ... luego " "hablaremos." #: Source/translation_dummy.cpp:967 msgid "" "I don't have to explain myself to every tourist that walks by! Don't you " "have some monsters to kill? Maybe we'll talk later. If you live..." msgstr "" "¡No tengo que dar explicaciones a todos los turistas que pasan! ¿No tienes " "algunos monstruos que matar? Quizás hablemos más tarde. Si sigues vivo .." #: Source/translation_dummy.cpp:968 msgid "" "Quit bugging me. I'm looking for someone really heroic. And you're not " "it. I can't trust you, you're going to get eaten by monsters any day now... " "I need someone who's an experienced hero." msgstr "" "Deja de molestarme. Busco a alguien realmente heroico. Y no lo eres. No " "puedo confiar en ti, vas a ser devorado por monstruos en cualquier " "momento ... Necesito a alguien que sea un héroe experimentado." #: Source/translation_dummy.cpp:969 msgid "" "All right, I'll cut the bull. I didn't mean to steer you wrong. I was " "sitting at home, feeling moo-dy, when things got really un-stable; a whole " "stampede of monsters came out of the floor! I just cowed. I just happened " "to be wearing this Jersey when I ran out the door, and now I look udderly " "ridiculous. If only I had something normal to wear, it wouldn't be so bad. " "Hey! Can you go back to my place and get my suit for me? The brown one, " "not the gray one, that's for evening wear. I'd do it myself, but I don't " "want anyone seeing me like this. Here, take this, you might need it... to " "kill those things that have overgrown everything. You can't miss my house, " "it's just south of the fork in the river... you know... the one with the " "overgrown vegetable garden." msgstr "" "Muy bien, iré al grano. No quise guiarte mal. Estaba sentado en casa, " "sintiéndome malhumorado, cuando las cosas se pusieron realmente inestables; " "¡Toda una estampida de monstruos salió del suelo! Solo me acobarde. Estaba " "usando esta camiseta cuando salí corriendo por la puerta, y ahora me veo " "terriblemente ridículo. Si tan solo tuviera algo normal para ponerme, no " "estaría tan mal. ¡Oye! ¿Puedes volver a mi casa y traerme mi traje? El " "marrón, no el gris, es para la noche. Lo haría yo mismo, pero no quiero que " "nadie me vea así. Ten, toma esto, puede que lo necesites ... para matar esas " "cosas que han crecido demasiado. No puedes perderte, mi casa está justo al " "sur de la bifurcación del río ... ya sabes ... la del huerto descuidado." #: Source/translation_dummy.cpp:970 msgid "" "I have tried spells, threats, abjuration and bargaining with this foul " "creature -- to no avail. My methods of enslaving lesser demons seem to have " "no effect on this fearsome beast." msgstr "" "He intentado hechizos, amenazas, abjuración y regateo con esta criatura " "repugnante, sin éxito. Mis métodos para esclavizar a los demonios menores " "parecen no tener ningún efecto sobre esta temible bestia." #: Source/translation_dummy.cpp:971 msgid "" "My home is slowly becoming corrupted by the vileness of this unwanted " "prisoner. The crypts are full of shadows that move just beyond the corners " "of my vision. The faint scrabble of claws dances at the edges of my " "hearing. They are searching, I think, for this journal." msgstr "" "Mi hogar se está corrompiendo lentamente por la vileza de este prisionero no " "deseado. Las criptas están\tllenas de sombras que se mueven un poco más allá " "de las esquinas de mi visión. El leve roce de las garras baila en los bordes " "de mi oído. Están buscando, creo, este diario." #: Source/translation_dummy.cpp:972 msgid "" "In its ranting, the creature has let slip its name -- Na-Krul. I have " "attempted to research the name, but the smaller demons have somehow " "destroyed my library. Na-Krul... The name fills me with a cold dread. I " "prefer to think of it only as The Creature rather than ponder its true name." msgstr "" "En su despotricar, la criatura ha dejado escapar su nombre: Na-Krul. Intenté " "investigar el nombre, pero los demonios más pequeños de alguna manera " "destruyeron mi biblioteca. Na-Krul ... El nombre me llena de un pavor frío. " "Prefiero pensar en él solo como La Criatura en lugar de reflexionar sobre su " "verdadero nombre." #: Source/translation_dummy.cpp:973 msgid "" "The entrapped creature's howls of fury keep me from gaining much needed " "sleep. It rages against the one who sent it to the Void, and it calls foul " "curses upon me for trapping it here. Its words fill my heart with terror, " "and yet I cannot block out its voice." msgstr "" "Los aullidos de furia de la criatura atrapada me impiden conseguir el sueño " "que tanto necesitaba. Se enfurece contra quien la envió al Vacío, y me " "maldice por haberla atrapado aquí. Sus palabras me llenan el corazón de " "terror y, sin embargo, no puedo bloquear su voz." #: Source/translation_dummy.cpp:974 msgid "" "My time is quickly running out. I must record the ways to weaken the demon, " "and then conceal that text, lest his minions find some way to use my " "knowledge to free their lord. I hope that whoever finds this journal will " "seek the knowledge." msgstr "" "Mi tiempo se acaba rápidamente. Debo registrar las formas de debilitar al " "demonio y luego ocultar ese texto, no sea que sus secuaces encuentren alguna " "manera de usar mi conocimiento para liberar a su señor. Espero que quien " "encuentre este diario busque el conocimiento." #: Source/translation_dummy.cpp:975 msgid "" "Whoever finds this scroll is charged with stopping the demonic creature that " "lies within these walls. My time is over. Even now, its hellish minions " "claw at the frail door behind which I hide. \n" " \n" "I have hobbled the demon with arcane magic and encased it within great " "walls, but I fear that will not be enough. \n" " \n" "The spells found in my three grimoires will provide you protected entrance " "to his domain, but only if cast in their proper sequence. The levers at the " "entryway will remove the barriers and free the demon; touch them not! Use " "only these spells to gain entry or his power may be too great for you to " "defeat." msgstr "" "Quien encuentre este pergamino está encargado de detener a la criatura " "demoníaca que se encuentra dentro de estos muros. Mi tiempo se acabado. " "Incluso ahora, sus infernales secuaces se aferran a la frágil puerta detrás " "de la cual me escondo.\n" " \n" "He restringido al demonio con magia arcana y lo he encerrado dentro de " "grandes muros, pero me temo que eso no será suficiente.\n" " \n" "Los hechizos que se encuentran en mis tres grimorios te proporcionarán una " "entrada protegida a su dominio, pero solo si se lanzan en la secuencia " "adecuada. Las palancas en la entrada quitarán las barreras y liberarán al " "demonio ¡No las toques! Usa solo estos hechizos para lograr entrar o su " "poder puede ser demasiado grande para derrotarlo." #: Source/translation_dummy.cpp:976 msgid "In Spiritu Sanctum." msgstr "In Spiritu Sanctum." #: Source/translation_dummy.cpp:977 msgid "Praedictum Otium." msgstr "Praedictum Otium." #: Source/translation_dummy.cpp:978 msgid "Efficio Obitus Ut Inimicus." msgstr "Efficio Obitus Ut Inimicus." #: Source/translation_dummy.cpp:979 msgctxt "monster" msgid "Hellboar" msgstr "Jabinfierno" #: Source/translation_dummy.cpp:980 msgctxt "monster" msgid "Stinger" msgstr "Escorpión" #: Source/translation_dummy.cpp:981 msgctxt "monster" msgid "Psychorb" msgstr "Psychorb" #: Source/translation_dummy.cpp:982 msgctxt "monster" msgid "Arachnon" msgstr "Arachnon" #: Source/translation_dummy.cpp:983 msgctxt "monster" msgid "Felltwin" msgstr "Felltwin" #: Source/translation_dummy.cpp:984 msgctxt "monster" msgid "Hork Spawn" msgstr "Engendro de Hork" #: Source/translation_dummy.cpp:985 msgctxt "monster" msgid "Venomtail" msgstr "Cola Venenosa" #: Source/translation_dummy.cpp:986 msgctxt "monster" msgid "Necromorb" msgstr "Necromorb" #: Source/translation_dummy.cpp:987 msgctxt "monster" msgid "Spider Lord" msgstr "Señor Araña" #: Source/translation_dummy.cpp:988 msgctxt "monster" msgid "Lashworm" msgstr "Gusano de Pestañas" #: Source/translation_dummy.cpp:989 msgctxt "monster" msgid "Torchant" msgstr "Torchant" #: Source/translation_dummy.cpp:990 msgctxt "monster" msgid "Hell Bug" msgstr "Insecto del Infierno" #: Source/translation_dummy.cpp:991 msgctxt "monster" msgid "Gravedigger" msgstr "Sepulturero" #: Source/translation_dummy.cpp:992 msgctxt "monster" msgid "Tomb Rat" msgstr "Rata de Tumba" #: Source/translation_dummy.cpp:993 msgctxt "monster" msgid "Firebat" msgstr "Murciélago de Fuego" #: Source/translation_dummy.cpp:994 msgctxt "monster" msgid "Skullwing" msgstr "Calavera Alada" #: Source/translation_dummy.cpp:995 msgctxt "monster" msgid "Lich" msgstr "Lich" #: Source/translation_dummy.cpp:996 msgctxt "monster" msgid "Crypt Demon" msgstr "Demonio de la Cripta" #: Source/translation_dummy.cpp:997 msgctxt "monster" msgid "Hellbat" msgstr "Murciélago Infernal" #: Source/translation_dummy.cpp:998 msgctxt "monster" msgid "Bone Demon" msgstr "Demonio de Hueso" #: Source/translation_dummy.cpp:999 msgctxt "monster" msgid "Arch Lich" msgstr "Arch Lich" #: Source/translation_dummy.cpp:1000 msgctxt "monster" msgid "Biclops" msgstr "Biclope" #: Source/translation_dummy.cpp:1001 msgctxt "monster" msgid "Flesh Thing" msgstr "Cosa de Carne" #: Source/translation_dummy.cpp:1002 msgctxt "monster" msgid "Reaper" msgstr "Segador" #: Source/translation_dummy.cpp:1003 msgid "Giant's Knuckle" msgstr "Nudillo de Gigante" #: Source/translation_dummy.cpp:1004 msgid "Mercurial Ring" msgstr "Anillo Mercurial" #: Source/translation_dummy.cpp:1005 msgid "Xorine's Ring" msgstr "Anillo de Xorine" #: Source/translation_dummy.cpp:1006 msgid "Karik's Ring" msgstr "Anillo de Karik" #: Source/translation_dummy.cpp:1007 msgid "Ring of Magma" msgstr "Anillo de Magma" #: Source/translation_dummy.cpp:1008 msgid "Ring of the Mystics" msgstr "Anillo de los Místicos" #: Source/translation_dummy.cpp:1009 msgid "Ring of Thunder" msgstr "Anillo de Trueno" #: Source/translation_dummy.cpp:1010 msgid "Amulet of Warding" msgstr "Amuleto de Protección" #: Source/translation_dummy.cpp:1011 msgid "Gnat Sting" msgstr "Picadura de Mosquito" #: Source/translation_dummy.cpp:1012 msgid "Flambeau" msgstr "Antorcha" #: Source/translation_dummy.cpp:1013 msgid "Armor of Gloom" msgstr "Armadura de Penumbra" #: Source/translation_dummy.cpp:1014 msgid "Blitzen" msgstr "Rayo" #: Source/translation_dummy.cpp:1015 msgid "Thunderclap" msgstr "Tronido" #: Source/translation_dummy.cpp:1016 msgid "Shirotachi" msgstr "Shirotachi" #: Source/translation_dummy.cpp:1017 msgid "Eater of Souls" msgstr "Devorador de Almas" #: Source/translation_dummy.cpp:1018 msgid "Diamondedge" msgstr "Filo Diamantado" #: Source/translation_dummy.cpp:1019 msgid "Bone Chain Armor" msgstr "Cota de Malla de Hueso" #: Source/translation_dummy.cpp:1020 msgid "Demon Plate Armor" msgstr "Armadura de Placas de Demonio" #: Source/translation_dummy.cpp:1021 msgid "Acolyte's Amulet" msgstr "Amuleto de Acólito" #: Source/translation_dummy.cpp:1022 msgid "Gladiator's Ring" msgstr "Anillo de Gladiador" #: Source/translation_dummy.cpp:1023 msgid "Jester's" msgstr "del bufón" # ** #: Source/translation_dummy.cpp:1024 msgid "Crystalline" msgstr "cristalino" #: Source/translation_dummy.cpp:1025 msgid "Doppelganger's" msgstr "del doppelgänger" #: Source/translation_dummy.cpp:1026 msgid "devastation" msgstr "de la devastación" #: Source/translation_dummy.cpp:1027 msgid "decay" msgstr "de la decadencia" #: Source/translation_dummy.cpp:1028 msgid "peril" msgstr "del riesgo" #: Source/translation_dummy.cpp:1029 msgctxt "spell" msgid "Mana" msgstr "Maná" #: Source/translation_dummy.cpp:1030 msgctxt "spell" msgid "the Magi" msgstr "los Magos" #: Source/translation_dummy.cpp:1031 msgctxt "spell" msgid "the Jester" msgstr "el Bufón" #: Source/translation_dummy.cpp:1032 msgctxt "spell" msgid "Lightning Wall" msgstr "Pared de Relámpagos" #: Source/translation_dummy.cpp:1033 msgctxt "spell" msgid "Immolation" msgstr "Inmolación" #: Source/translation_dummy.cpp:1034 msgctxt "spell" msgid "Warp" msgstr "Deformación" #: Source/translation_dummy.cpp:1035 msgctxt "spell" msgid "Reflect" msgstr "Reflejar" #: Source/translation_dummy.cpp:1036 msgctxt "spell" msgid "Berserk" msgstr "Berserk" #: Source/translation_dummy.cpp:1037 msgctxt "spell" msgid "Ring of Fire" msgstr "Anillo de Fuego" #: Source/translation_dummy.cpp:1038 msgctxt "spell" msgid "Search" msgstr "Buscar" #: Source/translation_dummy.cpp:1039 msgctxt "spell" msgid "Rune of Fire" msgstr "Runa de Fuego" #: Source/translation_dummy.cpp:1040 msgctxt "spell" msgid "Rune of Light" msgstr "Runa de Luz" #: Source/translation_dummy.cpp:1041 msgctxt "spell" msgid "Rune of Nova" msgstr "Runa de Nova" #: Source/translation_dummy.cpp:1042 msgctxt "spell" msgid "Rune of Immolation" msgstr "Runa de Inmolación" #: Source/translation_dummy.cpp:1043 msgctxt "spell" msgid "Rune of Stone" msgstr "Runa de Piedra" #. TRANSLATORS: Thousands separator #: Source/utils/format_int.cpp:28 Source/utils/format_int.cpp:64 msgid "," msgstr "," #~ msgid "Trying to drop a floor item?" #~ msgstr "¿Está intentando lanzar un objeto al suelo?" #~ msgid "Heart" #~ msgstr "Corazón" #~ msgid "Decrease Gamma" #~ msgstr "Disminuir Gamma" #~ msgid "Increase Gamma" #~ msgstr "Incrementar Gamma" #~ msgid "Restart In Town" #~ msgstr "Reiniciar en el pueblo" #~ msgid "" #~ "Forces waiting for Vertical Sync. Prevents tearing effect when drawing a " #~ "frame. Disabling it can help with mouse lag on some systems." #~ msgstr "" #~ "Fuerzas la espera de sincronización vertical. Evita el efecto de desgarro " #~ "al dibujar un marco. Deshabilitarlo puede ayudar con el retraso del mouse " #~ "en algunos sistemas." #~ msgid "FPS Limiter" #~ msgstr "Limitador de FPS" #~ msgid "FPS is limited to avoid high CPU load. Limit considers refresh rate." #~ msgstr "" #~ "Los FPS están limitados para evitar altas cargas de CPU. El límite " #~ "considera la frecuencia de actualización." #~ msgid "To hit" #~ msgstr "Atacar" #~ msgid "Indestructible, " #~ msgstr "Indestructible, " #~ msgid "No required attributes" #~ msgstr "No requiere atributos" #~ msgid "/help" #~ msgstr "/ayuda" #~ msgid "({command})" #~ msgstr "({command})" #~ msgid "/arena" #~ msgstr "/arena" #~ msgid "Command \"" #~ msgstr "Comando \"" #~ msgid "No automap available in town" #~ msgstr "Automapa no disponible en la ciudad" #~ msgid "" #~ "Beyond the Hall of Heroes lies the Chamber of Bone. Eternal death awaits " #~ "any who would seek to steal the treasures secured within this room. So " #~ "speaks the Lord of Terror, and so it is written." #~ msgstr "" #~ "Más allá del Salón de los Héroes se encuentra la Cámara de Hueso. La " #~ "muerte eterna aguarda a cualquiera que busque robar los tesoros guardados " #~ "dentro de esta habitación. Así habla el Señor del Terror, y así está " #~ "escrito." #~ msgid "" #~ "The armories of Hell are home to the Warlord of Blood. In his wake lay " #~ "the mutilated bodies of thousands. Angels and man alike have been cut " #~ "down to fulfill his endless sacrifices to the Dark ones who scream for " #~ "one thing - blood." #~ msgstr "" #~ "Las armerías del Infierno son el hogar del Señor de la Guerra de la " #~ "Sangre. A su paso yacían los cuerpos mutilados de miles. Tanto los " #~ "Ángeles como los hombres han sido liquidados para cumplir con sus " #~ "sacrificios interminables a los Oscuros que gritan por una cosa: sangre." #~ msgid "" #~ "Cloudy and cooler today. Casting the nets of necromancy across the void " #~ "landed two new subspecies of flying horror; a good day's work. Must " #~ "remember to order some more bat guano and black candles from Adria; I'm " #~ "running a bit low." #~ msgstr "" #~ "Hoy nublado y fresco. Lanzando las redes de la nigromancia en el vacío " #~ "caerán dos nuevas subespecies de terror volador; un buen día de trabajo. " #~ "Debes recordar pedir más guano de murciélago y velas negras de Adria; Me " #~ "estoy quedando un poco bajo." #~ msgid "left-click to target" #~ msgstr "clic izquierdo para apuntar" #~ msgid "Select from spell book, then" #~ msgstr "Seleccionar desde el libro de hechizos, entonces" #~ msgid "cast to read" #~ msgstr "lanzar para leer" #~ msgid "Options:" #~ msgstr "Opciones" #~ msgid "version {:s}" #~ msgstr "versión {:s}" #~ msgid "recover life" #~ msgstr "recuperar la vida" #~ msgid "deadly heal" #~ msgstr "curación mortal" #~ msgid "decrease strength" #~ msgstr "disminuiye la fuerza" #~ msgid "decrease dexterity" #~ msgstr "disminuye la destreza" #~ msgid "decrease vitality" #~ msgstr "disminuye la vitalidad" #~ msgid "you can't heal" #~ msgstr "no puedes curar" #~ msgid "hit monster doesn't heal" #~ msgstr "golpear al monstruo no cura" #~ msgid "Faster attack swing" #~ msgstr "Giro de ataque más rápido" #~ msgid "see with infravision" #~ msgstr "ver con infravision" #~ msgid "Failed to open player archive for writing." #~ msgstr "No se pudo abrir el archivo del reproductor para escribir." #~ msgid "Unable to read to save file archive" #~ msgstr "No se puede leer para guardar el archivo de archivo" #~ msgid "Unable to write to save file archive" #~ msgstr "No se puede escribir para guardar el archivo" ================================================ FILE: Translations/et.po ================================================ #, fuzzy msgid "" msgstr "" "Project-Id-Version: DevilutionX\n" "POT-Creation-Date: 2025-10-02 15:20+0200\n" "PO-Revision-Date: \n" "Last-Translator: gpt-po v1.1.1\n" "Language-Team: \n" "Language: et\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.6\n" "X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-KeywordsList: ;_;N_;ngettext:1,2;pgettext:1c,2;P_:1c,2\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: Source\n" #: Source/DiabloUI/credits_lines.cpp:9 msgid "Game Design" msgstr "Mängu disain" #: Source/DiabloUI/credits_lines.cpp:12 msgid "Senior Designers" msgstr "Vanemdisainerid" #: Source/DiabloUI/credits_lines.cpp:15 Source/DiabloUI/credits_lines.cpp:234 msgid "Additional Design" msgstr "Lisakujundus" #: Source/DiabloUI/credits_lines.cpp:18 Source/DiabloUI/credits_lines.cpp:217 msgid "Lead Programmer" msgstr "Pearmarenduse juht" #: Source/DiabloUI/credits_lines.cpp:21 msgid "Senior Programmers" msgstr "Vanemprogrammeerijad" #: Source/DiabloUI/credits_lines.cpp:25 msgid "Programming" msgstr "Programmeerimine" #: Source/DiabloUI/credits_lines.cpp:28 msgid "Special Guest Programmers" msgstr "Eri Külalisprogrammeerijad" #: Source/DiabloUI/credits_lines.cpp:31 msgid "Battle.net Programming" msgstr "Battle.net programmeerimine" #: Source/DiabloUI/credits_lines.cpp:34 msgid "Serial Communications Programming" msgstr "Jadaside kommunikatsiooni programmeerimine" #: Source/DiabloUI/credits_lines.cpp:37 msgid "Installer Programming" msgstr "Paigaldaja programmeerimine" #: Source/DiabloUI/credits_lines.cpp:40 msgid "Art Directors" msgstr "Kunstijuhid" #: Source/DiabloUI/credits_lines.cpp:43 msgid "Artwork" msgstr "Kunstiteos" #: Source/DiabloUI/credits_lines.cpp:50 msgid "Technical Artwork" msgstr "Tehniline Kunstiteos" #: Source/DiabloUI/credits_lines.cpp:54 msgid "Cinematic Art Directors" msgstr "Kinematograafilise kunsti direktorid" #: Source/DiabloUI/credits_lines.cpp:57 msgid "3D Cinematic Artwork" msgstr "3D kinematograafilised kunstiteosed" #: Source/DiabloUI/credits_lines.cpp:63 msgid "Cinematic Technical Artwork" msgstr "Kinemaatiline tehniline kunstiteos" #: Source/DiabloUI/credits_lines.cpp:66 msgid "Executive Producer" msgstr "Tegevprodutsent" #: Source/DiabloUI/credits_lines.cpp:69 msgid "Producer" msgstr "Produtsent" #: Source/DiabloUI/credits_lines.cpp:72 msgid "Associate Producer" msgstr "Produtsendi assistent" #. TRANSLATORS: Keep Strike Team as Name #: Source/DiabloUI/credits_lines.cpp:75 msgid "Diablo Strike Team" msgstr "Diablo Strike Team" #: Source/DiabloUI/credits_lines.cpp:79 Source/gamemenu.cpp:79 msgid "Music" msgstr "Muusika" #: Source/DiabloUI/credits_lines.cpp:82 msgid "Sound Design" msgstr "Helikujundus" #: Source/DiabloUI/credits_lines.cpp:85 msgid "Cinematic Music & Sound" msgstr "Kinemaatiline muusika ja heli" #: Source/DiabloUI/credits_lines.cpp:88 msgid "Voice Production, Direction & Casting" msgstr "Hääle tootmine, lavastus ja casting" #: Source/DiabloUI/credits_lines.cpp:91 msgid "Script & Story" msgstr "Stsenaarium ja lugu" #: Source/DiabloUI/credits_lines.cpp:95 msgid "Voice Editing" msgstr "Hääle redigeerimine" #: Source/DiabloUI/credits_lines.cpp:98 Source/DiabloUI/credits_lines.cpp:252 msgid "Voices" msgstr "Hääled" #: Source/DiabloUI/credits_lines.cpp:103 msgid "Recording Engineer" msgstr "Heliinsener" #: Source/DiabloUI/credits_lines.cpp:106 msgid "Manual Design & Layout" msgstr "Käsitsi kujundamine ja paigutus" #: Source/DiabloUI/credits_lines.cpp:110 msgid "Manual Artwork" msgstr "Käsitsi valmistatud kunstiteos" #: Source/DiabloUI/credits_lines.cpp:114 msgid "Provisional Director of QA (Lead Tester)" msgstr "QA ajutine direktor (peatestija)" #: Source/DiabloUI/credits_lines.cpp:117 msgid "QA Assault Team (Testers)" msgstr "QA rünnakumeeskond (testijad)" #: Source/DiabloUI/credits_lines.cpp:122 msgid "QA Special Ops Team (Compatibility Testers)" msgstr "QA erioperatsioonide meeskond (ühilduvuse testijad)" #: Source/DiabloUI/credits_lines.cpp:125 msgid "QA Artillery Support (Additional Testers) " msgstr "QA suurtükiväe tugi (lisatestijad) " #: Source/DiabloUI/credits_lines.cpp:129 msgid "QA Counterintelligence" msgstr "QA vastuluure" #. TRANSLATORS: A group of people #: Source/DiabloUI/credits_lines.cpp:132 msgid "Order of Network Information Services" msgstr "Võrguteabe Teenuste Ordu" #: Source/DiabloUI/credits_lines.cpp:136 msgid "Customer Support" msgstr "Klienditugi" #: Source/DiabloUI/credits_lines.cpp:141 msgid "Sales" msgstr "Müük" #: Source/DiabloUI/credits_lines.cpp:144 msgid "Dunsel" msgstr "Dunsel" #: Source/DiabloUI/credits_lines.cpp:147 msgid "Mr. Dabiri's Background Vocalists" msgstr "Hr. Dabiri taustalauljad" #: Source/DiabloUI/credits_lines.cpp:151 msgid "Public Relations" msgstr "Avalikud suhted" #: Source/DiabloUI/credits_lines.cpp:154 msgid "Marketing" msgstr "Turundus" #: Source/DiabloUI/credits_lines.cpp:157 msgid "International Sales" msgstr "Rahvusvaheline müük" #: Source/DiabloUI/credits_lines.cpp:160 msgid "U.S. Sales" msgstr "USA müük" #: Source/DiabloUI/credits_lines.cpp:163 msgid "Manufacturing" msgstr "Tootmine" #: Source/DiabloUI/credits_lines.cpp:166 msgid "Legal & Business" msgstr "Õigus ja äri" #: Source/DiabloUI/credits_lines.cpp:169 msgid "Special Thanks To" msgstr "Erilised tänud" #: Source/DiabloUI/credits_lines.cpp:173 msgid "Thanks To" msgstr "Tänu" #: Source/DiabloUI/credits_lines.cpp:202 msgid "In memory of" msgstr "Mälestuseks" #: Source/DiabloUI/credits_lines.cpp:208 msgid "Very Special Thanks to" msgstr "Eriti suured tänud" #: Source/DiabloUI/credits_lines.cpp:214 msgid "General Manager" msgstr "Peadirektor" #: Source/DiabloUI/credits_lines.cpp:220 msgid "Software Engineering" msgstr "Tarkvarainsener" #: Source/DiabloUI/credits_lines.cpp:223 msgid "Art Director" msgstr "Kunstijuht" #: Source/DiabloUI/credits_lines.cpp:226 msgid "Artists" msgstr "Kunstnikud" #: Source/DiabloUI/credits_lines.cpp:230 msgid "Design" msgstr "Disain" #: Source/DiabloUI/credits_lines.cpp:237 msgid "Sound Design, SFX & Audio Engineering" msgstr "Helidisain, heliefektid ja audiotehnika" #: Source/DiabloUI/credits_lines.cpp:240 msgid "Quality Assurance Lead" msgstr "Kvaliteedikontrolli juht" #: Source/DiabloUI/credits_lines.cpp:243 msgid "Testers" msgstr "Testijad" #: Source/DiabloUI/credits_lines.cpp:248 msgid "Manual" msgstr "Käsiraamat" #: Source/DiabloUI/credits_lines.cpp:257 msgid "\tAdditional Work" msgstr "\tLisatöö" #: Source/DiabloUI/credits_lines.cpp:259 msgid "Quest Text Writing" msgstr "Missiooni teksti kirjutamine" #: Source/DiabloUI/credits_lines.cpp:262 Source/DiabloUI/credits_lines.cpp:297 msgid "Thanks to" msgstr "Tänu" #: Source/DiabloUI/credits_lines.cpp:267 msgid "\t\t\tSpecial Thanks to Blizzard Entertainment" msgstr "\t\t\tEritingilised tänud Blizzard Entertainmentile" #: Source/DiabloUI/credits_lines.cpp:272 msgid "\t\t\tSierra On-Line Inc. Northwest" msgstr "\t\t\tSierra On-Line Inc. Northwest" #: Source/DiabloUI/credits_lines.cpp:274 msgid "Quality Assurance Manager" msgstr "Kvaliteedikontrolli juht" #: Source/DiabloUI/credits_lines.cpp:277 msgid "Quality Assurance Lead Tester" msgstr "Kvaliteedi tagamise juhttestija" #: Source/DiabloUI/credits_lines.cpp:280 msgid "Main Testers" msgstr "Peamised testijad" #: Source/DiabloUI/credits_lines.cpp:283 msgid "Additional Testers" msgstr "Lisatestijad" #: Source/DiabloUI/credits_lines.cpp:288 msgid "Product Marketing Manager" msgstr "Tooteturunduse juht" #: Source/DiabloUI/credits_lines.cpp:291 msgid "Public Relations Manager" msgstr "Avalike suhete juht" #: Source/DiabloUI/credits_lines.cpp:294 msgid "Associate Product Manager" msgstr "Tootejuhi assistent" #: Source/DiabloUI/credits_lines.cpp:303 msgid "The Ring of One Thousand" msgstr "Tuhande Sõrmus" #: Source/DiabloUI/credits_lines.cpp:549 msgid "\tNo souls were sold in the making of this game." msgstr "\tSelle mängu loomisel ei müüdud ühtegi hinge." #: Source/DiabloUI/dialogs.cpp:97 Source/DiabloUI/dialogs.cpp:109 #: Source/DiabloUI/hero/selhero.cpp:199 Source/DiabloUI/hero/selhero.cpp:225 #: Source/DiabloUI/hero/selhero.cpp:310 Source/DiabloUI/hero/selhero.cpp:550 #: Source/DiabloUI/multi/selconn.cpp:94 Source/DiabloUI/multi/selgame.cpp:187 #: Source/DiabloUI/multi/selgame.cpp:350 Source/DiabloUI/multi/selgame.cpp:376 #: Source/DiabloUI/multi/selgame.cpp:518 Source/DiabloUI/multi/selgame.cpp:595 #: Source/DiabloUI/selok.cpp:82 msgid "OK" msgstr "Olgu" #: Source/DiabloUI/hero/selhero.cpp:168 msgid "Choose Class" msgstr "Vali klass" #: Source/DiabloUI/hero/selhero.cpp:202 Source/DiabloUI/hero/selhero.cpp:228 #: Source/DiabloUI/hero/selhero.cpp:313 Source/DiabloUI/hero/selhero.cpp:558 #: Source/DiabloUI/multi/selconn.cpp:97 Source/DiabloUI/progress.cpp:50 msgid "Cancel" msgstr "Tühista" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Multi Player Hero" msgstr "Uus mitmikmängu kangelane" #: Source/DiabloUI/hero/selhero.cpp:208 Source/DiabloUI/hero/selhero.cpp:298 msgid "New Single Player Hero" msgstr "Uus üksikmängija kangelane" #: Source/DiabloUI/hero/selhero.cpp:217 msgid "Save File Exists" msgstr "Salvestusfail olemas" #: Source/DiabloUI/hero/selhero.cpp:220 Source/gamemenu.cpp:50 msgid "Load Game" msgstr "Laadi mäng" #: Source/DiabloUI/hero/selhero.cpp:221 Source/multi.cpp:835 msgid "New Game" msgstr "Uus mäng" #: Source/DiabloUI/hero/selhero.cpp:231 Source/DiabloUI/hero/selhero.cpp:564 msgid "Single Player Characters" msgstr "Üksikmängija tegelased" #: Source/DiabloUI/hero/selhero.cpp:290 msgid "" "The Rogue and Sorcerer are only available in the full retail version of " "Diablo. Visit https://www.gog.com/game/diablo to purchase." msgstr "" "Rogue ja Sorcerer on saadaval ainult Diablo täisversioonis. Külastage " "https://www.gog.com/game/diablo, et osta." #: Source/DiabloUI/hero/selhero.cpp:304 Source/DiabloUI/hero/selhero.cpp:307 msgid "Enter Name" msgstr "Sisesta nimi" #: Source/DiabloUI/hero/selhero.cpp:336 msgid "" "Invalid name. A name cannot contain spaces, reserved characters, or reserved " "words.\n" msgstr "" "Vigane nimi. Nimi ei tohi sisaldada tühikuid, reserveeritud märke ega " "reserveeritud sõnu.\n" #. TRANSLATORS: Error Message #: Source/DiabloUI/hero/selhero.cpp:343 msgid "Unable to create character." msgstr "Ei saa tegelast luua." #: Source/DiabloUI/hero/selhero.cpp:509 msgid "Level:" msgstr "Tase:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Strength:" msgstr "Jõud:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Magic:" msgstr "Maagia:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Dexterity:" msgstr "Osavus:" #: Source/DiabloUI/hero/selhero.cpp:513 msgid "Vitality:" msgstr "Elujõud:" #: Source/DiabloUI/hero/selhero.cpp:515 msgid "Savegame:" msgstr "Salvestusmäng:" #: Source/DiabloUI/hero/selhero.cpp:534 msgid "Select Hero" msgstr "Vali Kangelane" #: Source/DiabloUI/hero/selhero.cpp:542 msgid "New Hero" msgstr "Uus Kangelane" #: Source/DiabloUI/hero/selhero.cpp:553 msgid "Delete" msgstr "Kustuta" #: Source/DiabloUI/hero/selhero.cpp:562 msgid "Multi Player Characters" msgstr "Mitmikmängu tegelased" #: Source/DiabloUI/hero/selhero.cpp:613 msgid "Delete Multi Player Hero" msgstr "Kustuta mitmikmängu kangelane" #: Source/DiabloUI/hero/selhero.cpp:615 msgid "Delete Single Player Hero" msgstr "Kustuta üksikmängija kangelane" #: Source/DiabloUI/hero/selhero.cpp:617 #, c++-format msgid "Are you sure you want to delete the character \"{:s}\"?" msgstr "Kas oled kindel, et soovid kustutada tegelase \"{:s}\"?" #: Source/DiabloUI/mainmenu.cpp:48 msgid "Single Player" msgstr "Üksikmängija" #: Source/DiabloUI/mainmenu.cpp:49 msgid "Multi Player" msgstr "Mitu mängijaga" #: Source/DiabloUI/mainmenu.cpp:50 Source/DiabloUI/settingsmenu.cpp:384 msgid "Settings" msgstr "Seaded" #: Source/DiabloUI/mainmenu.cpp:51 msgid "Support" msgstr "Tugi" #: Source/DiabloUI/mainmenu.cpp:52 msgid "Show Credits" msgstr "Kuva krediidid" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Hellfire" msgstr "Välju Hellfire'ist" #: Source/DiabloUI/mainmenu.cpp:54 msgid "Exit Diablo" msgstr "Välju Diablos" #: Source/DiabloUI/mainmenu.cpp:71 msgid "Shareware" msgstr "Jagamisvara" #: Source/DiabloUI/multi/selconn.cpp:26 msgid "Client-Server (TCP)" msgstr "Klient-server (TCP)" #: Source/DiabloUI/multi/selconn.cpp:27 msgid "Offline" msgstr "Võrguühenduseta" #: Source/DiabloUI/multi/selconn.cpp:68 Source/DiabloUI/multi/selgame.cpp:662 #: Source/DiabloUI/multi/selgame.cpp:688 msgid "Multi Player Game" msgstr "Mitmikmäng" #: Source/DiabloUI/multi/selconn.cpp:74 msgid "Requirements:" msgstr "Nõuded:" #: Source/DiabloUI/multi/selconn.cpp:80 msgid "no gateway needed" msgstr "väravat pole vaja" #: Source/DiabloUI/multi/selconn.cpp:86 msgid "Select Connection" msgstr "Vali ühendus" #: Source/DiabloUI/multi/selconn.cpp:89 msgid "Change Gateway" msgstr "Muuda lüüs" #: Source/DiabloUI/multi/selconn.cpp:122 msgid "All computers must be connected to a TCP-compatible network." msgstr "Kõik arvutid peavad olema ühendatud TCP-ühilduva võrguga." #: Source/DiabloUI/multi/selconn.cpp:126 msgid "All computers must be connected to the internet." msgstr "Kõik arvutid peavad olema internetiga ühendatud." #: Source/DiabloUI/multi/selconn.cpp:130 msgid "Play by yourself with no network exposure." msgstr "Mängi üksi ilma võrguühenduseta." #: Source/DiabloUI/multi/selconn.cpp:135 #, c++-format msgid "Players Supported: {:d}" msgstr "Mängijate tugi: {:d}" #: Source/DiabloUI/multi/selgame.cpp:100 Source/options.cpp:425 #: Source/options.cpp:473 Source/translation_dummy.cpp:630 msgid "Diablo" msgstr "Diablo" #: Source/DiabloUI/multi/selgame.cpp:103 msgid "Diablo Shareware" msgstr "Diablo jagamisversioon" #: Source/DiabloUI/multi/selgame.cpp:106 Source/options.cpp:427 #: Source/options.cpp:487 msgid "Hellfire" msgstr "Hellfire" #: Source/DiabloUI/multi/selgame.cpp:109 msgid "Hellfire Shareware" msgstr "Põrgutule jagamisvara" #: Source/DiabloUI/multi/selgame.cpp:112 msgid "The host is running a different game than you." msgstr "Host käitab teistsugust mängu kui sina." #: Source/DiabloUI/multi/selgame.cpp:114 #, c++-format msgid "The host is running a different game mode ({:s}) than you." msgstr "Host töötab erinevas mängurežiimis ({:s}) kui sina." #. TRANSLATORS: Error message when somebody tries to join a game running another version. #: Source/DiabloUI/multi/selgame.cpp:116 #, c++-format msgid "Your version {:s} does not match the host {:d}.{:d}.{:d}." msgstr "Teie versioon {:s} ei ühti hosti versiooniga {:d}.{:d}.{:d}." #: Source/DiabloUI/multi/selgame.cpp:153 Source/DiabloUI/multi/selgame.cpp:581 msgid "Description:" msgstr "Kirjeldus:" #: Source/DiabloUI/multi/selgame.cpp:159 msgid "Select Action" msgstr "Vali Tegevus" #: Source/DiabloUI/multi/selgame.cpp:162 Source/DiabloUI/multi/selgame.cpp:338 #: Source/DiabloUI/multi/selgame.cpp:499 msgid "Create Game" msgstr "Loo mäng" #: Source/DiabloUI/multi/selgame.cpp:164 msgid "Create Public Game" msgstr "Loo avalik mäng" #: Source/DiabloUI/multi/selgame.cpp:165 msgid "Join Game" msgstr "Liitu mänguga" #: Source/DiabloUI/multi/selgame.cpp:169 msgid "Public Games" msgstr "Avalikud mängud" #: Source/DiabloUI/multi/selgame.cpp:174 Source/diablo_msg.cpp:72 msgid "Loading..." msgstr "Laadimine..." #. TRANSLATORS: type of dungeon (i.e. Cathedral, Caves) #: Source/DiabloUI/multi/selgame.cpp:176 Source/discord/discord.cpp:86 #: Source/options.cpp:459 Source/options.cpp:730 #: Source/panels/charpanel.cpp:142 msgid "None" msgstr "Puudub" #: Source/DiabloUI/multi/selgame.cpp:190 Source/DiabloUI/multi/selgame.cpp:353 #: Source/DiabloUI/multi/selgame.cpp:379 Source/DiabloUI/multi/selgame.cpp:521 #: Source/DiabloUI/multi/selgame.cpp:598 msgid "CANCEL" msgstr "TÜHISTA" #: Source/DiabloUI/multi/selgame.cpp:229 msgid "Create a new game with a difficulty setting of your choice." msgstr "Loo uus mäng oma valitud raskusastmega." #: Source/DiabloUI/multi/selgame.cpp:232 msgid "" "Create a new public game that anyone can join with a difficulty setting of " "your choice." msgstr "" "Loo uus avalik mäng, millega saavad liituda kõik, ja vali ise raskusaste." #: Source/DiabloUI/multi/selgame.cpp:236 msgid "Enter Game ID to join a game already in progress." msgstr "Sisestage mängu ID, et liituda juba käimasoleva mänguga." #: Source/DiabloUI/multi/selgame.cpp:238 msgid "Enter an IP or a hostname to join a game already in progress." msgstr "" "Sisestage IP-aadress või hostinimi, et liituda juba käimasoleva mänguga." #: Source/DiabloUI/multi/selgame.cpp:243 msgid "Join the public game already in progress." msgstr "Liitu juba käimasoleva avaliku mänguga." #: Source/DiabloUI/multi/selgame.cpp:249 Source/DiabloUI/multi/selgame.cpp:343 #: Source/DiabloUI/multi/selgame.cpp:404 Source/DiabloUI/multi/selgame.cpp:510 #: Source/DiabloUI/multi/selgame.cpp:530 Source/automap.cpp:1461 #: Source/discord/discord.cpp:114 msgid "Normal" msgstr "Tavaline" #: Source/DiabloUI/multi/selgame.cpp:252 Source/DiabloUI/multi/selgame.cpp:344 #: Source/DiabloUI/multi/selgame.cpp:408 Source/automap.cpp:1464 #: Source/discord/discord.cpp:114 msgid "Nightmare" msgstr "Õudusunenägu" #: Source/DiabloUI/multi/selgame.cpp:255 Source/DiabloUI/multi/selgame.cpp:345 #: Source/DiabloUI/multi/selgame.cpp:412 Source/automap.cpp:1467 #: Source/discord/discord.cpp:81 Source/discord/discord.cpp:114 msgid "Hell" msgstr "Põrgu" #. TRANSLATORS: {:s} means: Game Difficulty. #: Source/DiabloUI/multi/selgame.cpp:258 Source/automap.cpp:1471 #, c++-format msgid "Difficulty: {:s}" msgstr "Raskusaste: {:s}" #: Source/DiabloUI/multi/selgame.cpp:262 Source/gamemenu.cpp:165 msgid "Speed: Normal" msgstr "Kiirus: Tavaline" #: Source/DiabloUI/multi/selgame.cpp:265 Source/gamemenu.cpp:163 msgid "Speed: Fast" msgstr "Kiirus: Kiire" #: Source/DiabloUI/multi/selgame.cpp:268 Source/gamemenu.cpp:161 msgid "Speed: Faster" msgstr "Kiirus: Kiirem" #: Source/DiabloUI/multi/selgame.cpp:271 Source/gamemenu.cpp:159 msgid "Speed: Fastest" msgstr "Kiirus: Kõige kiirem" #: Source/DiabloUI/multi/selgame.cpp:279 msgid "Players: " msgstr "Mängijad: " #: Source/DiabloUI/multi/selgame.cpp:341 msgid "Select Difficulty" msgstr "Vali Raskusaste" #: Source/DiabloUI/multi/selgame.cpp:359 #, c++-format msgid "Join {:s} Games" msgstr "Liitu {:s} mängudega" #: Source/DiabloUI/multi/selgame.cpp:364 msgid "Enter Game ID" msgstr "Sisesta mängu ID" #: Source/DiabloUI/multi/selgame.cpp:366 msgid "Enter address" msgstr "Sisesta aadress" #: Source/DiabloUI/multi/selgame.cpp:405 msgid "" "Normal Difficulty\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Tavaline raskusaste\n" "Siin peaks alustama oma teekonda Diablo alistamiseks algaja tegelane." #: Source/DiabloUI/multi/selgame.cpp:409 msgid "" "Nightmare Difficulty\n" "The denizens of the Labyrinth have been bolstered and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Õudusunenäo raskusaste\n" "Labürindi elanikud on tugevdatud ja pakuvad suuremat väljakutset. See on " "soovitatav ainult kogenud tegelastele." #: Source/DiabloUI/multi/selgame.cpp:413 msgid "" "Hell Difficulty\n" "The most powerful of the underworld's creatures lurk at the gateway into " "Hell. Only the most experienced characters should venture in this realm." msgstr "" "Põrgu Raskusaste\n" "Allilma kõige võimsamad olendid varitsevad Põrgu väravas. Ainult kõige " "kogenumad tegelased peaksid sellesse valdkonda sisenema." #: Source/DiabloUI/multi/selgame.cpp:428 msgid "" "Your character must reach level 20 before you can enter a multiplayer game " "of Nightmare difficulty." msgstr "" "Sinu tegelane peab jõudma 20. tasemele, enne kui saad siseneda mitmikmängu " "Nightmare raskusastmel." #: Source/DiabloUI/multi/selgame.cpp:430 msgid "" "Your character must reach level 30 before you can enter a multiplayer game " "of Hell difficulty." msgstr "" "Sinu tegelane peab jõudma 30. tasemele, enne kui saad siseneda mitmikmängu " "Põrgu raskusastmel." #: Source/DiabloUI/multi/selgame.cpp:508 msgid "Select Game Speed" msgstr "Vali mängu kiirus" #: Source/DiabloUI/multi/selgame.cpp:511 Source/DiabloUI/multi/selgame.cpp:534 msgid "Fast" msgstr "Kiire" #: Source/DiabloUI/multi/selgame.cpp:512 Source/DiabloUI/multi/selgame.cpp:538 msgid "Faster" msgstr "Kiirem" #: Source/DiabloUI/multi/selgame.cpp:513 Source/DiabloUI/multi/selgame.cpp:542 msgid "Fastest" msgstr "Kiireim" #: Source/DiabloUI/multi/selgame.cpp:531 msgid "" "Normal Speed\n" "This is where a starting character should begin the quest to defeat Diablo." msgstr "" "Tavaline kiirus\n" "Siin peaks algaja tegelane alustama oma teekonda Diablo alistamiseks." #: Source/DiabloUI/multi/selgame.cpp:535 msgid "" "Fast Speed\n" "The denizens of the Labyrinth have been hastened and will prove to be a " "greater challenge. This is recommended for experienced characters only." msgstr "" "Kiire kiirus\n" "Labürindi elanikud on kiirendatud ja pakuvad suuremat väljakutset. See on " "soovitatav ainult kogenud tegelastele." #: Source/DiabloUI/multi/selgame.cpp:539 msgid "" "Faster Speed\n" "Most monsters of the dungeon will seek you out quicker than ever before. " "Only an experienced champion should try their luck at this speed." msgstr "" "Kiirem kiirus\n" "Enamik koopas elavaid koletisi otsib sind üles kiiremini kui kunagi varem. " "Ainult kogenud meister peaks proovima oma õnne sellel kiirusel." #: Source/DiabloUI/multi/selgame.cpp:543 msgid "" "Fastest Speed\n" "The minions of the underworld will rush to attack without hesitation. Only a " "true speed demon should enter at this pace." msgstr "" "Kiireim kiirus\n" "Allilma käsilased tormavad ründama kõhklemata. Ainult tõeline kiirusdeemon " "peaks sellise tempoga sisenema." #: Source/DiabloUI/multi/selgame.cpp:587 Source/DiabloUI/multi/selgame.cpp:592 msgid "Enter Password" msgstr "Sisesta parool" #: Source/DiabloUI/selstart.cpp:49 msgid "Enter Hellfire" msgstr "Sisene Põrgutulle" #: Source/DiabloUI/selstart.cpp:50 msgid "Switch to Diablo" msgstr "Lülita Diablole" #: Source/DiabloUI/selyesno.cpp:68 Source/stores.cpp:967 msgid "Yes" msgstr "Jah" #: Source/DiabloUI/selyesno.cpp:69 Source/stores.cpp:968 msgid "No" msgstr "Ei" #: Source/DiabloUI/settingsmenu.cpp:162 msgid "Press gamepad buttons to change." msgstr "Vajutage mängupuldi nuppe, et muuta." #: Source/DiabloUI/settingsmenu.cpp:439 msgid "Bound key:" msgstr "Seotud klahv:" #: Source/DiabloUI/settingsmenu.cpp:488 msgid "Press any key to change." msgstr "Vajuta suvalist klahvi, et muuta." #: Source/DiabloUI/settingsmenu.cpp:490 msgid "Unbind key" msgstr "Eemalda klahv" #: Source/DiabloUI/settingsmenu.cpp:494 msgid "Bound button combo:" msgstr "Seotud nupu kombinatsioon:" #: Source/DiabloUI/settingsmenu.cpp:503 msgid "Unbind button combo" msgstr "Eemalda nupu kombinatsioon" #: Source/DiabloUI/settingsmenu.cpp:547 Source/gamemenu.cpp:73 msgid "Previous Menu" msgstr "Eelmine menüü" #: Source/DiabloUI/support_lines.cpp:10 msgid "" "We maintain a chat server at Discord.gg/devilutionx Follow the links to join " "our community where we talk about things related to Diablo, and the Hellfire " "expansion." msgstr "" "Meil on Discord.gg/devilutionx vestlusserver. Järgige linke, et liituda meie " "kogukonnaga, kus arutame Diablo ja Hellfire'i laiendusega seotud teemasid." #: Source/DiabloUI/support_lines.cpp:12 msgid "" "DevilutionX is maintained by Diasurgical, issues and bugs can be reported at " "this address: https://github.com/diasurgical/devilutionX To help us better " "serve you, please be sure to include the version number, operating system, " "and the nature of the problem." msgstr "" "DevilutionX-i haldab Diasurgical, probleeme ja vigu saab raporteerida sellel " "aadressil: https://github.com/diasurgical/devilutionX Et saaksime teid " "paremini teenindada, palun lisage kindlasti versiooninumber, " "operatsioonisüsteem ja probleemi olemus." #: Source/DiabloUI/support_lines.cpp:15 msgid "Disclaimer:" msgstr "Vastutusest loobumine:" #: Source/DiabloUI/support_lines.cpp:16 msgid "" "\tDevilutionX is not supported or maintained by Blizzard Entertainment, nor " "GOG.com. Neither Blizzard Entertainment nor GOG.com has tested or certified " "the quality or compatibility of DevilutionX. All inquiries regarding " "DevilutionX should be directed to Diasurgical, not to Blizzard Entertainment " "or GOG.com." msgstr "" "\tDevilutionX-i ei toeta ega halda Blizzard Entertainment ega GOG.com. Ei " "Blizzard Entertainment ega GOG.com pole testinud ega sertifitseerinud " "DevilutionX-i kvaliteeti või ühilduvust. Kõik DevilutionX-iga seotud " "päringud tuleks suunata Diasurgicalile, mitte Blizzard Entertainmentile ega " "GOG.com-ile." #: Source/DiabloUI/support_lines.cpp:19 msgid "" "\tThis port makes use of Charis SIL, New Athena Unicode, Unifont, and Noto " "which are licensed under the SIL Open Font License, as well as Twitmoji " "which is licensed under CC-BY 4.0. The port also makes use of SDL which is " "licensed under the zlib-license. See the ReadMe for further details." msgstr "" "\tSee port kasutab fonte Charis SIL, New Athena Unicode, Unifont ja Noto, " "mis on litsentsitud SIL Open Font License'i alusel, samuti Twitmoji, mis on " "litsentsitud CC-BY 4.0 alusel. Port kasutab ka SDL-i, mis on litsentsitud " "zlib-litsentsi alusel. Täpsemat teavet leiate ReadMe failist." #: Source/DiabloUI/title.cpp:67 msgid "Copyright © 1996-2001 Blizzard Entertainment" msgstr "Autoriõigus © 1996-2001 Blizzard Entertainment" #: Source/appfat.cpp:63 msgid "Error" msgstr "Viga" #. TRANSLATORS: Error message that displays relevant information for bug report #: Source/appfat.cpp:77 #, c++-format msgid "" "{:s}\n" "\n" "The error occurred at: {:s} line {:d}" msgstr "" "{:s}\n" "\n" "Viga ilmnes: {:s} rida {:d}" #: Source/appfat.cpp:83 msgid "Data File Error" msgstr "Andmefaili viga" #: Source/appfat.cpp:84 #, c++-format msgid "" "Unable to open main data archive ({:s}).\n" "\n" "Make sure that it is in the game folder." msgstr "" "Peamine andmearhiiv ei saa avada ({:s}).\n" "\n" "Veendu, et see on mängu kaustas." #: Source/appfat.cpp:93 msgid "Read-Only Directory Error" msgstr "Kirjutuskaitstud kataloogi viga" #. TRANSLATORS: Error when Program is not allowed to write data #: Source/appfat.cpp:94 #, c++-format msgid "" "Unable to write to location:\n" "{:s}" msgstr "" "Ei saa asukohta kirjutada:\n" "{:s}" #: Source/automap.cpp:1416 msgid "Game: " msgstr "Mäng: " #: Source/automap.cpp:1424 msgid "Offline Game" msgstr "Võrguühenduseta mäng" #: Source/automap.cpp:1426 msgid "Password: " msgstr "Parool: " #: Source/automap.cpp:1429 msgid "Public Game" msgstr "Avalik mäng" #: Source/automap.cpp:1443 #, c++-format msgid "Level: Nest {:d}" msgstr "Tase: Pesa {:d}" #: Source/automap.cpp:1446 #, c++-format msgid "Level: Crypt {:d}" msgstr "Tase: Krüpt {:d}" #: Source/automap.cpp:1449 Source/discord/discord.cpp:81 Source/objects.cpp:157 msgid "Town" msgstr "Linn" #: Source/automap.cpp:1452 #, c++-format msgid "Level: {:d}" msgstr "Tase: {:d}" #: Source/control.cpp:203 msgid "Tab" msgstr "Vahekaart" #: Source/control.cpp:203 msgid "Esc" msgstr "Esc" #: Source/control.cpp:203 msgid "Enter" msgstr "Sisene" #: Source/control.cpp:206 msgid "Character Information" msgstr "Tegelase teave" #: Source/control.cpp:207 msgid "Quests log" msgstr "Ülesannete logi" #: Source/control.cpp:208 msgid "Automap" msgstr "Automaatkaart" #: Source/control.cpp:209 msgid "Main Menu" msgstr "Põhimenüü" #: Source/control.cpp:210 Source/diablo.cpp:1912 Source/diablo.cpp:2264 msgid "Inventory" msgstr "Inventuur" #: Source/control.cpp:211 msgid "Spell book" msgstr "Loitsuraamat" #: Source/control.cpp:212 msgid "Send Message" msgstr "Saada sõnum" #: Source/control.cpp:622 msgid "Available Commands:" msgstr "Saadaolevad käsud:" #: Source/control.cpp:630 Source/control.cpp:814 msgid "Command " msgstr "Käsklus " #: Source/control.cpp:630 Source/control.cpp:814 msgid " is unknown." msgstr " on tundmatu." #: Source/control.cpp:633 Source/control.cpp:634 msgid "Description: " msgstr "Kirjeldus: " #: Source/control.cpp:633 msgid "" "\n" "Parameters: No additional parameter needed." msgstr "" "\n" "Parameetrid: Täiendavaid parameetreid pole vaja." #: Source/control.cpp:634 msgid "" "\n" "Parameters: " msgstr "" "\n" "Parameetrid: " #: Source/control.cpp:648 Source/control.cpp:680 msgid "Arenas are only supported in multiplayer." msgstr "Areenid on toetatud ainult mitmikmängus." #: Source/control.cpp:653 msgid "What arena do you want to visit?" msgstr "Millist areeni sa külastada tahad?" #: Source/control.cpp:661 msgid "Invalid arena-number. Valid numbers are:" msgstr "Vigane areeni number. Kehtivad numbrid on:" #: Source/control.cpp:667 msgid "To enter a arena, you need to be in town or another arena." msgstr "Arena sisenemiseks pead olema linnas või teises arenas." #: Source/control.cpp:705 msgid "Inspecting only supported in multiplayer." msgstr "Uurimine on toetatud ainult mitmikmängus." #: Source/control.cpp:710 Source/control.cpp:1001 msgid "Stopped inspecting players." msgstr "Mängijate kontrollimine peatatud." #: Source/control.cpp:725 msgid "No players found with such a name" msgstr "Sellise nimega mängijaid ei leitud" #: Source/control.cpp:731 msgid "Inspecting player: " msgstr "Mängija kontrollimine: " #: Source/control.cpp:800 msgid "Prints help overview or help for a specific command." msgstr "Prindib abi ülevaate või abi konkreetse käsu jaoks." #: Source/control.cpp:800 msgid "[command]" msgstr "[käsk]" #: Source/control.cpp:801 msgid "Enter a PvP Arena." msgstr "Sisene PvP areenile." #: Source/control.cpp:801 msgid "" msgstr "" #: Source/control.cpp:802 msgid "Gives Arena Potions." msgstr "Annab areeni potioneid." #: Source/control.cpp:802 msgid "" msgstr "" #: Source/control.cpp:803 msgid "Inspects stats and equipment of another player." msgstr "Uurib teise mängija statistikat ja varustust." #: Source/control.cpp:803 msgid "" msgstr "" #: Source/control.cpp:804 msgid "Show seed infos for current level." msgstr "Kuva praeguse taseme seemneinfo." #: Source/control.cpp:1311 msgid "Player friendly" msgstr "Mängijasõbralik" #: Source/control.cpp:1313 msgid "Player attack" msgstr "Mängija rünnak" #: Source/control.cpp:1316 #, c++-format msgid "Hotkey: {:s}" msgstr "Kiirklahv: {:s}" #: Source/control.cpp:1328 msgid "Select current spell button" msgstr "Vali praegune loitsu nupp" #: Source/control.cpp:1331 msgid "Hotkey: 's'" msgstr "Kiirklahv: 's'" #: Source/control.cpp:1337 Source/panels/spell_list.cpp:153 #, c++-format msgid "{:s} Skill" msgstr "{:s} oskus" #: Source/control.cpp:1340 Source/panels/spell_list.cpp:160 #, c++-format msgid "{:s} Spell" msgstr "{:s} loits" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:165 msgid "Spell Level 0 - Unusable" msgstr "Loitsu tase 0 - Kasutuskõlbmatu" #: Source/control.cpp:1342 Source/panels/spell_list.cpp:167 #, c++-format msgid "Spell Level {:d}" msgstr "Loitsu tase {:d}" #: Source/control.cpp:1345 Source/panels/spell_list.cpp:174 #, c++-format msgid "Scroll of {:s}" msgstr "{:s} kerimine" #: Source/control.cpp:1349 Source/panels/spell_list.cpp:178 #, c++-format msgid "{:d} Scroll" msgid_plural "{:d} Scrolls" msgstr[0] "{:d} Kirjarull" msgstr[1] "{:d} Kirjarullid" #: Source/control.cpp:1352 Source/panels/spell_list.cpp:185 #, c++-format msgid "Staff of {:s}" msgstr "{:s} sau" #: Source/control.cpp:1353 Source/panels/spell_list.cpp:187 #, c++-format msgid "{:d} Charge" msgid_plural "{:d} Charges" msgstr[0] "{:d} Laeng" msgstr[1] "{:d} Laengut" #: Source/control.cpp:1487 Source/inv.cpp:1979 Source/inv.cpp:1980 #: Source/items.cpp:3808 #, c++-format msgid "{:s} gold piece" msgid_plural "{:s} gold pieces" msgstr[0] "{:s} kuldmünt" msgstr[1] "{:s} kuldmünti" #: Source/control.cpp:1489 msgid "Requirements not met" msgstr "Nõuded ei ole täidetud" #: Source/control.cpp:1518 #, c++-format msgid "{:s}, Level: {:d}" msgstr "{:s}, Tase: {:d}" #: Source/control.cpp:1519 #, c++-format msgid "Hit Points {:d} of {:d}" msgstr "Elupunktid {:d} / {:d}" #: Source/control.cpp:1525 #, fuzzy #| msgid "Right-click to use" msgid "Right click to inspect" msgstr "Paremklõpsake kasutamiseks" #: Source/control.cpp:1573 msgid "Level Up" msgstr "Taseme tõus" #: Source/control.cpp:1687 msgid "You have died" msgstr "" #: Source/control.cpp:1695 msgid "ESC" msgstr "" #: Source/control.cpp:1701 msgid "Menu Button" msgstr "" #: Source/control.cpp:1709 #, c++-format msgid "Press {} to load last save." msgstr "" #: Source/control.cpp:1711 #, c++-format msgid "Press {} to return to Main Menu." msgstr "" #: Source/control.cpp:1714 #, c++-format msgid "Press {} to restart in town." msgstr "" #. TRANSLATORS: {:s} is a number with separators. Dialog is shown when splitting a stash of Gold. #: Source/control.cpp:1732 #, c++-format msgid "You have {:s} gold piece. How many do you want to remove?" msgid_plural "You have {:s} gold pieces. How many do you want to remove?" msgstr[0] "Sul on {:s} kuldmünt. Mitu soovid eemaldada?" msgstr[1] "Sul on {:s} kuldmünti. Mitu soovid eemaldada?" #: Source/cursor.cpp:621 msgid "Town Portal" msgstr "Linnaportaal" #: Source/cursor.cpp:622 #, c++-format msgid "from {:s}" msgstr "pärit {:s}" #: Source/cursor.cpp:635 msgid "Portal to" msgstr "Portaal siia" #: Source/cursor.cpp:636 msgid "The Unholy Altar" msgstr "Püha Altar" #: Source/cursor.cpp:636 msgid "level 15" msgstr "tase 15" #. TRANSLATORS: Error message when a data file is missing or corrupt. Arguments are {file name} #: Source/data/file.cpp:52 #, c++-format msgid "Unable to load data from file {0}" msgstr "Ei saa laadida andmeid failist {0}" #. TRANSLATORS: Error message when a data file is empty or only contains the header row. Arguments are {file name} #: Source/data/file.cpp:57 #, c++-format msgid "{0} is incomplete, please check the file contents." msgstr "{0} on puudulik, palun kontrollige faili sisu." #. TRANSLATORS: Error message when a data file doesn't contain the expected columns. Arguments are {file name} #: Source/data/file.cpp:62 #, c++-format msgid "" "Your {0} file doesn't have the expected columns, please make sure it matches " "the documented format." msgstr "" "Teie {0} failis ei ole oodatud veerge, palun veenduge, et see vastab " "dokumenteeritud vormingule." #. TRANSLATORS: Error message when parsing a data file and a text value is encountered when a number is expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:77 #, c++-format msgid "Non-numeric value {0} for {1} in {2} at row {3} and column {4}" msgstr "Mittearvuline väärtus {0} jaoks {1} failis {2} reas {3} ja veerus {4}" #. TRANSLATORS: Error message when parsing a data file and we find a number larger than expected. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:83 #, c++-format msgid "Out of range value {0} for {1} in {2} at row {3} and column {4}" msgstr "Väärtus {0} on vahemikust väljas {1} failis {2} reas {3} ja veerus {4}" #. TRANSLATORS: Error message when we find an unrecognised value in a key column. Arguments are {found value}, {column heading}, {file name}, {row/record number}, {column/field number} #: Source/data/file.cpp:89 #, c++-format msgid "Invalid value {0} for {1} in {2} at row {3} and column {4}" msgstr "Vigane väärtus {0} {1} jaoks failis {2} reas {3} ja veerus {4}" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:989 msgid "Print this message and exit" msgstr "Prindi see sõnum ja välju" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:990 msgid "Print the version and exit" msgstr "Prindi versioon ja välju" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:991 msgid "Specify the folder of diabdat.mpq" msgstr "Määrake diabdat.mpq kaust" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:992 msgid "Specify the folder of save files" msgstr "Määrake salvestusfailide kaust" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:993 msgid "Specify the location of diablo.ini" msgstr "Määrake diablo.ini asukoht" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:994 msgid "Specify the language code (e.g. en or pt_BR)" msgstr "Määrake keelekood (nt en või pt_BR)" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:995 msgid "Skip startup videos" msgstr "Jäta käivitamise videod vahele" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:996 msgid "Display frames per second" msgstr "Kuva kaadreid sekundis" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:997 msgid "Enable verbose logging" msgstr "Luba üksikasjalik logimine" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:999 msgid "Log to a file instead of stderr" msgstr "" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1002 msgid "Record a demo file" msgstr "Salvesta demo fail" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1003 msgid "Play a demo file" msgstr "Esita demofaili" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1004 msgid "Disable all frame limiting during demo playback" msgstr "Lülita demo taasesituse ajal kõik kaadrisageduse piirangud välja" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1007 msgid "Game selection:" msgstr "Mängu valik:" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1009 msgid "Force Shareware mode" msgstr "Sunni jagatud tarkvara režiimi" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1010 msgid "Force Diablo mode" msgstr "Sunni Diablo režiim" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1011 msgid "Force Hellfire mode" msgstr "Sunniviige Hellfire-režiim" #. TRANSLATORS: Commandline Option #: Source/diablo.cpp:1012 msgid "Hellfire options:" msgstr "Põrgutule valikud:" #: Source/diablo.cpp:1022 msgid "Report bugs at https://github.com/diasurgical/devilutionX/" msgstr "Teata vigadest aadressil https://github.com/diasurgical/devilutionX/" #: Source/diablo.cpp:1202 msgid "Please update devilutionx.mpq and fonts.mpq to the latest version" msgstr "Palun uuenda devilutionx.mpq ja fonts.mpq uusimale versioonile" #: Source/diablo.cpp:1204 msgid "" "Failed to load UI resources.\n" "\n" "Make sure devilutionx.mpq is in the game folder and that it is up to date." msgstr "" "Kasutajaliidese ressursside laadimine ebaõnnestus.\n" "\n" "Veenduge, et devilutionx.mpq on mängu kaustas ja et see on ajakohane." #: Source/diablo.cpp:1208 msgid "Please update fonts.mpq to the latest version" msgstr "Palun uuenda fonts.mpq uusimale versioonile" #: Source/diablo.cpp:1551 msgid "-- Network timeout --" msgstr "-- Võrgu ajutine katkestus --" #: Source/diablo.cpp:1552 msgid "-- Waiting for players --" msgstr "-- Ootan mängijaid --" #: Source/diablo.cpp:1575 msgid "No help available" msgstr "Abi pole saadaval" #: Source/diablo.cpp:1576 msgid "while in stores" msgstr "kauplustes olles" #: Source/diablo.cpp:1774 Source/diablo.cpp:2094 #, c++-format msgid "Belt item {}" msgstr "Vöö ese {}" #: Source/diablo.cpp:1775 Source/diablo.cpp:2095 msgid "Use Belt item." msgstr "Kasutage vöö eset." #: Source/diablo.cpp:1790 Source/diablo.cpp:2110 #, c++-format msgid "Quick spell {}" msgstr "Kiirloits {}" #: Source/diablo.cpp:1791 Source/diablo.cpp:2111 msgid "Hotkey for skill or spell." msgstr "Klahvivajutus oskuse või loitsu jaoks." #: Source/diablo.cpp:1809 msgid "Previous quick spell" msgstr "Eelmine kiire loits" #: Source/diablo.cpp:1810 msgid "Selects the previous quick spell (cycles)." msgstr "Valib eelmise kiire loitsu (tsüklis)." #: Source/diablo.cpp:1817 msgid "Next quick spell" msgstr "Järgmine kiire loits" #: Source/diablo.cpp:1818 msgid "Selects the next quick spell (cycles)." msgstr "Valib järgmise kiire loitsu (tsüklitena)." #: Source/diablo.cpp:1825 Source/diablo.cpp:2238 msgid "Use health potion" msgstr "Kasutage tervise eliksiiri" #: Source/diablo.cpp:1826 Source/diablo.cpp:2239 msgid "Use health potions from belt." msgstr "Kasutage tervise potioneid vööst." #: Source/diablo.cpp:1833 Source/diablo.cpp:2246 msgid "Use mana potion" msgstr "Kasutage mana jooki" #: Source/diablo.cpp:1834 Source/diablo.cpp:2247 msgid "Use mana potions from belt." msgstr "Kasutage vööst mana potioneid." #: Source/diablo.cpp:1841 Source/diablo.cpp:2294 msgid "Speedbook" msgstr "Kiirraamat" #: Source/diablo.cpp:1842 Source/diablo.cpp:2295 msgid "Open Speedbook." msgstr "Ava kiirusraamat." #: Source/diablo.cpp:1849 Source/diablo.cpp:2451 msgid "Quick save" msgstr "Kiirsalvestus" #: Source/diablo.cpp:1850 Source/diablo.cpp:2452 msgid "Saves the game." msgstr "Salvestab mängu." #: Source/diablo.cpp:1857 Source/diablo.cpp:2459 msgid "Quick load" msgstr "Kiirlaadimine" #: Source/diablo.cpp:1858 Source/diablo.cpp:2460 msgid "Loads the game." msgstr "Laeb mängu." #: Source/diablo.cpp:1866 msgid "Quit game" msgstr "Lõpeta mäng" #: Source/diablo.cpp:1867 msgid "Closes the game." msgstr "Sulgeb mängu." #: Source/diablo.cpp:1873 msgid "Stop hero" msgstr "Peatu, kangelane" #: Source/diablo.cpp:1874 msgid "Stops walking and cancel pending actions." msgstr "Lõpetab kõndimise ja tühistab ootel olevad toimingud." #: Source/diablo.cpp:1881 Source/diablo.cpp:2467 msgid "Item highlighting" msgstr "Esemete esiletõstmine" #: Source/diablo.cpp:1882 Source/diablo.cpp:2468 msgid "Show/hide items on ground." msgstr "Kuva/peida maas olevaid esemeid." #: Source/diablo.cpp:1888 Source/diablo.cpp:2474 msgid "Toggle item highlighting" msgstr "Lülita esiletõstetud esemed sisse/välja" #: Source/diablo.cpp:1889 Source/diablo.cpp:2475 msgid "Permanent show/hide items on ground." msgstr "Püsivalt näita/peida esemeid maapinnal." #: Source/diablo.cpp:1895 Source/diablo.cpp:2304 msgid "Toggle automap" msgstr "Lülita automaatkaart sisse/välja" #: Source/diablo.cpp:1896 Source/diablo.cpp:2305 msgid "Toggles if automap is displayed." msgstr "Lülitab sisse/välja automaadi kuvamise." #: Source/diablo.cpp:1903 msgid "Cycle map type" msgstr "Vaheta kaarditüüpi" #: Source/diablo.cpp:1904 msgid "Opaque -> Transparent -> Minimap -> None" msgstr "Läbipaistmatu -> Läbipaistev -> Minikaart -> Puudub" #: Source/diablo.cpp:1913 Source/diablo.cpp:2265 msgid "Open Inventory screen." msgstr "Ava laoseis." #: Source/diablo.cpp:1920 Source/diablo.cpp:2254 msgid "Character" msgstr "Tegelane" #: Source/diablo.cpp:1921 Source/diablo.cpp:2255 msgid "Open Character screen." msgstr "Ava tegelase ekraan." #: Source/diablo.cpp:1928 msgid "Party" msgstr "" #: Source/diablo.cpp:1929 msgid "Open side Party panel." msgstr "" #: Source/diablo.cpp:1936 Source/diablo.cpp:2274 msgid "Quest log" msgstr "Ülesannete logi" #: Source/diablo.cpp:1937 Source/diablo.cpp:2275 msgid "Open Quest log." msgstr "Ava ülesannete logi." #: Source/diablo.cpp:1944 Source/diablo.cpp:2284 msgid "Spellbook" msgstr "Loitsuraamat" #: Source/diablo.cpp:1945 Source/diablo.cpp:2285 msgid "Open Spellbook." msgstr "Ava loitsuraamat." #: Source/diablo.cpp:1953 #, c++-format msgid "Quick Message {}" msgstr "Kiire sõnum {}" #: Source/diablo.cpp:1954 msgid "Use Quick Message in chat." msgstr "Kasutage vestluses kiirsõnumit." #: Source/diablo.cpp:1963 Source/diablo.cpp:2481 msgid "Hide Info Screens" msgstr "Peida infoekraanid" #: Source/diablo.cpp:1964 Source/diablo.cpp:2482 msgid "Hide all info screens." msgstr "Peida kõik infoekraanid." #: Source/diablo.cpp:1987 Source/diablo.cpp:2505 Source/options.cpp:737 msgid "Zoom" msgstr "Suumi" #: Source/diablo.cpp:1988 Source/diablo.cpp:2506 msgid "Zoom Game Screen." msgstr "Suumi mänguekraani." #: Source/diablo.cpp:1998 Source/diablo.cpp:2516 msgid "Pause Game" msgstr "Pausi mäng" #: Source/diablo.cpp:1999 Source/diablo.cpp:2005 Source/diablo.cpp:2517 msgid "Pauses the game." msgstr "Peatab mängu." #: Source/diablo.cpp:2004 msgid "Pause Game (Alternate)" msgstr "Pausi mäng (alternatiivne)" #: Source/diablo.cpp:2010 Source/diablo.cpp:2522 #, fuzzy #| msgid "Increase screen brightness." msgid "Decrease Brightness" msgstr "Suurenda ekraani heledust." #: Source/diablo.cpp:2011 Source/diablo.cpp:2523 msgid "Reduce screen brightness." msgstr "Vähenda ekraani heledust." #: Source/diablo.cpp:2018 Source/diablo.cpp:2530 #, fuzzy #| msgid "Increase screen brightness." msgid "Increase Brightness" msgstr "Suurenda ekraani heledust." #: Source/diablo.cpp:2019 Source/diablo.cpp:2531 msgid "Increase screen brightness." msgstr "Suurenda ekraani heledust." #: Source/diablo.cpp:2026 Source/diablo.cpp:2538 msgid "Help" msgstr "Aita" #: Source/diablo.cpp:2027 Source/diablo.cpp:2539 msgid "Open Help Screen." msgstr "Ava Abi-ekraan." #: Source/diablo.cpp:2034 Source/diablo.cpp:2546 msgid "Screenshot" msgstr "Kuvatõmmis" #: Source/diablo.cpp:2035 Source/diablo.cpp:2547 msgid "Takes a screenshot." msgstr "Teeb ekraanipildi." #: Source/diablo.cpp:2041 Source/diablo.cpp:2553 msgid "Game info" msgstr "Mängu info" #: Source/diablo.cpp:2042 Source/diablo.cpp:2554 msgid "Displays game infos." msgstr "Kuvab mängu infot." #. TRANSLATORS: {:s} means: Character Name, Game Version, Game Difficulty. #: Source/diablo.cpp:2046 Source/diablo.cpp:2558 #, c++-format msgid "{:s} {:s}" msgstr "{:s} {:s}" #: Source/diablo.cpp:2055 Source/diablo.cpp:2575 msgid "Chat Log" msgstr "Vestluse logi" #: Source/diablo.cpp:2056 Source/diablo.cpp:2576 msgid "Displays chat log." msgstr "Kuvab vestluslogi." #: Source/diablo.cpp:2063 Source/diablo.cpp:2567 msgid "Sort Inventory" msgstr "Sorteeri inventar" #: Source/diablo.cpp:2064 Source/diablo.cpp:2568 msgid "Sorts the inventory." msgstr "Järjestab inventari." #: Source/diablo.cpp:2072 msgid "Console" msgstr "Konsool" #: Source/diablo.cpp:2073 msgid "Opens Lua console." msgstr "Avab Lua konsooli." #: Source/diablo.cpp:2129 msgid "Primary action" msgstr "Põhitegevus" #: Source/diablo.cpp:2130 msgid "Attack monsters, talk to towners, lift and place inventory items." msgstr "" "Ründa koletisi, räägi linnaelanikega, tõsta ja paiguta inventari esemeid." #: Source/diablo.cpp:2144 msgid "Secondary action" msgstr "Teisene tegevus" #: Source/diablo.cpp:2145 msgid "Open chests, interact with doors, pick up items." msgstr "Ava kirste, suhtle ustega, korja esemeid." #: Source/diablo.cpp:2159 msgid "Spell action" msgstr "Loitsu tegevus" #: Source/diablo.cpp:2160 msgid "Cast the active spell." msgstr "Loitsu aktiivne loits." #: Source/diablo.cpp:2174 msgid "Cancel action" msgstr "Tühista toiming" #: Source/diablo.cpp:2175 msgid "Close menus." msgstr "Sulge menüüd." #: Source/diablo.cpp:2200 msgid "Move up" msgstr "Liigu üles" #: Source/diablo.cpp:2201 msgid "Moves the player character up." msgstr "Liigutab mängijategelast üles." #: Source/diablo.cpp:2206 msgid "Move down" msgstr "Liigu alla" #: Source/diablo.cpp:2207 msgid "Moves the player character down." msgstr "Liigutab mängijategelast alla." #: Source/diablo.cpp:2212 msgid "Move left" msgstr "Liigu vasakule" #: Source/diablo.cpp:2213 msgid "Moves the player character left." msgstr "Liigutab mängijategelast vasakule." #: Source/diablo.cpp:2218 msgid "Move right" msgstr "Liigu paremale" #: Source/diablo.cpp:2219 msgid "Moves the player character right." msgstr "Liigutab mängijategelast paremale." #: Source/diablo.cpp:2224 msgid "Stand ground" msgstr "Seisa paigal" #: Source/diablo.cpp:2225 msgid "Hold to prevent the player from moving." msgstr "Hoia all, et takistada mängijal liikumist." #: Source/diablo.cpp:2230 msgid "Toggle stand ground" msgstr "Lülita paigale jäämine sisse/välja" #: Source/diablo.cpp:2231 msgid "Toggle whether the player moves." msgstr "Lülita mängija liikumine sisse või välja." #: Source/diablo.cpp:2310 #, fuzzy #| msgid "Automap" msgid "Automap Move Up" msgstr "Automaatkaart" #: Source/diablo.cpp:2311 msgid "Moves the automap up when active." msgstr "" #: Source/diablo.cpp:2316 #, fuzzy #| msgid "Move down" msgid "Automap Move Down" msgstr "Liigu alla" #: Source/diablo.cpp:2317 msgid "Moves the automap down when active." msgstr "" #: Source/diablo.cpp:2322 #, fuzzy #| msgid "Move left" msgid "Automap Move Left" msgstr "Liigu vasakule" #: Source/diablo.cpp:2323 #, fuzzy #| msgid "Moves the player character up." msgid "Moves the automap left when active." msgstr "Liigutab mängijategelast üles." #: Source/diablo.cpp:2328 #, fuzzy #| msgid "Move right" msgid "Automap Move Right" msgstr "Liigu paremale" #: Source/diablo.cpp:2329 msgid "Moves the automap right when active." msgstr "" #: Source/diablo.cpp:2334 msgid "Move mouse up" msgstr "Liiguta hiirt üles" #: Source/diablo.cpp:2335 msgid "Simulates upward mouse movement." msgstr "Simuleerib hiire liikumist ülespoole." #: Source/diablo.cpp:2340 msgid "Move mouse down" msgstr "Liiguta hiirt alla" #: Source/diablo.cpp:2341 msgid "Simulates downward mouse movement." msgstr "Simuleerib hiire liikumist allapoole." #: Source/diablo.cpp:2346 msgid "Move mouse left" msgstr "Liiguta hiirt vasakule" #: Source/diablo.cpp:2347 msgid "Simulates leftward mouse movement." msgstr "Simuleerib hiire liikumist vasakule." #: Source/diablo.cpp:2352 msgid "Move mouse right" msgstr "Liiguta hiirt paremale" #: Source/diablo.cpp:2353 msgid "Simulates rightward mouse movement." msgstr "Simuleerib hiire liikumist paremale." #: Source/diablo.cpp:2371 Source/diablo.cpp:2378 msgid "Left mouse click" msgstr "Vasaku hiireklõps" #: Source/diablo.cpp:2372 Source/diablo.cpp:2379 msgid "Simulates the left mouse button." msgstr "Simuleerib vasaku hiireklahvi." #: Source/diablo.cpp:2396 Source/diablo.cpp:2403 msgid "Right mouse click" msgstr "Parem hiireklõps" #: Source/diablo.cpp:2397 Source/diablo.cpp:2404 msgid "Simulates the right mouse button." msgstr "Simuleerib parema hiireklahvi." #: Source/diablo.cpp:2410 msgid "Gamepad hotspell menu" msgstr "Mängupuldi kiirloitsude menüü" #: Source/diablo.cpp:2411 msgid "Hold to set or use spell hotkeys." msgstr "Hoia all, et määrata või kasutada loitsu kiirklahve." #: Source/diablo.cpp:2417 msgid "Gamepad menu navigator" msgstr "Mängupuldi menüü navigeerija" #: Source/diablo.cpp:2418 msgid "Hold to access gamepad menu navigation." msgstr "Hoia all, et pääseda mängupuldi menüü navigeerimisele." #: Source/diablo.cpp:2433 Source/diablo.cpp:2442 msgid "Toggle game menu" msgstr "Lülita mängumenüü" #: Source/diablo.cpp:2434 Source/diablo.cpp:2443 msgid "Opens the game menu." msgstr "Avab mängu menüü." #: Source/diablo_msg.cpp:63 msgid "Game saved" msgstr "Mäng salvestatud" #: Source/diablo_msg.cpp:64 msgid "No multiplayer functions in demo" msgstr "Demos pole mitmikmängu funktsioone" #: Source/diablo_msg.cpp:65 msgid "Direct Sound Creation Failed" msgstr "Direct Soundi loomine ebaõnnestus" #: Source/diablo_msg.cpp:66 msgid "Not available in shareware version" msgstr "Pole saadaval jagamisversioonis" #: Source/diablo_msg.cpp:67 msgid "Not enough space to save" msgstr "Pole piisavalt ruumi salvestamiseks" #: Source/diablo_msg.cpp:68 msgid "No Pause in town" msgstr "Linnas ei saa pausi teha" #: Source/diablo_msg.cpp:69 msgid "Copying to a hard disk is recommended" msgstr "Soovitatav on kopeerida kõvakettale" #: Source/diablo_msg.cpp:70 msgid "Multiplayer sync problem" msgstr "Mitmikmängu sünkroonimise probleem" #: Source/diablo_msg.cpp:71 msgid "No pause in multiplayer" msgstr "Mitmikmängus pole pausi" #: Source/diablo_msg.cpp:73 msgid "Saving..." msgstr "Salvestamine..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:74 msgid "Some are weakened as one grows strong" msgstr "Mõned nõrgenevad, kui üks tugevneb" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:75 msgid "New strength is forged through destruction" msgstr "Uus jõud sepistatakse hävingu kaudu" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:76 msgid "Those who defend seldom attack" msgstr "Need, kes kaitsevad, ründavad harva" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:77 msgid "The sword of justice is swift and sharp" msgstr "Õigluse mõõk on kiire ja terav" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:78 msgid "While the spirit is vigilant the body thrives" msgstr "Kui vaim on valvas, siis keha õitseb" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:79 msgid "The powers of mana refocused renews" msgstr "Mana jõudude keskendumine uuendab" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:80 msgid "Time cannot diminish the power of steel" msgstr "Aeg ei suuda terase jõudu kahandada" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:81 msgid "Magic is not always what it seems to be" msgstr "Maagia ei ole alati see, mis ta näib olevat" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:82 msgid "What once was opened now is closed" msgstr "Mis kord oli avatud, on nüüd suletud" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:83 msgid "Intensity comes at the cost of wisdom" msgstr "Intensiivsus tuleb tarkuse hinnaga" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:84 msgid "Arcane power brings destruction" msgstr "Arkaaniline jõud toob hävingu" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:85 msgid "That which cannot be held cannot be harmed" msgstr "Seda, mida ei saa hoida, ei saa ka kahjustada" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:86 msgid "Crimson and Azure become as the sun" msgstr "Karmiin ja asuur muutuvad kui päike" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:87 msgid "Knowledge and wisdom at the cost of self" msgstr "Teadmised ja tarkus enese arvelt" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:88 msgid "Drink and be refreshed" msgstr "Joo ja tunne end värskena" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:89 msgid "Wherever you go, there you are" msgstr "Kuhu iganes sa lähed, seal sa oled" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:90 msgid "Energy comes at the cost of wisdom" msgstr "Energia tuleb tarkuse hinnaga" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:91 msgid "Riches abound when least expected" msgstr "Rikkus tuleb siis, kui seda kõige vähem ootad" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:92 msgid "Where avarice fails, patience gains reward" msgstr "Kus ahnus jääb hätta, seal kannatlikkus toob tasu" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:93 msgid "Blessed by a benevolent companion!" msgstr "Õnnistatud heatahtliku kaaslase poolt!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:94 msgid "The hands of men may be guided by fate" msgstr "Inimeste käsi võib saatus juhtida" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:95 msgid "Strength is bolstered by heavenly faith" msgstr "Jõud on taevasest usust tugevdatud" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:96 msgid "The essence of life flows from within" msgstr "Elu olemus voolab seestpoolt" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:97 msgid "The way is made clear when viewed from above" msgstr "Tee saab selgeks, kui vaadata ülevalt" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:98 msgid "Salvation comes at the cost of wisdom" msgstr "Pääsemine tuleb tarkuse hinnaga" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:99 msgid "Mysteries are revealed in the light of reason" msgstr "Mõistuse valguses paljastuvad saladused" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:100 msgid "Those who are last may yet be first" msgstr "Need, kes on viimased, võivad veel esimesteks saada" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:101 msgid "Generosity brings its own rewards" msgstr "Heldus toob endaga kaasa oma tasu" #: Source/diablo_msg.cpp:102 msgid "You must be at least level 8 to use this." msgstr "Selle kasutamiseks peab sul olema vähemalt 8. tase." #: Source/diablo_msg.cpp:103 msgid "You must be at least level 13 to use this." msgstr "Sa pead olema vähemalt 13. tasemel, et seda kasutada." #: Source/diablo_msg.cpp:104 msgid "You must be at least level 17 to use this." msgstr "Sa pead olema vähemalt 17. tasemel, et seda kasutada." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:105 msgid "Arcane knowledge gained!" msgstr "Salapärane teadmine omandatud!" #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:106 msgid "That which does not kill you..." msgstr "See, mis sind ei tapa..." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:107 msgid "Knowledge is power." msgstr "Teadmine on jõud." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:108 msgid "Give and you shall receive." msgstr "Anna ja sulle antakse." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:109 msgid "Some experience is gained by touch." msgstr "Mõningaid kogemusi saadakse puudutuse kaudu." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:110 msgid "There's no place like home." msgstr "Kodu pole kusagil mujal." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:111 msgid "Spiritual energy is restored." msgstr "Vaimne energia on taastatud." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:112 msgid "You feel more agile." msgstr "Sa tunned end väledamana." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:113 msgid "You feel stronger." msgstr "Sa tunned end tugevamana." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:114 msgid "You feel wiser." msgstr "Sa tunned end targemana." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:115 msgid "You feel refreshed." msgstr "Sa tunned end värskena." #. TRANSLATORS: Shrine Text. Keep atmospheric. :) #: Source/diablo_msg.cpp:116 msgid "That which can break will." msgstr "See, mis võib puruneda, purunebki." #: Source/discord/discord.cpp:81 msgid "Cathedral" msgstr "Katedraal" #: Source/discord/discord.cpp:81 msgid "Catacombs" msgstr "Katakombid" #: Source/discord/discord.cpp:81 msgid "Caves" msgstr "Koopad" #: Source/discord/discord.cpp:81 msgid "Nest" msgstr "Pesa" #: Source/discord/discord.cpp:81 msgid "Crypt" msgstr "Krüpt" #. TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3" #: Source/discord/discord.cpp:97 #, c++-format msgid "{} {}" msgstr "{} {}" #. TRANSLATORS: Discord character, i.e. "Lv 6 Warrior" #: Source/discord/discord.cpp:104 #, c++-format msgid "Lv {} {}" msgstr "Tase {} {}" #. TRANSLATORS: Discord state i.e. "Nightmare difficulty" #: Source/discord/discord.cpp:116 #, c++-format msgid "{} difficulty" msgstr "{} raskusaste" #. TRANSLATORS: Discord activity, not in game #: Source/discord/discord.cpp:197 msgid "In Menu" msgstr "Menüüs" #: Source/dvlnet/loopback.cpp:117 msgid "loopback" msgstr "tagasiside" #: Source/dvlnet/tcp_client.cpp:112 msgid "Unable to connect" msgstr "Ühendamine ebaõnnestus" #: Source/dvlnet/tcp_client.cpp:150 msgid "error: read 0 bytes from server" msgstr "viga: serverilt loeti 0 baiti" #: Source/engine/assets.cpp:244 #, c++-format msgid "" "Failed to open file:\n" "{:s}\n" "\n" "{:s}\n" "\n" "The MPQ file(s) might be damaged. Please check the file integrity." msgstr "" #: Source/engine/assets.cpp:426 msgid "diabdat.mpq or spawn.mpq" msgstr "diabdat.mpq või spawn.mpq" #: Source/engine/assets.cpp:464 msgid "Some Hellfire MPQs are missing" msgstr "Mõned Hellfire MPQ-d on puudu" #: Source/engine/assets.cpp:464 msgid "" "Not all Hellfire MPQs were found.\n" "Please copy all the hf*.mpq files." msgstr "" "Mitte kõiki Hellfire MPQ-sid ei leitud. Palun kopeerige kõik hf*.mpq failid." #: Source/engine/demomode.cpp:181 Source/options.cpp:535 msgid "Resolution" msgstr "Lahendus" #: Source/engine/demomode.cpp:183 Source/options.cpp:784 msgid "Run in Town" msgstr "Jookse linnas" #: Source/engine/demomode.cpp:184 Source/options.cpp:787 msgid "Theo Quest" msgstr "Theo Otsing" #: Source/engine/demomode.cpp:185 Source/options.cpp:788 msgid "Cow Quest" msgstr "Lehma ülesanne" #: Source/engine/demomode.cpp:186 Source/options.cpp:800 msgid "Auto Gold Pickup" msgstr "Automaatne kulla korjamine" #: Source/engine/demomode.cpp:187 Source/options.cpp:801 msgid "Auto Elixir Pickup" msgstr "Automaatne eliksiiri korjamine" #: Source/engine/demomode.cpp:188 Source/options.cpp:802 msgid "Auto Oil Pickup" msgstr "Automaatne õli korjamine" #: Source/engine/demomode.cpp:189 Source/options.cpp:803 msgid "Auto Pickup in Town" msgstr "Automaatne korjamine linnas" #: Source/engine/demomode.cpp:190 Source/options.cpp:804 msgid "Adria Refills Mana" msgstr "Adria täidab mana" #: Source/engine/demomode.cpp:191 Source/options.cpp:805 msgid "Auto Equip Weapons" msgstr "Relvade automaatne varustamine" #: Source/engine/demomode.cpp:192 Source/options.cpp:806 msgid "Auto Equip Armor" msgstr "Automaatne soomuse varustamine" #: Source/engine/demomode.cpp:193 Source/options.cpp:807 msgid "Auto Equip Helms" msgstr "Kiivrite automaatne varustamine" #: Source/engine/demomode.cpp:194 Source/options.cpp:808 msgid "Auto Equip Shields" msgstr "Automaatne kilpide varustus" #: Source/engine/demomode.cpp:195 Source/options.cpp:809 msgid "Auto Equip Jewelry" msgstr "Ehete automaatne varustamine" #: Source/engine/demomode.cpp:196 Source/options.cpp:810 msgid "Randomize Quests" msgstr "Juhuslikud ülesanded" #: Source/engine/demomode.cpp:197 Source/options.cpp:812 msgid "Show Item Labels" msgstr "Kuva üksuste sildid" #: Source/engine/demomode.cpp:198 Source/options.cpp:813 msgid "Auto Refill Belt" msgstr "Vöö automaatne täitmine" #: Source/engine/demomode.cpp:199 Source/options.cpp:814 msgid "Disable Crippling Shrines" msgstr "Keela halvavad pühamud" #: Source/engine/demomode.cpp:203 Source/options.cpp:816 msgid "Heal Potion Pickup" msgstr "Tervendamisjoogi korjamine" #: Source/engine/demomode.cpp:204 Source/options.cpp:817 msgid "Full Heal Potion Pickup" msgstr "Täieliku tervendamise joogi korje" #: Source/engine/demomode.cpp:205 Source/options.cpp:818 msgid "Mana Potion Pickup" msgstr "Mana poti valimine" #: Source/engine/demomode.cpp:206 Source/options.cpp:819 msgid "Full Mana Potion Pickup" msgstr "Täis mana poti korjamine" #: Source/engine/demomode.cpp:207 Source/options.cpp:820 msgid "Rejuvenation Potion Pickup" msgstr "Noorendamise joogi korjamine" #: Source/engine/demomode.cpp:208 Source/options.cpp:821 msgid "Full Rejuvenation Potion Pickup" msgstr "Täieliku noorendamise joogi korjamine" #: Source/gamemenu.cpp:48 Source/gamemenu.cpp:60 msgid "Options" msgstr "Valikud" #: Source/gamemenu.cpp:49 msgid "Save Game" msgstr "Salvesta mäng" #: Source/gamemenu.cpp:51 Source/gamemenu.cpp:61 #, fuzzy #| msgid "Main Menu" msgid "Exit to Main Menu" msgstr "Põhimenüü" #: Source/gamemenu.cpp:52 Source/gamemenu.cpp:62 msgid "Quit Game" msgstr "Lõpeta mäng" #: Source/gamemenu.cpp:71 msgid "Gamma" msgstr "Gamma" #: Source/gamemenu.cpp:72 Source/gamemenu.cpp:171 msgid "Speed" msgstr "Kiirus" #: Source/gamemenu.cpp:80 msgid "Music Disabled" msgstr "Muusika on keelatud" #: Source/gamemenu.cpp:84 msgid "Sound" msgstr "Heli" #: Source/gamemenu.cpp:85 msgid "Sound Disabled" msgstr "Heli on keelatud" #: Source/gmenu.cpp:179 msgid "Pause" msgstr "Paus" #: Source/help.cpp:28 msgid "$Keyboard Shortcuts:" msgstr "$Klaviatuuri otseteed:" #: Source/help.cpp:29 msgid "F1: Open Help Screen" msgstr "F1: Ava abiaken" #: Source/help.cpp:30 msgid "Esc: Display Main Menu" msgstr "Esc: Kuva põhimenüü" #: Source/help.cpp:31 msgid "Tab: Display Auto-map" msgstr "Tab: Kuva automaatkaarti" #: Source/help.cpp:32 msgid "Space: Hide all info screens" msgstr "Tühik: Peida kõik infoekraanid" #: Source/help.cpp:33 msgid "S: Open Speedbook" msgstr "S: Ava kiirusraamat" #: Source/help.cpp:34 msgid "B: Open Spellbook" msgstr "B: Ava loitsuraamat" #: Source/help.cpp:35 msgid "I: Open Inventory screen" msgstr "I: Ava inventari ekraan" #: Source/help.cpp:36 msgid "C: Open Character screen" msgstr "C: Ava oma tegelaskuju ekraan" #: Source/help.cpp:37 msgid "Q: Open Quest log" msgstr "K: Ava ülesannete logi" #: Source/help.cpp:38 msgid "F: Reduce screen brightness" msgstr "F: Vähenda ekraani heledust" #: Source/help.cpp:39 msgid "G: Increase screen brightness" msgstr "G: Suurenda ekraani heledust" #: Source/help.cpp:40 msgid "Z: Zoom Game Screen" msgstr "Z: Suurenda mänguekraani" #: Source/help.cpp:41 msgid "+ / -: Zoom Automap" msgstr "+ / -: Suumi automaatkaarti" #: Source/help.cpp:42 msgid "1 - 8: Use Belt item" msgstr "1 - 8: Kasuta vöö eset" #: Source/help.cpp:43 msgid "F5, F6, F7, F8: Set hotkey for skill or spell" msgstr "F5, F6, F7, F8: Määra oskuse või loitsu kiirklahv" #: Source/help.cpp:44 msgid "Shift + Left Mouse Button: Attack without moving" msgstr "Shift + Vasak hiireklahv: Ründa ilma liikumata" #: Source/help.cpp:45 msgid "Shift + Left Mouse Button (on character screen): Assign all stat points" msgstr "" "Shift + Vasak hiireklõps (tegelase ekraanil): Määra kõik statistika punktid" #: Source/help.cpp:46 msgid "" "Shift + Left Mouse Button (on inventory): Move item to belt or equip/unequip " "item" msgstr "" "Shift + Vasak hiireklõps (inventaris): Liiguta ese vööle või varusta/eemalda " "ese" #: Source/help.cpp:47 msgid "Shift + Left Mouse Button (on belt): Move item to inventory" msgstr "Shift + Vasak hiireklõps (vööl): Liiguta ese inventari" #: Source/help.cpp:49 msgid "$Movement:" msgstr "$Liikumine:" #: Source/help.cpp:50 msgid "" "If you hold the mouse button down while moving, the character will continue " "to move in that direction." msgstr "" "Kui hoiad hiire nuppu all liikumise ajal, liigub tegelane jätkuvalt selles " "suunas." #: Source/help.cpp:53 msgid "$Combat:" msgstr "$Võitlus:" #: Source/help.cpp:54 msgid "" "Holding down the shift key and then left-clicking allows the character to " "attack without moving." msgstr "" "Shift-klahvi all hoidmine ja seejärel vasakklõpsamine võimaldab tegelasel " "rünnata ilma liikumata." #: Source/help.cpp:57 msgid "$Auto-map:" msgstr "$Automaatkaart:" #: Source/help.cpp:58 msgid "" "To access the auto-map, click the 'MAP' button on the Information Bar or " "press 'TAB' on the keyboard. Zooming in and out of the map is done with the " "+ and - keys. Scrolling the map uses the arrow keys." msgstr "" "Automaatkaardi avamiseks klõpsa teaberibal nupule 'MAP' või vajuta " "klaviatuuril 'TAB'. Kaardi sisse- ja väljasuumimine toimub + ja - " "klahvidega. Kaardi kerimiseks kasuta nooleklahve." #: Source/help.cpp:63 msgid "$Picking up Objects:" msgstr "$Esemete korjamine:" #: Source/help.cpp:64 msgid "" "Useable items that are small in size, such as potions or scrolls, are " "automatically placed in your 'belt' located at the top of the Interface " "bar . When an item is placed in the belt, a small number appears in that " "box. Items may be used by either pressing the corresponding number or right-" "clicking on the item." msgstr "" "Kasutatavad esemed, mis on väikesed, nagu näiteks joogid või rullid, " "paigutatakse automaatselt sinu 'vööle', mis asub liideseriba ülaosas. Kui " "ese asetatakse vööle, ilmub sellesse kasti väike number. Esmeid saab " "kasutada kas vastava numbri vajutamisega või esemel paremklõpsuga." #: Source/help.cpp:70 msgid "$Gold:" msgstr "$Kuld:" #: Source/help.cpp:71 msgid "" "You can select a specific amount of gold to drop by right-clicking on a pile " "of gold in your inventory." msgstr "" "Sa saad valida konkreetse koguse kulda, mida maha panna, klõpsates oma " "inventaris kulla kuhjal parema hiireklahviga." #: Source/help.cpp:74 msgid "$Skills & Spells:" msgstr "$Osavused ja Loitsud:" #: Source/help.cpp:75 msgid "" "You can access your list of skills and spells by left-clicking on the " "'SPELLS' button in the interface bar. Memorized spells and those available " "through staffs are listed here. Left-clicking on the spell you wish to cast " "will ready the spell. A readied spell may be cast by simply right-clicking " "in the play area." msgstr "" "Oma oskuste ja loitsude loendi saad avada, klõpsates liideseribal " "vasakklõpsuga nupul 'LOITSUD'. Siin on loetletud meelde jäetud loitsud ja " "need, mis on saadaval keppide kaudu. Loitsu, mida soovid kasutada, saad " "valmis seada, klõpsates sellel vasakklõpsuga. Valmis seatud loitsu saab " "kasutada, klõpsates mängualal lihtsalt parema hiireklõpsuga." #: Source/help.cpp:81 msgid "$Using the Speedbook for Spells:" msgstr "$Loitsude kiirraamatu kasutamine:" #: Source/help.cpp:82 msgid "" "Left-clicking on the 'readied spell' button will open the 'Speedbook' which " "allows you to select a skill or spell for immediate use. To use a readied " "skill or spell, simply right-click in the main play area." msgstr "" "Vasakklõps 'valmis loitsu' nupul avab 'Kiiraamatu', mis võimaldab valida " "oskuse või loitsu koheseks kasutamiseks. Valmis oskuse või loitsu " "kasutamiseks klõpsa lihtsalt parema hiireklahviga mänguala peal." #: Source/help.cpp:86 msgid "" "Shift + Left-clicking on the 'select current spell' button will clear the " "readied spell." msgstr "" "Shift + vasakklõps nupul 'vali praegune loits' tühistab valmis pandud loitsu." #: Source/help.cpp:88 msgid "$Setting Spell Hotkeys:" msgstr "$Loitsuklahvide seadistamine:" #: Source/help.cpp:89 msgid "" "You can assign up to four Hotkeys for skills, spells or scrolls. Start by " "opening the 'speedbook' as described in the section above. Press the F5, F6, " "F7 or F8 keys after highlighting the spell you wish to assign." msgstr "" "Saate määrata kuni neli kiirklahvi oskuste, loitsude või rullide jaoks. " "Alustage 'kiirraamatu' avamisega, nagu on kirjeldatud ülaltoodud jaotises. " "Vajutage F5, F6, F7 või F8 klahve pärast loitsu esiletõstmist, mille soovite " "määrata." #: Source/help.cpp:94 msgid "$Spell Books:" msgstr "$Loitsuraamatud:" #: Source/help.cpp:95 msgid "" "Reading more than one book increases your knowledge of that spell, allowing " "you to cast the spell more effectively." msgstr "" "Rohkem kui ühe raamatu lugemine suurendab sinu teadmisi sellest loitsust, " "võimaldades sul loitsu tõhusamalt kasutada." #: Source/help.cpp:200 msgid "Shareware Hellfire Help" msgstr "Shareware Hellfire Abi" #: Source/help.cpp:200 msgid "Hellfire Help" msgstr "Põrgutule Abi" #: Source/help.cpp:202 msgid "Shareware Diablo Help" msgstr "Shareware Diablo Abi" #: Source/help.cpp:202 msgid "Diablo Help" msgstr "Diablo Abi" #: Source/help.cpp:234 Source/qol/chatlog.cpp:202 msgid "Press ESC to end or the arrow keys to scroll." msgstr "Vajuta ESC, et lõpetada, või nooleklahve, et kerida." #: Source/init.cpp:130 msgid "Unable to create main window" msgstr "Peaakna loomine ebaõnnestus" #: Source/inv.cpp:2228 msgid "No room for item" msgstr "Pole ruumi esemele" #: Source/items.cpp:212 Source/translation_dummy.cpp:298 msgid "Oil of Accuracy" msgstr "Täpsuse õli" #: Source/items.cpp:213 msgid "Oil of Mastery" msgstr "Meisterlikkuse õli" #: Source/items.cpp:214 Source/translation_dummy.cpp:299 msgid "Oil of Sharpness" msgstr "Teravuse õli" #: Source/items.cpp:215 msgid "Oil of Death" msgstr "Surmaõli" #: Source/items.cpp:216 msgid "Oil of Skill" msgstr "Oskuste õli" #: Source/items.cpp:217 Source/translation_dummy.cpp:251 msgid "Blacksmith Oil" msgstr "Sepa õli" #: Source/items.cpp:218 msgid "Oil of Fortitude" msgstr "Vastupidavuse õli" #: Source/items.cpp:219 msgid "Oil of Permanence" msgstr "Püsivuse õli" #: Source/items.cpp:220 msgid "Oil of Hardening" msgstr "Kõvastamise õli" #: Source/items.cpp:221 msgid "Oil of Imperviousness" msgstr "Läbimatuse õli" #. TRANSLATORS: Constructs item names. Format: {Item} of {Spell}. Example: War Staff of Firewall #: Source/items.cpp:1104 #, c++-format msgctxt "spell" msgid "{0} of {1}" msgstr "{0} {1}-st" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Spell}. Example: King's War Staff of Firewall #: Source/items.cpp:1116 #, c++-format msgctxt "spell" msgid "{0} {1} of {2}" msgstr "{0} {1} {2}-st" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item} of {Suffix}. Example: King's Long Sword of the Whale #: Source/items.cpp:1154 #, c++-format msgid "{0} {1} of {2}" msgstr "{0} {1} {2}-st" #. TRANSLATORS: Constructs item names. Format: {Prefix} {Item}. Example: King's Long Sword #: Source/items.cpp:1158 #, c++-format msgid "{0} {1}" msgstr "{0} {1}" #. TRANSLATORS: Constructs item names. Format: {Item} of {Suffix}. Example: Long Sword of the Whale #: Source/items.cpp:1162 #, c++-format msgid "{0} of {1}" msgstr "{0} {1}" #: Source/items.cpp:1643 Source/items.cpp:1651 msgid "increases a weapon's" msgstr "suureneb relva" #: Source/items.cpp:1644 msgid "chance to hit" msgstr "tabamuse tõenäosus" #: Source/items.cpp:1647 msgid "greatly increases a" msgstr "suuresti suurendab a" #: Source/items.cpp:1648 msgid "weapon's chance to hit" msgstr "relva tabamuse tõenäosus" #: Source/items.cpp:1652 msgid "damage potential" msgstr "kahjustuste potentsiaal" #: Source/items.cpp:1655 msgid "greatly increases a weapon's" msgstr "suuresti suurendab relva" #: Source/items.cpp:1656 msgid "damage potential - not bows" msgstr "kahjustuste potentsiaal - mitte vibud" #: Source/items.cpp:1659 msgid "reduces attributes needed" msgstr "vähendab vajalikke atribuute" #: Source/items.cpp:1660 msgid "to use armor or weapons" msgstr "kasutada turvist või relvi" #: Source/items.cpp:1663 #, no-c-format msgid "restores 20% of an" msgstr "taastab 20%" #: Source/items.cpp:1664 msgid "item's durability" msgstr "eseme vastupidavus" #: Source/items.cpp:1667 msgid "increases an item's" msgstr "suurendab eseme" #: Source/items.cpp:1668 msgid "current and max durability" msgstr "praegune ja maksimaalne vastupidavus" #: Source/items.cpp:1671 msgid "makes an item indestructible" msgstr "muudab eseme purunematuks" #: Source/items.cpp:1674 msgid "increases the armor class" msgstr "suurendab soomusklassi" #: Source/items.cpp:1675 msgid "of armor and shields" msgstr "rüüde ja kilpide" #: Source/items.cpp:1678 msgid "greatly increases the armor" msgstr "suurendab oluliselt turvist" #: Source/items.cpp:1679 msgid "class of armor and shields" msgstr "rüüde ja kilpide klass" #: Source/items.cpp:1682 Source/items.cpp:1689 msgid "sets fire trap" msgstr "seab püünise põlema" #: Source/items.cpp:1686 msgid "sets lightning trap" msgstr "seab välgu lõksu" #: Source/items.cpp:1692 msgid "sets petrification trap" msgstr "seab kivistamise lõksu" #: Source/items.cpp:1695 msgid "restore all life" msgstr "taasta kogu elu" #: Source/items.cpp:1698 msgid "restore some life" msgstr "taasta veidi elu" #: Source/items.cpp:1701 msgid "restore some mana" msgstr "taasta veidi mana" #: Source/items.cpp:1704 msgid "restore all mana" msgstr "taasta kogu mana" #: Source/items.cpp:1707 msgid "increase strength" msgstr "tõsta jõudu" #: Source/items.cpp:1710 msgid "increase magic" msgstr "suurenda maagiat" #: Source/items.cpp:1713 msgid "increase dexterity" msgstr "suurenda osavust" #: Source/items.cpp:1716 msgid "increase vitality" msgstr "suurenda vitaalsust" #: Source/items.cpp:1719 msgid "restore some life and mana" msgstr "taasta veidi elu ja mana" #: Source/items.cpp:1722 Source/items.cpp:1725 msgid "restore all life and mana" msgstr "taasta kogu elu ja mana" #: Source/items.cpp:1726 msgid "(works only in arenas)" msgstr "(toimib ainult areenidel)" #: Source/items.cpp:1761 msgid "Right-click to view" msgstr "Paremklõpsake, et vaadata" #: Source/items.cpp:1764 msgid "Right-click to use" msgstr "Paremklõpsake kasutamiseks" #: Source/items.cpp:1766 msgid "" "Right-click to read, then\n" "left-click to target" msgstr "" "Paremklõps lugemiseks, seejärel\n" "vasakklõps sihtimiseks" #: Source/items.cpp:1768 msgid "Right-click to read" msgstr "Paremklõpsake, et lugeda" #: Source/items.cpp:1775 msgid "Activate to view" msgstr "Aktiveeri vaatamiseks" #: Source/items.cpp:1779 Source/items.cpp:1804 msgid "Open inventory to use" msgstr "Ava laoseis kasutamiseks" #: Source/items.cpp:1781 msgid "Activate to use" msgstr "Aktiveeri kasutamiseks" #: Source/items.cpp:1784 msgid "" "Select from spell book, then\n" "cast spell to read" msgstr "" "Vali loitsuraamatust, seejärel\n" "loitsu lugemiseks kasuta loitsu" #: Source/items.cpp:1786 msgid "Activate to read" msgstr "Aktiveeri lugemiseks" #: Source/items.cpp:1800 #, c++-format msgid "{} to view" msgstr "{} vaatamiseks" #: Source/items.cpp:1806 #, c++-format msgid "{} to use" msgstr "{} kasutada" #: Source/items.cpp:1809 #, c++-format msgid "" "Select from spell book,\n" "then {} to read" msgstr "Vali loitsuraamatust, seejärel {} lugemiseks" #: Source/items.cpp:1811 #, c++-format msgid "{} to read" msgstr "{} lugemiseks" #: Source/items.cpp:1818 #, c++-format msgctxt "player" msgid "Level: {:d}" msgstr "Tase: {:d}" #: Source/items.cpp:1822 msgid "Doubles gold capacity" msgstr "Kahekordistab kulla mahutavuse" #: Source/items.cpp:1855 Source/stores.cpp:327 msgid "Required:" msgstr "Nõutud:" #: Source/items.cpp:1857 Source/stores.cpp:329 #, c++-format msgid " {:d} Str" msgstr " {:d} Jõud" #: Source/items.cpp:1859 Source/stores.cpp:331 #, c++-format msgid " {:d} Mag" msgstr " {:d} Mana" #: Source/items.cpp:1861 Source/stores.cpp:333 #, c++-format msgid " {:d} Dex" msgstr " {:d} Osavus" #. TRANSLATORS: {:s} will be a spell name #: Source/items.cpp:2217 #, c++-format msgid "Book of {:s}" msgstr "{:s} raamat" #. TRANSLATORS: {:s} will be a Character Name #: Source/items.cpp:2220 #, c++-format msgid "Ear of {:s}" msgstr "{:s} kõrv" #: Source/items.cpp:3874 #, c++-format msgid "chance to hit: {:+d}%" msgstr "tabamuse tõenäosus: {:+d}%" #: Source/items.cpp:3877 #, no-c-format, c++-format msgid "{:+d}% damage" msgstr "{:+d}% kahju" #: Source/items.cpp:3880 Source/items.cpp:4062 #, c++-format msgid "to hit: {:+d}%, {:+d}% damage" msgstr "tabamus: {:+d}%, {:+d}% kahju" #: Source/items.cpp:3883 #, no-c-format, c++-format msgid "{:+d}% armor" msgstr "{:+d}% soomus" #: Source/items.cpp:3886 #, c++-format msgid "armor class: {:d}" msgstr "soomusklass: {:d}" #: Source/items.cpp:3890 #, c++-format msgid "Resist Fire: {:+d}%" msgstr "Tulekindluse vastupanu: {:+d}%" #: Source/items.cpp:3892 #, c++-format msgid "Resist Fire: {:+d}% MAX" msgstr "Tulekindluse vastupanu: {:+d}% MAX" #: Source/items.cpp:3896 #, c++-format msgid "Resist Lightning: {:+d}%" msgstr "Vastupanu välgule: {:+d}%" #: Source/items.cpp:3898 #, c++-format msgid "Resist Lightning: {:+d}% MAX" msgstr "Vastupanu välgule: {:+d}% MAX" #: Source/items.cpp:3902 #, c++-format msgid "Resist Magic: {:+d}%" msgstr "Maagiakindlus: {:+d}%" #: Source/items.cpp:3904 #, c++-format msgid "Resist Magic: {:+d}% MAX" msgstr "Võlujõu vastupanu: {:+d}% MAX" #: Source/items.cpp:3907 #, c++-format msgid "Resist All: {:+d}%" msgstr "Kõikide vastupanu: {:+d}%" #: Source/items.cpp:3909 #, c++-format msgid "Resist All: {:+d}% MAX" msgstr "Kõikide vastupanu: {:+d}% MAX" #: Source/items.cpp:3912 #, c++-format msgid "spells are increased {:d} level" msgid_plural "spells are increased {:d} levels" msgstr[0] "loitsud on suurendatud {:d} tase" msgstr[1] "loitsud on suurendatud {:d} tasemele" #: Source/items.cpp:3914 #, c++-format msgid "spells are decreased {:d} level" msgid_plural "spells are decreased {:d} levels" msgstr[0] "loitsud on vähendatud {:d} tase" msgstr[1] "loitsud on vähendatud {:d} tasemele" #: Source/items.cpp:3916 msgid "spell levels unchanged (?)" msgstr "loitsutasemed muutmata (?)" #: Source/items.cpp:3918 msgid "Extra charges" msgstr "Lisatasud" #: Source/items.cpp:3920 #, c++-format msgid "{:d} {:s} charge" msgid_plural "{:d} {:s} charges" msgstr[0] "{:d} {:s} laeng" msgstr[1] "{:d} {:s} laengut" #: Source/items.cpp:3923 #, c++-format msgid "Fire hit damage: {:d}" msgstr "Tulekahju tabamiskahju: {:d}" #: Source/items.cpp:3925 #, c++-format msgid "Fire hit damage: {:d}-{:d}" msgstr "Tulekahju tabamiskahju: {:d}-{:d}" #: Source/items.cpp:3928 #, c++-format msgid "Lightning hit damage: {:d}" msgstr "Välgu tabamiskahju: {:d}" #: Source/items.cpp:3930 #, c++-format msgid "Lightning hit damage: {:d}-{:d}" msgstr "Välgu tabamiskahju: {:d}-{:d}" #: Source/items.cpp:3933 #, c++-format msgid "{:+d} to strength" msgstr "{:+d} jõudu" #: Source/items.cpp:3936 #, c++-format msgid "{:+d} to magic" msgstr "{:+d} maagiale" #: Source/items.cpp:3939 #, c++-format msgid "{:+d} to dexterity" msgstr "{:+d} osavusele" #: Source/items.cpp:3942 #, c++-format msgid "{:+d} to vitality" msgstr "{:+d} vitaalsusele" #: Source/items.cpp:3945 #, c++-format msgid "{:+d} to all attributes" msgstr "{:+d} kõigile atribuutidele" #: Source/items.cpp:3948 #, c++-format msgid "{:+d} damage from enemies" msgstr "{:+d} kahju vaenlastelt" #: Source/items.cpp:3951 #, c++-format msgid "Hit Points: {:+d}" msgstr "Elupunktid: {:+d}" #: Source/items.cpp:3954 #, c++-format msgid "Mana: {:+d}" msgstr "Mana: {:+d}" #: Source/items.cpp:3956 msgid "high durability" msgstr "kõrge vastupidavus" #: Source/items.cpp:3958 msgid "decreased durability" msgstr "vähenenud vastupidavus" #: Source/items.cpp:3960 msgid "indestructible" msgstr "hävimatu" #: Source/items.cpp:3962 #, no-c-format, c++-format msgid "+{:d}% light radius" msgstr "+{:d}% valgusraadius" #: Source/items.cpp:3964 #, no-c-format, c++-format msgid "-{:d}% light radius" msgstr "-{:d}% valgusraadius" #: Source/items.cpp:3966 msgid "multiple arrows per shot" msgstr "mitu noolt ühe lasu kohta" #: Source/items.cpp:3969 #, c++-format msgid "fire arrows damage: {:d}" msgstr "tule noolte kahju: {:d}" #: Source/items.cpp:3971 #, c++-format msgid "fire arrows damage: {:d}-{:d}" msgstr "tule noolte kahju: {:d}-{:d}" #: Source/items.cpp:3974 #, c++-format msgid "lightning arrows damage {:d}" msgstr "välgu nooled teevad kahju {:d}" #: Source/items.cpp:3976 #, c++-format msgid "lightning arrows damage {:d}-{:d}" msgstr "välgunooled teevad kahju {:d}-{:d}" #: Source/items.cpp:3979 #, c++-format msgid "fireball damage: {:d}" msgstr "tulepalli kahju: {:d}" #: Source/items.cpp:3981 #, c++-format msgid "fireball damage: {:d}-{:d}" msgstr "tulepalli kahju: {:d}-{:d}" #: Source/items.cpp:3983 msgid "attacker takes 1-3 damage" msgstr "rünnakut sooritaja saab 1-3 kahju" #: Source/items.cpp:3985 msgid "user loses all mana" msgstr "kasutaja kaotab kogu mana" #: Source/items.cpp:3987 msgid "absorbs half of trap damage" msgstr "neelab poole lõksu tekitatud kahjust" #: Source/items.cpp:3989 msgid "knocks target back" msgstr "lööb sihtmärgi tagasi" #: Source/items.cpp:3991 #, no-c-format msgid "+200% damage vs. demons" msgstr "+200% kahju deemonite vastu" #: Source/items.cpp:3993 msgid "All Resistance equals 0" msgstr "Kõik vastupidavused on 0" #: Source/items.cpp:3996 #, no-c-format msgid "hit steals 3% mana" msgstr "tabamus röövib 3% mana" #: Source/items.cpp:3998 #, no-c-format msgid "hit steals 5% mana" msgstr "tabamus röövib 5% mana" #: Source/items.cpp:4002 #, no-c-format msgid "hit steals 3% life" msgstr "tabamus varastab 3% elu" #: Source/items.cpp:4004 #, no-c-format msgid "hit steals 5% life" msgstr "tabamus varastab 5% elu" #: Source/items.cpp:4007 msgid "penetrates target's armor" msgstr "läbib sihtmärgi soomust" #: Source/items.cpp:4010 msgid "quick attack" msgstr "kiire rünnak" #: Source/items.cpp:4012 msgid "fast attack" msgstr "kiire rünnak" #: Source/items.cpp:4014 msgid "faster attack" msgstr "kiirem rünnak" #: Source/items.cpp:4016 msgid "fastest attack" msgstr "kiireim rünnak" #: Source/items.cpp:4017 Source/items.cpp:4025 Source/items.cpp:4072 msgid "Another ability (NW)" msgstr "Teine oskus (NW)" #: Source/items.cpp:4020 msgid "fast hit recovery" msgstr "kiire löögist taastumine" #: Source/items.cpp:4022 msgid "faster hit recovery" msgstr "kiirem löögist taastumine" #: Source/items.cpp:4024 msgid "fastest hit recovery" msgstr "kiireim löögist taastumine" #: Source/items.cpp:4027 msgid "fast block" msgstr "kiire blokeerimine" #: Source/items.cpp:4029 #, c++-format msgid "adds {:d} point to damage" msgid_plural "adds {:d} points to damage" msgstr[0] "lisab {:d} punkti kahjustusele" msgstr[1] "lisab {:d} punkti kahjustusele" #: Source/items.cpp:4031 msgid "fires random speed arrows" msgstr "laseb juhusliku kiirusega nooli" #: Source/items.cpp:4033 msgid "unusual item damage" msgstr "ebatavaline eseme kahjustus" #: Source/items.cpp:4035 msgid "altered durability" msgstr "muudetud vastupidavus" #: Source/items.cpp:4037 msgid "one handed sword" msgstr "ühekäeline mõõk" #: Source/items.cpp:4039 msgid "constantly lose hit points" msgstr "kaotab pidevalt elupunkte" #: Source/items.cpp:4041 msgid "life stealing" msgstr "eluvargus varastamine" #: Source/items.cpp:4043 msgid "no strength requirement" msgstr "jõu nõue puudub" #: Source/items.cpp:4046 #, c++-format msgid "lightning damage: {:d}" msgstr "välgu kahjustus: {:d}" #: Source/items.cpp:4048 #, c++-format msgid "lightning damage: {:d}-{:d}" msgstr "välgu kahjustus: {:d}-{:d}" #: Source/items.cpp:4050 msgid "charged bolts on hits" msgstr "laetud poldid tabamustel" #: Source/items.cpp:4052 msgid "occasional triple damage" msgstr "aeg-ajalt kolmekordne kahju" #: Source/items.cpp:4054 #, no-c-format, c++-format msgid "decaying {:+d}% damage" msgstr "mädanev {:+d}% kahju" #: Source/items.cpp:4056 msgid "2x dmg to monst, 1x to you" msgstr "2x kahju koletistele, 1x sulle" #: Source/items.cpp:4058 #, no-c-format msgid "Random 0 - 600% damage" msgstr "Juhuslik 0 - 600% kahju" #: Source/items.cpp:4060 #, no-c-format, c++-format msgid "low dur, {:+d}% damage" msgstr "madal vastupidavus, {:+d}% kahju" #: Source/items.cpp:4064 msgid "extra AC vs demons" msgstr "lisakaitse vs deemonid" #: Source/items.cpp:4066 msgid "extra AC vs undead" msgstr "lisakaitse vs elavad surnud" #: Source/items.cpp:4068 msgid "50% Mana moved to Health" msgstr "50% Manast kantakse tervisesse" #: Source/items.cpp:4070 msgid "40% Health moved to Mana" msgstr "40% tervisest kantakse manasse" #: Source/items.cpp:4113 Source/items.cpp:4154 #, c++-format msgid "damage: {:d} Indestructible" msgstr "kahju: {:d} Hävimatu" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4115 Source/items.cpp:4156 #, c++-format msgid "damage: {:d} Dur: {:d}/{:d}" msgstr "kahju: {:d} Kestvus: {:d}/{:d}" #: Source/items.cpp:4118 Source/items.cpp:4159 #, c++-format msgid "damage: {:d}-{:d} Indestructible" msgstr "kahju: {:d}-{:d} Hävimatu" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4120 Source/items.cpp:4161 #, c++-format msgid "damage: {:d}-{:d} Dur: {:d}/{:d}" msgstr "kahju: {:d}-{:d} Kestvus: {:d}/{:d}" #: Source/items.cpp:4125 Source/items.cpp:4171 #, c++-format msgid "armor: {:d} Indestructible" msgstr "turvis: {:d} Hävimatu" #. TRANSLATORS: Dur: is durability #: Source/items.cpp:4127 Source/items.cpp:4173 #, c++-format msgid "armor: {:d} Dur: {:d}/{:d}" msgstr "turvis: {:d} Kestvus: {:d}/{:d}" #: Source/items.cpp:4130 Source/items.cpp:4164 Source/items.cpp:4177 #: Source/stores.cpp:301 #, c++-format msgid "Charges: {:d}/{:d}" msgstr "Laengud: {:d}/{:d}" #: Source/items.cpp:4139 msgid "unique item" msgstr "ainulaadne ese" #: Source/items.cpp:4167 Source/items.cpp:4175 Source/items.cpp:4181 msgid "Not Identified" msgstr "Pole tuvastatud" #: Source/levels/setmaps.cpp:27 msgid "Skeleton King's Lair" msgstr "Skeletikuningas Luur" #: Source/levels/setmaps.cpp:28 msgid "Chamber of Bone" msgstr "Luukambrikamber" #. TRANSLATORS: Quest Map #: Source/levels/setmaps.cpp:29 Source/quests.cpp:78 msgid "Maze" msgstr "Labürint" #: Source/levels/setmaps.cpp:30 Source/translation_dummy.cpp:637 msgid "Poisoned Water Supply" msgstr "Mürgitatud veevarustus" #: Source/levels/setmaps.cpp:31 msgid "Archbishop Lazarus' Lair" msgstr "Peapiiskop Lazaruse Luur" #: Source/levels/setmaps.cpp:32 msgid "Church Arena" msgstr "Kirikuvõitlusareen" #: Source/levels/setmaps.cpp:33 msgid "Hell Arena" msgstr "Põrgu Areen" #: Source/levels/setmaps.cpp:34 msgid "Circle of Life Arena" msgstr "Eluringi Elu Areen" #: Source/levels/trigs.cpp:355 msgid "Down to dungeon" msgstr "Alla koopasse" #: Source/levels/trigs.cpp:364 msgid "Down to catacombs" msgstr "Alla katakombidesse" #: Source/levels/trigs.cpp:374 msgid "Down to caves" msgstr "Alla koobastesse" #: Source/levels/trigs.cpp:384 msgid "Down to hell" msgstr "Alla põrgusse" #: Source/levels/trigs.cpp:394 msgid "Down to Hive" msgstr "Alla Tarusse" #: Source/levels/trigs.cpp:404 msgid "Down to Crypt" msgstr "Alla Krüpti" #: Source/levels/trigs.cpp:419 Source/levels/trigs.cpp:454 #: Source/levels/trigs.cpp:500 Source/levels/trigs.cpp:552 #, c++-format msgid "Up to level {:d}" msgstr "Kuni tasemeni {:d}" #: Source/levels/trigs.cpp:421 Source/levels/trigs.cpp:483 #: Source/levels/trigs.cpp:535 Source/levels/trigs.cpp:582 #: Source/levels/trigs.cpp:644 Source/levels/trigs.cpp:693 #: Source/levels/trigs.cpp:800 msgid "Up to town" msgstr "Linna poole" #: Source/levels/trigs.cpp:432 Source/levels/trigs.cpp:465 #: Source/levels/trigs.cpp:517 Source/levels/trigs.cpp:564 #: Source/levels/trigs.cpp:626 #, c++-format msgid "Down to level {:d}" msgstr "Alla tasemele {:d}" #: Source/levels/trigs.cpp:595 msgid "Down to Diablo" msgstr "Alla Diablo juurde" #: Source/levels/trigs.cpp:613 #, c++-format msgid "Up to Nest level {:d}" msgstr "Kuni pesa tasemeni {:d}" #: Source/levels/trigs.cpp:661 #, c++-format msgid "Up to Crypt level {:d}" msgstr "Kuni krüpti tasemeni {:d}" #: Source/levels/trigs.cpp:671 Source/translation_dummy.cpp:646 msgid "Cornerstone of the World" msgstr "Maailma nurgakivi" #: Source/levels/trigs.cpp:676 #, c++-format msgid "Down to Crypt level {:d}" msgstr "Alla Krüpti tasemele {:d}" #: Source/levels/trigs.cpp:724 Source/levels/trigs.cpp:738 #: Source/levels/trigs.cpp:752 #, c++-format msgid "Back to Level {:d}" msgstr "Tagasi tasemele {:d}" #: Source/loadsave.cpp:2013 Source/loadsave.cpp:2470 msgid "Unable to open save file archive" msgstr "Ei saa salvestusfaili arhiivi avada" #: Source/loadsave.cpp:2424 msgid "" "Stash version invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2443 msgid "" "Stash size invalid. If you attempt to access your stash, data will be " "overwritten!!" msgstr "" #: Source/loadsave.cpp:2474 msgid "Invalid save file" msgstr "Vigane salvestusfail" #: Source/loadsave.cpp:2506 msgid "Player is on a Hellfire only level" msgstr "Mängija on ainult Hellfire'i tasemel" #: Source/loadsave.cpp:2772 msgid "Invalid game state" msgstr "Kehtetu mänguseisund" #: Source/menu.cpp:157 msgid "Unable to display mainmenu" msgstr "Peamenüüd ei saa kuvada" #: Source/monstdat.cpp:331 Source/monstdat.cpp:344 msgid "Loading Monster Data Failed" msgstr "" #: Source/monstdat.cpp:331 #, c++-format msgid "" "Could not add a monster, since the maximum monster type number of {} has " "already been reached." msgstr "" #: Source/monstdat.cpp:344 #, c++-format msgid "A monster type already exists for ID \"{}\"." msgstr "" #: Source/monster.cpp:2990 msgid "Animal" msgstr "Loom" #: Source/monster.cpp:2992 msgid "Demon" msgstr "Demon" #: Source/monster.cpp:2994 msgid "Undead" msgstr "Elavad surnud" #: Source/monster.cpp:4413 #, c++-format msgid "Type: {:s} Kills: {:d}" msgstr "Tüüp: {:s} Tapmised: {:d}" #: Source/monster.cpp:4415 #, c++-format msgid "Total kills: {:d}" msgstr "Kokku tapetud: {:d}" #: Source/monster.cpp:4441 #, c++-format msgid "Hit Points: {:d}-{:d}" msgstr "Elupunktid: {:d}-{:d}" #: Source/monster.cpp:4446 msgid "No magic resistance" msgstr "Pole maagilist vastupanu" #: Source/monster.cpp:4449 msgid "Resists:" msgstr "Vastupanu:" #: Source/monster.cpp:4451 Source/monster.cpp:4461 msgid " Magic" msgstr " Maagia" #: Source/monster.cpp:4453 Source/monster.cpp:4463 msgid " Fire" msgstr " Tuli" #: Source/monster.cpp:4455 Source/monster.cpp:4465 msgid " Lightning" msgstr " Välk" #: Source/monster.cpp:4459 msgid "Immune:" msgstr "Immuunne:" #: Source/monster.cpp:4476 #, c++-format msgid "Type: {:s}" msgstr "Tüüp: {:s}" #: Source/monster.cpp:4481 Source/monster.cpp:4487 msgid "No resistances" msgstr "Pole vastupanuvõimet" #: Source/monster.cpp:4482 Source/monster.cpp:4491 msgid "No Immunities" msgstr "Pole immuunsusi" #: Source/monster.cpp:4485 msgid "Some Magic Resistances" msgstr "Mõned maagilised vastupidavused" #: Source/monster.cpp:4489 msgid "Some Magic Immunities" msgstr "Mõned maagilised immuunsused" #: Source/mpq/mpq_writer.cpp:174 msgid "Failed to open archive for writing." msgstr "Arhiivi avamine kirjutamiseks ebaõnnestus." #: Source/msg.cpp:1701 #, c++-format msgid "{:s} has cast an invalid spell." msgstr "{:s} on teinud kehtetu loitsu." #: Source/msg.cpp:1705 #, c++-format msgid "{:s} has cast an illegal spell." msgstr "{:s} on lausunud ebaseadusliku loitsu." #: Source/msg.cpp:2286 Source/multi.cpp:836 Source/multi.cpp:886 #, c++-format msgid "Player '{:s}' (level {:d}) just joined the game" msgstr "Mängija '{:s}' (tase {:d}) liitus just mänguga" #: Source/msg.cpp:2718 msgid "The game ended" msgstr "Mäng lõppes" #: Source/msg.cpp:2724 msgid "Unable to get level data" msgstr "Ei saa taseme andmeid kätte" #: Source/multi.cpp:283 #, c++-format msgid "Player '{:s}' just left the game" msgstr "Mängija '{:s}' lahkus just mängust" #: Source/multi.cpp:286 #, c++-format msgid "Player '{:s}' killed Diablo and left the game!" msgstr "Mängija '{:s}' tappis Diablo ja lahkus mängust!" #: Source/multi.cpp:290 #, c++-format msgid "Player '{:s}' dropped due to timeout" msgstr "Mängija '{:s}' eemaldati ajapiirangu tõttu" #: Source/multi.cpp:888 #, c++-format msgid "Player '{:s}' (level {:d}) is already in the game" msgstr "Mängija '{:s}' (tase {:d}) on juba mängus" #. TRANSLATORS: Shrine Name Block #: Source/objects.cpp:127 msgid "Mysterious" msgstr "Müstiline" #: Source/objects.cpp:128 msgid "Hidden" msgstr "Peidetud" #: Source/objects.cpp:129 msgid "Gloomy" msgstr "Sünge" #: Source/objects.cpp:130 Source/translation_dummy.cpp:460 msgid "Weird" msgstr "Kummaline" #: Source/objects.cpp:131 Source/objects.cpp:138 msgid "Magical" msgstr "Maagiline" #: Source/objects.cpp:132 msgid "Stone" msgstr "Kivi" #: Source/objects.cpp:133 msgid "Religious" msgstr "Religioosne" #: Source/objects.cpp:134 msgid "Enchanted" msgstr "Lummatud" #: Source/objects.cpp:135 msgid "Thaumaturgic" msgstr "Taumaturgiline" #: Source/objects.cpp:136 msgid "Fascinating" msgstr "Põnev" #: Source/objects.cpp:137 msgid "Cryptic" msgstr "Salapärane" #: Source/objects.cpp:139 msgid "Eldritch" msgstr "Eldritch" #: Source/objects.cpp:140 msgid "Eerie" msgstr "Õudne" #: Source/objects.cpp:141 msgid "Divine" msgstr "Jumalik" #: Source/objects.cpp:142 Source/translation_dummy.cpp:494 msgid "Holy" msgstr "Püha" #: Source/objects.cpp:143 msgid "Sacred" msgstr "Püha" #: Source/objects.cpp:144 msgid "Spiritual" msgstr "Vaimne" #: Source/objects.cpp:145 msgid "Spooky" msgstr "Kummituslik" #: Source/objects.cpp:146 msgid "Abandoned" msgstr "Mahajäetud" #: Source/objects.cpp:147 msgid "Creepy" msgstr "" #: Source/objects.cpp:4899 msgid "Slain Hero" msgstr "Tapetud kangelane" #. TRANSLATORS: {:s} will either be a chest or a door #: Source/objects.cpp:4912 #, c++-format msgid "Trapped {:s}" msgstr "Lõksus {:s}" #. TRANSLATORS: If user enabled diablo.ini setting "Disable Crippling Shrines" is set to 1; also used for Na-Kruls lever #: Source/objects.cpp:4917 #, c++-format msgid "{:s} (disabled)" msgstr "{:s} (keelatud)" #: Source/options.cpp:310 Source/options.cpp:447 Source/options.cpp:453 msgid "ON" msgstr "SEES" #: Source/options.cpp:310 Source/options.cpp:445 Source/options.cpp:451 msgid "OFF" msgstr "VÄLJAS" #: Source/options.cpp:422 Source/options.cpp:423 msgid "Game Mode" msgstr "Mängurežiim" #: Source/options.cpp:422 #, fuzzy #| msgid "Gameplay Settings" msgid "Game Mode Settings" msgstr "Mängu seaded" #: Source/options.cpp:423 msgid "Play Diablo or Hellfire." msgstr "Mängi Diablo või Hellfire." #: Source/options.cpp:429 msgid "Restrict to Shareware" msgstr "Piira jagatavaks tarkvaraks" #: Source/options.cpp:429 msgid "" "Makes the game compatible with the demo. Enables multiplayer with friends " "who don't own a full copy of Diablo." msgstr "" "Muudab mängu demoga ühilduvaks. Võimaldab mitmikmängu sõpradega, kellel pole " "Diablo täisversiooni." #: Source/options.cpp:442 msgid "Start Up" msgstr "Käivita" #: Source/options.cpp:442 msgid "Start Up Settings" msgstr "Käivitamise seaded" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Intro" msgstr "Sissejuhatus" #: Source/options.cpp:443 Source/options.cpp:449 msgid "Shown Intro cinematic." msgstr "Kuvatud sissejuhatav filmikunst." #: Source/options.cpp:455 msgid "Splash" msgstr "Plärtsatus" #: Source/options.cpp:455 msgid "Shown splash screen." msgstr "Kuvatud avakuva." #: Source/options.cpp:457 msgid "Logo and Title Screen" msgstr "Logo ja tiitliekraan" #: Source/options.cpp:458 msgid "Title Screen" msgstr "Tiitli ekraan" #: Source/options.cpp:473 msgid "Diablo specific Settings" msgstr "Diablo spetsiifilised seaded" #: Source/options.cpp:487 msgid "Hellfire specific Settings" msgstr "Põrgutule spetsiifilised seaded" #: Source/options.cpp:501 msgid "Audio" msgstr "Heli" #: Source/options.cpp:501 msgid "Audio Settings" msgstr "Heliseaded" #: Source/options.cpp:504 msgid "Walking Sound" msgstr "Kõndimise heli" #: Source/options.cpp:504 msgid "Player emits sound when walking." msgstr "Mängija teeb kõndides häält." #: Source/options.cpp:505 msgid "Auto Equip Sound" msgstr "Automaatne varustuse heli" #: Source/options.cpp:505 msgid "Automatically equipping items on pickup emits the equipment sound." msgstr "Esemete automaatne varustamine peale korjamist tekitab varustuse heli." #: Source/options.cpp:506 msgid "Item Pickup Sound" msgstr "Eseme korjamise heli" #: Source/options.cpp:506 msgid "Picking up items emits the items pickup sound." msgstr "Esemete üleskorjamine tekitab esemete üleskorjamise heli." #: Source/options.cpp:507 msgid "Sample Rate" msgstr "Proovivõtu sagedus" #: Source/options.cpp:507 msgid "Output sample rate (Hz)." msgstr "Väljundproovi sagedus (Hz)." #: Source/options.cpp:508 msgid "Channels" msgstr "Kanalid" #: Source/options.cpp:508 msgid "Number of output channels." msgstr "Väljundkanalite arv." #: Source/options.cpp:509 msgid "Buffer Size" msgstr "Puhvri suurus" #: Source/options.cpp:509 msgid "Buffer size (number of frames per channel)." msgstr "Puhvri suurus (kaadrite arv kanali kohta)." #: Source/options.cpp:510 msgid "Resampling Quality" msgstr "Ümberproovide kvaliteet" #: Source/options.cpp:510 #, fuzzy #| msgid "Quality of the resampler, from 0 (lowest) to 10 (highest)." msgid "Quality of the resampler, from 0 (lowest) to 5 (highest)." msgstr "Resampleri kvaliteet, vahemikus 0 (madalaim) kuni 10 (kõrgeim)." #: Source/options.cpp:535 msgid "" "Affect the game's internal resolution and determine your view area. Note: " "This can differ from screen resolution, when Upscaling, Integer Scaling or " "Fit to Screen is used." msgstr "" "Mõjutab mängu sisemist resolutsiooni ja määrab sinu vaateala. Märkus: See " "võib erineda ekraani resolutsioonist, kui kasutatakse üleskaalumist, " "täisarvulist skaleerimist või ekraanile sobitamist." #: Source/options.cpp:574 msgid "Resampler" msgstr "Ümberproovija" #: Source/options.cpp:574 msgid "Audio resampler" msgstr "Heli resampler" #: Source/options.cpp:631 msgid "Device" msgstr "Seade" #: Source/options.cpp:631 msgid "Audio device" msgstr "Heliseade" #: Source/options.cpp:688 msgid "Graphics" msgstr "Graafika" #: Source/options.cpp:688 msgid "Graphics Settings" msgstr "Graafika seaded" #: Source/options.cpp:689 msgid "Fullscreen" msgstr "Täisekraan" #: Source/options.cpp:689 msgid "Display the game in windowed or fullscreen mode." msgstr "Kuva mäng akna- või täisekraanirežiimis." #: Source/options.cpp:691 msgid "Fit to Screen" msgstr "Sobita ekraanile" #: Source/options.cpp:691 msgid "" "Automatically adjust the game window to your current desktop screen aspect " "ratio and resolution." msgstr "" "Kohanda mänguaken automaatselt vastavalt sinu praeguse töölaua ekraani " "kuvasuhtele ja resolutsioonile." #: Source/options.cpp:700 msgid "Upscale" msgstr "Kvaliteetne" #: Source/options.cpp:700 msgid "" "Enables image scaling from the game resolution to your monitor resolution. " "Prevents changing the monitor resolution and allows window resizing." msgstr "" "Võimaldab pildi skaleerimist mängu resolutsioonist teie monitori " "resolutsioonini. Takistab monitori resolutsiooni muutmist ja võimaldab akna " "suuruse muutmist." #: Source/options.cpp:707 msgid "Scaling Quality" msgstr "Skaleerimise kvaliteet" #: Source/options.cpp:707 msgid "Enables optional filters to the output image when upscaling." msgstr "Võimaldab valikulisi filtreid väljundpildile, kui suurendatakse." #: Source/options.cpp:709 msgid "Nearest Pixel" msgstr "Lähim piksel" #: Source/options.cpp:710 msgid "Bilinear" msgstr "Bilineaarne" #: Source/options.cpp:711 msgid "Anisotropic" msgstr "Anisotroopne" #: Source/options.cpp:713 msgid "Integer Scaling" msgstr "Täisarvu skaleerimine" #: Source/options.cpp:713 msgid "Scales the image using whole number pixel ratio." msgstr "Skaalib pilti, kasutades täisarvulist pikslite suhet." #: Source/options.cpp:721 msgid "Frame Rate Control" msgstr "" #: Source/options.cpp:722 msgid "" "Manages frame rate to balance performance, reduce tearing, or save power." msgstr "" #: Source/options.cpp:732 msgid "Vertical Sync" msgstr "Vertikaalne sünkroonimine" #: Source/options.cpp:734 msgid "Limit FPS" msgstr "" #: Source/options.cpp:737 msgid "Zoom on when enabled." msgstr "Suumi sisse, kui lubatud." #: Source/options.cpp:738 #, fuzzy #| msgid " Lightning" msgid "Per-pixel Lighting" msgstr " Välk" #: Source/options.cpp:738 msgid "Subtile lighting for smoother light gradients." msgstr "" #: Source/options.cpp:739 msgid "Color Cycling" msgstr "Värvide tsükkel" #: Source/options.cpp:739 msgid "Color cycling effect used for water, lava, and acid animation." msgstr "Vee, laava ja happe animatsiooni jaoks kasutatav värvide tsükliefekt." #: Source/options.cpp:740 msgid "Alternate nest art" msgstr "Alternatiivne pesa kunst" #: Source/options.cpp:740 msgid "The game will use an alternative palette for Hellfire’s nest tileset." msgstr "" "Mäng kasutab Hellfire'i pesa plaatide komplekti jaoks alternatiivset paletti." #: Source/options.cpp:742 msgid "Hardware Cursor" msgstr "Riistvaraline kursor" #: Source/options.cpp:742 msgid "Use a hardware cursor" msgstr "Kasutage riistvaralist kursorit" #: Source/options.cpp:743 msgid "Hardware Cursor For Items" msgstr "Riistvara kursor esemete jaoks" #: Source/options.cpp:743 msgid "Use a hardware cursor for items." msgstr "Kasutage esemete jaoks riistvaralist kursorit." #: Source/options.cpp:744 msgid "Hardware Cursor Maximum Size" msgstr "Riistvara kursori maksimaalne suurus" #: Source/options.cpp:744 msgid "" "Maximum width / height for the hardware cursor. Larger cursors fall back to " "software." msgstr "" "Riistvaralise kursoriga maksimaalne laius / kõrgus. Suuremad kursorid " "langevad tagasi tarkvaralisele." #: Source/options.cpp:746 msgid "Show FPS" msgstr "Kuva FPS" #: Source/options.cpp:746 msgid "Displays the FPS in the upper left corner of the screen." msgstr "Kuvab FPS-i ekraani vasakus ülanurgas." #: Source/options.cpp:782 msgid "Gameplay" msgstr "Mängukogemus" #: Source/options.cpp:782 msgid "Gameplay Settings" msgstr "Mängu seaded" #: Source/options.cpp:784 msgid "" "Enable jogging/fast walking in town for Diablo and Hellfire. This option was " "introduced in the expansion." msgstr "" "Luba Diablo ja Hellfire'is linnas sörkimine/kiirkõndimine. See valik toodi " "sisse laienduses." #: Source/options.cpp:785 msgid "Grab Input" msgstr "Haara sisend" #: Source/options.cpp:785 msgid "When enabled mouse is locked to the game window." msgstr "Kui see on lubatud, on hiir lukustatud mänguaknasse." #: Source/options.cpp:786 msgid "Pause Game When Window Loses Focus" msgstr "Peata mängu, kui aken kaotab fookuse" #: Source/options.cpp:786 msgid "When enabled, the game will pause when focus is lost." msgstr "Kui see on lubatud, peatub mäng fookuse kaotamisel." #: Source/options.cpp:787 msgid "Enable Little Girl quest." msgstr "Luba Tüdruku ülesande lubamine." #: Source/options.cpp:788 msgid "" "Enable Jersey's quest. Lester the farmer is replaced by the Complete Nut." msgstr "Luba Jersey ülesanne. Lester talunik asendatakse Täieliku Pähkliga." #: Source/options.cpp:789 msgid "Friendly Fire" msgstr "Sõbralik Tuli" #: Source/options.cpp:789 msgid "" "Allow arrow/spell damage between players in multiplayer even when the " "friendly mode is on." msgstr "" "Luba noole/kirja kahju mängijate vahel mitmikmängus isegi siis, kui sõbralik " "režiim on sisse lülitatud." #: Source/options.cpp:790 msgid "Full quests in Multiplayer" msgstr "Täielikud ülesanded mitmikmängus" #: Source/options.cpp:790 msgid "Enables the full/uncut singleplayer version of quests." msgstr "Võimaldab ülesannete täispika/lõikamata üksikmängu versiooni." #: Source/options.cpp:791 msgid "Test Bard" msgstr "Testi Bard" #: Source/options.cpp:791 msgid "Force the Bard character type to appear in the hero selection menu." msgstr "Sunni Bard tegelase tüüp ilmuma kangelase valiku menüüs." #: Source/options.cpp:792 msgid "Test Barbarian" msgstr "Testi Barbar" #: Source/options.cpp:792 msgid "" "Force the Barbarian character type to appear in the hero selection menu." msgstr "Sunni barbaari tegelase tüüp kangelase valiku menüüs ilmuma." #: Source/options.cpp:793 msgid "Experience Bar" msgstr "Kogemuste riba" #: Source/options.cpp:793 msgid "Experience Bar is added to the UI at the bottom of the screen." msgstr "Kogemuste riba lisatakse kasutajaliidesele ekraani alaossa." #: Source/options.cpp:794 msgid "Show Item Graphics in Stores" msgstr "Kuva poodides esemete graafika" #: Source/options.cpp:794 msgid "Show item graphics to the left of item descriptions in store menus." msgstr "Kuva poe menüüdes esemete kirjelduste vasakul pool esemete graafika." #: Source/options.cpp:795 msgid "Show health values" msgstr "Kuva tervise väärtused" #: Source/options.cpp:795 msgid "Displays current / max health value on health globe." msgstr "Kuvab tervisekeral praeguse / maksimaalse tervise väärtuse." #: Source/options.cpp:796 msgid "Show mana values" msgstr "Kuva mana väärtused" #: Source/options.cpp:796 msgid "Displays current / max mana value on mana globe." msgstr "Kuvab mana globil praeguse / maksimaalse mana väärtuse." #: Source/options.cpp:797 #, fuzzy #| msgid "Character Information" msgid "Show Party Information" msgstr "Tegelase teave" #: Source/options.cpp:797 msgid "" "Displays the health and mana of all connected multiplayer party members." msgstr "" #: Source/options.cpp:798 msgid "Enemy Health Bar" msgstr "Vaenlase terviseriba" #: Source/options.cpp:798 msgid "Enemy Health Bar is displayed at the top of the screen." msgstr "Vaenlase terviseriba kuvatakse ekraani ülaosas." #: Source/options.cpp:799 msgid "Floating Item Info Box" msgstr "" #: Source/options.cpp:799 msgid "Displays item info in a floating box when hovering over an item." msgstr "" #: Source/options.cpp:800 msgid "Gold is automatically collected when in close proximity to the player." msgstr "Kuld kogutakse automaatselt, kui see on mängijale lähedal." #: Source/options.cpp:801 msgid "" "Elixirs are automatically collected when in close proximity to the player." msgstr "Eliksiirid kogutakse automaatselt, kui mängija on neile lähedal." #: Source/options.cpp:802 msgid "Oils are automatically collected when in close proximity to the player." msgstr "Õlid korjatakse automaatselt üles, kui mängija on neile lähedal." #: Source/options.cpp:803 msgid "Automatically pickup items in town." msgstr "Korjas automaatselt esemeid linnas üles." #: Source/options.cpp:804 msgid "Adria will refill your mana when you visit her shop." msgstr "Adria täidab su mana, kui sa tema poodi külastad." #: Source/options.cpp:805 msgid "" "Weapons will be automatically equipped on pickup or purchase if enabled." msgstr "" "Relvad varustatakse automaatselt peale korjamist või ostmist, kui see on " "lubatud." #: Source/options.cpp:806 msgid "Armor will be automatically equipped on pickup or purchase if enabled." msgstr "" "Kaitsevarustus varustatakse automaatselt peale võtmist või ostmist, kui see " "on lubatud." #: Source/options.cpp:807 msgid "Helms will be automatically equipped on pickup or purchase if enabled." msgstr "" "Kiivrid varustatakse automaatselt peale korjamist või ostmist, kui see on " "lubatud." #: Source/options.cpp:808 msgid "" "Shields will be automatically equipped on pickup or purchase if enabled." msgstr "" "Kilbid varustatakse automaatselt, kui need on lubatud, kas korjamisel või " "ostmisel." #: Source/options.cpp:809 msgid "" "Jewelry will be automatically equipped on pickup or purchase if enabled." msgstr "" "Ehted varustatakse automaatselt peale korjamist või ostmist, kui see on " "lubatud." #: Source/options.cpp:810 msgid "Randomly selecting available quests for new games." msgstr "Uute mängude jaoks saadaval olevate ülesannete juhuslik valimine." #: Source/options.cpp:811 msgid "Show Monster Type" msgstr "Kuva koletise tüüp" #: Source/options.cpp:811 msgid "" "Hovering over a monster will display the type of monster in the description " "box in the UI." msgstr "" "Kui liigud koletise kohale, kuvatakse kasutajaliideses kirjelduskastis " "koletise tüüp." #: Source/options.cpp:812 msgid "Show labels for items on the ground when enabled." msgstr "Kuva maas olevate esemete sildid, kui see on lubatud." #: Source/options.cpp:813 msgid "Refill belt from inventory when belt item is consumed." msgstr "Vöö täitmine inventarist, kui vöö ese on tarbitud." #: Source/options.cpp:814 msgid "" "When enabled Cauldrons, Fascinating Shrines, Goat Shrines, Ornate Shrines, " "Sacred Shrines and Murphy's Shrines are not able to be clicked on and " "labeled as disabled." msgstr "" "Kui need on lubatud, ei saa katlaid, lummavaid pühamuid, kitsepühamuid, " "kaunistatud pühamuid, pühasid pühamuid ja Murphy pühamuid klõpsata ning need " "on märgitud keelatuks." #: Source/options.cpp:815 msgid "Quick Cast" msgstr "Kiirvalamine" #: Source/options.cpp:815 msgid "" "Spell hotkeys instantly cast the spell, rather than switching the readied " "spell." msgstr "" "Loitsu kiirklahvid lasevad loitsu koheselt, selle asemel et vahetada " "valmisolekus olevat loitsu." #: Source/options.cpp:816 msgid "Number of Healing potions to pick up automatically." msgstr "Tervendamisjoogide arv, mida automaatselt üles korjata." #: Source/options.cpp:817 msgid "Number of Full Healing potions to pick up automatically." msgstr "Täis tervendamise jookide arv, mida automaatselt üles korjata." #: Source/options.cpp:818 msgid "Number of Mana potions to pick up automatically." msgstr "Automaatne manapudelite kogus, mida üles korjata." #: Source/options.cpp:819 msgid "Number of Full Mana potions to pick up automatically." msgstr "Täis mana jookide arv, mida automaatselt üles korjata." #: Source/options.cpp:820 msgid "Number of Rejuvenation potions to pick up automatically." msgstr "Elustamisjoogi automaatseks korjamiseks mõeldud arv." #: Source/options.cpp:821 msgid "Number of Full Rejuvenation potions to pick up automatically." msgstr "Täis elustamisjoogi automaatseks korjamiseks mõeldud arv." #: Source/options.cpp:822 msgid "Enable floating numbers" msgstr "Luba hõljuvad numbrid" #: Source/options.cpp:822 msgid "Enables floating numbers on gaining XP / dealing damage etc." msgstr "Võimaldab ujuvaid numbreid XP saamisel / kahju tekitamisel jne." #: Source/options.cpp:824 msgid "Off" msgstr "Väljas" #: Source/options.cpp:825 msgid "Random Angles" msgstr "Juhuslikud nurgad" #: Source/options.cpp:826 msgid "Vertical Only" msgstr "Ainult vertikaalne" #: Source/options.cpp:880 msgid "Controller" msgstr "Kontroller" #: Source/options.cpp:880 msgid "Controller Settings" msgstr "Kontrolleri seaded" #: Source/options.cpp:889 msgid "Network" msgstr "Võrk" #: Source/options.cpp:889 msgid "Network Settings" msgstr "Võrgu seaded" #: Source/options.cpp:901 msgid "Chat" msgstr "Vestlus" #: Source/options.cpp:901 msgid "Chat Settings" msgstr "Vestluse seaded" #: Source/options.cpp:910 Source/options.cpp:1029 msgid "Language" msgstr "Keel" #: Source/options.cpp:910 msgid "Define what language to use in game." msgstr "Määrake, millist keelt mängus kasutada." #: Source/options.cpp:1029 msgid "Language Settings" msgstr "Keelesätted" #: Source/options.cpp:1040 msgid "Keymapping" msgstr "Klahvivajutus" #: Source/options.cpp:1040 msgid "Keymapping Settings" msgstr "Klahvivajutuse seaded" #: Source/options.cpp:1260 msgid "Padmapping" msgstr "Padi kaardistamine" #: Source/options.cpp:1260 msgid "Padmapping Settings" msgstr "Puldimapi seaded" #: Source/options.cpp:1512 msgid "Mods" msgstr "" #: Source/options.cpp:1512 #, fuzzy #| msgid "Settings" msgid "Mod Settings" msgstr "Seaded" #: Source/panels/charpanel.cpp:133 msgid "Level" msgstr "Tase" #: Source/panels/charpanel.cpp:135 msgid "Experience" msgstr "Kogemus" #: Source/panels/charpanel.cpp:139 msgid "Next level" msgstr "Järgmine tase" #: Source/panels/charpanel.cpp:148 msgid "Base" msgstr "Alus" #: Source/panels/charpanel.cpp:149 msgid "Now" msgstr "Nüüd" #: Source/panels/charpanel.cpp:150 msgid "Strength" msgstr "Jõud" #: Source/panels/charpanel.cpp:154 msgid "Magic" msgstr "Maagia" #: Source/panels/charpanel.cpp:158 msgid "Dexterity" msgstr "Osavus" #: Source/panels/charpanel.cpp:161 msgid "Vitality" msgstr "Elujõud" #: Source/panels/charpanel.cpp:164 msgid "Points to distribute" msgstr "Punkte jagamiseks" #: Source/panels/charpanel.cpp:170 Source/translation_dummy.cpp:216 msgid "Gold" msgstr "Kuld" #: Source/panels/charpanel.cpp:174 msgid "Armor class" msgstr "Rüü klass" #: Source/panels/charpanel.cpp:176 #, fuzzy #| msgid "chance to hit" msgid "Chance To Hit" msgstr "tabamuse tõenäosus" #: Source/panels/charpanel.cpp:178 msgid "Damage" msgstr "Kahju" #: Source/panels/charpanel.cpp:184 msgid "Life" msgstr "Elu" #: Source/panels/charpanel.cpp:188 msgid "Mana" msgstr "Mana" #: Source/panels/charpanel.cpp:193 msgid "Resist magic" msgstr "Võlujõu kaitse" #: Source/panels/charpanel.cpp:195 msgid "Resist fire" msgstr "Tule kaitse" #: Source/panels/charpanel.cpp:197 msgid "Resist lightning" msgstr "Välgu kaitse" #: Source/panels/mainpanel.cpp:91 msgid "char" msgstr "tegelane" #: Source/panels/mainpanel.cpp:92 msgid "quests" msgstr "ülesanded" #: Source/panels/mainpanel.cpp:93 msgid "map" msgstr "kaart" #: Source/panels/mainpanel.cpp:94 msgid "menu" msgstr "menüü" #: Source/panels/mainpanel.cpp:95 msgid "inv" msgstr "inv" #: Source/panels/mainpanel.cpp:96 msgid "spells" msgstr "loitsud" #: Source/panels/mainpanel.cpp:106 Source/panels/mainpanel.cpp:132 #: Source/panels/mainpanel.cpp:134 msgid "voice" msgstr "hääl" #: Source/panels/mainpanel.cpp:127 Source/panels/mainpanel.cpp:129 #: Source/panels/mainpanel.cpp:131 msgid "mute" msgstr "vaigistama" #: Source/panels/spell_book.cpp:105 msgid "Unusable" msgstr "Kasutamiskõlbmatu" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:108 msgid "Dmg: 1/3 target hp" msgstr "Kahju: 1/3 sihtmärgi hp" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:115 #, c++-format msgid "Heals: {:d} - {:d}" msgstr "Ravib: {:d} - {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:117 #, c++-format msgid "Damage: {:d} - {:d}" msgstr "Kahju: {:d} - {:d}" #: Source/panels/spell_book.cpp:172 Source/panels/spell_list.cpp:152 msgid "Skill" msgstr "Oskus" #: Source/panels/spell_book.cpp:176 #, c++-format msgid "Staff ({:d} charge)" msgid_plural "Staff ({:d} charges)" msgstr[0] "Kepp ({:d} laeng)" msgstr[1] "Kepp ({:d} laengut)" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:181 #, c++-format msgctxt "spellbook" msgid "Level {:d}" msgstr "Tase {:d}" #. TRANSLATORS: UI constraints, keep short please. #: Source/panels/spell_book.cpp:185 #, c++-format msgctxt "spellbook" msgid "Mana: {:d}" msgstr "Mana: {:d}" #: Source/panels/spell_list.cpp:159 msgid "Spell" msgstr "Loits" #: Source/panels/spell_list.cpp:162 msgid "Damages undead only" msgstr "Kahjustab ainult elavaid surnuid" #: Source/panels/spell_list.cpp:173 msgid "Scroll" msgstr "Käsk" #: Source/panels/spell_list.cpp:184 Source/translation_dummy.cpp:354 msgid "Staff" msgstr "Kepp" #: Source/panels/spell_list.cpp:194 #, c++-format msgid "Spell Hotkey {:s}" msgstr "Loitsu kiirklahv {:s}" #: Source/pfile.cpp:762 msgid "Unable to open archive" msgstr "Arhiivi avamine ebaõnnestus" #: Source/pfile.cpp:764 msgid "Unable to load character" msgstr "Ei saa tegelast laadida" #: Source/playerdat.cpp:320 msgid "Loading Class Data Failed" msgstr "" #: Source/playerdat.cpp:320 #, c++-format msgid "" "Could not add a class, since the maximum class number of {} has already been " "reached." msgstr "" #: Source/plrmsg.cpp:79 Source/qol/chatlog.cpp:130 #, c++-format msgid "{:s} (lvl {:d}): " msgstr "{:s} (tase {:d}): " #: Source/qol/chatlog.cpp:170 #, c++-format msgid "Chat History (Messages: {:d})" msgstr "Vestluse ajalugu (Sõnumid: {:d})" #: Source/qol/itemlabels.cpp:113 #, c++-format msgid "{:s} gold" msgstr "{:s} kuld" #: Source/qol/stash.cpp:648 msgid "How many gold pieces do you want to withdraw?" msgstr "Mitu kuldtükki soovite välja võtta?" #: Source/qol/xpbar.cpp:139 #, c++-format msgid "Level {:d}" msgstr "Tase {:d}" #: Source/qol/xpbar.cpp:145 Source/qol/xpbar.cpp:153 #, c++-format msgid "Experience: {:s}" msgstr "Kogemus: {:s}" #: Source/qol/xpbar.cpp:146 msgid "Maximum Level" msgstr "Maksimaalne tase" #: Source/qol/xpbar.cpp:155 #, c++-format msgid "Next Level: {:s}" msgstr "Järgmine tase: {:s}" #: Source/qol/xpbar.cpp:156 #, c++-format msgid "{:s} to Level {:d}" msgstr "{:s} tasemele {:d}" #. TRANSLATORS: Quest Map #: Source/quests.cpp:76 msgid "King Leoric's Tomb" msgstr "Kuningas Leorici haud" #. TRANSLATORS: Quest Map #: Source/quests.cpp:77 Source/translation_dummy.cpp:638 msgid "The Chamber of Bone" msgstr "Luukambrike" #. TRANSLATORS: Quest Map #: Source/quests.cpp:79 msgid "A Dark Passage" msgstr "Tume läbikäik" #. TRANSLATORS: Quest Map #: Source/quests.cpp:80 msgid "Unholy Altar" msgstr "Püha altar" #. TRANSLATORS: Used for Quest Portals. {:s} is a Map Name #: Source/quests.cpp:355 #, c++-format msgid "To {:s}" msgstr "Asukohta {:s}" #: Source/quick_messages.cpp:10 msgid "I need help! Come here!" msgstr "Mul on abi vaja! Tule siia!" #: Source/quick_messages.cpp:11 msgid "Follow me." msgstr "Järgne mulle." #: Source/quick_messages.cpp:12 msgid "Here's something for you." msgstr "Siin on midagi sinu jaoks." #: Source/quick_messages.cpp:13 msgid "Now you DIE!" msgstr "Nüüd sa SURED!" #: Source/quick_messages.cpp:14 msgid "Heal yourself!" msgstr "Tervenda end!" #: Source/quick_messages.cpp:15 msgid "Watch out!" msgstr "Ole ettevaatlik!" #: Source/quick_messages.cpp:16 msgid "Thanks." msgstr "Aitäh." #: Source/quick_messages.cpp:17 msgid "Retreat!" msgstr "Tagasi!" #: Source/quick_messages.cpp:18 msgid "Sorry." msgstr "Vabandust." #: Source/quick_messages.cpp:19 msgid "I'm waiting." msgstr "Ma ootan." #: Source/stores.cpp:131 msgid "Griswold" msgstr "Griswold" #: Source/stores.cpp:132 msgid "Pepin" msgstr "Pepin" #: Source/stores.cpp:134 msgid "Ogden" msgstr "Ogden" #: Source/stores.cpp:135 msgid "Cain" msgstr "Kain" #: Source/stores.cpp:136 msgid "Farnham" msgstr "Farnham" #: Source/stores.cpp:137 msgid "Adria" msgstr "Adria" #: Source/stores.cpp:138 Source/stores.cpp:1267 msgid "Gillian" msgstr "Gillian" #: Source/stores.cpp:139 msgid "Wirt" msgstr "Wirt" #: Source/stores.cpp:265 Source/stores.cpp:272 msgid "Back" msgstr "Tagasi" #: Source/stores.cpp:294 Source/stores.cpp:300 Source/stores.cpp:326 msgid ", " msgstr ", " #: Source/stores.cpp:311 #, c++-format msgid "Damage: {:d}-{:d} " msgstr "Kahju: {:d}-{:d} " #: Source/stores.cpp:313 #, c++-format msgid "Armor: {:d} " msgstr "Rüü: {:d} " #: Source/stores.cpp:315 #, c++-format msgid "Dur: {:d}/{:d}" msgstr "Vastupidavus: {:d}/{:d}" #: Source/stores.cpp:317 msgid "Indestructible" msgstr "Purunematu" #: Source/stores.cpp:387 Source/stores.cpp:1035 Source/stores.cpp:1254 msgid "Welcome to the" msgstr "Tere tulemast" #: Source/stores.cpp:388 msgid "Blacksmith's shop" msgstr "Sepa pood" #: Source/stores.cpp:389 Source/stores.cpp:686 Source/stores.cpp:1037 #: Source/stores.cpp:1080 Source/stores.cpp:1256 Source/stores.cpp:1268 #: Source/stores.cpp:1281 msgid "Would you like to:" msgstr "Kas soovite:" #: Source/stores.cpp:390 msgid "Talk to Griswold" msgstr "Räägi Griswoldiga" #: Source/stores.cpp:391 msgid "Buy basic items" msgstr "Osta põhiesemeid" #: Source/stores.cpp:392 msgid "Buy premium items" msgstr "Osta premium-esemeid" #: Source/stores.cpp:393 Source/stores.cpp:689 msgid "Sell items" msgstr "Müü esemeid" #: Source/stores.cpp:394 msgid "Repair items" msgstr "Paranda esemeid" #: Source/stores.cpp:395 msgid "Leave the shop" msgstr "Lahku poest" #: Source/stores.cpp:423 Source/stores.cpp:725 Source/stores.cpp:1057 msgid "I have these items for sale:" msgstr "Mul on need esemed müügiks:" #: Source/stores.cpp:472 msgid "I have these premium items for sale:" msgstr "Mul on müüa need esmaklassilised esemed:" #: Source/stores.cpp:568 Source/stores.cpp:818 msgid "You have nothing I want." msgstr "Sul pole midagi, mida ma tahaksin." #: Source/stores.cpp:579 Source/stores.cpp:830 msgid "Which item is for sale?" msgstr "Milline ese on müügil?" #: Source/stores.cpp:647 msgid "You have nothing to repair." msgstr "Sul pole midagi parandada." #: Source/stores.cpp:658 msgid "Repair which item?" msgstr "Millist eset parandada?" #: Source/stores.cpp:685 msgid "Witch's shack" msgstr "Nõiaonn" #: Source/stores.cpp:687 msgid "Talk to Adria" msgstr "Räägi Adriaga" #: Source/stores.cpp:688 Source/stores.cpp:1039 msgid "Buy items" msgstr "Osta esemeid" #: Source/stores.cpp:690 msgid "Recharge staves" msgstr "Laadige sauasid" #: Source/stores.cpp:691 msgid "Leave the shack" msgstr "Jäta hütt" #: Source/stores.cpp:892 msgid "You have nothing to recharge." msgstr "Sul pole midagi laadida." #: Source/stores.cpp:903 msgid "Recharge which item?" msgstr "Millist eset laadida?" #: Source/stores.cpp:916 msgid "You do not have enough gold" msgstr "Sul pole piisavalt kulda" #: Source/stores.cpp:924 msgid "You do not have enough room in inventory" msgstr "Sul pole piisavalt ruumi oma varudes" #: Source/stores.cpp:942 msgid "Do we have a deal?" msgstr "Kas meil on kokkulepe?" #: Source/stores.cpp:945 msgid "Are you sure you want to identify this item?" msgstr "Kas oled kindel, et soovid selle eseme tuvastada?" #: Source/stores.cpp:951 msgid "Are you sure you want to buy this item?" msgstr "Kas oled kindel, et soovid seda eset osta?" #: Source/stores.cpp:954 msgid "Are you sure you want to recharge this item?" msgstr "Kas olete kindel, et soovite seda eset laadida?" #: Source/stores.cpp:958 msgid "Are you sure you want to sell this item?" msgstr "Kas oled kindel, et soovid selle eseme müüa?" #: Source/stores.cpp:961 msgid "Are you sure you want to repair this item?" msgstr "Kas olete kindel, et soovite seda eset parandada?" #: Source/stores.cpp:975 Source/towners.cpp:785 msgid "Wirt the Peg-legged boy" msgstr "Wirt, puujalaga poiss" #: Source/stores.cpp:978 Source/stores.cpp:985 msgid "Talk to Wirt" msgstr "Räägi Wirtiga" #: Source/stores.cpp:979 msgid "I have something for sale," msgstr "Mul on midagi müügiks," #: Source/stores.cpp:980 msgid "but it will cost 50 gold" msgstr "aga see maksab 50 kulda" #: Source/stores.cpp:981 msgid "just to take a look. " msgstr "lihtsalt pilgu heitmiseks. " #: Source/stores.cpp:982 msgid "What have you got?" msgstr "Mida sul on pakkuda?" #: Source/stores.cpp:983 Source/stores.cpp:986 Source/stores.cpp:1083 #: Source/stores.cpp:1271 msgid "Say goodbye" msgstr "Ütle hüvasti" #: Source/stores.cpp:996 msgid "I have this item for sale:" msgstr "Mul on see ese müügiks:" #: Source/stores.cpp:1013 msgid "Leave" msgstr "Lahku" #: Source/stores.cpp:1036 msgid "Healer's home" msgstr "Tervendaja kodu" #: Source/stores.cpp:1038 msgid "Talk to Pepin" msgstr "Räägi Pepiniga" #: Source/stores.cpp:1040 msgid "Leave Healer's home" msgstr "Lahku Ravitseja kodust" #: Source/stores.cpp:1079 msgid "The Town Elder" msgstr "Linna vanem" #: Source/stores.cpp:1081 msgid "Talk to Cain" msgstr "Räägi Cainiga" #: Source/stores.cpp:1082 msgid "Identify an item" msgstr "Tuvasta ese" #: Source/stores.cpp:1175 msgid "You have nothing to identify." msgstr "Sul pole midagi tuvastada." #: Source/stores.cpp:1186 msgid "Identify which item?" msgstr "Määrake, milline ese?" #: Source/stores.cpp:1201 msgid "This item is:" msgstr "See ese on:" #: Source/stores.cpp:1204 msgid "Done" msgstr "Valmis" #: Source/stores.cpp:1213 #, c++-format msgid "Talk to {:s}" msgstr "Räägi {:s}-ga" #: Source/stores.cpp:1216 #, c++-format msgid "Talking to {:s}" msgstr "Vestlus {:s}-ga" #: Source/stores.cpp:1217 msgid "is not available" msgstr "ei ole saadaval" #: Source/stores.cpp:1218 msgid "in the shareware" msgstr "jagatud tarkvaras" #: Source/stores.cpp:1219 msgid "version" msgstr "versioon" #: Source/stores.cpp:1246 msgid "Gossip" msgstr "Klatš" #: Source/stores.cpp:1255 msgid "Rising Sun" msgstr "Tõusev Päike" #: Source/stores.cpp:1257 msgid "Talk to Ogden" msgstr "Räägi Ogdeniga" #: Source/stores.cpp:1258 msgid "Leave the tavern" msgstr "Lahku kõrtsist" #: Source/stores.cpp:1269 msgid "Talk to Gillian" msgstr "Räägi Gillianiga" #: Source/stores.cpp:1270 msgid "Access Storage" msgstr "Juurdepääs laole" #: Source/stores.cpp:1280 Source/towners.cpp:782 msgid "Farnham the Drunk" msgstr "Joodik Farnham" #: Source/stores.cpp:1282 msgid "Talk to Farnham" msgstr "Räägi Farnhamiga" #: Source/stores.cpp:1283 msgid "Say Goodbye" msgstr "Ütle hüvasti" #: Source/stores.cpp:2413 #, c++-format msgid "Your gold: {:s}" msgstr "Sinu kuld: {:s}" #: Source/textdat.cpp:72 msgid "Loading Text Data Failed" msgstr "" #: Source/textdat.cpp:72 #, c++-format msgid "A text data entry already exists for ID \"{}\"." msgstr "" #: Source/towners.cpp:269 msgid "Slain Townsman" msgstr "Tapetud linnamees" #: Source/towners.cpp:777 msgid "Griswold the Blacksmith" msgstr "Sepp Griswold" #: Source/towners.cpp:778 msgid "Pepin the Healer" msgstr "Pepin Ravitseja" #: Source/towners.cpp:779 msgid "Wounded Townsman" msgstr "Haavatud linnakodanik" #: Source/towners.cpp:780 msgid "Ogden the Tavern owner" msgstr "Ogden kõrtsi omanik" #: Source/towners.cpp:781 msgid "Cain the Elder" msgstr "Vanem Cain" #: Source/towners.cpp:783 msgid "Adria the Witch" msgstr "Nõid Adria" #: Source/towners.cpp:784 msgid "Gillian the Barmaid" msgstr "Gillian, kõrtsitüdruk" #: Source/towners.cpp:786 msgid "Cow" msgstr "Lehm" #: Source/towners.cpp:787 msgid "Lester the farmer" msgstr "Lester talunik" #: Source/towners.cpp:788 msgid "Celia" msgstr "Celia" #: Source/towners.cpp:789 msgid "Complete Nut" msgstr "Täielik Mutrike" #: Source/translation_dummy.cpp:11 msgid "Warrior" msgstr "Sõdalane" #: Source/translation_dummy.cpp:12 msgid "Rogue" msgstr "Varas" #: Source/translation_dummy.cpp:13 msgid "Sorcerer" msgstr "Võlur" #: Source/translation_dummy.cpp:14 msgid "Monk" msgstr "Munk" #: Source/translation_dummy.cpp:15 msgid "Bard" msgstr "Bard" #: Source/translation_dummy.cpp:16 msgid "Barbarian" msgstr "Barbar" #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie" msgstr "Zombi" #: Source/translation_dummy.cpp:18 msgctxt "monster" msgid "Ghoul" msgstr "Koll" #: Source/translation_dummy.cpp:19 msgctxt "monster" msgid "Rotting Carcass" msgstr "Mädanev korjus" #: Source/translation_dummy.cpp:20 msgctxt "monster" msgid "Black Death" msgstr "Must Surm" #: Source/translation_dummy.cpp:21 msgctxt "monster" msgid "Fallen One" msgstr "Langenud" #: Source/translation_dummy.cpp:22 msgctxt "monster" msgid "Carver" msgstr "Raiuja" #: Source/translation_dummy.cpp:23 msgctxt "monster" msgid "Devil Kin" msgstr "Kuratipoeg" #: Source/translation_dummy.cpp:24 msgctxt "monster" msgid "Dark One" msgstr "Tume Üks" #: Source/translation_dummy.cpp:25 msgctxt "monster" msgid "Skeleton" msgstr "Skelett" #: Source/translation_dummy.cpp:26 msgctxt "monster" msgid "Corpse Axe" msgstr "Laiba Kirves" #: Source/translation_dummy.cpp:27 msgctxt "monster" msgid "Burning Dead" msgstr "Põlev Surnu" #: Source/translation_dummy.cpp:28 msgctxt "monster" msgid "Horror" msgstr "Õudus" #: Source/translation_dummy.cpp:29 msgctxt "monster" msgid "Scavenger" msgstr "Röövel" #: Source/translation_dummy.cpp:30 msgctxt "monster" msgid "Plague Eater" msgstr "Katkuõgija" #: Source/translation_dummy.cpp:31 msgctxt "monster" msgid "Shadow Beast" msgstr "Varjuelukas" #: Source/translation_dummy.cpp:32 msgctxt "monster" msgid "Bone Gasher" msgstr "Luu Purustaja" #: Source/translation_dummy.cpp:33 msgctxt "monster" msgid "Corpse Bow" msgstr "Laibavibu" #: Source/translation_dummy.cpp:34 msgctxt "monster" msgid "Skeleton Captain" msgstr "Skeletikapten" #: Source/translation_dummy.cpp:35 msgctxt "monster" msgid "Corpse Captain" msgstr "Laiba kapten" #: Source/translation_dummy.cpp:36 msgctxt "monster" msgid "Burning Dead Captain" msgstr "Põlevate Surnute Kapten" #: Source/translation_dummy.cpp:37 msgctxt "monster" msgid "Horror Captain" msgstr "Õuduse kapten" #: Source/translation_dummy.cpp:38 msgctxt "monster" msgid "Invisible Lord" msgstr "Nähtamatu isand" #: Source/translation_dummy.cpp:39 msgctxt "monster" msgid "Hidden" msgstr "Peidetud" #: Source/translation_dummy.cpp:40 msgctxt "monster" msgid "Stalker" msgstr "Jälitaja" #: Source/translation_dummy.cpp:41 msgctxt "monster" msgid "Unseen" msgstr "Nähtamatu" #: Source/translation_dummy.cpp:42 msgctxt "monster" msgid "Illusion Weaver" msgstr "Illusioonikuduja" #: Source/translation_dummy.cpp:43 msgctxt "monster" msgid "Satyr Lord" msgstr "Saatüürisand" #: Source/translation_dummy.cpp:44 msgctxt "monster" msgid "Flesh Clan" msgstr "Lihaklann" #: Source/translation_dummy.cpp:45 msgctxt "monster" msgid "Stone Clan" msgstr "Kiviklan" #: Source/translation_dummy.cpp:46 msgctxt "monster" msgid "Fire Clan" msgstr "Tule klann" #: Source/translation_dummy.cpp:47 msgctxt "monster" msgid "Night Clan" msgstr "Ööklann" #: Source/translation_dummy.cpp:48 msgctxt "monster" msgid "Fiend" msgstr "Deemon" #: Source/translation_dummy.cpp:49 msgctxt "monster" msgid "Blink" msgstr "Pilgutõmme" #: Source/translation_dummy.cpp:50 msgctxt "monster" msgid "Gloom" msgstr "Hämarus" #: Source/translation_dummy.cpp:51 msgctxt "monster" msgid "Familiar" msgstr "Tuttav" #: Source/translation_dummy.cpp:52 msgctxt "monster" msgid "Acid Beast" msgstr "Happemadu elajas" #: Source/translation_dummy.cpp:53 msgctxt "monster" msgid "Poison Spitter" msgstr "Mürgiprits" #: Source/translation_dummy.cpp:54 msgctxt "monster" msgid "Pit Beast" msgstr "Koopabeast" #: Source/translation_dummy.cpp:55 msgctxt "monster" msgid "Lava Maw" msgstr "Lava Lõug" #: Source/translation_dummy.cpp:56 msgctxt "monster" msgid "Skeleton King" msgstr "Luukuningas" #: Source/translation_dummy.cpp:57 msgctxt "monster" msgid "The Butcher" msgstr "Lihunik" #: Source/translation_dummy.cpp:58 msgctxt "monster" msgid "Overlord" msgstr "Ülemus" #: Source/translation_dummy.cpp:59 msgctxt "monster" msgid "Mud Man" msgstr "Mudamees" #: Source/translation_dummy.cpp:60 msgctxt "monster" msgid "Toad Demon" msgstr "Konnadeemon" #: Source/translation_dummy.cpp:61 msgctxt "monster" msgid "Flayed One" msgstr "Nülitud" #: Source/translation_dummy.cpp:62 msgctxt "monster" msgid "Wyrm" msgstr "Loheuss" #: Source/translation_dummy.cpp:63 msgctxt "monster" msgid "Cave Slug" msgstr "Koobas" #: Source/translation_dummy.cpp:64 msgctxt "monster" msgid "Devil Wyrm" msgstr "Kuradi Uss" #: Source/translation_dummy.cpp:65 msgctxt "monster" msgid "Devourer" msgstr "Neelaja" #: Source/translation_dummy.cpp:66 msgctxt "monster" msgid "Magma Demon" msgstr "Magma deemon" #: Source/translation_dummy.cpp:67 msgctxt "monster" msgid "Blood Stone" msgstr "Verikivi" #: Source/translation_dummy.cpp:68 msgctxt "monster" msgid "Hell Stone" msgstr "Põrgu Kivi" #: Source/translation_dummy.cpp:69 msgctxt "monster" msgid "Lava Lord" msgstr "Lava Isand" #: Source/translation_dummy.cpp:70 msgctxt "monster" msgid "Horned Demon" msgstr "Sarviline deemon" #: Source/translation_dummy.cpp:71 msgctxt "monster" msgid "Mud Runner" msgstr "Mudajooks" #: Source/translation_dummy.cpp:72 msgctxt "monster" msgid "Frost Charger" msgstr "Külmalaadija" #: Source/translation_dummy.cpp:73 msgctxt "monster" msgid "Obsidian Lord" msgstr "Obsidiaanist Isand" #: Source/translation_dummy.cpp:74 msgctxt "monster" msgid "oldboned" msgstr "vanaluine" #: Source/translation_dummy.cpp:75 msgctxt "monster" msgid "Red Death" msgstr "Punane Surm" #: Source/translation_dummy.cpp:76 msgctxt "monster" msgid "Litch Demon" msgstr "Litsidemon" #: Source/translation_dummy.cpp:77 msgctxt "monster" msgid "Undead Balrog" msgstr "Elav Balrog" #: Source/translation_dummy.cpp:78 msgctxt "monster" msgid "Incinerator" msgstr "Põletaja" #: Source/translation_dummy.cpp:79 msgctxt "monster" msgid "Flame Lord" msgstr "Leegilord" #: Source/translation_dummy.cpp:80 msgctxt "monster" msgid "Doom Fire" msgstr "Hukatusetuli" #: Source/translation_dummy.cpp:81 msgctxt "monster" msgid "Hell Burner" msgstr "Põrgu Põletaja" #: Source/translation_dummy.cpp:82 msgctxt "monster" msgid "Red Storm" msgstr "Punane Torm" #: Source/translation_dummy.cpp:83 msgctxt "monster" msgid "Storm Rider" msgstr "Tormisõitja" #: Source/translation_dummy.cpp:84 msgctxt "monster" msgid "Storm Lord" msgstr "Tormisandur" #: Source/translation_dummy.cpp:85 msgctxt "monster" msgid "Maelstrom" msgstr "Maelstrom" #: Source/translation_dummy.cpp:86 msgctxt "monster" msgid "Devil Kin Brute" msgstr "Kuradi Kinni Brutaal" #: Source/translation_dummy.cpp:87 msgctxt "monster" msgid "Winged-Demon" msgstr "Tiivul-demon" #: Source/translation_dummy.cpp:88 msgctxt "monster" msgid "Gargoyle" msgstr "Gargoil" #: Source/translation_dummy.cpp:89 msgctxt "monster" msgid "Blood Claw" msgstr "Veriküünis" #: Source/translation_dummy.cpp:90 msgctxt "monster" msgid "Death Wing" msgstr "Surmatiib" #: Source/translation_dummy.cpp:91 msgctxt "monster" msgid "Slayer" msgstr "Tapja" #: Source/translation_dummy.cpp:92 msgctxt "monster" msgid "Guardian" msgstr "Kaitsja" #: Source/translation_dummy.cpp:93 msgctxt "monster" msgid "Vortex Lord" msgstr "Vortexi isand" #: Source/translation_dummy.cpp:94 msgctxt "monster" msgid "Balrog" msgstr "Balrog" #: Source/translation_dummy.cpp:95 msgctxt "monster" msgid "Cave Viper" msgstr "Koobas Madu" #: Source/translation_dummy.cpp:96 msgctxt "monster" msgid "Fire Drake" msgstr "Tulelohe" #: Source/translation_dummy.cpp:97 msgctxt "monster" msgid "Gold Viper" msgstr "Kuldne Madu" #: Source/translation_dummy.cpp:98 msgctxt "monster" msgid "Azure Drake" msgstr "Asuurdrake" #: Source/translation_dummy.cpp:99 msgctxt "monster" msgid "Black Knight" msgstr "Must rüütel" #: Source/translation_dummy.cpp:100 msgctxt "monster" msgid "Doom Guard" msgstr "Hukatusvalvur" #: Source/translation_dummy.cpp:101 msgctxt "monster" msgid "Steel Lord" msgstr "Terasisandur" #: Source/translation_dummy.cpp:102 msgctxt "monster" msgid "Blood Knight" msgstr "Vererüütel" #: Source/translation_dummy.cpp:103 msgctxt "monster" msgid "The Shredded" msgstr "Rebitud" #: Source/translation_dummy.cpp:104 msgctxt "monster" msgid "Hollow One" msgstr "Tühi Üks" #: Source/translation_dummy.cpp:105 msgctxt "monster" msgid "Pain Master" msgstr "Valumeister" #: Source/translation_dummy.cpp:106 msgctxt "monster" msgid "Reality Weaver" msgstr "Reaalsuse Kuduja" #: Source/translation_dummy.cpp:107 msgctxt "monster" msgid "Succubus" msgstr "Sukkubus" #: Source/translation_dummy.cpp:108 msgctxt "monster" msgid "Snow Witch" msgstr "Lumewiit" #: Source/translation_dummy.cpp:109 msgctxt "monster" msgid "Hell Spawn" msgstr "Põrgu Sigitis" #: Source/translation_dummy.cpp:110 msgctxt "monster" msgid "Soul Burner" msgstr "Hingepõletaja" #: Source/translation_dummy.cpp:111 msgctxt "monster" msgid "Counselor" msgstr "Nõunik" #: Source/translation_dummy.cpp:112 msgctxt "monster" msgid "Magistrate" msgstr "Magistraat" #: Source/translation_dummy.cpp:113 msgctxt "monster" msgid "Cabalist" msgstr "Kabalist" #: Source/translation_dummy.cpp:114 msgctxt "monster" msgid "Advocate" msgstr "Advokaat" #: Source/translation_dummy.cpp:115 msgctxt "monster" msgid "Golem" msgstr "Golem" #: Source/translation_dummy.cpp:116 msgctxt "monster" msgid "The Dark Lord" msgstr "Tume Isand" #: Source/translation_dummy.cpp:117 msgctxt "monster" msgid "The Arch-Litch Malignus" msgstr "Ülilits Malignus" #: Source/translation_dummy.cpp:118 msgctxt "monster" msgid "Gharbad the Weak" msgstr "Nõrk Gharbad" #: Source/translation_dummy.cpp:119 msgctxt "monster" msgid "Zhar the Mad" msgstr "Zhar Hullumeelne" #: Source/translation_dummy.cpp:120 msgctxt "monster" msgid "Snotspill" msgstr "Tatiprits" #: Source/translation_dummy.cpp:121 msgctxt "monster" msgid "Arch-Bishop Lazarus" msgstr "Ülempiiskop Lazarus" #: Source/translation_dummy.cpp:122 msgctxt "monster" msgid "Red Vex" msgstr "Punane Vex" #: Source/translation_dummy.cpp:123 msgctxt "monster" msgid "Black Jade" msgstr "Must jadeiit" #: Source/translation_dummy.cpp:124 msgctxt "monster" msgid "Lachdanan" msgstr "Lachdanan" #: Source/translation_dummy.cpp:125 msgctxt "monster" msgid "Warlord of Blood" msgstr "Verelord" #: Source/translation_dummy.cpp:126 msgctxt "monster" msgid "Hork Demon" msgstr "Hork deemon" #: Source/translation_dummy.cpp:127 msgctxt "monster" msgid "The Defiler" msgstr "Rüvetaja" #: Source/translation_dummy.cpp:128 msgctxt "monster" msgid "Na-Krul" msgstr "Na-Krul" #: Source/translation_dummy.cpp:129 msgctxt "monster" msgid "Bonehead Keenaxe" msgstr "Luukere Teravkirves" #: Source/translation_dummy.cpp:130 msgctxt "monster" msgid "Bladeskin the Slasher" msgstr "Viilutaja Teranahk" #: Source/translation_dummy.cpp:131 msgctxt "monster" msgid "Soulpus" msgstr "Hingemäda" #: Source/translation_dummy.cpp:132 msgctxt "monster" msgid "Pukerat the Unclean" msgstr "Pukerat Rüve" #: Source/translation_dummy.cpp:133 msgctxt "monster" msgid "Boneripper" msgstr "Kondirabaja" #: Source/translation_dummy.cpp:134 msgctxt "monster" msgid "Rotfeast the Hungry" msgstr "Näljas Mädapidu" #: Source/translation_dummy.cpp:135 msgctxt "monster" msgid "Gutshank the Quick" msgstr "Kiire Gutshank" #: Source/translation_dummy.cpp:136 msgctxt "monster" msgid "Brokenhead Bangshield" msgstr "Murtudpea Kilpimüts" #: Source/translation_dummy.cpp:137 msgctxt "monster" msgid "Bongo" msgstr "Bongo" #: Source/translation_dummy.cpp:138 msgctxt "monster" msgid "Rotcarnage" msgstr "Verelõikur" #: Source/translation_dummy.cpp:139 msgctxt "monster" msgid "Shadowbite" msgstr "Varjuhammustus" #: Source/translation_dummy.cpp:140 msgctxt "monster" msgid "Deadeye" msgstr "Surnusilm" #: Source/translation_dummy.cpp:141 msgctxt "monster" msgid "Madeye the Dead" msgstr "Surnud Hullsilme" #: Source/translation_dummy.cpp:142 msgctxt "monster" msgid "El Chupacabras" msgstr "El Chupacabras" #: Source/translation_dummy.cpp:143 msgctxt "monster" msgid "Skullfire" msgstr "Koljuleek" #: Source/translation_dummy.cpp:144 msgctxt "monster" msgid "Warpskull" msgstr "Sõjakolju" #: Source/translation_dummy.cpp:145 msgctxt "monster" msgid "Goretongue" msgstr "Verikeel" #: Source/translation_dummy.cpp:146 msgctxt "monster" msgid "Pulsecrawler" msgstr "Pulssipug" #: Source/translation_dummy.cpp:147 msgctxt "monster" msgid "Moonbender" msgstr "Kuu Painutaja" #: Source/translation_dummy.cpp:148 msgctxt "monster" msgid "Wrathraven" msgstr "Raevuviha" #: Source/translation_dummy.cpp:149 msgctxt "monster" msgid "Spineeater" msgstr "Selgroogija" #: Source/translation_dummy.cpp:150 msgctxt "monster" msgid "Blackash the Burning" msgstr "Põlev Musttuha" #: Source/translation_dummy.cpp:151 msgctxt "monster" msgid "Shadowcrow" msgstr "Varjukaar" #: Source/translation_dummy.cpp:152 msgctxt "monster" msgid "Blightstone the Weak" msgstr "Nõrkuse Rõhukivi" #: Source/translation_dummy.cpp:153 msgctxt "monster" msgid "Bilefroth the Pit Master" msgstr "Bilefroth, Sügaviku Isand" #: Source/translation_dummy.cpp:154 msgctxt "monster" msgid "Bloodskin Darkbow" msgstr "Verinahk Nahkjas" #: Source/translation_dummy.cpp:155 msgctxt "monster" msgid "Foulwing" msgstr "" #: Source/translation_dummy.cpp:156 msgctxt "monster" msgid "Shadowdrinker" msgstr "Varjudejooja" #: Source/translation_dummy.cpp:157 msgctxt "monster" msgid "Hazeshifter" msgstr "Udusenihe" #: Source/translation_dummy.cpp:158 msgctxt "monster" msgid "Deathspit" msgstr "Surmasülg" #: Source/translation_dummy.cpp:159 msgctxt "monster" msgid "Bloodgutter" msgstr "Verelõikur" #: Source/translation_dummy.cpp:160 msgctxt "monster" msgid "Deathshade Fleshmaul" msgstr "Surmavarju Lihanuia" #: Source/translation_dummy.cpp:161 msgctxt "monster" msgid "Warmaggot the Mad" msgstr "Hullumeelne Warmaggot" #: Source/translation_dummy.cpp:162 msgctxt "monster" msgid "Glasskull the Jagged" msgstr "Klaaskolju Sakiline" #: Source/translation_dummy.cpp:163 msgctxt "monster" msgid "Blightfire" msgstr "Kõduleek" #: Source/translation_dummy.cpp:164 msgctxt "monster" msgid "Nightwing the Cold" msgstr "Öötiib Tihe Külm" #: Source/translation_dummy.cpp:165 msgctxt "monster" msgid "Gorestone" msgstr "Verikivi" #: Source/translation_dummy.cpp:166 msgctxt "monster" msgid "Bronzefist Firestone" msgstr "Tulekivi Pronksinupp" #: Source/translation_dummy.cpp:167 msgctxt "monster" msgid "Wrathfire the Doomed" msgstr "Hukatuslik Vihatuled" #: Source/translation_dummy.cpp:168 msgctxt "monster" msgid "Firewound the Grim" msgstr "Firewound, See Sünge" #: Source/translation_dummy.cpp:169 msgctxt "monster" msgid "Baron Sludge" msgstr "Parun Lörts" #: Source/translation_dummy.cpp:170 msgctxt "monster" msgid "Blighthorn Steelmace" msgstr "Blighthorni Terasnuia" #: Source/translation_dummy.cpp:171 msgctxt "monster" msgid "Chaoshowler" msgstr "Kaosikarjeja" #: Source/translation_dummy.cpp:172 msgctxt "monster" msgid "Doomgrin the Rotting" msgstr "Mädanik Doomgrin" #: Source/translation_dummy.cpp:173 msgctxt "monster" msgid "Madburner" msgstr "Hullupõletaja" #: Source/translation_dummy.cpp:174 msgctxt "monster" msgid "Bonesaw the Litch" msgstr "Luuasaag Lits" #: Source/translation_dummy.cpp:175 msgctxt "monster" msgid "Breakspine" msgstr "Selgroogmurdja" #: Source/translation_dummy.cpp:176 msgctxt "monster" msgid "Devilskull Sharpbone" msgstr "Kuradikolju Teravluu" #: Source/translation_dummy.cpp:177 msgctxt "monster" msgid "Brokenstorm" msgstr "Murtudtorm" #: Source/translation_dummy.cpp:178 msgctxt "monster" msgid "Stormbane" msgstr "Tormivits" #: Source/translation_dummy.cpp:179 msgctxt "monster" msgid "Oozedrool" msgstr "Limaoige" #: Source/translation_dummy.cpp:180 msgctxt "monster" msgid "Goldblight of the Flame" msgstr "Leegikuma Leegitseja" #: Source/translation_dummy.cpp:181 msgctxt "monster" msgid "Blackstorm" msgstr "Musttorm" #: Source/translation_dummy.cpp:182 msgctxt "monster" msgid "Plaguewrath" msgstr "Katkupõlgus" #: Source/translation_dummy.cpp:183 msgctxt "monster" msgid "The Flayer" msgstr "Nülgija" #: Source/translation_dummy.cpp:184 msgctxt "monster" msgid "Bluehorn" msgstr "Sinisarv" #: Source/translation_dummy.cpp:185 msgctxt "monster" msgid "Warpfire Hellspawn" msgstr "Tulepõrgu Järeltulija" #: Source/translation_dummy.cpp:186 msgctxt "monster" msgid "Fangspeir" msgstr "Kihvapiik" #: Source/translation_dummy.cpp:187 msgctxt "monster" msgid "Festerskull" msgstr "Mädakolju" #: Source/translation_dummy.cpp:188 msgctxt "monster" msgid "Lionskull the Bent" msgstr "Kõver Lõvikolju" #: Source/translation_dummy.cpp:189 msgctxt "monster" msgid "Blacktongue" msgstr "Mustkeel" #: Source/translation_dummy.cpp:190 msgctxt "monster" msgid "Viletouch" msgstr "Vilepuude" #: Source/translation_dummy.cpp:191 msgctxt "monster" msgid "Viperflame" msgstr "Viperileek" #: Source/translation_dummy.cpp:192 msgctxt "monster" msgid "Fangskin" msgstr "Kihvnahk" #: Source/translation_dummy.cpp:193 msgctxt "monster" msgid "Witchfire the Unholy" msgstr "Nõiatuli Pühaduseta" #: Source/translation_dummy.cpp:194 msgctxt "monster" msgid "Blackskull" msgstr "Mustkolju" #: Source/translation_dummy.cpp:195 msgctxt "monster" msgid "Soulslash" msgstr "Hingelõige" #: Source/translation_dummy.cpp:196 msgctxt "monster" msgid "Windspawn" msgstr "Tuulekülv" #: Source/translation_dummy.cpp:197 msgctxt "monster" msgid "Lord of the Pit" msgstr "Augu isand" #: Source/translation_dummy.cpp:198 msgctxt "monster" msgid "Rustweaver" msgstr "Roostekuduja" #: Source/translation_dummy.cpp:199 msgctxt "monster" msgid "Howlingire the Shade" msgstr "Ulguvari Varjund" #: Source/translation_dummy.cpp:200 msgctxt "monster" msgid "Doomcloud" msgstr "Hukatuspilv" #: Source/translation_dummy.cpp:201 msgctxt "monster" msgid "Bloodmoon Soulfire" msgstr "Veremoon Hingetuli" #: Source/translation_dummy.cpp:202 msgctxt "monster" msgid "Witchmoon" msgstr "Nõiakuu" #: Source/translation_dummy.cpp:203 msgctxt "monster" msgid "Gorefeast" msgstr "Veremöll" #: Source/translation_dummy.cpp:204 msgctxt "monster" msgid "Graywar the Slayer" msgstr "Tapja Hallusõdalane" #: Source/translation_dummy.cpp:205 msgctxt "monster" msgid "Dreadjudge" msgstr "Kohutavkohtunik" #: Source/translation_dummy.cpp:206 msgctxt "monster" msgid "Stareye the Witch" msgstr "Nõid Tähetäht" #: Source/translation_dummy.cpp:207 msgctxt "monster" msgid "Steelskull the Hunter" msgstr "Teraskolju Jahimees" #: Source/translation_dummy.cpp:208 msgctxt "monster" msgid "Sir Gorash" msgstr "Sir Gorash" #: Source/translation_dummy.cpp:209 msgctxt "monster" msgid "The Vizier" msgstr "Vezier" #: Source/translation_dummy.cpp:210 msgctxt "monster" msgid "Zamphir" msgstr "Zamphir" #: Source/translation_dummy.cpp:211 msgctxt "monster" msgid "Bloodlust" msgstr "Verejanu" #: Source/translation_dummy.cpp:212 msgctxt "monster" msgid "Webwidow" msgstr "Veebilesk" #: Source/translation_dummy.cpp:213 msgctxt "monster" msgid "Fleshdancer" msgstr "Lihavõlur" #: Source/translation_dummy.cpp:214 msgctxt "monster" msgid "Grimspike" msgstr "Grimspike" #: Source/translation_dummy.cpp:215 msgctxt "monster" msgid "Doomlock" msgstr "Hukatuslukk" #: Source/translation_dummy.cpp:217 msgid "Short Sword" msgstr "Lühike mõõk" #: Source/translation_dummy.cpp:218 msgid "Buckler" msgstr "Väike kilp" #: Source/translation_dummy.cpp:219 msgid "Club" msgstr "Nuia" #: Source/translation_dummy.cpp:220 msgid "Short Bow" msgstr "Lühike vibu" #: Source/translation_dummy.cpp:221 msgid "Short Staff of Mana" msgstr "Mana Lühiõõts" #: Source/translation_dummy.cpp:222 msgid "Cleaver" msgstr "Lihunikirves" #: Source/translation_dummy.cpp:223 msgid "The Undead Crown" msgstr "Undeadi kroon" #: Source/translation_dummy.cpp:224 msgid "Empyrean Band" msgstr "Empyrean Band" #: Source/translation_dummy.cpp:225 msgid "Magic Rock" msgstr "Maagiline Kivi" #: Source/translation_dummy.cpp:226 msgid "Optic Amulet" msgstr "Optiline amulett" #: Source/translation_dummy.cpp:227 msgid "Ring of Truth" msgstr "Tõe sõrmus" #: Source/translation_dummy.cpp:228 msgid "Tavern Sign" msgstr "Kõrtsi silt" #: Source/translation_dummy.cpp:229 msgid "Harlequin Crest" msgstr "Harlekiini Hari" #: Source/translation_dummy.cpp:230 msgid "Veil of Steel" msgstr "Teraseloor" #: Source/translation_dummy.cpp:231 msgid "Golden Elixir" msgstr "Kuldne Eliksiir" #: Source/translation_dummy.cpp:232 msgid "Anvil of Fury" msgstr "Raevualasi" #: Source/translation_dummy.cpp:233 msgid "Black Mushroom" msgstr "Must seen" #: Source/translation_dummy.cpp:234 msgid "Brain" msgstr "Aju" #: Source/translation_dummy.cpp:235 msgid "Fungal Tome" msgstr "Seenetome" #: Source/translation_dummy.cpp:236 msgid "Spectral Elixir" msgstr "Spektraalne eliksiir" #: Source/translation_dummy.cpp:237 msgid "Blood Stone" msgstr "Verikivi" #: Source/translation_dummy.cpp:238 msgid "Cathedral Map" msgstr "Katedraali kaart" #: Source/translation_dummy.cpp:239 msgid "Ear" msgstr "" #: Source/translation_dummy.cpp:240 msgid "Potion of Healing" msgstr "Tervendamise jook" #: Source/translation_dummy.cpp:241 msgid "Potion of Mana" msgstr "Mana jook" #: Source/translation_dummy.cpp:242 msgid "Scroll of Identify" msgstr "Identifitseerimise rull" #: Source/translation_dummy.cpp:243 msgid "Scroll of Town Portal" msgstr "Linna Portaali Rull" #: Source/translation_dummy.cpp:244 msgid "Arkaine's Valor" msgstr "Arkaine'i Vapruse" #: Source/translation_dummy.cpp:245 msgid "Potion of Full Healing" msgstr "Täieliku tervendamise jook" #: Source/translation_dummy.cpp:246 msgid "Potion of Full Mana" msgstr "Täismana jook" #: Source/translation_dummy.cpp:247 msgid "Griswold's Edge" msgstr "Griswoldi Serv" #: Source/translation_dummy.cpp:248 msgid "Bovine Plate" msgstr "Veiseplaat" #: Source/translation_dummy.cpp:249 msgid "Staff of Lazarus" msgstr "Lazari sau" #: Source/translation_dummy.cpp:250 msgid "Scroll of Resurrect" msgstr "Ülestõusmise rull" #: Source/translation_dummy.cpp:252 msgid "Short Staff" msgstr "Lühike sau" #: Source/translation_dummy.cpp:253 msgid "Sword" msgstr "Mõõk" #: Source/translation_dummy.cpp:254 msgid "Dagger" msgstr "Pistoda" #: Source/translation_dummy.cpp:255 msgid "Rune Bomb" msgstr "Ruunipomm" #: Source/translation_dummy.cpp:256 msgid "Theodore" msgstr "Theodore" #: Source/translation_dummy.cpp:257 msgid "Auric Amulet" msgstr "Auriku amulett" #: Source/translation_dummy.cpp:258 msgid "Torn Note 1" msgstr "Rebitud Märkus 1" #: Source/translation_dummy.cpp:259 msgid "Torn Note 2" msgstr "Rebitud Märkus 2" #: Source/translation_dummy.cpp:260 msgid "Torn Note 3" msgstr "Rebitud Märkus 3" #: Source/translation_dummy.cpp:261 msgid "Reconstructed Note" msgstr "Rekonstrueeritud märkus" #: Source/translation_dummy.cpp:262 msgid "Brown Suit" msgstr "Pruun ülikond" #: Source/translation_dummy.cpp:263 msgid "Grey Suit" msgstr "Hallikas ülikond" #: Source/translation_dummy.cpp:264 msgid "Cap" msgstr "Müts" #: Source/translation_dummy.cpp:265 msgid "Skull Cap" msgstr "Kolju kiiver" #: Source/translation_dummy.cpp:266 msgid "Helm" msgstr "Kiiver" #: Source/translation_dummy.cpp:267 msgid "Full Helm" msgstr "Täis kiiver" #: Source/translation_dummy.cpp:268 msgid "Crown" msgstr "Kroon" #: Source/translation_dummy.cpp:269 msgid "Great Helm" msgstr "Suur kiiver" #: Source/translation_dummy.cpp:270 msgid "Cape" msgstr "Keep" #: Source/translation_dummy.cpp:271 msgid "Rags" msgstr "Kaltsud" #: Source/translation_dummy.cpp:272 msgid "Cloak" msgstr "Keep" #: Source/translation_dummy.cpp:273 msgid "Robe" msgstr "Rüü" #: Source/translation_dummy.cpp:274 msgid "Quilted Armor" msgstr "Vatevattest soomusrüü" #: Source/translation_dummy.cpp:276 msgid "Leather Armor" msgstr "Nahksoomus" #: Source/translation_dummy.cpp:277 msgid "Hard Leather Armor" msgstr "Kõva nahast turvis" #: Source/translation_dummy.cpp:278 msgid "Studded Leather Armor" msgstr "Nahknastudega turvis" #: Source/translation_dummy.cpp:279 msgid "Ring Mail" msgstr "Rõõmsoomus" #: Source/translation_dummy.cpp:280 msgid "Mail" msgstr "Rüü" #: Source/translation_dummy.cpp:281 msgid "Chain Mail" msgstr "Rüü" #: Source/translation_dummy.cpp:282 msgid "Scale Mail" msgstr "Soomusrüü" #: Source/translation_dummy.cpp:283 msgid "Breast Plate" msgstr "Rinnaturvis" #: Source/translation_dummy.cpp:284 msgid "Plate" msgstr "Rüü" #: Source/translation_dummy.cpp:285 msgid "Splint Mail" msgstr "Liistturvis" #: Source/translation_dummy.cpp:286 msgid "Plate Mail" msgstr "Plaatsoomusrüü" #: Source/translation_dummy.cpp:287 msgid "Field Plate" msgstr "Plaatsoomusrüü" #: Source/translation_dummy.cpp:288 msgid "Gothic Plate" msgstr "Gooti plaatrüü" #: Source/translation_dummy.cpp:289 msgid "Full Plate Mail" msgstr "Täisplaatrüü" #: Source/translation_dummy.cpp:290 msgid "Shield" msgstr "Kilp" #: Source/translation_dummy.cpp:291 msgid "Small Shield" msgstr "Väike kilp" #: Source/translation_dummy.cpp:292 msgid "Large Shield" msgstr "Suur kilp" #: Source/translation_dummy.cpp:293 msgid "Kite Shield" msgstr "Loorskilp" #: Source/translation_dummy.cpp:294 msgid "Tower Shield" msgstr "Tornikaitse" #: Source/translation_dummy.cpp:295 msgid "Gothic Shield" msgstr "Gooti kilp" #: Source/translation_dummy.cpp:296 msgid "Potion of Rejuvenation" msgstr "Noorendamise jook" #: Source/translation_dummy.cpp:297 msgid "Potion of Full Rejuvenation" msgstr "Täieliku noorendamise eliksiir" #: Source/translation_dummy.cpp:300 msgid "Oil" msgstr "Õli" #: Source/translation_dummy.cpp:301 msgid "Elixir of Strength" msgstr "Tugevuse eliksiir" #: Source/translation_dummy.cpp:302 msgid "Elixir of Magic" msgstr "Maagia eliksiir" #: Source/translation_dummy.cpp:303 msgid "Elixir of Dexterity" msgstr "Osavuse eliksiir" #: Source/translation_dummy.cpp:304 msgid "Elixir of Vitality" msgstr "Eliksiir Elujõust" #: Source/translation_dummy.cpp:305 msgid "Scroll of Healing" msgstr "Tervendamise rull" #: Source/translation_dummy.cpp:306 msgid "Scroll of Search" msgstr "Otsingurull" #: Source/translation_dummy.cpp:307 msgid "Scroll of Lightning" msgstr "Välgu rull" #: Source/translation_dummy.cpp:308 msgid "Scroll of Fire Wall" msgstr "Tulemüüri rull" #: Source/translation_dummy.cpp:309 msgid "Scroll of Inferno" msgstr "Põrgu kerimine" #: Source/translation_dummy.cpp:310 msgid "Scroll of Flash" msgstr "Välgu rull" #: Source/translation_dummy.cpp:311 msgid "Scroll of Infravision" msgstr "Infranägemise rull" #: Source/translation_dummy.cpp:312 msgid "Scroll of Phasing" msgstr "Faasi kerimine" #: Source/translation_dummy.cpp:313 msgid "Scroll of Mana Shield" msgstr "Mana Kilbi Rull" #: Source/translation_dummy.cpp:314 msgid "Scroll of Flame Wave" msgstr "Leegilaine rull" #: Source/translation_dummy.cpp:315 msgid "Scroll of Fireball" msgstr "Tulepalli kerimine" #: Source/translation_dummy.cpp:316 msgid "Scroll of Stone Curse" msgstr "Kivineeduse rull" #: Source/translation_dummy.cpp:317 msgid "Scroll of Chain Lightning" msgstr "Ahelkiri Kettvälk" #: Source/translation_dummy.cpp:318 msgid "Scroll of Guardian" msgstr "Valvuri rull" #: Source/translation_dummy.cpp:319 msgid "Scroll of Nova" msgstr "Nova kerimine" #: Source/translation_dummy.cpp:320 msgid "Scroll of Golem" msgstr "Golemi rull" #: Source/translation_dummy.cpp:321 msgid "Scroll of Teleport" msgstr "Teleporteerimise rull" #: Source/translation_dummy.cpp:322 msgid "Scroll of Apocalypse" msgstr "Apokalüpsise rull" #: Source/translation_dummy.cpp:323 msgid "Falchion" msgstr "Falchion" #: Source/translation_dummy.cpp:324 msgid "Scimitar" msgstr "Kaheterane mõõk" #: Source/translation_dummy.cpp:325 msgid "Claymore" msgstr "Claymore" #: Source/translation_dummy.cpp:326 msgid "Blade" msgstr "Tera" #: Source/translation_dummy.cpp:327 msgid "Sabre" msgstr "Saabel" #: Source/translation_dummy.cpp:328 msgid "Long Sword" msgstr "Pikk mõõk" #: Source/translation_dummy.cpp:329 msgid "Broad Sword" msgstr "Lai mõõk" #: Source/translation_dummy.cpp:330 msgid "Bastard Sword" msgstr "Pärispea mõõk" #: Source/translation_dummy.cpp:331 msgid "Two-Handed Sword" msgstr "Kahesekäeline mõõk" #: Source/translation_dummy.cpp:332 msgid "Great Sword" msgstr "Suur mõõk" #: Source/translation_dummy.cpp:333 msgid "Small Axe" msgstr "Väike kirves" #: Source/translation_dummy.cpp:334 msgid "Axe" msgstr "Kirves" #: Source/translation_dummy.cpp:335 msgid "Large Axe" msgstr "Suur kirves" #: Source/translation_dummy.cpp:336 msgid "Broad Axe" msgstr "Lai kirves" #: Source/translation_dummy.cpp:337 msgid "Battle Axe" msgstr "Lahingukirves" #: Source/translation_dummy.cpp:338 msgid "Great Axe" msgstr "Võimas kirves" #: Source/translation_dummy.cpp:339 msgid "Mace" msgstr "Nuia" #: Source/translation_dummy.cpp:340 msgid "Morning Star" msgstr "Hommikutäht" #: Source/translation_dummy.cpp:341 msgid "War Hammer" msgstr "Sõjahaamer" #: Source/translation_dummy.cpp:342 msgid "Hammer" msgstr "Vasar" #: Source/translation_dummy.cpp:343 msgid "Spiked Club" msgstr "Ogalikuga nui" #: Source/translation_dummy.cpp:344 msgid "Flail" msgstr "Vihmapiits" #: Source/translation_dummy.cpp:345 msgid "Maul" msgstr "Nuia" #: Source/translation_dummy.cpp:346 msgid "Bow" msgstr "Vibu" #: Source/translation_dummy.cpp:347 msgid "Hunter's Bow" msgstr "Küti vibu" #: Source/translation_dummy.cpp:348 msgid "Long Bow" msgstr "Pikk vibu" #: Source/translation_dummy.cpp:349 msgid "Composite Bow" msgstr "Komposiitvibu" #: Source/translation_dummy.cpp:350 msgid "Short Battle Bow" msgstr "Lühike lahinguvibu" #: Source/translation_dummy.cpp:351 msgid "Long Battle Bow" msgstr "Pikk lahinguvibu" #: Source/translation_dummy.cpp:352 msgid "Short War Bow" msgstr "Lühike sõjasõrmus" #: Source/translation_dummy.cpp:353 msgid "Long War Bow" msgstr "Pikk sõjasulg" #: Source/translation_dummy.cpp:355 msgid "Long Staff" msgstr "Pikk sau" #: Source/translation_dummy.cpp:356 msgid "Composite Staff" msgstr "Komposiitkepp" #: Source/translation_dummy.cpp:357 msgid "Quarter Staff" msgstr "Veerandkepp" #: Source/translation_dummy.cpp:358 msgid "War Staff" msgstr "Sõjasau" #: Source/translation_dummy.cpp:359 msgid "Ring" msgstr "Sõrmus" #: Source/translation_dummy.cpp:360 msgid "Amulet" msgstr "Amulett" #: Source/translation_dummy.cpp:361 msgid "Rune of Fire" msgstr "Tuleruuna" #: Source/translation_dummy.cpp:362 msgid "Rune" msgstr "Ruun" #: Source/translation_dummy.cpp:363 msgid "Rune of Lightning" msgstr "Välgu ruun" #: Source/translation_dummy.cpp:364 msgid "Greater Rune of Fire" msgstr "Suurem Tuleruun" #: Source/translation_dummy.cpp:365 msgid "Greater Rune of Lightning" msgstr "Suurem välgu ruun" #: Source/translation_dummy.cpp:366 msgid "Rune of Stone" msgstr "Kiviruuni" #: Source/translation_dummy.cpp:367 msgid "Short Staff of Charged Bolt" msgstr "Laetud noole lühike sau" #: Source/translation_dummy.cpp:368 msgid "Arena Potion" msgstr "Arena jook" #: Source/translation_dummy.cpp:369 msgid "The Butcher's Cleaver" msgstr "Lihuniku kirves" #: Source/translation_dummy.cpp:370 #, fuzzy #| msgid "Lightsabre" msgid "Lightforge" msgstr "Valgusemõõk" #: Source/translation_dummy.cpp:371 msgid "The Rift Bow" msgstr "Lõhe Vibu" #: Source/translation_dummy.cpp:372 msgid "The Needler" msgstr "Nõelaja" #: Source/translation_dummy.cpp:373 msgid "The Celestial Bow" msgstr "Taevalik vibu" #: Source/translation_dummy.cpp:374 msgid "Deadly Hunter" msgstr "Surmav Kütt" #: Source/translation_dummy.cpp:375 msgid "Bow of the Dead" msgstr "Surnute vibu" #: Source/translation_dummy.cpp:376 msgid "The Blackoak Bow" msgstr "Musttamme vibu" #: Source/translation_dummy.cpp:377 msgid "Flamedart" msgstr "Tuleodav" #: Source/translation_dummy.cpp:378 msgid "Fleshstinger" msgstr "Lihasõel" #: Source/translation_dummy.cpp:379 msgid "Windforce" msgstr "Tuulejõud" #: Source/translation_dummy.cpp:380 msgid "Eaglehorn" msgstr "Kotkasarv" #: Source/translation_dummy.cpp:381 msgid "Gonnagal's Dirk" msgstr "Gonnagali pistoda" #: Source/translation_dummy.cpp:382 msgid "The Defender" msgstr "Kaitsja" #: Source/translation_dummy.cpp:383 msgid "Gryphon's Claw" msgstr "Grüfoni Küünis" #: Source/translation_dummy.cpp:384 msgid "Black Razor" msgstr "Must Razor" #: Source/translation_dummy.cpp:385 msgid "Gibbous Moon" msgstr "Kahane Kuu" #: Source/translation_dummy.cpp:386 msgid "Ice Shank" msgstr "Jääteravik" #: Source/translation_dummy.cpp:387 msgid "The Executioner's Blade" msgstr "Timuka Mõõk" #: Source/translation_dummy.cpp:388 msgid "The Bonesaw" msgstr "Luunasaag" #: Source/translation_dummy.cpp:389 msgid "Shadowhawk" msgstr "Varjukull" #: Source/translation_dummy.cpp:390 msgid "Wizardspike" msgstr "Võluripiik" #: Source/translation_dummy.cpp:391 msgid "Lightsabre" msgstr "Valgusemõõk" #: Source/translation_dummy.cpp:392 msgid "The Falcon's Talon" msgstr "Pistrikuküünis" #: Source/translation_dummy.cpp:393 msgid "Inferno" msgstr "Põrgu" #: Source/translation_dummy.cpp:394 msgid "Doombringer" msgstr "Hukatusetooja" #: Source/translation_dummy.cpp:395 msgid "The Grizzly" msgstr "Grisli" #: Source/translation_dummy.cpp:396 msgid "The Grandfather" msgstr "Vanaisa" #: Source/translation_dummy.cpp:397 msgid "The Mangler" msgstr "Purustaja" #: Source/translation_dummy.cpp:398 msgid "Sharp Beak" msgstr "Terav Nokk" #: Source/translation_dummy.cpp:399 msgid "BloodSlayer" msgstr "Veretapja" #: Source/translation_dummy.cpp:400 msgid "The Celestial Axe" msgstr "Taevalik kirves" #: Source/translation_dummy.cpp:401 msgid "Wicked Axe" msgstr "