├── .clang-format ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CMakeLists.txt ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── data ├── death_royal.txt ├── death_tomb.txt ├── help.txt ├── help_wizard.txt ├── rl_help.txt ├── rl_help_wizard.txt ├── scores.dat ├── splash.txt.in ├── versions.txt.in └── welcome.txt ├── historical ├── CHANGELOG ├── README.md ├── dragons.md ├── errors.md ├── faq.md ├── features.md ├── history.md └── manual.md ├── src ├── character.cpp ├── character.h ├── config.cpp ├── config.h ├── curses.h ├── data_creatures.cpp ├── data_player.cpp ├── data_recall.cpp ├── data_store_owners.cpp ├── data_stores.cpp ├── data_tables.cpp ├── data_treasure.cpp ├── dice.cpp ├── dice.h ├── dungeon.cpp ├── dungeon.h ├── dungeon_generate.cpp ├── dungeon_los.cpp ├── dungeon_tile.h ├── game.cpp ├── game.h ├── game_death.cpp ├── game_files.cpp ├── game_objects.cpp ├── game_run.cpp ├── game_save.cpp ├── headers.h ├── helpers.cpp ├── helpers.h ├── identification.cpp ├── identification.h ├── inventory.cpp ├── inventory.h ├── mage_spells.cpp ├── mage_spells.h ├── main.cpp ├── monster.cpp ├── monster.h ├── monster_manager.cpp ├── player.cpp ├── player.h ├── player_bash.cpp ├── player_eat.cpp ├── player_magic.cpp ├── player_move.cpp ├── player_pray.cpp ├── player_quaff.cpp ├── player_run.cpp ├── player_stats.cpp ├── player_throw.cpp ├── player_traps.cpp ├── player_tunnel.cpp ├── recall.cpp ├── recall.h ├── rng.cpp ├── rng.h ├── scores.cpp ├── scores.h ├── scrolls.cpp ├── scrolls.h ├── spells.cpp ├── spells.h ├── staves.cpp ├── staves.h ├── store.cpp ├── store.h ├── store_inventory.cpp ├── treasure.cpp ├── treasure.h ├── types.h ├── ui.cpp ├── ui.h ├── ui_inventory.cpp ├── ui_io.cpp ├── version.h ├── wizard.cpp └── wizard.h └── umoria.spec /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveDeclarations: false 8 | AlignEscapedNewlinesLeft: false 9 | AlignOperands: true 10 | AlignTrailingComments: true 11 | AllowAllParametersOfDeclarationOnNextLine: true 12 | AllowShortBlocksOnASingleLine: false 13 | AllowShortCaseLabelsOnASingleLine: false 14 | AllowShortFunctionsOnASingleLine: None 15 | AllowShortIfStatementsOnASingleLine: false 16 | AllowShortLoopsOnASingleLine: false 17 | AlwaysBreakAfterReturnType: None 18 | AlwaysBreakBeforeMultilineStrings: false 19 | AlwaysBreakTemplateDeclarations: false 20 | BinPackArguments: true 21 | BinPackParameters: true 22 | BraceWrapping: 23 | AfterClass: false 24 | AfterControlStatement: false 25 | AfterEnum: false 26 | AfterFunction: false 27 | AfterNamespace: false 28 | AfterObjCDeclaration: false 29 | AfterStruct: false 30 | AfterUnion: false 31 | BeforeCatch: false 32 | BeforeElse: false 33 | IndentBraces: false 34 | BreakBeforeBinaryOperators: None 35 | BreakBeforeBraces: Attach 36 | BreakBeforeTernaryOperators: true 37 | BreakConstructorInitializersBeforeComma: false 38 | BreakStringLiterals: false 39 | ColumnLimit: 180 40 | CommentPragmas: '^ IWYU pragma:' 41 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 42 | ConstructorInitializerIndentWidth: 4 43 | ContinuationIndentWidth: 4 44 | Cpp11BracedListStyle: true 45 | DerivePointerAlignment: false 46 | DisableFormat: false 47 | ExperimentalAutoDetectBinPacking: false 48 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 49 | IncludeCategories: 50 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 51 | Priority: 2 52 | - Regex: '^(<|"(gtest|isl|json)/)' 53 | Priority: 3 54 | - Regex: '.*' 55 | Priority: 1 56 | IncludeIsMainRegex: '$' 57 | IndentCaseLabels: true 58 | IndentWidth: 4 59 | IndentWrappedFunctionNames: false 60 | KeepEmptyLinesAtTheStartOfBlocks: false 61 | MacroBlockBegin: '' 62 | MacroBlockEnd: '' 63 | MaxEmptyLinesToKeep: 1 64 | NamespaceIndentation: None 65 | ObjCBlockIndentWidth: 4 66 | ObjCSpaceAfterProperty: false 67 | ObjCSpaceBeforeProtocolList: true 68 | PenaltyBreakBeforeFirstCallParameter: 19 69 | PenaltyBreakComment: 300 70 | PenaltyBreakFirstLessLess: 120 71 | PenaltyBreakString: 1000 72 | PenaltyExcessCharacter: 1000000 73 | PenaltyReturnTypeOnItsOwnLine: 60 74 | PointerAlignment: Right 75 | ReflowComments: true 76 | SortIncludes: false 77 | SpaceAfterCStyleCast: true 78 | SpaceBeforeAssignmentOperators: true 79 | SpaceBeforeParens: ControlStatements 80 | SpaceInEmptyParentheses: false 81 | SpacesBeforeTrailingComments: 1 82 | SpacesInAngles: false 83 | SpacesInContainerLiterals: true 84 | SpacesInCStyleCastParentheses: false 85 | SpacesInParentheses: false 86 | SpacesInSquareBrackets: false 87 | Standard: Cpp11 88 | TabWidth: 4 89 | UseTab: Never 90 | ... 91 | 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # CMake 4 | Makefile 5 | CMakeCache.txt 6 | CMakeFiles 7 | CMakeScripts 8 | cmake_install.cmake 9 | install_manifest.txt 10 | CPackConfig.cmake 11 | CPackSourceConfig.cmake 12 | CTestTestfile.cmake 13 | cmake-build-debug 14 | cmake-build-release 15 | _CPack_Packages 16 | 17 | # Prerequisites 18 | *.d 19 | 20 | # Object files 21 | *.o 22 | *.ko 23 | *.obj 24 | *.elf 25 | 26 | # Linker output 27 | *.ilk 28 | *.map 29 | *.exp 30 | 31 | # Precompiled Headers 32 | *.gch 33 | *.pch 34 | 35 | # Libraries 36 | *.lib 37 | *.a 38 | *.la 39 | *.lo 40 | 41 | # Shared objects (inc. Windows DLLs) 42 | *.dll 43 | *.so 44 | *.so.* 45 | *.dylib 46 | 47 | # Executables 48 | *.exe 49 | *.out 50 | *.app 51 | *.i*86 52 | *.x86_64 53 | *.hex 54 | 55 | # Debug files 56 | *.dSYM/ 57 | *.su 58 | *.idb 59 | *.pdb 60 | 61 | # Kernel Module Compile Results 62 | *.mod* 63 | *.cmd 64 | modules.order 65 | Module.symvers 66 | Mkfile.old 67 | dkms.conf 68 | 69 | *.cbp 70 | 71 | # build directories 72 | umoria.xcodeproj 73 | nbproject 74 | umoria 75 | 76 | compile_commands.json 77 | *.gz 78 | *.zip 79 | 80 | .DS_Store 81 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Moria / Umoria Authors 2 | 3 | Robert Alan Koeneke created Moria from 1981-86. James E. Wilson took 4 | the VMS Moria sources and created Umoria from 1987-94. 5 | 6 | In 2008 Moria/Umoria was re-licensed under the GNU General Public License 7 | version 3.0-or-later. All new additions to the code will fall under this license. 8 | 9 | 10 | ## Umoria 5.7.x 11 | 12 | Michael R. Cook [-MRC-] (GPL-3.0-or-later) 13 | Umoria restoration project: 14 | Windows and macOS support 15 | deprecation of old computer systems (Amiga, Atari ST, etc.) 16 | major refactoring of the source code 17 | 18 | 19 | ## VMS Moria 0.1 - 4.8 / Umoria 4.8 - 5.6 20 | 21 | Many people have contributed to the game since Koeneke released Moria 22 | in 1983. Their names, license details, and contributions summary are 23 | presented below. 24 | 25 | 26 | Robert Alan Koeneke [-RAK-] (GPL v2) 27 | Moria creator 28 | 29 | James E. Wilson [-JEW-] (GPL v2) 30 | Umoria creator 31 | 32 | David J. Grabiner [-DJG-] (GPL v2) 33 | handle large items in chests 34 | as maintainer 35 | 36 | Jimmey Wayne Todd Jr. [-JWT-] 37 | character generation (race, class, stats, etc.) 38 | character adjustments (AC, HP, INT, WIS, damage, to hit) 39 | calculate total player points 40 | print player character/history 41 | game save/load code 42 | add player to scores 43 | various other features and functions 44 | 45 | D. G. Kneller (Public Domain) 46 | MSDOS Moria port 47 | reduced map display 48 | 16 bit integers 49 | 50 | Christopher J. Stuart [-CJS-] (GPL v2) 51 | recall 52 | options 53 | inventory 54 | running code 55 | 56 | Curtis McCauley (GPL v2) 57 | Macintosh Moria port for MPW C 58 | 59 | Stephen A. Jacobs (GPL v2) 60 | Atari ST Moria port for MW C 61 | 62 | William Setzer (GPL v2) 63 | object naming code 64 | 65 | Dan Bernstein [-DJB-] (Public Domain) 66 | "no need to bargain" functionality 67 | update bargin info 68 | UNIX hangup signal fix 69 | many bug fixes 70 | 71 | Corey Gehman (GPL v2) 72 | Amiga Moria port 73 | 74 | Ralph Waters (GPL v2) 75 | VMS support code 76 | IBM-PC Turbo C bug fixes 77 | 78 | Joshua Delahunty (GPL v2) 79 | VMS support code 80 | 81 | Todd Pierzina (GPL v2) 82 | VMS support code 83 | 84 | Joseph Hall (Public Domain) 85 | line of sight code 86 | monster compiler 87 | 88 | Eric Vaitl (GPL v2) 89 | PC-Curses replacement for Turbo-C 90 | 91 | Scott Kolodzieski (GPL v2) 92 | Atari ST port for GNU C 93 | 94 | Hildo Biersma (Public Domain) 95 | Atari ST port for Turbo C 96 | 97 | Ben Schreiber (GPL v2) 98 | Macintosh port for Think C 99 | 100 | Carlton Hommel (Public Domain) 101 | the 'print items' utility 102 | 103 | Wayne Schlitt (GPL v2) 104 | the 'calculate hits' utility 105 | 106 | Brian W. Johnson (GPL v2) 107 | Linux support 108 | 109 | Ronald Cook (Public Domain) 110 | lattice C support 111 | 112 | Eric Bazin (Public Domain) 113 | merge monster memories 114 | 115 | Berthold Gunreben (GPL v2) 116 | HP-UX support 117 | 118 | Andrew Chernov (Public Domain) 119 | 386 BSD support 120 | 121 | Harry Johnston (GPL v2) 122 | specify an item by its numeric inscription 123 | 124 | Rene Weber [-RJW-] (GPL v2) 125 | compiling and doc updates for Umoria v5.6 126 | 127 | 128 | The following signature is currently unknown but listed here for completeness. 129 | 130 | -RGM- (contributed > v4.8.5.2 and <= 4.8.7 ) 131 | created get_all_stats() for character class loop 132 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6 FATAL_ERROR) 2 | 3 | # Do not allow in-source builds 4 | if (${CMAKE_SOURCE_DIR} STREQUAL "${PROJECT_SOURCE_DIR}/src") 5 | message(FATAL_ERROR "CMake generation is not allowed within the source directory!") 6 | endif () 7 | 8 | project(umoria 9 | LANGUAGES CXX 10 | ) 11 | 12 | # 13 | # Set a default build type 14 | # 15 | set(default_build_type "Release") 16 | if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 17 | message(STATUS "Setting build type to '${default_build_type}' as none was specified.") 18 | set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE STRING "Choose the type of build." FORCE) 19 | # Set the possible values of build type for cmake-gui 20 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") 21 | else () 22 | message(STATUS "Build type set to '${CMAKE_BUILD_TYPE}'") 23 | endif () 24 | 25 | # Compiler settings 26 | set(CMAKE_CXX_STANDARD 14) 27 | set(CMAKE_CXX_STANDARD_REQUIRED on) 28 | 29 | # 30 | # Core set of warnings 31 | # 32 | set(cxx_warnings "-Wall") 33 | set(cxx_warnings "${cxx_warnings} -Wextra") 34 | set(cxx_warnings "${cxx_warnings} -Wpedantic") 35 | set(cxx_warnings "${cxx_warnings} -Wshadow") 36 | set(cxx_warnings "${cxx_warnings} -Werror") 37 | set(cxx_warnings "${cxx_warnings} -pedantic-errors") 38 | set(cxx_warnings "${cxx_warnings} -Weffc++ ") 39 | 40 | # Additional warnings 41 | set(cxx_warnings "${cxx_warnings} -Wcast-align") 42 | set(cxx_warnings "${cxx_warnings} -Wdisabled-optimization") 43 | set(cxx_warnings "${cxx_warnings} -Wfloat-equal") 44 | set(cxx_warnings "${cxx_warnings} -Winline") 45 | set(cxx_warnings "${cxx_warnings} -Winvalid-pch") 46 | set(cxx_warnings "${cxx_warnings} -Wmissing-format-attribute") 47 | set(cxx_warnings "${cxx_warnings} -Wmissing-include-dirs") 48 | set(cxx_warnings "${cxx_warnings} -Wpacked") 49 | set(cxx_warnings "${cxx_warnings} -Wredundant-decls") 50 | set(cxx_warnings "${cxx_warnings} -Wswitch-default") 51 | set(cxx_warnings "${cxx_warnings} -Wswitch-enum") 52 | set(cxx_warnings "${cxx_warnings} -Wunreachable-code") 53 | set(cxx_warnings "${cxx_warnings} -Wwrite-strings") 54 | set(cxx_warnings "${cxx_warnings} -Wno-deprecated-declarations") 55 | 56 | # Some very strict warnings, that will be nice to use, but require some hefty refactoring 57 | # set(cxx_warnings "${cxx_warnings} -Wcast-qual") 58 | # set(cxx_warnings "${cxx_warnings} -Wconversion") 59 | # set(cxx_warnings "${cxx_warnings} -Wformat=2") 60 | # set(cxx_warnings "${cxx_warnings} -Wpadded") 61 | # set(cxx_warnings "${cxx_warnings} -Wstrict-overflow") 62 | # set(cxx_warnings "${cxx_warnings} -fno-strict-aliasing") 63 | 64 | # Temporary support for GCC 8. 65 | if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") 66 | set(cxx_warnings "${cxx_warnings} -Wno-format-overflow") 67 | endif() 68 | 69 | # 70 | # Set the flags and warnings for the debug/release builds 71 | # 72 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g3 -O0 ${cxx_warnings}") 73 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2 ${cxx_warnings}") 74 | 75 | 76 | # 77 | # Source files and directories 78 | # 79 | set(source_dir src) 80 | set( 81 | source_files 82 | ${source_dir}/character.h 83 | ${source_dir}/config.h 84 | ${source_dir}/curses.h 85 | ${source_dir}/dice.h 86 | ${source_dir}/dungeon.h 87 | ${source_dir}/dungeon_tile.h 88 | ${source_dir}/game.h 89 | ${source_dir}/headers.h 90 | ${source_dir}/helpers.h 91 | ${source_dir}/identification.h 92 | ${source_dir}/inventory.h 93 | ${source_dir}/mage_spells.h 94 | ${source_dir}/monster.h 95 | ${source_dir}/player.h 96 | ${source_dir}/recall.h 97 | ${source_dir}/rng.h 98 | ${source_dir}/scores.h 99 | ${source_dir}/scrolls.h 100 | ${source_dir}/spells.h 101 | ${source_dir}/staves.h 102 | ${source_dir}/store.h 103 | ${source_dir}/treasure.h 104 | ${source_dir}/types.h 105 | ${source_dir}/ui.h 106 | ${source_dir}/version.h 107 | ${source_dir}/wizard.h 108 | ${source_dir}/config.cpp 109 | ${source_dir}/helpers.cpp 110 | ${source_dir}/rng.cpp 111 | ${source_dir}/main.cpp 112 | ${source_dir}/data_creatures.cpp 113 | ${source_dir}/data_player.cpp 114 | ${source_dir}/data_recall.cpp 115 | ${source_dir}/data_store_owners.cpp 116 | ${source_dir}/data_stores.cpp 117 | ${source_dir}/data_tables.cpp 118 | ${source_dir}/data_treasure.cpp 119 | ${source_dir}/character.cpp 120 | ${source_dir}/dice.cpp 121 | ${source_dir}/dungeon.cpp 122 | ${source_dir}/dungeon_generate.cpp 123 | ${source_dir}/dungeon_los.cpp 124 | ${source_dir}/game.cpp 125 | ${source_dir}/game_death.cpp 126 | ${source_dir}/game_files.cpp 127 | ${source_dir}/game_objects.cpp 128 | ${source_dir}/game_run.cpp 129 | ${source_dir}/game_save.cpp 130 | ${source_dir}/identification.cpp 131 | ${source_dir}/inventory.cpp 132 | ${source_dir}/mage_spells.cpp 133 | ${source_dir}/monster.cpp 134 | ${source_dir}/monster_manager.cpp 135 | ${source_dir}/player.cpp 136 | ${source_dir}/player_bash.cpp 137 | ${source_dir}/player_eat.cpp 138 | ${source_dir}/player_magic.cpp 139 | ${source_dir}/player_move.cpp 140 | ${source_dir}/player_pray.cpp 141 | ${source_dir}/player_quaff.cpp 142 | ${source_dir}/player_run.cpp 143 | ${source_dir}/player_stats.cpp 144 | ${source_dir}/player_throw.cpp 145 | ${source_dir}/player_traps.cpp 146 | ${source_dir}/player_tunnel.cpp 147 | ${source_dir}/recall.cpp 148 | ${source_dir}/scores.cpp 149 | ${source_dir}/scrolls.cpp 150 | ${source_dir}/spells.cpp 151 | ${source_dir}/staves.cpp 152 | ${source_dir}/store.cpp 153 | ${source_dir}/store_inventory.cpp 154 | ${source_dir}/treasure.cpp 155 | ${source_dir}/ui.cpp 156 | ${source_dir}/ui_inventory.cpp 157 | ${source_dir}/ui_io.cpp 158 | ${source_dir}/wizard.cpp 159 | ) 160 | 161 | 162 | # 163 | # Set up the install paths and files 164 | # 165 | set(build_dir "umoria") 166 | set(data_dir "${build_dir}/data") 167 | 168 | 169 | set(EXECUTABLE_OUTPUT_PATH ${build_dir}) 170 | 171 | # Core game data files 172 | set( 173 | data_files 174 | data/help.txt 175 | data/help_wizard.txt 176 | data/rl_help.txt 177 | data/rl_help_wizard.txt 178 | data/welcome.txt 179 | data/death_tomb.txt 180 | data/death_royal.txt 181 | ) 182 | file(COPY ${data_files} DESTINATION "${data_dir}") 183 | 184 | # Various support files (readme, etc.) 185 | set( 186 | support_files 187 | data/scores.dat 188 | AUTHORS 189 | LICENSE 190 | ) 191 | file(COPY ${support_files} DESTINATION "${build_dir}") 192 | 193 | 194 | # 195 | # Extract the Umoria version number from version.h 196 | # 197 | file(STRINGS "${source_dir}/version.h" VERSION_HEADER) 198 | 199 | string(REGEX MATCH "CURRENT_VERSION_MAJOR = ([0-9]+);" ${VERSION_HEADER}) 200 | set(umoria_version_major ${CMAKE_MATCH_1}) 201 | 202 | string(REGEX MATCH "CURRENT_VERSION_MINOR = ([0-9]+);" ${VERSION_HEADER}) 203 | set(umoria_version_minor ${CMAKE_MATCH_1}) 204 | 205 | string(REGEX MATCH "CURRENT_VERSION_PATCH = ([0-9]+);" ${VERSION_HEADER}) 206 | set(umoria_version_patch ${CMAKE_MATCH_1}) 207 | 208 | set(umoria_version "${umoria_version_major}.${umoria_version_minor}.${umoria_version_patch}") 209 | 210 | 211 | # 212 | # Update the data files with the current version number and date 213 | # 214 | 215 | # Fetch release date from CHANGELOG if it's there :-) 216 | file(READ "CHANGELOG.md" changelog) 217 | string(REGEX MATCH "## ${umoria_version} \\(([0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])\\)" results ${changelog}) 218 | list(LENGTH results match_count) 219 | if (match_count EQUAL 0) 220 | string(TIMESTAMP current_date "%Y-%m-%d" UTC) 221 | else () 222 | set(current_date ${CMAKE_MATCH_1}) 223 | endif () 224 | 225 | set( 226 | data_files_to_update 227 | data/splash.txt 228 | data/versions.txt 229 | ) 230 | foreach (data_file ${data_files_to_update}) 231 | configure_file(${data_file}.in ${build_dir}/${data_file}) 232 | endforeach () 233 | 234 | 235 | # All of the game resource files 236 | set(resources ${data_files} ${support_files}) 237 | 238 | # Also add resources to the target so they are visible in the IDE 239 | add_executable(umoria ${source_files} ${resources}) 240 | 241 | 242 | # 243 | # Get around the fact that Visual Studio doesn't have ssize_t 244 | # defined but uses SSIZE_T instead. 245 | # 246 | if(MSVC) 247 | add_definitions(-Dssize_t=SSIZE_T) 248 | endif(MSVC) 249 | 250 | # This is horrible, but needed bacause `find_package()` doesn't use the 251 | # include/lib inside the /mingw32 or /mingw64 directories, and with 252 | # `ncurses-devel` installed, it won't compile. 253 | if ((MSYS OR MINGW) AND "$ENV{MINGW}" STREQUAL "") 254 | message(FATAL_ERROR "You must set the MINGW environment variable ('mingw64' or 'mingw32'). Example: MINGW=mingw64 cmake .") 255 | message(FATAL_ERROR "This will be the directory used for locating the ncurses library files.") 256 | elseif ((MSYS OR MINGW) AND NOT "$ENV{MINGW}" STREQUAL "") 257 | message(STATUS "NOTE: Configuring build for Windows release...") 258 | 259 | # Make the ncurses library static 260 | set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -static -lpthread -DNCURSES_STATIC") 261 | set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -static -lpthread -DNCURSES_STATIC") 262 | 263 | set(CURSES_INCLUDE_DIR "/$ENV{MINGW}/include/") 264 | set(CURSES_LIBRARIES "/$ENV{MINGW}/lib/libncurses.a") 265 | else () 266 | message(STATUS "NOTE: Configuring build for macOS/Linux release...") 267 | set(CURSES_NEED_NCURSES TRUE) 268 | find_package(Curses REQUIRED) 269 | endif () 270 | 271 | include_directories(${CURSES_INCLUDE_DIR}) 272 | target_link_libraries(umoria ${CURSES_LIBRARIES}) 273 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Umoria Contributor Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers 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, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [info@umoria.org](mailto:info@umoria.org). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Umoria 2 | 3 | Thanks for your interest in contributing to the Umoria project! 4 | 5 | The following is a set of guidelines for contributing to Umoria, which is hosted in the [Dungeons of Moria organization](https://github.com/dungeons-of-moria) on GitHub. The following are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. 6 | 7 | 8 | ## Code of Conduct 9 | 10 | This project and everyone participating in it is governed by the [Umoria Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [info@umoria.org](mailto:info@umoria.org). 11 | 12 | 13 | ## Questions and Discussions 14 | 15 | Currently the Umoria project does not have its own blog or community forum, however, if you wish to discuss the game or ask general questions about Umoria, the best place to start would be the [MoriaRL Reddit page](https://www.reddit.com/r/moriarl/). You'll get faster results than by using the resources below. 16 | 17 | For more general questions and discussions on the Roguelike genre, please visit the [Roguelike Reddit page](https://www.reddit.com/r/roguelikes/). 18 | 19 | These are both friendly and welcoming communities. 20 | 21 | 22 | ## What should I know before I get started? 23 | 24 | First, and most importantly, the current goal of the project is to not implement new gameplay changes or features to Umoria. The gameplay was fine-tuned over a period of many years and has been in its current state for more than 20 years. 25 | 26 | Perhaps at a future date gameplay changes will be considered, but for the moment it should not change. 27 | 28 | The focus of current work has been on cleaning up the code base; using _modern_ C/C++ coding styles, updating to compile against C++14, breaking up the many large and complex functions to create smaller more manageable blocks of code. Along with making the code easier to understand these changes will also make any future features easier to implement. One idea is to switch from NCurses to SDL to allow mouse support to be added. 29 | 30 | 31 | ## How Can I Contribute? 32 | 33 | ### Reporting Bugs 34 | 35 | This section guides you through submitting a bug report for Umoria. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports. 36 | 37 | Before creating bug reports, please check out the [current issues](https://github.com/dungeons-of-moria/umoria/issues) as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible, as indicated the next section. 38 | 39 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. 40 | 41 | 42 | #### How Do I Submit A (Good) Bug Report? 43 | 44 | Bugs are tracked as [GitHub issues](https://github.com/dungeons-of-moria/umoria/issues). Create an issue explaining the problem, and including any additional details that you think may help maintainers reproduce the problem: 45 | 46 | * **Use a clear and descriptive title** for the issue to identify the problem. 47 | * **Describe the exact steps to reproduce the problem** (if possible) with as many details as you can. 48 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. 49 | * **Explain which behavior you expected to see instead and why.** 50 | * **If you're reporting that Umoria crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a code block, a file attachment, or put it in a [gist](https://gist.github.com) and provide a link to that gist. 51 | 52 | Include details about your configuration and environment: 53 | 54 | * **Which version of Umoria are you using?** You can get the exact version by running `umoria -v` in your terminal. If you compiled directly from the source code, please state that. 55 | * **What's the name and version of the OS you're using**? 56 | * **Are you running Umoria in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? 57 | * **Which keyboard layout are you using?** Umoria or traditional Roguelike keys (`hjkl`)? 58 | 59 | 60 | ### Code Contributions 61 | 62 | General points worth remembering before making your Pull Request (PR): 63 | 64 | * Avoid platform-dependent code. 65 | * Format your source code to the provided [`.clang-format`](.clang-format) style. 66 | 67 | At present there are no strong style requirements, but here are a few ideas that I would like to start thinking about. 68 | 69 | #### General Formatting 70 | 71 | * Indentation: one indent should be *4 spaces*, so please be careful to avoid the use of _tabs_. 72 | 73 | #### Use Standard Types 74 | 75 | At present I'm using only _standard types_, so please do continue this practice. 76 | The common standard types are: 77 | 78 | * `int8_t`, `int16_t`, `int32_t`, `int64_t` — signed integers 79 | * `uint8_t`, `uint16_t`, `uint32_t`, `uint64_t` — unsigned integers 80 | * `float` — standard 32-bit floating point 81 | * `double` - standard 64-bit floating point 82 | 83 | When representing ASCII characters we should be using the `char` type. 84 | 85 | #### Naming 86 | 87 | * **Classes / Structs / Types**: `CamelCase`, with the first character uppercase. 88 | * **Functions**: `camelCase`, with the first character lowercase. 89 | * **Variables**: `snake_case`, and all lowercase. 90 | 91 | I like the easy visual distinction from naming like this. You can see immediately what its function is and without having _noisy_ suffixes. 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Umoria 2 | 3 | _The Dungeons of Moria_ is a single player dungeon simulation originally 4 | written by Robert Alan Koeneke, with its first public release in 1983. 5 | The game was originally developed using VMS Pascal before being ported to the 6 | C language by James E. Wilson in 1988, and released as _Umoria_. 7 | 8 | Moria/Umoria has had many variants over the years, with [_Angband_](http://rephial.org/) 9 | being the most well known. Umoria was also an inspiration for one the most 10 | commercially successful action roguelike games, _Diablo_! 11 | 12 | Supported Platforms: 13 | 14 | - Windows 15 | - macOS 16 | - Linux (Ubuntu/Debian) 17 | 18 | Compiling and limited testing has been done for other Linux based system 19 | including NetBSD 8.1 and Fedora 32. 20 | 21 | 22 | ## Umoria 5.7.x releases 23 | 24 | The main focus of the `5.7.0` release was to provide support for the three 25 | main operating systems: Windows, macOS, and Linux. Support for all other 26 | outdated computer systems such as MS DOS, "Classic" Mac OS (pre OSX), Amiga, 27 | and Atari ST was removed. 28 | 29 | _Note: there have been no intentional gameplay changes in the 5.7.x releases._ 30 | 31 | Since the initial 5.7 release, a great deal of _code restoration_ has been 32 | undertaken in the hope of aiding future development of the game. Some examples 33 | of the work done include reformatting the source code with the help of 34 | `clang-tidy` and `clang-format`, modernizing the code to use standard C types, 35 | breaking apart most large functions (many of which had hundreds of lines of code) 36 | into smaller, easier to read functions, and fixing all compiler warnings when 37 | compiling against recent versions of GCC and Clang. 38 | 39 | Full details of all changes can be found in the [CHANGELOG](CHANGELOG.md), and 40 | by browsing the commit history. 41 | 42 | Due to its lack of Windows and macOS support Moria was inaccessible to many 43 | people. Hopefully these changes will give many more people a chance to play 44 | this classic roguelike game. 45 | 46 | 47 | ## Notes on Compiling Umoria 48 | 49 | Umoria has been tested against GCC (`10` and `11`) and with `ncurses 6.x`, 50 | although recent earlier versions should also work fine. 51 | 52 | You will need these as well as `CMake` and the C++ build tools for your system. 53 | 54 | 55 | ### macOS and Linux 56 | 57 | Change to the `umoria` game directory and enter the following commands at the 58 | terminal: 59 | 60 | $ mkdir build && cd build 61 | $ cmake .. 62 | $ make 63 | 64 | NOTE: use `make -j $(nproc)` to speed up compilation on Linux. 65 | 66 | An `umoria` directory will be created in the current directory containing the 67 | game binary and data files, which can then be moved to any other location, such 68 | as the `home` directory. 69 | 70 | 71 | ### Windows 72 | 73 | MinGW is used to provide GCC and GNU Binutils for compiling on the Windows platform. 74 | The easiest solution to get set up is to use the [MSYS2 Installer](http://msys2.github.io/). 75 | Once installed, `pacman` can be used to install `GCC`, `ncurses`, and the 76 | `make`/`cmake` build tools. 77 | 78 | At present an environment variable for the MinGW system being compiled on will 79 | need to be specified. This will be either `mingw64` or `mingw32`. 80 | 81 | At the command prompt type the following, being sure to add the correct label 82 | to `MINGW=`: 83 | 84 | $ MINGW=mingw64 cmake . 85 | $ make 86 | 87 | To perform an out-of-source build, type the following: 88 | 89 | $ mkdir build 90 | $ cd build 91 | $ MINGW=mingw64 cmake .. 92 | $ make 93 | 94 | As with the macOS/Linux builds, all files will be installed into an `umoria` directory. 95 | 96 | 97 | ## Historical Documents 98 | 99 | Most of the original document files included in the Umoria 5.6 sources have 100 | been placed in the [historical](historical) directory. You will even find the 101 | old CHANGELOG, which tracks all code changes made between versions 4.81 and 102 | 5.5.2 (1987-2008). If you'd like to learn more on the development history of 103 | Umoria, these can make for interesting reading. 104 | 105 | There is also the original Moria Manual and FAQ. Although these are a little 106 | outdated now they are certainly worth reading as they contain a lot of 107 | interesting and useful information. 108 | 109 | 110 | ## Code of Conduct and Contributions 111 | 112 | See here for details on our [Code of Conduct](CODE_OF_CONDUCT.md). 113 | 114 | For details on how to contribute to the Umoria project, please read our 115 | [contributing](CONTRIBUTING.md) guide. 116 | 117 | 118 | ## License Information 119 | 120 | Umoria is released under the [GNU General Public License v3.0](LICENSE). 121 | 122 | In 2007 Ben Asselstine and Ben Shadwick started the 123 | [_free-moria_](http://free-moria.sourceforge.net/) project to re-license 124 | UMoria 5.5.2 under GPL-2 by obtaining permission from all the contributing 125 | authors. A year later they succeeded in their goal and in late 2008 official 126 | maintainer David Grabiner released Umoria 5.6 under a GPL-3.0-or-later license. 127 | -------------------------------------------------------------------------------- /data/death_royal.txt: -------------------------------------------------------------------------------- 1 | 2 | # 3 | ##### 4 | # 5 | ,,, $$$ ,,, 6 | ,,=$ "$$$$$" $=,, 7 | ,$$ $$$ $$, 8 | *> <*> <* 9 | $$ $$$ $$ 10 | "$$ $$$ $$" 11 | "$$ $$$ $$" 12 | *#########*#########* 13 | *#########*#########* 14 | 15 | 16 | Veni, Vidi, Vici! 17 | I came, I saw, I conquered! 18 | All Hail the Mighty 19 | -------------------------------------------------------------------------------- /data/death_tomb.txt: -------------------------------------------------------------------------------- 1 | 2 | _______________________ 3 | / \ ___ 4 | / \ ___ / \ ___ 5 | / RIP \ \ : : / \ 6 | / \ : _;,,,;_ : : 7 | / \,;_ _;,,,;_ 8 | | the | ___ 9 | | | / \ 10 | | | : : 11 | | | _;,,,;_ ____ 12 | | Level : | / \ 13 | | | : : 14 | | | : : 15 | | Died on Level : | _;,,,,;_ 16 | | killed by | 17 | | | 18 | | | 19 | *| * * * * * * | * 20 | ________)/\\_)_/___(\/___(//_\)/_\//__\\(/_|_)_______ 21 | -------------------------------------------------------------------------------- /data/help.txt: -------------------------------------------------------------------------------- 1 | Command Summary (@ is optional count, ~ is direction, ^R redraws, ESC aborts) 2 | a Aim and fire a wand | @ B ~ Bash item or monster 3 | b Browse a book | C Character description 4 | c ~ Close a door | @ D ~ Disarm a trap/chest 5 | d Drop an item | E Eat some food 6 | e Equipment list | F Fill lamp with oil 7 | f Fire/Throw an item | G Gain new magic spells 8 | i Inventory list | L Locate with map 9 | @ j ~ Jam a door with spike | M Map (shown reduced size) 10 | l ~ Look given direction | @ R Rest (count or *=restore) 11 | m Magic spell casting | S Search mode 12 | @ o ~ Open a door/chest | @ T ~ Tunnel in a direction 13 | p Pray | V View scoreboard 14 | q Quaff a potion | = Set options 15 | r Read a scroll | ? View this page 16 | @ s Search for traps or doors | { Inscribe an object 17 | t Take off an item | @ - Move without pickup 18 | u Use a staff | . ~ Run in direction 19 | v Version info and credits | / Identify a character 20 | w Wear/Wield an item | CTRL-K Quit the game 21 | x Exchange weapon | @ CTRL-P Repeat the last message 22 | < Go up an up-staircase | CTRL-X Save character and quit 23 | > Go down a down-staircase | @ ~ For movement 24 | Directions: 7 8 9 25 | 4 5 6 [5 to rest] 26 | 1 2 3 27 | 28 | To give a count to a command, type a '#', followed by the digits. A count 29 | of 0 defaults to a count of 99. Counts only work with some commands, and 30 | will be terminated by the same things that end a rest or a run. In 31 | particular, typing any character during the execution of a counted command 32 | will terminate the command. To count a movement command, hit space after 33 | the number, and you will be prompted for the command, which may be a digit. 34 | Counted searches or tunnels will terminate on success, or if you are 35 | attacked. A count with control-P will specify the number of previous messages 36 | to display. 37 | 38 | Control-R will redraw the screen whenever it is input, not only at command 39 | level. Control commands may be entered with a single key stroke, or with two 40 | key strokes by typing ^ and then a letter. 41 | 42 | Type ESCAPE to abort the look command at any point. 43 | 44 | Some commands will prompt for a spell, or an inventory item. Selection is 45 | by an alphabetic character - entering a capital causes a desription to be 46 | printed, and the selection may be aborted. 47 | 48 | Typing `R*' will make you rest until both your mana and your hp reach their 49 | maximum values. 50 | 51 | Umoria is free software, and you are welcome to distribute it 52 | under certain conditions; for details, type control-V. 53 | 54 | Umoria comes with ABSOLUTELY NO WARRANTY; for details, 55 | type control-V and view sections 15-17 of the License. 56 | -------------------------------------------------------------------------------- /data/help_wizard.txt: -------------------------------------------------------------------------------- 1 | : - Map area (panel) 2 | ^A - Cure all maladies and remove curses 3 | ^B - Print random objects sample to file 4 | ^D - Jump to depth # (level) 5 | ^E - Edit character stats 6 | ^F - Vanquish monsters within sight 7 | ^G - Generate random items 8 | ^H - Wizard Help 9 | ^I - Identify an item 10 | ^L - Wizard light 11 | ^T - Teleport player randomly 12 | ^U - Summon random monster 13 | ^W - Wizard mode on/off 14 | + - Gain experience 15 | % - Generate a dungeon item 16 | @ - Create an object *CAN CAUSE FATAL ERROR* 17 | -------------------------------------------------------------------------------- /data/rl_help.txt: -------------------------------------------------------------------------------- 1 | Command Summary (@ is optional count, ~ is direction, ^R redraws, ESC aborts) 2 | c ~ Close a door | C Character description 3 | d Drop an item | @ D ~ Disarm a trap/chest 4 | e Equipment list | E Eat some food 5 | @ f ~ Force (bash) item or mons.| F Fill lamp with oil 6 | i Inventory list | G Gain new magic spells 7 | m Magic spell casting | M Map (shown reduced size) 8 | @ o ~ Open a door/chest | P Peruse a book 9 | p Pray | Q Quit the game 10 | q Quaff a potion | @ R Rest (count or *=restore) 11 | r Read a scroll | @ S ~ Spike a door 12 | @ s Search for traps or doors | T Take off an item 13 | t Throw an item | V View scores 14 | v Version info and credits | W Where: locate self 15 | w Wear/Wield an item | X Exchange weapon 16 | x ~ Examine surroundings | Z Zap a staff 17 | z Zap a wand | # Search mode 18 | = Set options | < Go up an up-staircase 19 | / Identify a character | > Go down a down-staircase 20 | @ CTRL-P Previous message review | { Inscribe an object 21 | @ - ~ Move without pickup | ? View this page 22 | @ CTRL ~ Tunnel in a direction | CTRL-X Save character and quit 23 | @ SHIFT ~ Run in direction | @ ~ For movement 24 | Directions: y k u 25 | h . l [. to rest] 26 | b j n 27 | 28 | To give a count to a command, type the number in digits, then the command. 29 | A count of 0 defaults to a count of 99. Counts only work with some commands, 30 | and will be terminated by the same things that end a rest or a run. In 31 | particular, typing any character during the execution of a counted command 32 | will terminate the command. Counted searches or tunnels will terminate on 33 | success, or if you are attacked. A count with control-P will specify the 34 | number of previous messages to display. 35 | 36 | Control-R will redraw the screen whenever it is input, not only at command 37 | level. Control commands may be entered with a single key stroke, or with two 38 | key strokes by typing ^ and then a letter. 39 | 40 | Type ESCAPE to abort the look command at any point. 41 | 42 | Some commands will prompt for a spell, or an inventory item. Selection is 43 | by an alphabetic character - entering a capital causes a desription to be 44 | printed, and the selection may be aborted. 45 | 46 | Typing `R*' will make you rest until both your mana and your hp reach their 47 | maximum values. 48 | 49 | Using repeat counts with the tunnel left command does not work well, because 50 | tunnel left is the backspace character, and will delete the number you have 51 | just typed in. To avoid this, you can either enter ^H as two characters 52 | (^ and then H), or you can type ' ' (i.e. the space character) after the 53 | number at which point you will get a command prompt and backspace will 54 | work correctly. 55 | 56 | Umoria is free software, and you are welcome to distribute it 57 | under certain conditions; for details, type control-V. 58 | 59 | Umoria comes with ABSOLUTELY NO WARRANTY; for details, 60 | type control-V and view sections 15-17 of the License. 61 | -------------------------------------------------------------------------------- /data/rl_help_wizard.txt: -------------------------------------------------------------------------------- 1 | \ - Wizard Help 2 | : - Map area (panel) 3 | * - Wizard light 4 | ^A - Cure all maladies and remove curses 5 | ^D - Jump to depth # (level) 6 | ^E - Edit character stats 7 | ^F - Vanquish monsters within sight 8 | ^G - Generate random items 9 | ^I - Identify an item 10 | ^O - Print random objects sample to file 11 | ^T - Teleport player randomly 12 | ^W - Wizard mode on/off 13 | + - Gain experience 14 | & - Summon random monster 15 | % - Generate a dungeon item 16 | @ - Create an object *CAN CAUSE FATAL ERROR* 17 | -------------------------------------------------------------------------------- /data/scores.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dungeons-of-moria/umoria/e43c52524d0ad11357d99e6eb547728f20e1e390/data/scores.dat -------------------------------------------------------------------------------- /data/splash.txt.in: -------------------------------------------------------------------------------- 1 | 2 | Robert A. Koeneke's classic roguelike 3 | 4 | D U N G E O N S O F 5 | 6 | ## ## ####### ######## #### ### 7 | ### ### ## ## ## ## ## ## ## 8 | #### #### ## ## ## ## ## ## ## 9 | ## ### ## ## ## ######## ## ## ## 10 | ## ## ## ## ## ## ## ######### 11 | ## ## ## ## ## ## ## ## ## 12 | ## ## ####### ## ## #### ## ## 13 | 14 | Umoria ${umoria_version} 15 | 16 | 17 | Umoria is free software released under GPL-3.0 18 | and comes with ABSOLUTELY NO WARRANTY 19 | 20 | For more information: https://github.com/dungeons-of-moria/umoria 21 | -------------------------------------------------------------------------------- /data/versions.txt.in: -------------------------------------------------------------------------------- 1 | Umoria ${umoria_version} (${current_date}) 2 | 3 | Previous major releases: 4 | 5 | 2016-11-27 : v5.7.0 6 | 2008-10-13 : v5.6.0 7 | 1994-07-21 : v5.5.2 8 | 1992-08-13 : v5.5.0 9 | 1991-10-12 : v5.4.0 10 | 1991-03-25 : v5.3.0 11 | 1990-05-09 : v5.2.0 12 | 1989-02-26 : v5.0.0 13 | 1988-05-26 : v4.87 14 | 1987-10-26 : v4.85 15 | 16 | 17 | Umoria 5.7.x developed and maintained by Michael R. Cook. 18 | Bug reports and contributions can be made to the Umoria repository: 19 | https://github.com/dungeons-of-moria/umoria 20 | 21 | 22 | Umoria is free software released under GPL-3.0 23 | 24 | VMS Moria Version 4.8 25 | 26 | Version 4.0 : 1985-01-20 27 | Version 3.0 : 1984-11-20 28 | Version 2.0 : 1984-07-10 29 | Version 1.0 : 1984-05-01 30 | Version 0.1 : 1983-03-25 31 | 32 | VMS Moria Modules : 33 | 34 | V1.0 Dungeon Generator - RAK 35 | Character Generator - RAK & JWT 36 | Moria Module - RAK 37 | Miscellaneous - RAK & JWT 38 | V2.0 Town Level & Misc - RAK 39 | V3.0 Internal Help & Misc - RAK 40 | V4.0 Source Release Version - RAK 41 | 42 | Umoria is free software released under GNU General Public License v3.0 43 | See LICENSE and AUTHORS for more details 44 | 45 | Copyright (c) Robert A. Koeneke, James E. Wilson 46 | -------------------------------------------------------------------------------- /data/welcome.txt: -------------------------------------------------------------------------------- 1 | Welcome to Moria! 2 | 3 | Your first task is to generate a character. You must specify a gender, 4 | a race, and a class. Anytime before you have made these specifications, 5 | you can interrupt with control-C. You can also specify the race as 6 | often as you like by entering '%' to the class request. This will also 7 | regenerate your attributes. 8 | 9 | If this is your first time playing, you may want to read the manual first. 10 | -------------------------------------------------------------------------------- /historical/README.md: -------------------------------------------------------------------------------- 1 | # Historical Documents 2 | 3 | This directory contains various documents found in the original UMoria 4 | archives. They have been renamed, with some minor reformatting to the 5 | Markdown format, to help make consumption easier. 6 | 7 | 8 | ### Table of Contents 9 | 10 | * `history.md` - Articles posted to various mailing lists giving a brief history of Moria, by the original creator, Robert Koeneke, and long time contributor James Wilson. 11 | * `features.md` - A list of all of the important _user visible changes_ from UMoria 4.87 to UMoria 5.x. 12 | * `dragons.md` - Information on dragons. Contains spoilers. 13 | * `CHANGELOG` - A detailed list of all changes made between May 1987 to October 2008, covering versions 4.81 through 5.5.2. 14 | * `errors.txt` - A list of known errors for UMoria v5.x. 15 | 16 | 17 | ## Original Copyright Message 18 | 19 | The following copyright message was included in the original `main.c` source 20 | file. It is no longer valid, but has been included here for historical reasons. 21 | 22 | ``` 23 | Moria Version 4.8 COPYRIGHT (c) Robert Alan Koeneke 24 | 25 | I lovingly dedicate this game to hackers and adventurers 26 | everywhere... 27 | 28 | 29 | Designer and Programmer : Robert Alan Koeneke 30 | University of Oklahoma 31 | 32 | Assistant Programmers : Jimmey Wayne Todd 33 | University of Oklahoma 34 | 35 | Gary D. McAdoo 36 | University of Oklahoma 37 | 38 | UNIX Port : James E. Wilson 39 | UC Berkeley 40 | wilson@kithrup.com 41 | 42 | MSDOS Port : Don Kneller 43 | 1349 - 10th ave 44 | San Francisco, CA 94122 45 | kneller@cgl.ucsf.EDU 46 | ...ucbvax!ucsfcgl!kneller 47 | kneller@ucsf-cgl.BITNET 48 | 49 | BRUCE Moria : Christopher Stuart 50 | Monash University 51 | Melbourne, Victoria, AUSTRALIA 52 | cjs@moncsbruce.oz 53 | 54 | Amiga Port : Corey Gehman 55 | Clemson University 56 | cg377170@eng.clemson.edu 57 | 58 | Version 5.6 : David Grabiner 59 | grabiner@alumni.princeton.edu 60 | ``` -------------------------------------------------------------------------------- /historical/dragons.md: -------------------------------------------------------------------------------- 1 | # Dragon Information 2 | 3 | 4 | Every breath weapon does 1/3 of the monster's current hit points, 5 | except lightning, which does 1/4. 6 | 7 | Acid damage is then halved if you have any armor to corrode, or 8 | divided by 4 if you have resist acid and some armor. 9 | 10 | Cold or fire damage is divided by 3 if you have permanent resistance 11 | (from a ring, armor, or weapon) or temporary resistance (from a potion 12 | or priest spell), and by 9 if you have both. 13 | 14 | Lightning damage is divided by 3 if you have resistance. 15 | 16 | Gas damage always has full effect. 17 | 18 | Ancient Multi-Hued Dragon: (40/276) 19 | 20 | 2080 total hit points 21 | lightning: 520/173 22 | acid: 693/347/173 23 | fire/cold: 693/231/77 24 | gas: 693 25 | 26 | Even a 40th level half-troll warrior with 18/100 constitution, 27 | heroism, and superheroism would probably not have 693 hit points. 28 | Thus you must never fight AMHD's if you are giving them any 29 | opportunity to breathe on you. I don't like to go down to level 40 30 | unless I have 231 hit points and resistance, so that I can survive 4 31 | out of 5 breaths from off-screen AMHD's. 32 | 33 | Ancient Green Dragon: (39/269) 34 | 720 total hit points 35 | gas: 240 36 | 37 | ---- 38 | 39 | Ancient White Dragon: (38/263) 40 | 704 total hit points 41 | cold: 235/78/26 42 | 43 | ---- 44 | 45 | Ancient Blue Dragon: (39/268) 46 | 696 total hit points 47 | lightning: 174/58 48 | 49 | ---- 50 | 51 | Ancient Black Dragon: (39/270) 52 | 720 total hit points 53 | acid: 240/120/60 54 | 55 | ---- 56 | 57 | Ancient Red Dragon: (40/273) 58 | 840 total hit points 59 | fire: 280/93/31 60 | 61 | ---- 62 | 63 | Balrog: 64 | 3000 total hit points 65 | fire: 1000/333/111 66 | 67 | Thus a mage with less than 333 hp can be killed by one breath from the 68 | Balrog, before he gets a chance to drink invulnerability potions. For 69 | a mage to get 333 hp, he must be human, and he must use the grape 70 | jelly trick. 71 | 72 | 73 | Mature Multi-Hued Dragon: (38/262) 74 | 640 total hit points 75 | lightning: 160/53 76 | acid: 213/107/53 77 | fire/cold: 213/71/24 78 | gas: 213 79 | 80 | ---- 81 | 82 | Mature Green Dragon: (36/256) 83 | 384 total hit points 84 | gas: 128 85 | 86 | ---- 87 | 88 | Mature White Dragon: (35/250) 89 | 384 total hit points 90 | cold: 128/43/14 91 | 92 | ---- 93 | 94 | Mature Blue Dragon: (36/255) 95 | 384 total hit points 96 | lightning: 96/32 97 | 98 | ---- 99 | 100 | Mature Black Dragon: (37/261) 101 | 464 total hit points 102 | acid: 155/77/39 103 | 104 | ---- 105 | 106 | Mature Red Dragon: (37/260) 107 | 480 total hit points 108 | fire: 160/53/18 109 | 110 | ---- 111 | 112 | Young Multi-Hued Dragon: (36/254) 113 | 320 total hit points 114 | lightning: 80/27 115 | acid: 107/53/27 116 | fire/cold: 107/36/12 117 | gas: 107 118 | 119 | -------------------------------------------------------------------------------- /src/character.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Race type for the generated player character 9 | typedef struct { 10 | const char *name; // Type of race 11 | int16_t str_adjustment; // adjustments 12 | int16_t int_adjustment; 13 | int16_t wis_adjustment; 14 | int16_t dex_adjustment; 15 | int16_t con_adjustment; 16 | int16_t chr_adjustment; 17 | uint8_t base_age; // Base age of character 18 | uint8_t max_age; // Maximum age of character 19 | uint8_t male_height_base; // base height for males 20 | uint8_t male_height_mod; // mod height for males 21 | uint8_t male_weight_base; // base weight for males 22 | uint8_t male_weight_mod; // mod weight for males 23 | uint8_t female_height_base; // base height females 24 | uint8_t female_height_mod; // mod height for females 25 | uint8_t female_weight_base; // base weight for female 26 | uint8_t female_weight_mod; // mod weight for females 27 | int16_t disarm_chance_base; // base chance to disarm 28 | int16_t search_chance_base; // base chance for search 29 | int16_t stealth; // Stealth of character 30 | int16_t fos; // frequency of auto search 31 | int16_t base_to_hit; // adj base chance to hit 32 | int16_t base_to_hit_bows; // adj base to hit with bows 33 | int16_t saving_throw_base; // Race base for saving throw 34 | uint8_t hit_points_base; // Base hit points for race 35 | uint8_t infra_vision; // See infra-red 36 | uint8_t exp_factor_base; // Base experience factor 37 | uint8_t classes_bit_field; // Bit field for class types 38 | } Race_t; 39 | 40 | // Class type for the generated player character 41 | typedef struct { 42 | const char *title; // type of class 43 | uint8_t hit_points; // Adjust hit points 44 | uint8_t disarm_traps; // mod disarming traps 45 | uint8_t searching; // modifier to searching 46 | uint8_t stealth; // modifier to stealth 47 | uint8_t fos; // modifier to freq-of-search 48 | uint8_t base_to_hit; // modifier to base to hit 49 | uint8_t base_to_hit_with_bows; // modifier to base to hit - bows 50 | uint8_t saving_throw; // Class modifier to save 51 | int16_t strength; // Class modifier for strength 52 | int16_t intelligence; // Class modifier for intelligence 53 | int16_t wisdom; // Class modifier for wisdom 54 | int16_t dexterity; // Class modifier for dexterity 55 | int16_t constitution; // Class modifier for constitution 56 | int16_t charisma; // Class modifier for charisma 57 | uint8_t class_to_use_mage_spells; // class use mage spells 58 | uint8_t experience_factor; // Class experience factor 59 | uint8_t min_level_for_spell_casting; // First level where class can use spells. 60 | } Class_t; 61 | 62 | // Class background for the generated player character 63 | typedef struct { 64 | const char *info; // History information 65 | uint8_t roll; // Die roll needed for history 66 | uint8_t chart; // Table number 67 | uint8_t next; // Pointer to next table 68 | uint8_t bonus; // Bonus to the Social Class+50 69 | } Background_t; 70 | 71 | void characterCreate(); 72 | -------------------------------------------------------------------------------- /src/curses.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Sets up curses for the correct system. 7 | 8 | // clang-format off 9 | 10 | #ifdef _WIN32 11 | // this is defined in Windows and also in ncurses 12 | #undef KEY_EVENT 13 | #ifdef _MSVC_LANG 14 | // On Microsoft Visual Studio 2019, this constant also needs to 15 | // be undefined. Also, we need to use the PDCurses library rather 16 | // than a system library, and it has a different include file name. 17 | #undef MOUSE_MOVED 18 | #include 19 | #else 20 | #include 21 | #endif 22 | #elif __NetBSD__ 23 | #include 24 | #else 25 | #include 26 | #endif 27 | 28 | -------------------------------------------------------------------------------- /src/data_recall.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Player's memory: monster descriptions 7 | 8 | const char *recall_description_attack_type[25] = { 9 | "do something undefined", 10 | "attack", 11 | "weaken", 12 | "confuse", 13 | "terrify", 14 | "shoot flames", 15 | "shoot acid", 16 | "freeze", 17 | "shoot lightning", 18 | "corrode", 19 | "blind", 20 | "paralyse", 21 | "steal money", 22 | "steal things", 23 | "poison", 24 | "reduce dexterity", 25 | "reduce constitution", 26 | "drain intelligence", 27 | "drain wisdom", 28 | "lower experience", 29 | "call for help", 30 | "disenchant", 31 | "eat your food", 32 | "absorb light", 33 | "absorb charges", 34 | }; 35 | 36 | const char *recall_description_attack_method[20] = { 37 | "make an undefined advance", 38 | "hit", 39 | "bite", 40 | "claw", 41 | "sting", 42 | "touch", 43 | "kick", 44 | "gaze", 45 | "breathe", 46 | "spit", 47 | "wail", 48 | "embrace", 49 | "crawl on you", 50 | "release spores", 51 | "beg", 52 | "slime you", 53 | "crush", 54 | "trample", 55 | "drool", 56 | "insult", 57 | }; 58 | 59 | const char *recall_description_how_much[8] = { 60 | " not at all", " a bit", "", " quite", " very", " most", " highly", " extremely", 61 | }; 62 | 63 | const char *recall_description_move[6] = { 64 | "move invisibly", "open doors", "pass through walls", "kill weaker creatures", "pick up objects", "breed explosively", 65 | }; 66 | 67 | const char *recall_description_spell[15] = { 68 | "teleport short distances", 69 | "teleport long distances", 70 | "teleport its prey", 71 | "cause light wounds", 72 | "cause serious wounds", 73 | "paralyse its prey", 74 | "induce blindness", 75 | "confuse", 76 | "terrify", 77 | "summon a monster", 78 | "summon the undead", 79 | "slow its prey", 80 | "drain mana", 81 | "unknown 1", 82 | "unknown 2", 83 | }; 84 | 85 | const char *recall_description_breath[5] = { 86 | "lightning", "poison gases", "acid", "frost", "fire", 87 | }; 88 | 89 | const char *recall_description_weakness[6] = { 90 | "frost", "fire", "poison", "acid", "bright light", "rock remover", 91 | }; 92 | -------------------------------------------------------------------------------- /src/data_store_owners.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Store owner's speech text 7 | 8 | // clang-format off 9 | #include "headers.h" 10 | 11 | // Store owners have different characteristics for pricing and haggling 12 | // Note: Store owners should be added in groups, one for each store 13 | Owner_t store_owners[MAX_OWNERS] = { 14 | {"Erick the Honest (Human) General Store", 250, 175, 108, 4, 0, 12}, 15 | {"Mauglin the Grumpy (Dwarf) Armory", 32000, 200, 112, 4, 5, 5}, 16 | {"Arndal Beast-Slayer (Half-Elf) Weaponsmith", 10000, 185, 110, 5, 1, 8}, 17 | {"Hardblow the Humble (Human) Temple", 3500, 175, 109, 6, 0, 15}, 18 | {"Ga-nat the Greedy (Gnome) Alchemist", 12000, 220, 115, 4, 4, 9}, 19 | {"Valeria Starshine (Elf) Magic Shop", 32000, 175, 110, 5, 2, 11}, 20 | {"Andy the Friendly (Halfling) General Store", 200, 170, 108, 5, 3, 15}, 21 | {"Darg-Low the Grim (Human) Armory", 10000, 190, 111, 4, 0, 9}, 22 | {"Oglign Dragon-Slayer (Dwarf) Weaponsmith", 32000, 195, 112, 4, 5, 8}, 23 | {"Gunnar the Paladin (Human) Temple", 5000, 185, 110, 5, 0, 23}, 24 | {"Mauser the Chemist (Half-Elf) Alchemist", 10000, 190, 111, 5, 1, 8}, 25 | {"Gopher the Great! (Gnome) Magic Shop", 20000, 215, 113, 6, 4, 10}, 26 | {"Lyar-el the Comely (Elf) General Store", 300, 165, 107, 6, 2, 18}, 27 | {"Mauglim the Horrible (Half-Orc) Armory", 3000, 200, 113, 5, 6, 9}, 28 | {"Ithyl-Mak the Beastly (Half-Troll) Weaponsmith", 3000, 210, 115, 6, 7, 8}, 29 | {"Delilah the Pure (Half-Elf) Temple", 25000, 180, 107, 6, 1, 20}, 30 | {"Wizzle the Chaotic (Halfling) Alchemist", 10000, 190, 110, 6, 3, 8}, 31 | {"Inglorian the Mage (Human?) Magic Shop", 32000, 200, 110, 7, 0, 10}, 32 | }; 33 | 34 | const char *speech_sale_accepted[14] = { 35 | "Done!", 36 | "Accepted!", 37 | "Fine.", 38 | "Agreed!", 39 | "Ok.", 40 | "Taken!", 41 | "You drive a hard bargain, but taken.", 42 | "You'll force me bankrupt, but it's a deal.", 43 | "Sigh. I'll take it.", 44 | "My poor sick children may starve, but done!", 45 | "Finally! I accept.", 46 | "Robbed again.", 47 | "A pleasure to do business with you!", 48 | "My spouse will skin me, but accepted.", 49 | }; 50 | 51 | const char *speech_selling_haggle_final[3] = { 52 | "%A2 is my final offer; take it or leave it.", 53 | "I'll give you no more than %A2.", 54 | "My patience grows thin. %A2 is final.", 55 | }; 56 | 57 | const char *speech_selling_haggle[16] = { 58 | "%A1 for such a fine item? HA! No less than %A2.", 59 | "%A1 is an insult! Try %A2 gold pieces.", 60 | "%A1?!? You would rob my poor starving children?", 61 | "Why, I'll take no less than %A2 gold pieces.", 62 | "Ha! No less than %A2 gold pieces.", 63 | "Thou knave! No less than %A2 gold pieces.", 64 | "%A1 is far too little, how about %A2?", 65 | "I paid more than %A1 for it myself, try %A2.", 66 | "%A1? Are you mad?!? How about %A2 gold pieces?", 67 | "As scrap this would bring %A1. Try %A2 in gold.", 68 | "May the fleas of 1000 Orcs molest you. I want %A2.", 69 | "My mother you can get for %A1, this costs %A2.", 70 | "May your chickens grow lips. I want %A2 in gold!", 71 | "Sell this for such a pittance? Give me %A2 gold.", 72 | "May the Balrog find you tasty! %A2 gold pieces?", 73 | "Your mother was a Troll! %A2 or I'll tell.", 74 | }; 75 | 76 | const char *speech_buying_haggle_final[3] = { 77 | "I'll pay no more than %A1; take it or leave it.", 78 | "You'll get no more than %A1 from me.", 79 | "%A1 and that's final.", 80 | }; 81 | 82 | const char *speech_buying_haggle[15] = { 83 | "%A2 for that piece of junk? No more than %A1.", 84 | "For %A2 I could own ten of those. Try %A1.", 85 | "%A2? NEVER! %A1 is more like it.", 86 | "Let's be reasonable. How about %A1 gold pieces?", 87 | "%A1 gold for that junk, no more.", 88 | "%A1 gold pieces and be thankful for it!", 89 | "%A1 gold pieces and not a copper more.", 90 | "%A2 gold? HA! %A1 is more like it.", 91 | "Try about %A1 gold.", 92 | "I wouldn't pay %A2 for your children, try %A1.", 93 | "*CHOKE* For that!? Let's say %A1.", 94 | "How about %A1?", 95 | "That looks war surplus! Say %A1 gold.", 96 | "I'll buy it as scrap for %A1.", 97 | "%A2 is too much, let us say %A1 gold.", 98 | }; 99 | 100 | const char *speech_insulted_haggling_done[5] = { 101 | "ENOUGH! You have abused me once too often!", 102 | "THAT DOES IT! You shall waste my time no more!", 103 | "This is getting nowhere. I'm going home!", 104 | "BAH! No more shall you insult me!", 105 | "Begone! I have had enough abuse for one day.", 106 | }; 107 | 108 | const char *speech_get_out_of_my_store[5] = { 109 | "Out of my place!", "out... Out... OUT!!!", 110 | "Come back tomorrow.", "Leave my place. Begone!", 111 | "Come back when thou art richer.", 112 | }; 113 | 114 | const char *speech_haggling_try_again[10] = { 115 | "You will have to do better than that!", 116 | "That's an insult!", 117 | "Do you wish to do business or not?", 118 | "Hah! Try again.", 119 | "Ridiculous!", 120 | "You've got to be kidding!", 121 | "You'd better be kidding!", 122 | "You try my patience.", 123 | "I don't hear you.", 124 | "Hmmm, nice weather we're having.", 125 | }; 126 | 127 | const char *speech_sorry[5] = { 128 | "I must have heard you wrong.", "What was that?", 129 | "I'm sorry, say that again.", "What did you say?", 130 | "Sorry, what was that again?", 131 | }; 132 | -------------------------------------------------------------------------------- /src/data_stores.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Store data 7 | 8 | // clang-format off 9 | #include "headers.h" 10 | 11 | // Buying and selling adjustments for character race VS store owner race 12 | uint8_t race_gold_adjustments[PLAYER_MAX_RACES][PLAYER_MAX_RACES] = { 13 | //Hum, HfE, Elf, Hal, Gno, Dwa, HfO, HfT 14 | { 100, 105, 105, 110, 113, 115, 120, 125 }, // Human 15 | { 110, 100, 100, 105, 110, 120, 125, 130 }, // Half-Elf 16 | { 110, 105, 100, 105, 110, 120, 125, 130 }, // Elf 17 | { 115, 110, 105, 95, 105, 110, 115, 130 }, // Halfling 18 | { 115, 115, 110, 105, 95, 110, 115, 130 }, // Gnome 19 | { 115, 120, 120, 110, 110, 95, 125, 135 }, // Dwarf 20 | { 115, 120, 125, 115, 115, 130, 110, 115 }, // Half-Orc 21 | { 110, 115, 115, 110, 110, 130, 110, 110 }, // Half-Troll 22 | }; 23 | 24 | // game_objects[] index of objects that may appear in the store 25 | uint16_t store_choices[MAX_STORES][STORE_MAX_ITEM_TYPES] = { 26 | // General Store 27 | { 28 | 366, 365, 364, 84, 84, 365, 123, 366, 365, 350, 349, 348, 347, 29 | 346, 346, 345, 345, 345, 344, 344, 344, 344, 344, 344, 344, 344, 30 | }, 31 | // Armory 32 | { 33 | 94, 95, 96, 109, 103, 104, 105, 106, 110, 111, 112, 114, 116, 34 | 124, 125, 126, 127, 129, 103, 104, 124, 125, 91, 92, 95, 96, 35 | }, 36 | // Weaponsmith 37 | { 38 | 29, 30, 34, 37, 45, 49, 57, 58, 59, 65, 67, 68, 73, 39 | 74, 75, 77, 79, 80, 81, 83, 29, 30, 80, 83, 80, 83, 40 | }, 41 | // Temple 42 | { 43 | 322, 323, 324, 325, 180, 180, 233, 237, 240, 241, 361, 362, 57, 44 | 58, 59, 260, 358, 359, 265, 237, 237, 240, 240, 241, 323, 359, 45 | }, 46 | // Alchemy shop 47 | { 48 | 173, 174, 175, 351, 351, 352, 353, 354, 355, 356, 357, 206, 227, 49 | 230, 236, 252, 253, 352, 353, 354, 355, 356, 359, 363, 359, 359, 50 | }, 51 | // Magic-User store 52 | { 53 | 318, 141, 142, 153, 164, 167, 168, 140, 319, 320, 320, 321, 269, 54 | 270, 282, 286, 287, 292, 293, 294, 295, 308, 269, 290, 319, 282, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/data_tables.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Data tables for attack/RNG/etc. 7 | 8 | // clang-format off 9 | #include "headers.h" 10 | 11 | // Following are arrays for descriptive pieces 12 | const char *colors[MAX_COLORS] = { 13 | // Do not move the first three 14 | "Icky Green", "Light Brown", "Clear", 15 | "Azure", "Blue", "Blue Speckled", "Black", "Brown", "Brown Speckled", "Bubbling", 16 | "Chartreuse", "Cloudy", "Copper Speckled", "Crimson", "Cyan", "Dark Blue", 17 | "Dark Green", "Dark Red", "Gold Speckled", "Green", "Green Speckled", "Grey", 18 | "Grey Speckled", "Hazy", "Indigo", "Light Blue", "Light Green", "Magenta", 19 | "Metallic Blue", "Metallic Red", "Metallic Green", "Metallic Purple", "Misty", 20 | "Orange", "Orange Speckled", "Pink", "Pink Speckled", "Puce", "Purple", 21 | "Purple Speckled", "Red", "Red Speckled", "Silver Speckled", "Smoky", 22 | "Tangerine", "Violet", "Vermilion", "White", "Yellow", 23 | }; 24 | 25 | const char *mushrooms[MAX_MUSHROOMS] = { 26 | "Blue", "Black", "Black Spotted", "Brown", "Dark Blue", "Dark Green", "Dark Red", 27 | "Ecru", "Furry", "Green", "Grey", "Light Blue", "Light Green", "Plaid", "Red", 28 | "Slimy", "Tan", "White", "White Spotted", "Wooden", "Wrinkled", "Yellow", 29 | }; 30 | 31 | const char *woods[MAX_WOODS] = { 32 | "Aspen", "Balsa", "Banyan", "Birch", "Cedar", "Cottonwood", "Cypress", "Dogwood", 33 | "Elm", "Eucalyptus", "Hemlock", "Hickory", "Ironwood", "Locust", "Mahogany", 34 | "Maple", "Mulberry", "Oak", "Pine", "Redwood", "Rosewood", "Spruce", "Sycamore", 35 | "Teak", "Walnut", 36 | }; 37 | 38 | const char *metals[MAX_METALS] = { 39 | "Aluminum", "Cast Iron", "Chromium", "Copper", "Gold", "Iron", "Magnesium", 40 | "Molybdenum", "Nickel", "Rusty", "Silver", "Steel", "Tin", "Titanium", "Tungsten", 41 | "Zirconium", "Zinc", "Aluminum-Plated", "Copper-Plated", "Gold-Plated", 42 | "Nickel-Plated", "Silver-Plated", "Steel-Plated", "Tin-Plated", "Zinc-Plated", 43 | }; 44 | 45 | const char *rocks[MAX_ROCKS] = { 46 | "Alexandrite", "Amethyst", "Aquamarine", "Azurite", "Beryl", "Bloodstone", 47 | "Calcite", "Carnelian", "Corundum", "Diamond", "Emerald", "Fluorite", "Garnet", 48 | "Granite", "Jade", "Jasper", "Lapis Lazuli", "Malachite", "Marble", "Moonstone", 49 | "Onyx", "Opal", "Pearl", "Quartz", "Quartzite", "Rhodonite", "Ruby", "Sapphire", 50 | "Tiger Eye", "Topaz", "Turquoise", "Zircon", 51 | }; 52 | 53 | const char *amulets[MAX_AMULETS] = { 54 | "Amber", "Driftwood", "Coral", "Agate", "Ivory", "Obsidian", 55 | "Bone", "Brass", "Bronze", "Pewter", "Tortoise Shell", 56 | }; 57 | 58 | const char *syllables[MAX_SYLLABLES] = { 59 | "a", "ab", "ag", "aks", "ala", "an", "ankh", "app", "arg", 60 | "arze", "ash", "aus", "ban", "bar", "bat", "bek", "bie", "bin", 61 | "bit", "bjor", "blu", "bot", "bu", "byt", "comp", "con", "cos", 62 | "cre", "dalf", "dan", "den", "doe", "dok", "eep", "el", "eng", 63 | "er", "ere", "erk", "esh", "evs", "fa", "fid", "for", "fri", 64 | "fu", "gan", "gar", "glen", "gop", "gre", "ha", "he", "hyd", 65 | "i", "ing", "ion", "ip", "ish", "it", "ite", "iv", "jo", 66 | "kho", "kli", "klis", "la", "lech", "man", "mar", "me", "mi", 67 | "mic", "mik", "mon", "mung", "mur", "nej", "nelg", "nep", "ner", 68 | "nes", "nis", "nih", "nin", "o", "od", "ood", "org", "orn", 69 | "ox", "oxy", "pay", "pet", "ple", "plu", "po", "pot", "prok", 70 | "re", "rea", "rhov", "ri", "ro", "rog", "rok", "rol", "sa", 71 | "san", "sat", "see", "sef", "seh", "shu", "ski", "sna", "sne", 72 | "snik", "sno", "so", "sol", "sri", "sta", "sun", "ta", "tab", 73 | "tem", "ther", "ti", "tox", "trol", "tue", "turs", "u", "ulk", 74 | "um", "un", "uni", "ur", "val", "viv", "vly", "vom", "wah", 75 | "wed", "werg", "wex", "whon", "wun", "x", "yerg", "yp", "zun", 76 | }; 77 | 78 | // used to calculate the number of blows the player gets in combat 79 | uint8_t blows_table[7][6] = { 80 | // STR/W: 9 18 67 107 117 118 : DEX 81 | { 1, 1, 1, 1, 1, 1 }, // <2 82 | { 1, 1, 1, 1, 2, 2 }, // <3 83 | { 1, 1, 1, 2, 2, 3 }, // <4 84 | { 1, 1, 2, 2, 3, 3 }, // <5 85 | { 1, 2, 2, 3, 3, 4 }, // <7 86 | { 1, 2, 2, 3, 4, 4 }, // <9 87 | { 2, 2, 3, 3, 4, 4 }, // >9 88 | }; 89 | 90 | // this table is used to generate a pseudo-normal distribution. See 91 | // the function randomNumberNormalDistribution() in misc1.c, this is much faster than calling 92 | // transcendental function to calculate a true normal distribution. 93 | uint16_t normal_table[NORMAL_TABLE_SIZE] = { 94 | 206, 613, 1022, 1430, 1838, 2245, 2652, 3058, 95 | 3463, 3867, 4271, 4673, 5075, 5475, 5874, 6271, 96 | 6667, 7061, 7454, 7845, 8234, 8621, 9006, 9389, 97 | 9770, 10148, 10524, 10898, 11269, 11638, 12004, 12367, 98 | 12727, 13085, 13440, 13792, 14140, 14486, 14828, 15168, 99 | 15504, 15836, 16166, 16492, 16814, 17133, 17449, 17761, 100 | 18069, 18374, 18675, 18972, 19266, 19556, 19842, 20124, 101 | 20403, 20678, 20949, 21216, 21479, 21738, 21994, 22245, 102 | 22493, 22737, 22977, 23213, 23446, 23674, 23899, 24120, 103 | 24336, 24550, 24759, 24965, 25166, 25365, 25559, 25750, 104 | 25937, 26120, 26300, 26476, 26649, 26818, 26983, 27146, 105 | 27304, 27460, 27612, 27760, 27906, 28048, 28187, 28323, 106 | 28455, 28585, 28711, 28835, 28955, 29073, 29188, 29299, 107 | 29409, 29515, 29619, 29720, 29818, 29914, 30007, 30098, 108 | 30186, 30272, 30356, 30437, 30516, 30593, 30668, 30740, 109 | 30810, 30879, 30945, 31010, 31072, 31133, 31192, 31249, 110 | 31304, 31358, 31410, 31460, 31509, 31556, 31601, 31646, 111 | 31688, 31730, 31770, 31808, 31846, 31882, 31917, 31950, 112 | 31983, 32014, 32044, 32074, 32102, 32129, 32155, 32180, 113 | 32205, 32228, 32251, 32273, 32294, 32314, 32333, 32352, 114 | 32370, 32387, 32404, 32420, 32435, 32450, 32464, 32477, 115 | 32490, 32503, 32515, 32526, 32537, 32548, 32558, 32568, 116 | 32577, 32586, 32595, 32603, 32611, 32618, 32625, 32632, 117 | 32639, 32645, 32651, 32657, 32662, 32667, 32672, 32677, 118 | 32682, 32686, 32690, 32694, 32698, 32702, 32705, 32708, 119 | 32711, 32714, 32717, 32720, 32722, 32725, 32727, 32729, 120 | 32731, 32733, 32735, 32737, 32739, 32740, 32742, 32743, 121 | 32745, 32746, 32747, 32748, 32749, 32750, 32751, 32752, 122 | 32753, 32754, 32755, 32756, 32757, 32757, 32758, 32758, 123 | 32759, 32760, 32760, 32761, 32761, 32761, 32762, 32762, 124 | 32763, 32763, 32763, 32764, 32764, 32764, 32764, 32765, 125 | 32765, 32765, 32765, 32766, 32766, 32766, 32766, 32766, 126 | }; 127 | -------------------------------------------------------------------------------- /src/dice.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #include "headers.h" 7 | 8 | // generates damage for 2d6 style dice rolls 9 | int diceRoll(Dice_t const &dice) { 10 | auto sum = 0; 11 | for (auto i = 0; i < dice.dice; i++) { 12 | sum += randomNumber(dice.sides); 13 | } 14 | return sum; 15 | } 16 | 17 | // Returns max dice roll value -RAK- 18 | int maxDiceRoll(Dice_t const &dice) { 19 | return dice.dice * dice.sides; 20 | } 21 | -------------------------------------------------------------------------------- /src/dice.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | typedef struct { 9 | uint8_t dice; 10 | uint8_t sides; 11 | } Dice_t; 12 | 13 | int diceRoll(Dice_t const &dice); 14 | int maxDiceRoll(Dice_t const &dice); 15 | -------------------------------------------------------------------------------- /src/dungeon.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Dungeon size parameters 9 | constexpr uint8_t RATIO = 3; // Size ratio of the Map screen 10 | constexpr uint8_t MAX_HEIGHT = 66; // Multiple of 11; >= 22 11 | constexpr uint8_t MAX_WIDTH = 198; // Multiple of 33; >= 66 12 | constexpr uint8_t SCREEN_HEIGHT = 22; 13 | constexpr uint8_t SCREEN_WIDTH = 66; 14 | constexpr uint8_t QUART_HEIGHT = (SCREEN_HEIGHT / 4); 15 | constexpr uint8_t QUART_WIDTH = (SCREEN_WIDTH / 4); 16 | 17 | // DungeonObject_t is a base data object. 18 | // This holds data for any non-living object in the game such as 19 | // stairs, rubble, doors, gold, potions, weapons, wands, etc. 20 | typedef struct { 21 | const char *name; // Object name 22 | uint32_t flags; // Special flags 23 | uint8_t category_id; // Category number (tval) 24 | uint8_t sprite; // Character representation - ASCII symbol (tchar) 25 | int16_t misc_use; // Misc. use variable (p1) 26 | int32_t cost; // Cost of item 27 | uint8_t sub_category_id; // Sub-category number (subval) 28 | uint8_t items_count; // Number of items 29 | uint16_t weight; // Weight 30 | int16_t to_hit; // Plusses to hit 31 | int16_t to_damage; // Plusses to damage 32 | int16_t ac; // Normal AC 33 | int16_t to_ac; // Plusses to AC 34 | Dice_t damage; // Damage when hits 35 | uint8_t depth_first_found; // Dungeon level item first found 36 | } DungeonObject_t; 37 | 38 | typedef struct { 39 | // Dungeon size is either just big enough for town level, or the whole dungeon itself 40 | int16_t height; 41 | int16_t width; 42 | 43 | Panel_t panel; 44 | 45 | // Current turn of the game 46 | int32_t game_turn; 47 | 48 | // The current dungeon level 49 | int16_t current_level; 50 | 51 | // A `true` value means a new level will be generated on next loop iteration 52 | bool generate_new_level; 53 | 54 | // Floor definitions 55 | Tile_t floor[MAX_HEIGHT][MAX_WIDTH]; 56 | } Dungeon_t; 57 | 58 | extern Dungeon_t dg; 59 | extern DungeonObject_t game_objects[MAX_OBJECTS_IN_GAME]; 60 | 61 | void dungeonDisplayMap(); 62 | 63 | bool coordInBounds(Coord_t const &coord); 64 | int coordDistanceBetween(Coord_t const &from, Coord_t const &to); 65 | int coordWallsNextTo(Coord_t const &coord); 66 | int coordCorridorWallsNextTo(Coord_t const &coord); 67 | char caveGetTileSymbol(Coord_t const &coord); 68 | bool caveTileVisible(Coord_t const &coord); 69 | 70 | void dungeonSetTrap(Coord_t const &coord, int sub_type_id); 71 | void trapChangeVisibility(Coord_t const &coord); 72 | 73 | void dungeonPlaceRubble(Coord_t const &coord); 74 | void dungeonPlaceGold(Coord_t const &coord); 75 | 76 | void dungeonPlaceRandomObjectAt(Coord_t const &coord, bool must_be_small); 77 | void dungeonAllocateAndPlaceObject(bool (*set_function)(int), int object_type, int number); 78 | void dungeonPlaceRandomObjectNear(Coord_t coord, int tries); 79 | 80 | void dungeonMoveCreatureRecord(Coord_t const &from, Coord_t const &to); 81 | void dungeonLightRoom(Coord_t const &coord); 82 | void dungeonLiteSpot(Coord_t const &coord); 83 | void dungeonMoveCharacterLight(Coord_t const &from, Coord_t const &to); 84 | 85 | void dungeonDeleteMonster(int id); 86 | void dungeonRemoveMonsterFromLevel(int id); 87 | void dungeonDeleteMonsterRecord(int id); 88 | int dungeonSummonObject(Coord_t coord, int amount, int object_type); 89 | bool dungeonDeleteObject(Coord_t const &coord); 90 | 91 | // generate the dungeon 92 | void generateCave(); 93 | 94 | // Line of Sight 95 | bool los(Coord_t from, Coord_t to); 96 | void look(); 97 | -------------------------------------------------------------------------------- /src/dungeon_tile.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Tile_t holds data about a specific tile in the dungeon. 9 | typedef struct { 10 | uint8_t creature_id; // ID for any creature occupying the tile 11 | uint8_t treasure_id; // ID for any treasure item occupying the tile 12 | uint8_t feature_id; // ID of cave feature; walls, floors, open space, etc. 13 | 14 | bool perma_lit_room : 1; // Room should be lit with perm light, walls with this set should be perm lit after tunneled out. 15 | bool field_mark : 1; // Field mark, used for traps/doors/stairs, object is hidden if fm is false. 16 | bool permanent_light : 1; // Permanent light, used for walls and lighted rooms. 17 | bool temporary_light : 1; // Temporary light, used for player's lamp light,etc. 18 | } Tile_t; 19 | 20 | // `fval` definitions: these describe the various types of dungeon floors and 21 | // walls, if numbers above 15 are ever used, then the test against MIN_CAVE_WALL 22 | // will have to be changed, also the save routines will have to be changed. 23 | constexpr uint8_t TILE_NULL_WALL = 0; 24 | constexpr uint8_t TILE_DARK_FLOOR = 1; 25 | constexpr uint8_t TILE_LIGHT_FLOOR = 2; 26 | constexpr uint8_t MAX_CAVE_ROOM = 2; 27 | constexpr uint8_t TILE_CORR_FLOOR = 3; 28 | constexpr uint8_t TILE_BLOCKED_FLOOR = 4; // a corridor space with cl/st/se door or rubble 29 | constexpr uint8_t MAX_CAVE_FLOOR = 4; 30 | 31 | constexpr uint8_t MAX_OPEN_SPACE = 3; 32 | constexpr uint8_t MIN_CLOSED_SPACE = 4; 33 | 34 | constexpr uint8_t TMP1_WALL = 8; 35 | constexpr uint8_t TMP2_WALL = 9; 36 | 37 | constexpr uint8_t MIN_CAVE_WALL = 12; 38 | constexpr uint8_t TILE_GRANITE_WALL = 12; 39 | constexpr uint8_t TILE_MAGMA_WALL = 13; 40 | constexpr uint8_t TILE_QUARTZ_WALL = 14; 41 | constexpr uint8_t TILE_BOUNDARY_WALL = 15; 42 | -------------------------------------------------------------------------------- /src/game.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Game initialization and maintenance related functions 7 | 8 | #include "headers.h" 9 | #include "version.h" 10 | 11 | // holds the previous rnd state 12 | static uint32_t old_seed; 13 | 14 | Game_t game = Game_t{}; 15 | 16 | // gets a new random seed for the random number generator 17 | void seedsInitialize(uint32_t seed) { 18 | uint32_t clock_var; 19 | 20 | if (seed == 0) { 21 | clock_var = getCurrentUnixTime(); 22 | } else { 23 | clock_var = seed; 24 | } 25 | 26 | game.magic_seed = (int32_t) clock_var; 27 | 28 | clock_var += 8762; 29 | game.town_seed = (int32_t) clock_var; 30 | 31 | clock_var += 113452L; 32 | setRandomSeed(clock_var); 33 | 34 | // make it a little more random 35 | for (clock_var = (uint32_t) randomNumber(100); clock_var != 0; clock_var--) { 36 | (void) rnd(); 37 | } 38 | } 39 | 40 | // change to different random number generator state 41 | void seedSet(uint32_t seed) { 42 | old_seed = getRandomSeed(); 43 | 44 | // want reproducible state here 45 | setRandomSeed(seed); 46 | } 47 | 48 | // restore the normal random generator state 49 | void seedResetToOldSeed() { 50 | setRandomSeed(old_seed); 51 | } 52 | 53 | // Generates a random integer x where 1<=X<=MAXVAL -RAK- 54 | int randomNumber(int const max) { 55 | return (rnd() % max) + 1; 56 | } 57 | 58 | // Generates a random integer number of NORMAL distribution -RAK- 59 | int randomNumberNormalDistribution(int mean, int standard) { 60 | // alternate randomNumberNormalDistribution() code, slower but much smaller since no table 61 | // 2 per 1,000,000 will be > 4*SD, max is 5*SD 62 | // 63 | // tmp = diceRoll(8, 99); // mean 400, SD 81 64 | // tmp = (tmp - 400) * standard / 81; 65 | // return tmp + mean; 66 | 67 | int tmp = randomNumber(SHRT_MAX); 68 | 69 | // off scale, assign random value between 4 and 5 times SD 70 | if (tmp == SHRT_MAX) { 71 | int offset = 4 * standard + randomNumber(standard); 72 | 73 | // one half are negative 74 | if (randomNumber(2) == 1) { 75 | offset = -offset; 76 | } 77 | 78 | return mean + offset; 79 | } 80 | 81 | // binary search normal normal_table to get index that 82 | // matches tmp this takes up to 8 iterations. 83 | int low = 0; 84 | int iindex = NORMAL_TABLE_SIZE >> 1; 85 | int high = NORMAL_TABLE_SIZE; 86 | 87 | while (true) { 88 | if (normal_table[iindex] == tmp || high == low + 1) { 89 | break; 90 | } 91 | 92 | if (normal_table[iindex] > tmp) { 93 | high = iindex; 94 | iindex = low + ((iindex - low) >> 1); 95 | } else { 96 | low = iindex; 97 | iindex = iindex + ((high - iindex) >> 1); 98 | } 99 | } 100 | 101 | // might end up one below target, check that here 102 | if (normal_table[iindex] < tmp) { 103 | iindex = iindex + 1; 104 | } 105 | 106 | // normal_table is based on SD of 64, so adjust the 107 | // index value here, round the half way case up. 108 | int offset = ((standard * iindex) + (NORMAL_TABLE_SD >> 1)) / NORMAL_TABLE_SD; 109 | 110 | // one half should be negative 111 | if (randomNumber(2) == 1) { 112 | offset = -offset; 113 | } 114 | 115 | return mean + offset; 116 | } 117 | 118 | static struct { 119 | const char *o_prompt; 120 | bool *o_var; 121 | } game_options[] = { 122 | {"Running: cut known corners", &config::options::run_cut_corners}, 123 | {"Running: examine potential corners", &config::options::run_examine_corners}, 124 | {"Running: print self during run", &config::options::run_print_self}, 125 | {"Running: stop when map sector changes", &config::options::find_bound}, 126 | {"Running: run through open doors", &config::options::run_ignore_doors}, 127 | {"Prompt to pick up objects", &config::options::prompt_to_pickup}, 128 | {"Rogue like commands", &config::options::use_roguelike_keys}, 129 | {"Show weights in inventory", &config::options::show_inventory_weights}, 130 | {"Highlight and notice mineral seams", &config::options::highlight_seams}, 131 | {"Beep for invalid character", &config::options::error_beep_sound}, 132 | {"Display rest/repeat counts", &config::options::display_counts}, 133 | {nullptr, nullptr}, 134 | }; 135 | 136 | // Set or unset various boolean config::options::display_counts -CJS- 137 | void setGameOptions() { 138 | putStringClearToEOL(" ESC when finished, y/n to set options, or - to move cursor", Coord_t{0, 0}); 139 | 140 | int max; 141 | for (max = 0; game_options[max].o_prompt != nullptr; max++) { 142 | vtype_t str = {'\0'}; 143 | (void) snprintf(str, MORIA_MESSAGE_SIZE, "%-38s: %s", game_options[max].o_prompt, (*game_options[max].o_var ? "yes" : "no ")); 144 | putStringClearToEOL(str, Coord_t{max + 1, 0}); 145 | } 146 | eraseLine(Coord_t{max + 1, 0}); 147 | 148 | int option_id = 0; 149 | while (true) { 150 | moveCursor(Coord_t{option_id + 1, 40}); 151 | 152 | switch (getKeyInput()) { 153 | case ESCAPE: 154 | return; 155 | case '-': 156 | if (option_id > 0) { 157 | option_id--; 158 | } else { 159 | option_id = max - 1; 160 | } 161 | break; 162 | case ' ': 163 | case '\n': 164 | case '\r': 165 | if (option_id + 1 < max) { 166 | option_id++; 167 | } else { 168 | option_id = 0; 169 | } 170 | break; 171 | case 'y': 172 | case 'Y': 173 | putString("yes", Coord_t{option_id + 1, 40}); 174 | 175 | *game_options[option_id].o_var = true; 176 | 177 | if (option_id + 1 < max) { 178 | option_id++; 179 | } else { 180 | option_id = 0; 181 | } 182 | break; 183 | case 'n': 184 | case 'N': 185 | putString("no ", Coord_t{option_id + 1, 40}); 186 | 187 | *game_options[option_id].o_var = false; 188 | 189 | if (option_id + 1 < max) { 190 | option_id++; 191 | } else { 192 | option_id = 0; 193 | } 194 | break; 195 | default: 196 | terminalBellSound(); 197 | break; 198 | } 199 | } 200 | } 201 | 202 | // Support for Umoria 5.2.2 up to 5.7.x. 203 | // The save file format was frozen as of version 5.2.2. 204 | bool validGameVersion(uint8_t major, uint8_t minor, uint8_t patch) { 205 | if (major != 5) { 206 | return false; 207 | } 208 | 209 | if (minor < 2) { 210 | return false; 211 | } 212 | 213 | if (minor == 2 && patch < 2) { 214 | return false; 215 | } 216 | 217 | return minor <= 7; 218 | } 219 | 220 | bool isCurrentGameVersion(uint8_t major, uint8_t minor, uint8_t patch) { 221 | return major == CURRENT_VERSION_MAJOR && minor == CURRENT_VERSION_MINOR && patch == CURRENT_VERSION_PATCH; 222 | } 223 | 224 | int getRandomDirection() { 225 | int dir; 226 | 227 | do { 228 | dir = randomNumber(9); 229 | } while (dir == 5); 230 | 231 | return dir; 232 | } 233 | 234 | // map roguelike direction commands into numbers 235 | static char mapRoguelikeKeysToKeypad(char command) { 236 | switch (command) { 237 | case 'h': 238 | return '4'; 239 | case 'y': 240 | return '7'; 241 | case 'k': 242 | return '8'; 243 | case 'u': 244 | return '9'; 245 | case 'l': 246 | return '6'; 247 | case 'n': 248 | return '3'; 249 | case 'j': 250 | return '2'; 251 | case 'b': 252 | return '1'; 253 | case '.': 254 | return '5'; 255 | default: 256 | return command; 257 | } 258 | } 259 | 260 | // Prompts for a direction -RAK- 261 | // Direction memory added, for repeated commands. -CJS 262 | bool getDirectionWithMemory(char *prompt, int &direction) { 263 | // used in counted commands. -CJS- 264 | if (game.use_last_direction) { 265 | direction = py.prev_dir; 266 | return true; 267 | } 268 | 269 | if (prompt == CNIL) { 270 | prompt = (char *) "Which direction?"; 271 | } 272 | 273 | char command; 274 | 275 | while (true) { 276 | // Don't end a counted command. -CJS- 277 | int oldCount = game.command_count; 278 | if (!getCommand(prompt, command)) { 279 | game.player_free_turn = true; 280 | return false; 281 | } 282 | game.command_count = oldCount; 283 | 284 | if (config::options::use_roguelike_keys) { 285 | command = mapRoguelikeKeysToKeypad(command); 286 | } 287 | 288 | if (command >= '1' && command <= '9' && command != '5') { 289 | py.prev_dir = command - '0'; 290 | direction = py.prev_dir; 291 | return true; 292 | } 293 | 294 | terminalBellSound(); 295 | } 296 | } 297 | 298 | // Similar to getDirectionWithMemory(), except that no memory exists, 299 | // and it is allowed to enter the null direction. -CJS- 300 | bool getAllDirections(const char *prompt, int &direction) { 301 | char command; 302 | 303 | while (true) { 304 | if (!getCommand(prompt, command)) { 305 | game.player_free_turn = true; 306 | return false; 307 | } 308 | 309 | if (config::options::use_roguelike_keys) { 310 | command = mapRoguelikeKeysToKeypad(command); 311 | } 312 | 313 | if (command >= '1' && command <= '9') { 314 | direction = command - '0'; 315 | return true; 316 | } 317 | 318 | terminalBellSound(); 319 | } 320 | } 321 | 322 | // Restore the terminal and exit 323 | void exitProgram() { 324 | flushInputBuffer(); 325 | terminalRestore(); 326 | exit(0); 327 | } 328 | 329 | // Abort the program with a message displayed on the terminal. 330 | void abortProgram(const char *msg) { 331 | flushInputBuffer(); 332 | terminalRestore(); 333 | 334 | printf("Program was manually aborted with the message:\n"); 335 | printf("%s\n", msg); 336 | 337 | exit(0); 338 | } 339 | -------------------------------------------------------------------------------- /src/game.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | constexpr uint8_t TREASURE_MAX_LEVELS = 50; // Maximum level of magic in dungeon 9 | 10 | // Note that the following constants are all related, if you change one, you 11 | // must also change all succeeding ones. 12 | // Also, player_base_provisions[] and store_choices[] may also have to be changed. 13 | constexpr uint16_t MAX_OBJECTS_IN_GAME = 420; // Number of objects for universe 14 | constexpr uint16_t MAX_DUNGEON_OBJECTS = 344; // Number of dungeon objects 15 | constexpr uint16_t OBJECT_IDENT_SIZE = 448; // 7*64, see object_offset() in desc.cpp, could be MAX_OBJECTS o_o() rewritten 16 | 17 | // With LEVEL_MAX_OBJECTS set to 150, it's possible to get compacting 18 | // objects during level generation, although it is extremely rare. 19 | constexpr uint8_t LEVEL_MAX_OBJECTS = 175; // Max objects per level 20 | 21 | // definitions for the pseudo-normal distribution generation 22 | constexpr uint16_t NORMAL_TABLE_SIZE = 256; 23 | constexpr uint8_t NORMAL_TABLE_SD = 64; // the standard deviation for the table 24 | 25 | // Inventory command screen states. 26 | enum class Screen { 27 | Blank = 0, 28 | Equipment, 29 | Inventory, 30 | Wear, 31 | Help, 32 | Wrong, 33 | }; 34 | 35 | typedef struct Game_t { 36 | uint32_t magic_seed = 0; // Seed for initializing magic items (Potions, Wands, Staves, Scrolls, etc.) 37 | uint32_t town_seed = 0; // Seed for town generation 38 | 39 | bool character_generated = false; // Don't save score until character generation is finished 40 | bool character_saved = false; // Prevents save on kill after saving a character 41 | bool character_is_dead = false; // `true` if character has died 42 | 43 | bool total_winner = false; // Character beat the Balrog 44 | 45 | bool teleport_player = false; // Handle teleport traps 46 | bool player_free_turn = false; // Player has a free turn, so do not move creatures 47 | 48 | bool to_be_wizard = false; // Player requests to be Wizard - used during startup, when -w option used 49 | bool wizard_mode = false; // Character is a Wizard when true 50 | int16_t noscore = 0; // Don't save a score for this game. -CJS- 51 | 52 | bool use_last_direction = false; // `true` when repeat commands should use last known direction 53 | char doing_inventory_command = 0; // Track inventory commands -CJS- 54 | char last_command = ' '; // Save of the previous player command 55 | int command_count = 0; // How many times to repeat a specific command -CJS- 56 | 57 | vtype_t character_died_from = {'\0'}; // What the character died from: starvation, Bat, etc. 58 | 59 | struct { 60 | int16_t current_id = 0; // Current treasure heap ptr 61 | Inventory_t list[LEVEL_MAX_OBJECTS]{}; 62 | } treasure; 63 | 64 | // Keep track of the state of the current screen (inventory, equipment, help, etc.). 65 | struct { 66 | Screen current_screen_id = Screen::Blank; 67 | int screen_left_pos = 0; 68 | int screen_bottom_pos = 0; 69 | int wear_low_id = 0; 70 | int wear_high_id = 0; 71 | } screen; 72 | } Game_t; 73 | 74 | extern Game_t game; 75 | 76 | extern int16_t sorted_objects[MAX_DUNGEON_OBJECTS]; 77 | extern uint16_t normal_table[NORMAL_TABLE_SIZE]; 78 | extern int16_t treasure_levels[TREASURE_MAX_LEVELS + 1]; 79 | 80 | void seedsInitialize(uint32_t seed); 81 | void seedSet(uint32_t seed); 82 | void seedResetToOldSeed(); 83 | int randomNumber(int max); 84 | int randomNumberNormalDistribution(int mean, int standard); 85 | void setGameOptions(); 86 | bool validGameVersion(uint8_t major, uint8_t minor, uint8_t patch); 87 | bool isCurrentGameVersion(uint8_t major, uint8_t minor, uint8_t patch); 88 | 89 | int getRandomDirection(); 90 | bool getDirectionWithMemory(char *prompt, int &direction); 91 | bool getAllDirections(const char *prompt, int &direction); 92 | 93 | void exitProgram(); 94 | void abortProgram(const char *msg); 95 | 96 | // game object management 97 | int popt(); 98 | void pusht(uint8_t treasure_id); 99 | int itemGetRandomObjectId(int level, bool must_be_small); 100 | 101 | // game files 102 | bool initializeScoreFile(); 103 | void displaySplashScreen(); 104 | void displayTextHelpFile(const std::string &filename); 105 | void displayDeathFile(const std::string &filename); 106 | void outputRandomLevelObjectsToFile(); 107 | bool outputPlayerCharacterToFile(char *filename); 108 | 109 | // game death 110 | void endGame(); 111 | 112 | // save/load 113 | bool saveGame(); 114 | bool loadGame(bool &generate); 115 | void setFileptr(FILE *file); 116 | 117 | // game_run.cpp 118 | // (includes the playDungeon() main game loop) 119 | void startMoria(uint32_t seed, bool start_new_game, bool roguelike_keys); 120 | -------------------------------------------------------------------------------- /src/game_death.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Code executed when player dies 7 | 8 | #include "headers.h" 9 | 10 | // Prints the gravestone of the character -RAK- 11 | static void printTomb() { 12 | displayDeathFile(config::files::death_tomb); 13 | 14 | std::string text; 15 | 16 | text = std::string(py.misc.name); 17 | putString(text.c_str(), Coord_t{6, (int) (26 - text.length() / 2)}); 18 | 19 | if (!game.total_winner) { 20 | text = playerRankTitle(); 21 | } else { 22 | text = "Magnificent"; 23 | } 24 | putString(text.c_str(), Coord_t{8, (int) (26 - text.length() / 2)}); 25 | 26 | if (!game.total_winner) { 27 | text = classes[py.misc.class_id].title; 28 | } else if (playerIsMale()) { 29 | text = "*King*"; 30 | } else { 31 | text = "*Queen*"; 32 | } 33 | putString(text.c_str(), Coord_t{10, (int) (26 - text.length() / 2)}); 34 | 35 | text = std::to_string(py.misc.level); 36 | putString(text.c_str(), Coord_t{11, 30}); 37 | 38 | text = std::to_string(py.misc.exp) + " Exp"; 39 | putString(text.c_str(), Coord_t{12, (int) (26 - text.length() / 2)}); 40 | 41 | text = std::to_string(py.misc.au) + " Au"; 42 | putString(text.c_str(), Coord_t{13, (int) (26 - text.length() / 2)}); 43 | 44 | text = std::to_string(dg.current_level); 45 | putString(text.c_str(), Coord_t{14, 34}); 46 | 47 | text = std::string(game.character_died_from); 48 | putString(text.c_str(), Coord_t{16, (int) (26 - text.length() / 2)}); 49 | 50 | char day[11]; 51 | humanDateString(day); 52 | text = std::string(day); 53 | putString(text.c_str(), Coord_t{17, (int) (26 - text.length() / 2)}); 54 | 55 | retry: 56 | flushInputBuffer(); 57 | 58 | putString("(ESC to abort, return to print on screen, or file name)", Coord_t{23, 0}); 59 | putString("Character record?", Coord_t{22, 0}); 60 | 61 | vtype_t str = {'\0'}; 62 | if (getStringInput(str, Coord_t{22, 18}, 60)) { 63 | for (auto &item : py.inventory) { 64 | itemSetAsIdentified(item.category_id, item.sub_category_id); 65 | spellItemIdentifyAndRemoveRandomInscription(item); 66 | } 67 | 68 | playerRecalculateBonuses(); 69 | 70 | if (str[0] != 0) { 71 | if (!outputPlayerCharacterToFile(str)) { 72 | goto retry; 73 | } 74 | } else { 75 | clearScreen(); 76 | printCharacter(); 77 | putString("Type ESC to skip the inventory:", Coord_t{23, 0}); 78 | if (getKeyInput() != ESCAPE) { 79 | clearScreen(); 80 | printMessage("You are using:"); 81 | (void) displayEquipment(true, 0); 82 | printMessage(CNIL); 83 | printMessage("You are carrying:"); 84 | clearToBottom(1); 85 | (void) displayInventoryItems(0, py.pack.unique_items - 1, true, 0, CNIL); 86 | printMessage(CNIL); 87 | } 88 | } 89 | } 90 | } 91 | 92 | // Let the player know they did good. 93 | static void printCrown() { 94 | displayDeathFile(config::files::death_royal); 95 | if (playerIsMale()) { 96 | putString("King!", Coord_t{17, 45}); 97 | } else { 98 | putString("Queen!", Coord_t{17, 45}); 99 | } 100 | flushInputBuffer(); 101 | waitForContinueKey(23); 102 | } 103 | 104 | // Change the player into a King! -RAK- 105 | static void kingly() { 106 | // Change the character attributes. 107 | dg.current_level = 0; 108 | (void) strcpy(game.character_died_from, "Ripe Old Age"); 109 | 110 | (void) spellRestorePlayerLevels(); 111 | 112 | py.misc.level += PLAYER_MAX_LEVEL; 113 | py.misc.au += 250000L; 114 | py.misc.max_exp += 5000000L; 115 | py.misc.exp = py.misc.max_exp; 116 | 117 | printCrown(); 118 | } 119 | 120 | // What happens upon dying -RAK- 121 | // Handles the gravestone and top-twenty routines -RAK- 122 | void endGame() { 123 | printMessage(CNIL); 124 | 125 | // flush all input 126 | flushInputBuffer(); 127 | 128 | // If the game has been saved, then save sets turn back to -1, 129 | // which inhibits the printing of the tomb. 130 | if (dg.game_turn >= 0) { 131 | if (game.total_winner) { 132 | kingly(); 133 | } 134 | printTomb(); 135 | } 136 | 137 | // Save the memory at least. 138 | if (game.character_generated && !game.character_saved) { 139 | (void) saveGame(); 140 | } 141 | 142 | // add score to score file if applicable 143 | if (game.character_generated) { 144 | // Clear `game.character_saved`, strange thing to do, but it prevents 145 | // getKeyInput() from recursively calling endGame() when there has 146 | // been an eof on stdin detected. 147 | game.character_saved = false; 148 | recordNewHighScore(); 149 | showScoresScreen(); 150 | } 151 | eraseLine(Coord_t{23, 0}); 152 | 153 | exitProgram(); 154 | } 155 | -------------------------------------------------------------------------------- /src/game_objects.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Game object management 7 | 8 | #include "headers.h" 9 | 10 | int16_t sorted_objects[MAX_DUNGEON_OBJECTS]; 11 | int16_t treasure_levels[TREASURE_MAX_LEVELS + 1]; 12 | 13 | // If too many objects on floor level, delete some of them-RAK- 14 | static void compactObjects() { 15 | printMessage("Compacting objects..."); 16 | 17 | int counter = 0; 18 | int current_distance = 66; 19 | 20 | Coord_t coord = Coord_t{0, 0}; 21 | 22 | while (counter <= 0) { 23 | for (coord.y = 0; coord.y < dg.height; coord.y++) { 24 | for (coord.x = 0; coord.x < dg.width; coord.x++) { 25 | if (dg.floor[coord.y][coord.x].treasure_id != 0 && coordDistanceBetween(coord, py.pos) > current_distance) { 26 | int chance; 27 | 28 | switch (game.treasure.list[dg.floor[coord.y][coord.x].treasure_id].category_id) { 29 | case TV_VIS_TRAP: 30 | chance = 15; 31 | break; 32 | case TV_INVIS_TRAP: 33 | case TV_RUBBLE: 34 | case TV_OPEN_DOOR: 35 | case TV_CLOSED_DOOR: 36 | chance = 5; 37 | break; 38 | case TV_UP_STAIR: 39 | case TV_DOWN_STAIR: 40 | case TV_STORE_DOOR: 41 | // Stairs, don't delete them. 42 | // Shop doors, don't delete them. 43 | chance = 0; 44 | break; 45 | case TV_SECRET_DOOR: // secret doors 46 | chance = 3; 47 | break; 48 | default: 49 | chance = 10; 50 | } 51 | if (randomNumber(100) <= chance) { 52 | (void) dungeonDeleteObject(coord); 53 | counter++; 54 | } 55 | } 56 | } 57 | } 58 | 59 | if (counter == 0) { 60 | current_distance -= 6; 61 | } 62 | } 63 | 64 | if (current_distance < 66) { 65 | drawDungeonPanel(); 66 | } 67 | } 68 | 69 | // Gives pointer to next free space -RAK- 70 | int popt() { 71 | if (game.treasure.current_id == LEVEL_MAX_OBJECTS) { 72 | compactObjects(); 73 | } 74 | 75 | return game.treasure.current_id++; 76 | } 77 | 78 | // Pushes a record back onto free space list -RAK- 79 | // `dungeonDeleteObject()` should always be called instead, unless the object 80 | // in question is not in the dungeon, e.g. in store1.c and files.c 81 | void pusht(uint8_t treasure_id) { 82 | if (treasure_id != game.treasure.current_id - 1) { 83 | game.treasure.list[treasure_id] = game.treasure.list[game.treasure.current_id - 1]; 84 | 85 | // must change the treasure_id in the cave of the object just moved 86 | for (int y = 0; y < dg.height; y++) { 87 | for (int x = 0; x < dg.width; x++) { 88 | if (dg.floor[y][x].treasure_id == game.treasure.current_id - 1) { 89 | dg.floor[y][x].treasure_id = treasure_id; 90 | } 91 | } 92 | } 93 | } 94 | game.treasure.current_id--; 95 | 96 | inventoryItemCopyTo(config::dungeon::objects::OBJ_NOTHING, game.treasure.list[game.treasure.current_id]); 97 | } 98 | 99 | // Item too large to fit in chest? -DJG- 100 | // Use a DungeonObject_t since the item has not yet been created 101 | static bool itemBiggerThanChest(DungeonObject_t const &obj) { 102 | switch (obj.category_id) { 103 | case TV_CHEST: 104 | case TV_BOW: 105 | case TV_POLEARM: 106 | case TV_HARD_ARMOR: 107 | case TV_SOFT_ARMOR: 108 | case TV_STAFF: 109 | return true; 110 | case TV_HAFTED: 111 | case TV_SWORD: 112 | case TV_DIGGING: 113 | return (obj.weight > 150); 114 | default: 115 | return false; 116 | } 117 | } 118 | 119 | // Returns the array number of a random object -RAK- 120 | int itemGetRandomObjectId(int level, bool must_be_small) { 121 | if (level == 0) { 122 | return randomNumber(treasure_levels[0]) - 1; 123 | } 124 | 125 | if (level >= TREASURE_MAX_LEVELS) { 126 | level = TREASURE_MAX_LEVELS; 127 | } else if (randomNumber(config::treasure::TREASURE_CHANCE_OF_GREAT_ITEM) == 1) { 128 | level = level * TREASURE_MAX_LEVELS / randomNumber(TREASURE_MAX_LEVELS) + 1; 129 | if (level > TREASURE_MAX_LEVELS) { 130 | level = TREASURE_MAX_LEVELS; 131 | } 132 | } 133 | 134 | int object_id; 135 | 136 | // This code has been added to make it slightly more likely to get the 137 | // higher level objects. Originally a uniform distribution over all 138 | // objects less than or equal to the dungeon level. This distribution 139 | // makes a level n objects occur approx 2/n% of the time on level n, 140 | // and 1/2n are 0th level. 141 | do { 142 | if (randomNumber(2) == 1) { 143 | object_id = randomNumber(treasure_levels[level]) - 1; 144 | } else { 145 | // Choose three objects, pick the highest level. 146 | object_id = randomNumber(treasure_levels[level]) - 1; 147 | 148 | int j = randomNumber(treasure_levels[level]) - 1; 149 | 150 | if (object_id < j) { 151 | object_id = j; 152 | } 153 | 154 | j = randomNumber(treasure_levels[level]) - 1; 155 | 156 | if (object_id < j) { 157 | object_id = j; 158 | } 159 | 160 | int found_level = game_objects[sorted_objects[object_id]].depth_first_found; 161 | 162 | if (found_level == 0) { 163 | object_id = randomNumber(treasure_levels[0]) - 1; 164 | } else { 165 | object_id = randomNumber(treasure_levels[found_level] - treasure_levels[found_level - 1]) - 1 + treasure_levels[found_level - 1]; 166 | } 167 | } 168 | } while (must_be_small && itemBiggerThanChest(game_objects[sorted_objects[object_id]])); 169 | 170 | return object_id; 171 | } 172 | -------------------------------------------------------------------------------- /src/headers.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // System specific headers 7 | 8 | // clang-format off 9 | #pragma once 10 | 11 | #ifdef _WIN32 12 | #define _CRT_SECURE_NO_WARNINGS 13 | #define _CRT_NONSTDC_NO_DEPRECATE 14 | #define WIN32_LEAN_AND_MEAN 15 | 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | #elif __APPLE__ || __linux__ || __NetBSD__ || __MORPHOS__ 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #else 28 | # error "Unknown compiler" 29 | #endif 30 | 31 | 32 | // Headers we can use on all supported systems! 33 | 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | #include 44 | 45 | #include 46 | #include 47 | 48 | 49 | // General Umoria headers 50 | #include "config.h" 51 | #include "types.h" 52 | 53 | #include "character.h" 54 | #include "dice.h" 55 | #include "ui.h" // before dungeon.h 56 | #include "inventory.h" // before game.h 57 | #include "game.h" // before dungeon.h 58 | #include "dungeon_tile.h" 59 | #include "dungeon.h" 60 | #include "helpers.h" 61 | #include "identification.h" 62 | #include "mage_spells.h" 63 | #include "monster.h" 64 | #include "player.h" 65 | #include "recall.h" 66 | #include "rng.h" 67 | #include "scores.h" 68 | #include "scrolls.h" 69 | #include "spells.h" 70 | #include "staves.h" 71 | #include "store.h" 72 | #include "treasure.h" 73 | #include "wizard.h" 74 | -------------------------------------------------------------------------------- /src/helpers.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #include "headers.h" 7 | #include 8 | 9 | // Returns position of first set bit and clears that bit -RAK- 10 | int getAndClearFirstBit(uint32_t &flag) { 11 | uint32_t mask = 0x1; 12 | 13 | for (int i = 0; i < (int) sizeof(flag) * 8; i++) { 14 | if ((flag & mask) != 0u) { 15 | flag &= ~mask; 16 | return i; 17 | } 18 | mask <<= 1; 19 | } 20 | 21 | // no one bits found 22 | return -1; 23 | } 24 | 25 | // Insert a long number into a string (was `insert_lnum()` function) 26 | void insertNumberIntoString(char *to_string, const char *from_string, int32_t number, bool show_sign) { 27 | size_t from_len = strlen(from_string); 28 | char *to_str_tmp = to_string; 29 | char *str = nullptr; 30 | 31 | // must be int for strncmp() 32 | int flag = 1; 33 | 34 | while (flag != 0) { 35 | str = strchr(to_str_tmp, from_string[0]); 36 | if (str == nullptr) { 37 | flag = 0; 38 | } else { 39 | flag = strncmp(str, from_string, from_len); 40 | if (flag != 0) { 41 | to_str_tmp = str + 1; 42 | } 43 | } 44 | } 45 | 46 | if (str != nullptr) { 47 | vtype_t str1 = {'\0'}; 48 | vtype_t str2 = {'\0'}; 49 | 50 | (void) strncpy(str1, to_string, str - to_string); 51 | str1[str - to_string] = '\0'; 52 | (void) strcpy(str2, str + from_len); 53 | 54 | if (number >= 0 && show_sign) { 55 | (void) snprintf(to_string, MORIA_MESSAGE_SIZE, "%s+%d%s", str1, number, str2); 56 | } else { 57 | (void) snprintf(to_string, MORIA_MESSAGE_SIZE, "%s%d%s", str1, number, str2); 58 | } 59 | } 60 | } 61 | 62 | // Inserts a string into a string 63 | void insertStringIntoString(char *to_string, const char *from_string, const char *str_to_insert) { 64 | auto from_len = (int) strlen(from_string); 65 | auto to_len = (int) strlen(to_string); 66 | 67 | char *bound = to_string + to_len - from_len; 68 | char *pc = nullptr; 69 | 70 | for (pc = to_string; pc <= bound; pc++) { 71 | char *temp_obj = pc; 72 | const char *temp_mtc = from_string; 73 | 74 | int i; 75 | for (i = 0; i < from_len; i++) { 76 | if (*temp_obj++ != *temp_mtc++) { 77 | break; 78 | } 79 | } 80 | if (i == from_len) { 81 | break; 82 | } 83 | } 84 | 85 | if (pc <= bound) { 86 | vtype_t new_string; 87 | 88 | (void) strncpy(new_string, to_string, (pc - to_string)); 89 | 90 | new_string[pc - to_string] = '\0'; 91 | 92 | if (str_to_insert != nullptr) { 93 | (void) strcat(new_string, str_to_insert); 94 | } 95 | 96 | (void) strcat(new_string, (pc + from_len)); 97 | (void) strcpy(to_string, new_string); 98 | } 99 | } 100 | 101 | bool isVowel(char ch) { 102 | switch (ch) { 103 | case 'a': 104 | case 'e': 105 | case 'i': 106 | case 'o': 107 | case 'u': 108 | case 'A': 109 | case 'E': 110 | case 'I': 111 | case 'O': 112 | case 'U': 113 | return true; 114 | default: 115 | return false; 116 | } 117 | } 118 | 119 | // http://rus.har.mn/blog/2014-05-19/strtol-error-checking/ 120 | bool stringToNumber(const char *str, int &number) { 121 | // we need to reset `errno` 122 | errno = 0; 123 | 124 | char *endptr = nullptr; 125 | long num = strtol(str, &endptr, 10); 126 | 127 | if (errno == ERANGE) { 128 | switch (num) { 129 | case (int) LONG_MIN: // underflow 130 | case (int) LONG_MAX: // overflow 131 | break; 132 | default: 133 | // impossible 134 | assert(false); 135 | } 136 | return false; 137 | } 138 | 139 | // something else happened. die die die 140 | if (errno != 0) { 141 | return false; 142 | } 143 | 144 | // garbage at end of string 145 | if (*endptr != '\0') { 146 | return false; 147 | } 148 | 149 | number = (int) num; 150 | return true; 151 | } 152 | 153 | uint32_t getCurrentUnixTime() { 154 | return static_cast(time(nullptr)); 155 | } 156 | 157 | void humanDateString(char *day) { 158 | time_t now = time(nullptr); 159 | struct tm *datetime = localtime(&now); 160 | 161 | #ifdef _WIN32 162 | strftime(day, 11, "%a %b %d", datetime); 163 | #else 164 | strftime(day, 11, "%a %b %e", datetime); 165 | #endif 166 | } 167 | -------------------------------------------------------------------------------- /src/helpers.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Generic helper functions used throughout the code 7 | 8 | // Eventually we want these helpers to have no external dependencies 9 | // other than the standard library functions. 10 | 11 | #pragma once 12 | 13 | int getAndClearFirstBit(uint32_t &flag); 14 | void insertNumberIntoString(char *to_string, const char *from_string, int32_t number, bool show_sign); 15 | void insertStringIntoString(char *to_string, const char *from_string, const char *str_to_insert); 16 | bool isVowel(char ch); 17 | bool stringToNumber(const char *str, int &number); 18 | uint32_t getCurrentUnixTime(); 19 | void humanDateString(char *day); 20 | -------------------------------------------------------------------------------- /src/identification.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // indexes into the special name table 9 | enum SpecialNameIds { 10 | SN_NULL, 11 | SN_R, 12 | SN_RA, 13 | SN_RF, 14 | SN_RC, 15 | SN_RL, 16 | SN_HA, 17 | SN_DF, 18 | SN_SA, 19 | SN_SD, 20 | SN_SE, 21 | SN_SU, 22 | SN_FT, 23 | SN_FB, 24 | SN_FREE_ACTION, 25 | SN_SLAYING, 26 | SN_CLUMSINESS, 27 | SN_WEAKNESS, 28 | SN_SLOW_DESCENT, 29 | SN_SPEED, 30 | SN_STEALTH, 31 | SN_SLOWNESS, 32 | SN_NOISE, 33 | SN_GREAT_MASS, 34 | SN_INTELLIGENCE, 35 | SN_WISDOM, 36 | SN_INFRAVISION, 37 | SN_MIGHT, 38 | SN_LORDLINESS, 39 | SN_MAGI, 40 | SN_BEAUTY, 41 | SN_SEEING, 42 | SN_REGENERATION, 43 | SN_STUPIDITY, 44 | SN_DULLNESS, 45 | SN_BLINDNESS, 46 | SN_TIMIDNESS, 47 | SN_TELEPORTATION, 48 | SN_UGLINESS, 49 | SN_PROTECTION, 50 | SN_IRRITATION, 51 | SN_VULNERABILITY, 52 | SN_ENVELOPING, 53 | SN_FIRE, 54 | SN_SLAY_EVIL, 55 | SN_DRAGON_SLAYING, 56 | SN_EMPTY, 57 | SN_LOCKED, 58 | SN_POISON_NEEDLE, 59 | SN_GAS_TRAP, 60 | SN_EXPLOSION_DEVICE, 61 | SN_SUMMONING_RUNES, 62 | SN_MULTIPLE_TRAPS, 63 | SN_DISARMED, 64 | SN_UNLOCKED, 65 | SN_SLAY_ANIMAL, 66 | SN_ARRAY_SIZE, // 56th item (size value for arrays) 67 | }; 68 | 69 | constexpr uint8_t MAX_COLORS = 49; // Used with potions 70 | constexpr uint8_t MAX_MUSHROOMS = 22; // Used with mushrooms 71 | constexpr uint8_t MAX_WOODS = 25; // Used with staffs 72 | constexpr uint8_t MAX_METALS = 25; // Used with wands 73 | constexpr uint8_t MAX_ROCKS = 32; // Used with rings 74 | constexpr uint8_t MAX_AMULETS = 11; // Used with amulets 75 | constexpr uint8_t MAX_TITLES = 45; // Used with scrolls 76 | constexpr uint8_t MAX_SYLLABLES = 153; // Used with scrolls 77 | 78 | extern uint8_t objects_identified[OBJECT_IDENT_SIZE]; 79 | extern const char *special_item_names[SpecialNameIds::SN_ARRAY_SIZE]; 80 | 81 | // Following are arrays for descriptive pieces 82 | extern const char *colors[MAX_COLORS]; 83 | extern const char *mushrooms[MAX_MUSHROOMS]; 84 | extern const char *woods[MAX_WOODS]; 85 | extern const char *metals[MAX_METALS]; 86 | extern const char *rocks[MAX_ROCKS]; 87 | extern const char *amulets[MAX_AMULETS]; 88 | extern const char *syllables[MAX_SYLLABLES]; 89 | 90 | void identifyGameObject(); 91 | 92 | void magicInitializeItemNames(); 93 | int16_t objectPositionOffset(int category_id, int sub_category_id); 94 | void itemSetAsIdentified(int category_id, int sub_category_id); 95 | bool itemSetColorlessAsIdentified(int category_id, int sub_category_id, int identification); 96 | void spellItemIdentifyAndRemoveRandomInscription(Inventory_t &item); 97 | bool spellItemIdentified(Inventory_t const &item); 98 | void spellItemRemoveIdentification(Inventory_t &item); 99 | void itemIdentificationClearEmpty(Inventory_t &item); 100 | void itemIdentifyAsStoreBought(Inventory_t &item); 101 | void itemSetAsTried(Inventory_t const &item); 102 | void itemIdentify(Inventory_t &item, int &item_id); 103 | void itemRemoveMagicNaming(Inventory_t &item); 104 | void itemDescription(obj_desc_t description, Inventory_t const &item, bool add_prefix); 105 | void itemChargesRemainingDescription(int item_id); 106 | void itemTypeRemainingCountDescription(int item_id); 107 | void itemInscribe(); 108 | void itemAppendToInscription(Inventory_t &item, uint8_t item_ident_type); 109 | void itemReplaceInscription(Inventory_t &item, const char *inscription); 110 | 111 | void objectBlockedByMonster(int monster_id); 112 | -------------------------------------------------------------------------------- /src/inventory.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Size of inventory array (DO NOT CHANGE) 9 | constexpr uint8_t PLAYER_INVENTORY_SIZE = 34; 10 | 11 | // Inventory stacking `sub_category_id`s - these never stack 12 | constexpr uint8_t ITEM_NEVER_STACK_MIN = 0; 13 | constexpr uint8_t ITEM_NEVER_STACK_MAX = 63; 14 | // these items always stack with others of same `sub_category_id`s, always treated as 15 | // single objects, must be power of 2; 16 | constexpr uint8_t ITEM_SINGLE_STACK_MIN = 64; 17 | constexpr uint8_t ITEM_SINGLE_STACK_MAX = 192; // see NOTE below 18 | // these items stack with others only if have same `sub_category_id`s and same `misc_use`, 19 | // they are treated as a group for wielding, etc. 20 | constexpr uint8_t ITEM_GROUP_MIN = 192; 21 | constexpr uint8_t ITEM_GROUP_MAX = 255; 22 | // NOTE: items with `sub_category_id`s = 192 are treated as single objects, 23 | // but only stack with others of same `sub_category_id`s if have the same 24 | // `misc_use` value, only used for torches. 25 | 26 | // Size of an inscription in the Inventory_t. Notice alignment, must be 4*x + 1 27 | constexpr uint8_t INSCRIP_SIZE = 13; 28 | 29 | // Inventory_t is created for an item the player may wear about 30 | // their person, or store in their inventory pack. 31 | // 32 | // Only damage, ac, and tchar are constant; level could possibly be made 33 | // constant by changing index instead; all are used rarely. 34 | // 35 | // Extra fields x and y for location in dungeon would simplify pusht(). 36 | // 37 | // Making inscrip[] a pointer and malloc-ing space does not work, there are 38 | // two many places where `Inventory_t` are copied, which results in dangling 39 | // pointers, so we use a char array for them instead. 40 | typedef struct { 41 | uint16_t id; // Index to object_list 42 | uint8_t special_name_id; // Object special name 43 | char inscription[INSCRIP_SIZE]; // Object inscription 44 | uint32_t flags; // Special flags 45 | uint8_t category_id; // Category number (tval) 46 | uint8_t sprite; // Character representation - ASCII symbol (tchar) 47 | int16_t misc_use; // Misc. use variable (p1) 48 | int32_t cost; // Cost of item 49 | uint8_t sub_category_id; // Sub-category number 50 | uint8_t items_count; // Number of items 51 | uint16_t weight; // Weight 52 | int16_t to_hit; // Plusses to hit 53 | int16_t to_damage; // Plusses to damage 54 | int16_t ac; // Normal AC 55 | int16_t to_ac; // Plusses to AC 56 | Dice_t damage; // Damage when hits 57 | uint8_t depth_first_found; // Dungeon level item first found 58 | uint8_t identification; // Identify information 59 | } Inventory_t; 60 | 61 | // magic numbers for players equipment inventory array 62 | enum PlayerEquipment { 63 | Wield = 22, // must be first item in equipment list 64 | Head, 65 | Neck, 66 | Body, 67 | Arm, 68 | Hands, 69 | Right, 70 | Left, 71 | Feet, 72 | Outer, 73 | Light, 74 | Auxiliary, 75 | }; 76 | 77 | uint32_t inventoryCollectAllItemFlags(); 78 | 79 | void inventoryDestroyItem(int item_id); 80 | void inventoryTakeOneItem(Inventory_t *to_item, Inventory_t *from_item); 81 | void inventoryDropItem(int item_id, bool drop_all); 82 | bool inventoryDiminishLightAttack(bool noticed); 83 | bool inventoryDiminishChargesAttack(uint8_t creature_level, int16_t &monster_hp, bool noticed); 84 | bool executeDisenchantAttack(); 85 | bool inventoryCanCarryItemCount(Inventory_t const &item); 86 | bool inventoryCanCarryItem(Inventory_t const &item); 87 | int inventoryCarryItem(Inventory_t &new_item); 88 | bool inventoryFindRange(int item_id_start, int item_id_end, int &j, int &k); 89 | void inventoryItemCopyTo(int from_item_id, Inventory_t &to_item); 90 | bool inventoryItemSingleStackable(Inventory_t const &item); 91 | bool inventoryItemStackable(Inventory_t const &item); 92 | 93 | bool inventoryItemIsCursed(const Inventory_t &item); 94 | void inventoryItemRemoveCurse(Inventory_t &item); 95 | 96 | bool setNull(Inventory_t *item); 97 | bool setFrostDestroyableItems(Inventory_t *item); 98 | bool setLightningDestroyableItems(Inventory_t *item); 99 | bool setAcidDestroyableItems(Inventory_t *item); 100 | bool setFireDestroyableItems(Inventory_t *item); 101 | 102 | void damageCorrodingGas(const char *creature_name); 103 | void damagePoisonedGas(int damage, const char *creature_name); 104 | void damageFire(int damage, const char *creature_name); 105 | void damageCold(int damage, const char *creature_name); 106 | void damageLightningBolt(int damage, const char *creature_name); 107 | void damageAcid(int damage, const char *creature_name); 108 | -------------------------------------------------------------------------------- /src/mage_spells.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Code for mage spells 7 | 8 | #include "headers.h" 9 | 10 | // names based on spell_names[62] in data_player.cpp 11 | enum class MageSpellId { 12 | MagicMissile = 1, 13 | DetectMonsters, 14 | PhaseDoor, 15 | LightArea, 16 | CureLightWounds, 17 | FindHiddenTrapsDoors, 18 | StinkingCloud, 19 | Confusion, 20 | LightningBolt, 21 | TrapDoorDestruction, 22 | Sleep1, 23 | CurePoison, 24 | TeleportSelf, 25 | RemoveCurse, 26 | FrostBolt, 27 | WallToMud, 28 | CreateFood, 29 | RechargeItem1, 30 | Sleep2, 31 | PolymorphOther, 32 | IdentifyItem, 33 | Sleep3, 34 | FireBolt, 35 | SpeedMonster, 36 | FrostBall, 37 | RechargeItem2, 38 | TeleportOther, 39 | HasteSelf, 40 | FireBall, 41 | WordOfDestruction, 42 | Genocide, 43 | }; 44 | 45 | static bool canReadSpells() { 46 | if (py.flags.blind > 0) { 47 | printMessage("You can't see to read your spell book!"); 48 | return false; 49 | } 50 | 51 | if (playerNoLight()) { 52 | printMessage("You have no light to read by."); 53 | return false; 54 | } 55 | 56 | if (py.flags.confused > 0) { 57 | printMessage("You are too confused."); 58 | return false; 59 | } 60 | 61 | if (classes[py.misc.class_id].class_to_use_mage_spells != config::spells::SPELL_TYPE_MAGE) { 62 | printMessage("You can't cast spells!"); 63 | return false; 64 | } 65 | 66 | return true; 67 | } 68 | 69 | static void castSpell(int spell_id) { 70 | int dir; 71 | 72 | switch ((MageSpellId) spell_id) { 73 | case MageSpellId::MagicMissile: 74 | if (getDirectionWithMemory(CNIL, dir)) { 75 | spellFireBolt(py.pos, dir, diceRoll(Dice_t{2, 6}), MagicSpellFlags::MagicMissile, spell_names[0]); 76 | } 77 | break; 78 | case MageSpellId::DetectMonsters: 79 | (void) spellDetectMonsters(); 80 | break; 81 | case MageSpellId::PhaseDoor: 82 | playerTeleport(10); 83 | break; 84 | case MageSpellId::LightArea: 85 | (void) spellLightArea(py.pos); 86 | break; 87 | case MageSpellId::CureLightWounds: 88 | (void) spellChangePlayerHitPoints(diceRoll(Dice_t{4, 4})); 89 | break; 90 | case MageSpellId::FindHiddenTrapsDoors: 91 | (void) spellDetectSecretDoorssWithinVicinity(); 92 | (void) spellDetectTrapsWithinVicinity(); 93 | break; 94 | case MageSpellId::StinkingCloud: 95 | if (getDirectionWithMemory(CNIL, dir)) { 96 | spellFireBall(py.pos, dir, 12, MagicSpellFlags::PoisonGas, spell_names[6]); 97 | } 98 | break; 99 | case MageSpellId::Confusion: 100 | if (getDirectionWithMemory(CNIL, dir)) { 101 | (void) spellConfuseMonster(py.pos, dir); 102 | } 103 | break; 104 | case MageSpellId::LightningBolt: 105 | if (getDirectionWithMemory(CNIL, dir)) { 106 | spellFireBolt(py.pos, dir, diceRoll(Dice_t{4, 8}), MagicSpellFlags::Lightning, spell_names[8]); 107 | } 108 | break; 109 | case MageSpellId::TrapDoorDestruction: 110 | (void) spellDestroyAdjacentDoorsTraps(); 111 | break; 112 | case MageSpellId::Sleep1: 113 | if (getDirectionWithMemory(CNIL, dir)) { 114 | (void) spellSleepMonster(py.pos, dir); 115 | } 116 | break; 117 | case MageSpellId::CurePoison: 118 | (void) playerCurePoison(); 119 | break; 120 | case MageSpellId::TeleportSelf: 121 | playerTeleport((py.misc.level * 5)); 122 | break; 123 | case MageSpellId::RemoveCurse: 124 | for (int id = 22; id < PLAYER_INVENTORY_SIZE; id++) { 125 | inventoryItemRemoveCurse(py.inventory[id]); 126 | } 127 | break; 128 | case MageSpellId::FrostBolt: 129 | if (getDirectionWithMemory(CNIL, dir)) { 130 | spellFireBolt(py.pos, dir, diceRoll(Dice_t{6, 8}), MagicSpellFlags::Frost, spell_names[14]); 131 | } 132 | break; 133 | case MageSpellId::WallToMud: 134 | if (getDirectionWithMemory(CNIL, dir)) { 135 | (void) spellWallToMud(py.pos, dir); 136 | } 137 | break; 138 | case MageSpellId::CreateFood: 139 | spellCreateFood(); 140 | break; 141 | case MageSpellId::RechargeItem1: 142 | (void) spellRechargeItem(20); 143 | break; 144 | case MageSpellId::Sleep2: 145 | (void) monsterSleep(py.pos); 146 | break; 147 | case MageSpellId::PolymorphOther: 148 | if (getDirectionWithMemory(CNIL, dir)) { 149 | (void) spellPolymorphMonster(py.pos, dir); 150 | } 151 | break; 152 | case MageSpellId::IdentifyItem: 153 | (void) spellIdentifyItem(); 154 | break; 155 | case MageSpellId::Sleep3: 156 | (void) spellSleepAllMonsters(); 157 | break; 158 | case MageSpellId::FireBolt: 159 | if (getDirectionWithMemory(CNIL, dir)) { 160 | spellFireBolt(py.pos, dir, diceRoll(Dice_t{9, 8}), MagicSpellFlags::Fire, spell_names[22]); 161 | } 162 | break; 163 | case MageSpellId::SpeedMonster: 164 | if (getDirectionWithMemory(CNIL, dir)) { 165 | (void) spellSpeedMonster(py.pos, dir, -1); 166 | } 167 | break; 168 | case MageSpellId::FrostBall: 169 | if (getDirectionWithMemory(CNIL, dir)) { 170 | spellFireBall(py.pos, dir, 48, MagicSpellFlags::Frost, spell_names[24]); 171 | } 172 | break; 173 | case MageSpellId::RechargeItem2: 174 | (void) spellRechargeItem(60); 175 | break; 176 | case MageSpellId::TeleportOther: 177 | if (getDirectionWithMemory(CNIL, dir)) { 178 | (void) spellTeleportAwayMonsterInDirection(py.pos, dir); 179 | } 180 | break; 181 | case MageSpellId::HasteSelf: 182 | py.flags.fast += randomNumber(20) + py.misc.level; 183 | break; 184 | case MageSpellId::FireBall: 185 | if (getDirectionWithMemory(CNIL, dir)) { 186 | spellFireBall(py.pos, dir, 72, MagicSpellFlags::Fire, spell_names[28]); 187 | } 188 | break; 189 | case MageSpellId::WordOfDestruction: 190 | spellDestroyArea(py.pos); 191 | break; 192 | case MageSpellId::Genocide: 193 | (void) spellGenocide(); 194 | break; 195 | default: 196 | // All cases are handled, so this should never be reached! 197 | break; 198 | } 199 | } 200 | 201 | // Throw a magic spell -RAK- 202 | void getAndCastMagicSpell() { 203 | game.player_free_turn = true; 204 | 205 | if (!canReadSpells()) { 206 | return; 207 | } 208 | 209 | int i, j; 210 | if (!inventoryFindRange(TV_MAGIC_BOOK, TV_NEVER, i, j)) { 211 | printMessage("But you are not carrying any spell-books!"); 212 | return; 213 | } 214 | 215 | int item_val; 216 | if (!inventoryGetInputForItemId(item_val, "Use which spell-book?", i, j, CNIL, CNIL)) { 217 | return; 218 | } 219 | 220 | int choice, chance; 221 | int result = castSpellGetId("Cast which spell?", item_val, choice, chance); 222 | if (result < 0) { 223 | printMessage("You don't know any spells in that book."); 224 | return; 225 | } 226 | if (result == 0) { 227 | return; 228 | } 229 | 230 | game.player_free_turn = false; 231 | 232 | Spell_t const &magic_spell = magic_spells[py.misc.class_id - 1][choice]; 233 | 234 | if (randomNumber(100) < chance) { 235 | printMessage("You failed to get the spell off!"); 236 | } else { 237 | castSpell(choice + 1); 238 | 239 | if (!game.player_free_turn && (py.flags.spells_worked & (1L << choice)) == 0) { 240 | py.misc.exp += magic_spell.exp_gain_for_learning << 2; 241 | py.flags.spells_worked |= (1L << choice); 242 | 243 | displayCharacterExperience(); 244 | } 245 | } 246 | 247 | if (game.player_free_turn) { 248 | return; 249 | } 250 | 251 | if (magic_spell.mana_required > py.misc.current_mana) { 252 | printMessage("You faint from the effort!"); 253 | 254 | py.flags.paralysis = (int16_t) randomNumber((5 * (magic_spell.mana_required - py.misc.current_mana))); 255 | py.misc.current_mana = 0; 256 | py.misc.current_mana_fraction = 0; 257 | 258 | if (randomNumber(3) == 1) { 259 | printMessage("You have damaged your health!"); 260 | (void) playerStatRandomDecrease(PlayerAttr::A_CON); 261 | } 262 | } else { 263 | py.misc.current_mana -= magic_spell.mana_required; 264 | } 265 | 266 | printCharacterCurrentMana(); 267 | } 268 | 269 | // Returns spell chance of failure for class_to_use_mage_spells -RAK- 270 | int spellChanceOfSuccess(int spell_id) { 271 | Spell_t const &spell = magic_spells[py.misc.class_id - 1][spell_id]; 272 | 273 | int chance = spell.failure_chance - 3 * (py.misc.level - spell.level_required); 274 | 275 | int stat; 276 | if (classes[py.misc.class_id].class_to_use_mage_spells == config::spells::SPELL_TYPE_MAGE) { 277 | stat = PlayerAttr::A_INT; 278 | } else { 279 | stat = PlayerAttr::A_WIS; 280 | } 281 | 282 | chance -= 3 * (playerStatAdjustmentWisdomIntelligence(stat) - 1); 283 | 284 | if (spell.mana_required > py.misc.current_mana) { 285 | chance += 5 * (spell.mana_required - py.misc.current_mana); 286 | } 287 | 288 | if (chance > 95) { 289 | chance = 95; 290 | } else if (chance < 5) { 291 | chance = 5; 292 | } 293 | 294 | return chance; 295 | } 296 | -------------------------------------------------------------------------------- /src/mage_spells.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | void getAndCastMagicSpell(); 9 | int spellChanceOfSuccess(int spell_id); 10 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Initialization, main() function and main loop 7 | 8 | #include "headers.h" 9 | #include "version.h" 10 | 11 | static bool parseGameSeed(const char *argv, uint32_t &seed); 12 | 13 | static const char *usage_instructions = R"( 14 | Usage: 15 | umoria [OPTIONS] SAVEGAME 16 | 17 | SAVEGAME is an optional save game filename (default: game.sav) 18 | 19 | Options: 20 | -n Force start of new game 21 | -r Enable classic roguelike keys on startup (default: disabled, or save game settings) 22 | -d Display high scores and exit 23 | -s NUMBER Game Seed, as a decimal number (max: 2147483647) 24 | 25 | -v Print version info and exit 26 | -h Display this message 27 | )"; 28 | 29 | // Initialize, restore, and get the ball rolling. -RAK- 30 | int main(int argc, char *argv[]) { 31 | uint32_t seed = 0; 32 | bool new_game = false; 33 | bool roguelike_keys = false; 34 | 35 | // call this routine to grab a file pointer to the high score file 36 | // and prepare things to relinquish setuid privileges 37 | if (!initializeScoreFile()) { 38 | std::cerr << "Can't open score file '" << config::files::scores << "'\n"; 39 | return 1; 40 | } 41 | 42 | // Make sure we have access to all files -MRC- 43 | if (!checkFilePermissions()) { 44 | return 1; 45 | } 46 | 47 | if (!terminalInitialize()) { 48 | return 1; 49 | } 50 | 51 | // check for user interface option 52 | for (--argc, ++argv; argc > 0 && argv[0][0] == '-'; --argc, ++argv) { 53 | switch (argv[0][1]) { 54 | case 'v': 55 | terminalRestore(); 56 | printf("%d.%d.%d\n", CURRENT_VERSION_MAJOR, CURRENT_VERSION_MINOR, CURRENT_VERSION_PATCH); 57 | return 0; 58 | case 'n': 59 | new_game = true; 60 | break; 61 | case 'r': 62 | roguelike_keys = true; 63 | break; 64 | case 'd': 65 | showScoresScreen(); 66 | exitProgram(); 67 | break; 68 | case 's': 69 | // No NUMBER provided? 70 | if (argv[1] == nullptr) { 71 | break; 72 | } 73 | 74 | // Move onto the NUMBER value 75 | --argc; 76 | ++argv; 77 | 78 | if (!parseGameSeed(argv[0], seed)) { 79 | terminalRestore(); 80 | printf("Game seed must be a decimal number between 1 and 2147483647\n"); 81 | return -1; 82 | } 83 | 84 | break; 85 | case 'w': 86 | game.to_be_wizard = true; 87 | break; 88 | default: 89 | terminalRestore(); 90 | 91 | printf("Robert A. Koeneke's classic dungeon crawler.\n"); 92 | printf("Umoria %d.%d.%d is released under a GPL-3.0-or-later license.\n", CURRENT_VERSION_MAJOR, CURRENT_VERSION_MINOR, CURRENT_VERSION_PATCH); 93 | printf("%s", usage_instructions); 94 | return 0; 95 | } 96 | } 97 | 98 | // Auto-restart of saved file 99 | if (argv[0] != CNIL) { 100 | // (void) strcpy(config::files::save_game, argv[0]); 101 | config::files::save_game = argv[0]; 102 | } 103 | 104 | startMoria(seed, new_game, roguelike_keys); 105 | 106 | return 0; 107 | } 108 | 109 | static bool parseGameSeed(const char *argv, uint32_t &seed) { 110 | int value; 111 | 112 | if (!stringToNumber(argv, value)) { 113 | return false; 114 | } 115 | if (value <= 0 || value > INT_MAX) { 116 | return false; 117 | } 118 | 119 | seed = (uint32_t) value; 120 | 121 | return true; 122 | } 123 | -------------------------------------------------------------------------------- /src/monster.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Monster_t is created for any living monster found on the current dungeon level 9 | typedef struct { 10 | int16_t hp; // Hit points 11 | int16_t sleep_count; // Inactive counter 12 | int16_t speed; // Movement speed 13 | uint16_t creature_id; // Pointer into creature 14 | 15 | // Note: fy, fx, and cdis constrain dungeon size to less than 256 by 256 16 | Coord_t pos; // (y,x) Pointer into map 17 | uint8_t distance_from_player; // Current distance from player 18 | 19 | bool lit; 20 | uint8_t stunned_amount; 21 | uint8_t confused_amount; 22 | } Monster_t; 23 | 24 | // Creature_t is a base data object. 25 | // Holds the base game data for any given creature in the game such 26 | // as: Kobold, Orc, Giant Red Ant, Quasit, Young Black Dragon, etc. 27 | typedef struct { 28 | const char *name; // Description of creature 29 | uint32_t movement; // Bit field 30 | uint32_t spells; // Creature spells 31 | uint16_t defenses; // Bit field 32 | uint16_t kill_exp_value; // Exp value for kill 33 | uint8_t sleep_counter; // Inactive counter / 10 34 | uint8_t area_affect_radius; // Area affect radius 35 | uint8_t ac; // AC 36 | uint8_t speed; // Movement speed+10 (NOTE: +10 so that it can be an unsigned int) 37 | uint8_t sprite; // Character representation (cchar) 38 | Dice_t hit_die; // Creatures hit die 39 | uint8_t damage[4]; // Type attack and damage 40 | uint8_t level; // Level of creature 41 | } Creature_t; 42 | 43 | // MonsterAttack_t is a base data object. 44 | // Holds the data for a monster's attack and damage type 45 | typedef struct { 46 | uint8_t type_id; 47 | uint8_t description_id; 48 | Dice_t dice; 49 | } MonsterAttack_t; 50 | 51 | // Creature constants 52 | constexpr uint16_t MON_MAX_CREATURES = 279; // Number of creatures defined for univ 53 | constexpr uint8_t MON_ATTACK_TYPES = 215; // Number of monster attack types. 54 | 55 | // With MON_TOTAL_ALLOCATIONS set to 101, it is possible to get compacting 56 | // monsters messages while breeding/cloning monsters. 57 | constexpr uint8_t MON_TOTAL_ALLOCATIONS = 125; // Max that can be allocated 58 | constexpr uint8_t MON_MAX_LEVELS = 40; // Maximum level of creatures 59 | constexpr uint8_t MON_MAX_ATTACKS = 4; // Max num attacks (used in mons memory) -CJS- 60 | 61 | extern int hack_monptr; 62 | extern Creature_t creatures_list[MON_MAX_CREATURES]; 63 | extern Monster_t monsters[MON_TOTAL_ALLOCATIONS]; 64 | extern int16_t monster_levels[MON_MAX_LEVELS + 1]; 65 | extern MonsterAttack_t monster_attacks[MON_ATTACK_TYPES]; 66 | extern Monster_t blank_monster; 67 | extern int16_t next_free_monster_id; 68 | extern int16_t monster_multiply_total; 69 | 70 | void monsterUpdateVisibility(int monster_id); 71 | bool monsterMultiply(Coord_t coord, int creature_id, int monster_id); 72 | void updateMonsters(bool attack); 73 | uint32_t monsterDeath(Coord_t coord, uint32_t flags); 74 | int monsterTakeHit(int monster_id, int damage); 75 | void printMonsterActionText(const std::string &name, const std::string &action); 76 | std::string monsterNameDescription(const std::string &real_name, bool is_lit); 77 | bool monsterSleep(Coord_t coord); 78 | 79 | // monster management 80 | bool compactMonsters(); 81 | bool monsterPlaceNew(Coord_t coord, int creature_id, bool sleeping); 82 | void monsterPlaceWinning(); 83 | void monsterPlaceNewWithinDistance(int number, int distance_from_source, bool sleeping); 84 | bool monsterSummon(Coord_t &coord, bool sleeping); 85 | bool monsterSummonUndead(Coord_t &coord); 86 | -------------------------------------------------------------------------------- /src/player_bash.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // The running algorithm: -CJS- 7 | 8 | #include "headers.h" 9 | #include "dice.h" 10 | 11 | static void playerBashAttack(Coord_t coord); 12 | static void playerBashPosition(Coord_t coord); 13 | static void playerBashClosedDoor(Coord_t coord, int dir, Tile_t &tile, Inventory_t &item); 14 | static void playerBashClosedChest(Inventory_t &item); 15 | 16 | // Bash open a door or chest -RAK- 17 | // Note: Affected by strength and weight of character 18 | // 19 | // For a closed door, `misc_use` is positive if locked; negative if stuck. A disarm spell 20 | // unlocks and unjams doors! 21 | // 22 | // For an open door, `misc_use` is positive for a broken door. 23 | // 24 | // A closed door can be opened - harder if locked. Any door might be bashed open 25 | // (and thereby broken). Bashing a door is (potentially) faster! You move into the 26 | // door way. To open a stuck door, it must be bashed. A closed door can be jammed 27 | // (which makes it stuck if previously locked). 28 | // 29 | // Creatures can also open doors. A creature with open door ability will (if not 30 | // in the line of sight) move though a closed or secret door with no changes. If 31 | // in the line of sight, closed door are opened, & secret door revealed. Whether 32 | // in the line of sight or not, such a creature may unlock or unstick a door. 33 | // 34 | // A creature with no such ability will attempt to bash a non-secret door. 35 | void playerBash() { 36 | int dir; 37 | if (!getDirectionWithMemory(CNIL, dir)) { 38 | return; 39 | } 40 | 41 | if (py.flags.confused > 0) { 42 | printMessage("You are confused."); 43 | dir = getRandomDirection(); 44 | } 45 | 46 | Coord_t coord = py.pos; 47 | (void) playerMovePosition(dir, coord); 48 | 49 | Tile_t &tile = dg.floor[coord.y][coord.x]; 50 | 51 | if (tile.creature_id > 1) { 52 | playerBashPosition(coord); 53 | return; 54 | } 55 | 56 | if (tile.treasure_id != 0) { 57 | Inventory_t &item = game.treasure.list[tile.treasure_id]; 58 | 59 | if (item.category_id == TV_CLOSED_DOOR) { 60 | playerBashClosedDoor(coord, dir, tile, item); 61 | } else if (item.category_id == TV_CHEST) { 62 | playerBashClosedChest(item); 63 | } else { 64 | // Can't give free turn, or else player could try 65 | // directions until they find the invisible creature 66 | printMessage("You bash it, but nothing interesting happens."); 67 | } 68 | return; 69 | } 70 | 71 | if (tile.feature_id < MIN_CAVE_WALL) { 72 | printMessage("You bash at empty space."); 73 | return; 74 | } 75 | 76 | // same message for wall as for secret door 77 | printMessage("You bash it, but nothing interesting happens."); 78 | } 79 | 80 | // Make a bash attack on someone. -CJS- 81 | // Used to be part of bash above. 82 | static void playerBashAttack(Coord_t coord) { 83 | int monster_id = dg.floor[coord.y][coord.x].creature_id; 84 | 85 | Monster_t &monster = monsters[monster_id]; 86 | Creature_t const &creature = creatures_list[monster.creature_id]; 87 | 88 | monster.sleep_count = 0; 89 | 90 | // Does the player know what they're fighting? 91 | vtype_t name = {'\0'}; 92 | if (!monster.lit) { 93 | (void) strcpy(name, "it"); 94 | } else { 95 | (void) snprintf(name, MORIA_MESSAGE_SIZE, "the %s", creature.name); 96 | } 97 | 98 | int base_to_hit = py.stats.used[PlayerAttr::A_STR]; 99 | base_to_hit += py.inventory[PlayerEquipment::Arm].weight / 2; 100 | base_to_hit += py.misc.weight / 10; 101 | 102 | if (!monster.lit) { 103 | base_to_hit /= 2; 104 | base_to_hit -= py.stats.used[PlayerAttr::A_DEX] * (BTH_PER_PLUS_TO_HIT_ADJUST - 1); 105 | base_to_hit -= py.misc.level * class_level_adj[py.misc.class_id][PlayerClassLevelAdj::BTH] / 2; 106 | } 107 | 108 | if (playerTestBeingHit(base_to_hit, (int) py.misc.level, (int) py.stats.used[PlayerAttr::A_DEX], (int) creature.ac, PlayerClassLevelAdj::BTH)) { 109 | vtype_t msg = {'\0'}; 110 | (void) snprintf(msg, MORIA_MESSAGE_SIZE, "You hit %s.", name); 111 | printMessage(msg); 112 | 113 | int damage = diceRoll(py.inventory[PlayerEquipment::Arm].damage); 114 | damage = playerWeaponCriticalBlow(py.inventory[PlayerEquipment::Arm].weight / 4 + py.stats.used[PlayerAttr::A_STR], 0, damage, PlayerClassLevelAdj::BTH); 115 | damage += py.misc.weight / 60; 116 | damage += 3; 117 | 118 | if (damage < 0) { 119 | damage = 0; 120 | } 121 | 122 | // See if we done it in. 123 | if (monsterTakeHit(monster_id, damage) >= 0) { 124 | (void) snprintf(msg, MORIA_MESSAGE_SIZE, "You have slain %s.", name); 125 | printMessage(msg); 126 | displayCharacterExperience(); 127 | } else { 128 | name[0] = (char) toupper((int) name[0]); // Capitalize 129 | 130 | // Can not stun Balrog 131 | int avg_max_hp; 132 | if ((creature.defenses & config::monsters::defense::CD_MAX_HP) != 0) { 133 | avg_max_hp = maxDiceRoll(creature.hit_die); 134 | } else { 135 | // TODO: use maxDiceRoll(), just be careful about the bit shift 136 | avg_max_hp = (creature.hit_die.dice * (creature.hit_die.sides + 1)) >> 1; 137 | } 138 | 139 | if (100 + randomNumber(400) + randomNumber(400) > monster.hp + avg_max_hp) { 140 | monster.stunned_amount += randomNumber(3) + 1; 141 | if (monster.stunned_amount > 24) { 142 | monster.stunned_amount = 24; 143 | } 144 | 145 | (void) snprintf(msg, MORIA_MESSAGE_SIZE, "%s appears stunned!", name); 146 | } else { 147 | (void) snprintf(msg, MORIA_MESSAGE_SIZE, "%s ignores your bash!", name); 148 | } 149 | printMessage(msg); 150 | } 151 | } else { 152 | vtype_t msg = {'\0'}; 153 | (void) snprintf(msg, MORIA_MESSAGE_SIZE, "You miss %s.", name); 154 | printMessage(msg); 155 | } 156 | 157 | if (randomNumber(150) > py.stats.used[PlayerAttr::A_DEX]) { 158 | printMessage("You are off balance."); 159 | py.flags.paralysis = (int16_t) (1 + randomNumber(2)); 160 | } 161 | } 162 | 163 | static void playerBashPosition(Coord_t coord) { 164 | // Is a Coward? 165 | if (py.flags.afraid > 0) { 166 | printMessage("You are afraid!"); 167 | return; 168 | } 169 | 170 | playerBashAttack(coord); 171 | } 172 | 173 | static void playerBashClosedDoor(Coord_t coord, int dir, Tile_t &tile, Inventory_t &item) { 174 | printMessageNoCommandInterrupt("You smash into the door!"); 175 | 176 | int chance = py.stats.used[PlayerAttr::A_STR] + py.misc.weight / 2; 177 | 178 | // Use (roughly) similar method as for monsters. 179 | auto abs_misc_use = (int) std::abs((std::intmax_t) item.misc_use); 180 | if (randomNumber(chance * (20 + abs_misc_use)) < 10 * (chance - abs_misc_use)) { 181 | printMessage("The door crashes open!"); 182 | 183 | inventoryItemCopyTo(config::dungeon::objects::OBJ_OPEN_DOOR, game.treasure.list[tile.treasure_id]); 184 | 185 | // 50% chance of breaking door 186 | item.misc_use = (int16_t) (1 - randomNumber(2)); 187 | 188 | tile.feature_id = TILE_CORR_FLOOR; 189 | 190 | if (py.flags.confused == 0) { 191 | playerMove(dir, false); 192 | } else { 193 | dungeonLiteSpot(coord); 194 | } 195 | 196 | return; 197 | } 198 | 199 | if (randomNumber(150) > py.stats.used[PlayerAttr::A_DEX]) { 200 | printMessage("You are off-balance."); 201 | py.flags.paralysis = (int16_t) (1 + randomNumber(2)); 202 | return; 203 | } 204 | 205 | if (game.command_count == 0) { 206 | printMessage("The door holds firm."); 207 | } 208 | } 209 | 210 | static void playerBashClosedChest(Inventory_t &item) { 211 | if (randomNumber(10) == 1) { 212 | printMessage("You have destroyed the chest."); 213 | printMessage("and its contents!"); 214 | 215 | item.id = config::dungeon::objects::OBJ_RUINED_CHEST; 216 | item.flags = 0; 217 | 218 | return; 219 | } 220 | 221 | if (((item.flags & config::treasure::chests::CH_LOCKED) != 0u) && randomNumber(10) == 1) { 222 | printMessage("The lock breaks open!"); 223 | 224 | item.flags &= ~config::treasure::chests::CH_LOCKED; 225 | 226 | return; 227 | } 228 | 229 | printMessageNoCommandInterrupt("The chest holds firm."); 230 | } 231 | -------------------------------------------------------------------------------- /src/player_eat.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Food code 7 | 8 | #include "headers.h" 9 | 10 | enum class FoodMagicTypes { 11 | Poison = 1, 12 | Blindness, 13 | Paranoia, 14 | Confusion, 15 | Hallucination, 16 | CurePoison, 17 | CureBlindness, 18 | CureParanoia, 19 | CureConfusion, 20 | Weakness, 21 | Unhealth, 22 | // 12-15 are no longer used 23 | RestoreSTR = 16, 24 | RestoreCON, 25 | RestoreINT, 26 | RestoreWIS, 27 | RestoreDEX, 28 | RestoreCHR, 29 | FirstAid, 30 | MinorCures, 31 | LightCures, 32 | // 25 no longer used 33 | MajorCures = 26, 34 | PoisonousFood, 35 | }; 36 | 37 | // Eat some food. -RAK- 38 | void playerEat() { 39 | game.player_free_turn = true; 40 | 41 | if (py.pack.unique_items == 0) { 42 | printMessage("But you are not carrying anything."); 43 | return; 44 | } 45 | 46 | int item_pos_start, item_pos_end; 47 | if (!inventoryFindRange(TV_FOOD, TV_NEVER, item_pos_start, item_pos_end)) { 48 | printMessage("You are not carrying any food."); 49 | return; 50 | } 51 | 52 | int item_id; 53 | if (!inventoryGetInputForItemId(item_id, "Eat what?", item_pos_start, item_pos_end, CNIL, CNIL)) { 54 | return; 55 | } 56 | 57 | game.player_free_turn = false; 58 | 59 | bool identified = false; 60 | 61 | Inventory_t *item = &py.inventory[item_id]; 62 | uint32_t item_flags = item->flags; 63 | 64 | while (item_flags != 0) { 65 | switch ((FoodMagicTypes) (getAndClearFirstBit(item_flags) + 1)) { 66 | case FoodMagicTypes::Poison: 67 | py.flags.poisoned += randomNumber(10) + item->depth_first_found; 68 | identified = true; 69 | break; 70 | case FoodMagicTypes::Blindness: 71 | py.flags.blind += randomNumber(250) + 10 * item->depth_first_found + 100; 72 | drawCavePanel(); 73 | printMessage("A veil of darkness surrounds you."); 74 | identified = true; 75 | break; 76 | case FoodMagicTypes::Paranoia: 77 | py.flags.afraid += randomNumber(10) + item->depth_first_found; 78 | printMessage("You feel terrified!"); 79 | identified = true; 80 | break; 81 | case FoodMagicTypes::Confusion: 82 | py.flags.confused += randomNumber(10) + item->depth_first_found; 83 | printMessage("You feel drugged."); 84 | identified = true; 85 | break; 86 | case FoodMagicTypes::Hallucination: 87 | py.flags.image += randomNumber(200) + 25 * item->depth_first_found + 200; 88 | printMessage("You feel drugged."); 89 | identified = true; 90 | break; 91 | case FoodMagicTypes::CurePoison: 92 | identified = playerCurePoison(); 93 | break; 94 | case FoodMagicTypes::CureBlindness: 95 | identified = playerCureBlindness(); 96 | break; 97 | case FoodMagicTypes::CureParanoia: 98 | if (py.flags.afraid > 1) { 99 | py.flags.afraid = 1; 100 | identified = true; 101 | } 102 | break; 103 | case FoodMagicTypes::CureConfusion: 104 | identified = playerCureConfusion(); 105 | break; 106 | case FoodMagicTypes::Weakness: 107 | spellLoseSTR(); 108 | identified = true; 109 | break; 110 | case FoodMagicTypes::Unhealth: 111 | spellLoseCON(); 112 | identified = true; 113 | break; 114 | #if 0 // 12 through 15 are no longer used 115 | case 12: 116 | lose_int(); 117 | identified = true; 118 | break; 119 | case 13: 120 | lose_wis(); 121 | identified = true; 122 | break; 123 | case 14: 124 | lose_dex(); 125 | identified = true; 126 | break; 127 | case 15: 128 | lose_chr(); 129 | identified = true; 130 | break; 131 | #endif 132 | case FoodMagicTypes::RestoreSTR: 133 | if (playerStatRestore(PlayerAttr::A_STR)) { 134 | printMessage("You feel your strength returning."); 135 | identified = true; 136 | } 137 | break; 138 | case FoodMagicTypes::RestoreCON: 139 | if (playerStatRestore(PlayerAttr::A_CON)) { 140 | printMessage("You feel your health returning."); 141 | identified = true; 142 | } 143 | break; 144 | case FoodMagicTypes::RestoreINT: 145 | if (playerStatRestore(PlayerAttr::A_INT)) { 146 | printMessage("Your head spins a moment."); 147 | identified = true; 148 | } 149 | break; 150 | case FoodMagicTypes::RestoreWIS: 151 | if (playerStatRestore(PlayerAttr::A_WIS)) { 152 | printMessage("You feel your wisdom returning."); 153 | identified = true; 154 | } 155 | break; 156 | case FoodMagicTypes::RestoreDEX: 157 | if (playerStatRestore(PlayerAttr::A_DEX)) { 158 | printMessage("You feel more dexterous."); 159 | identified = true; 160 | } 161 | break; 162 | case FoodMagicTypes::RestoreCHR: 163 | if (playerStatRestore(PlayerAttr::A_CHR)) { 164 | printMessage("Your skin stops itching."); 165 | identified = true; 166 | } 167 | break; 168 | case FoodMagicTypes::FirstAid: 169 | identified = spellChangePlayerHitPoints(randomNumber(6)); 170 | break; 171 | case FoodMagicTypes::MinorCures: 172 | identified = spellChangePlayerHitPoints(randomNumber(12)); 173 | break; 174 | case FoodMagicTypes::LightCures: 175 | identified = spellChangePlayerHitPoints(randomNumber(18)); 176 | break; 177 | #if 0 // 25 is no longer used 178 | case 25: 179 | identified = hp_player(damroll(3, 6)); 180 | break; 181 | #endif 182 | case FoodMagicTypes::MajorCures: 183 | identified = spellChangePlayerHitPoints(diceRoll(Dice_t{3, 12})); 184 | break; 185 | case FoodMagicTypes::PoisonousFood: 186 | playerTakesHit(randomNumber(18), "poisonous food."); 187 | identified = true; 188 | break; 189 | #if 0 // 28 through 30 are no longer used 190 | case 28: 191 | take_hit(randint(8), "poisonous food."); 192 | identified = true; 193 | break; 194 | case 29: 195 | take_hit(damroll(2, 8), "poisonous food."); 196 | identified = true; 197 | break; 198 | case 30: 199 | take_hit(damroll(3, 8), "poisonous food."); 200 | identified = true; 201 | break; 202 | #endif 203 | default: 204 | // All cases are handled, so this should never be reached! 205 | printMessage("Internal error in playerEat()"); 206 | break; 207 | } 208 | } 209 | 210 | if (identified) { 211 | if (!itemSetColorlessAsIdentified(item->category_id, item->sub_category_id, item->identification)) { 212 | // use identified it, gain experience 213 | // round half-way case up 214 | py.misc.exp += (item->depth_first_found + (py.misc.level >> 1)) / py.misc.level; 215 | 216 | displayCharacterExperience(); 217 | 218 | itemIdentify(py.inventory[item_id], item_id); 219 | item = &py.inventory[item_id]; 220 | } 221 | } else if (!itemSetColorlessAsIdentified(item->category_id, item->sub_category_id, item->identification)) { 222 | itemSetAsTried(*item); 223 | } 224 | 225 | playerIngestFood(item->misc_use); 226 | 227 | py.flags.status &= ~(config::player::status::PY_WEAK | config::player::status::PY_HUNGRY); 228 | 229 | printCharacterHungerStatus(); 230 | 231 | itemTypeRemainingCountDescription(item_id); 232 | inventoryDestroyItem(item_id); 233 | } 234 | 235 | // Add to the players food time -RAK- 236 | void playerIngestFood(int amount) { 237 | if (py.flags.food < 0) { 238 | py.flags.food = 0; 239 | } 240 | 241 | py.flags.food += amount; 242 | 243 | if (py.flags.food > config::player::PLAYER_FOOD_MAX) { 244 | printMessage("You are bloated from overeating."); 245 | 246 | // Calculate how much of amount is responsible for the bloating. Give the 247 | // player food credit for 1/50, and also slow them for that many turns. 248 | int extra = py.flags.food - config::player::PLAYER_FOOD_MAX; 249 | if (extra > amount) { 250 | extra = amount; 251 | } 252 | int penalty = extra / 50; 253 | 254 | py.flags.slow += penalty; 255 | 256 | if (extra == amount) { 257 | py.flags.food = (int16_t) (py.flags.food - amount + penalty); 258 | } else { 259 | py.flags.food = (int16_t) (config::player::PLAYER_FOOD_MAX + penalty); 260 | } 261 | } else if (py.flags.food > config::player::PLAYER_FOOD_FULL) { 262 | printMessage("You are full."); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/player_magic.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Player magic functions 7 | 8 | #include "headers.h" 9 | 10 | // Cure players confusion -RAK- 11 | bool playerCureConfusion() { 12 | if (py.flags.confused > 1) { 13 | py.flags.confused = 1; 14 | return true; 15 | } 16 | return false; 17 | } 18 | 19 | // Cure players blindness -RAK- 20 | bool playerCureBlindness() { 21 | if (py.flags.blind > 1) { 22 | py.flags.blind = 1; 23 | return true; 24 | } 25 | return false; 26 | } 27 | 28 | // Cure poisoning -RAK- 29 | bool playerCurePoison() { 30 | if (py.flags.poisoned > 1) { 31 | py.flags.poisoned = 1; 32 | return true; 33 | } 34 | return false; 35 | } 36 | 37 | // Cure the players fear -RAK- 38 | bool playerRemoveFear() { 39 | if (py.flags.afraid > 1) { 40 | py.flags.afraid = 1; 41 | return true; 42 | } 43 | return false; 44 | } 45 | 46 | // Evil creatures don't like this. -RAK- 47 | bool playerProtectEvil() { 48 | bool is_protected = py.flags.protect_evil == 0; 49 | 50 | py.flags.protect_evil += randomNumber(25) + 3 * py.misc.level; 51 | 52 | return is_protected; 53 | } 54 | 55 | // Bless -RAK- 56 | void playerBless(int adjustment) { 57 | py.flags.blessed += adjustment; 58 | } 59 | 60 | // Detect Invisible for period of time -RAK- 61 | void playerDetectInvisible(int adjustment) { 62 | py.flags.detect_invisible += adjustment; 63 | } 64 | 65 | // Special damage due to magical abilities of object -RAK- 66 | int itemMagicAbilityDamage(Inventory_t const &item, int total_damage, int monster_id) { 67 | bool is_ego_weapon = (item.flags & config::treasure::flags::TR_EGO_WEAPON) != 0; 68 | bool is_projectile = item.category_id >= TV_SLING_AMMO && item.category_id <= TV_ARROW; 69 | bool is_hafted_sword = item.category_id >= TV_HAFTED && item.category_id <= TV_SWORD; 70 | bool is_flask = item.category_id == TV_FLASK; 71 | 72 | if (is_ego_weapon && (is_projectile || is_hafted_sword || is_flask)) { 73 | Creature_t const &creature = creatures_list[monster_id]; 74 | Recall_t &memory = creature_recall[monster_id]; 75 | 76 | // Slay Dragon 77 | if (((creature.defenses & config::monsters::defense::CD_DRAGON) != 0) && ((item.flags & config::treasure::flags::TR_SLAY_DRAGON) != 0u)) { 78 | memory.defenses |= config::monsters::defense::CD_DRAGON; 79 | return total_damage * 4; 80 | } 81 | 82 | // Slay Undead 83 | if (((creature.defenses & config::monsters::defense::CD_UNDEAD) != 0) && ((item.flags & config::treasure::flags::TR_SLAY_UNDEAD) != 0u)) { 84 | memory.defenses |= config::monsters::defense::CD_UNDEAD; 85 | return total_damage * 3; 86 | } 87 | 88 | // Slay Animal 89 | if (((creature.defenses & config::monsters::defense::CD_ANIMAL) != 0) && ((item.flags & config::treasure::flags::TR_SLAY_ANIMAL) != 0u)) { 90 | memory.defenses |= config::monsters::defense::CD_ANIMAL; 91 | return total_damage * 2; 92 | } 93 | 94 | // Slay Evil 95 | if (((creature.defenses & config::monsters::defense::CD_EVIL) != 0) && ((item.flags & config::treasure::flags::TR_SLAY_EVIL) != 0u)) { 96 | memory.defenses |= config::monsters::defense::CD_EVIL; 97 | return total_damage * 2; 98 | } 99 | 100 | // Frost 101 | if (((creature.defenses & config::monsters::defense::CD_FROST) != 0) && ((item.flags & config::treasure::flags::TR_FROST_BRAND) != 0u)) { 102 | memory.defenses |= config::monsters::defense::CD_FROST; 103 | return total_damage * 3 / 2; 104 | } 105 | 106 | // Fire 107 | if (((creature.defenses & config::monsters::defense::CD_FIRE) != 0) && ((item.flags & config::treasure::flags::TR_FLAME_TONGUE) != 0u)) { 108 | memory.defenses |= config::monsters::defense::CD_FIRE; 109 | return total_damage * 3 / 2; 110 | } 111 | } 112 | 113 | return total_damage; 114 | } 115 | -------------------------------------------------------------------------------- /src/player_pray.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Code for priest spells 7 | 8 | #include "headers.h" 9 | 10 | static bool playerCanPray(int &item_pos_begin, int &item_pos_end) { 11 | if (py.flags.blind > 0) { 12 | printMessage("You can't see to read your prayer!"); 13 | return false; 14 | } 15 | 16 | if (playerNoLight()) { 17 | printMessage("You have no light to read by."); 18 | return false; 19 | } 20 | 21 | if (py.flags.confused > 0) { 22 | printMessage("You are too confused."); 23 | return false; 24 | } 25 | 26 | if (classes[py.misc.class_id].class_to_use_mage_spells != config::spells::SPELL_TYPE_PRIEST) { 27 | printMessage("Pray hard enough and your prayers may be answered."); 28 | return false; 29 | } 30 | 31 | if (py.pack.unique_items == 0) { 32 | printMessage("But you are not carrying anything!"); 33 | return false; 34 | } 35 | 36 | if (!inventoryFindRange(TV_PRAYER_BOOK, TV_NEVER, item_pos_begin, item_pos_end)) { 37 | printMessage("You are not carrying any Holy Books!"); 38 | return false; 39 | } 40 | 41 | return true; 42 | } 43 | 44 | // names based on spell_names[62] in data_player.cpp 45 | enum class PriestSpellTypes { 46 | DetectEvil = 1, 47 | CureLightWounds, 48 | Bless, 49 | RemoveFear, 50 | CallLight, 51 | FindTraps, 52 | DetectDoorsStairs, 53 | SlowPoison, 54 | BlindCreature, 55 | Portal, 56 | CureMediumWounds, 57 | Chant, 58 | Sanctuary, 59 | CreateFood, 60 | RemoveCurse, 61 | ResistHeadCold, 62 | NeutralizePoison, 63 | OrbOfDraining, 64 | CureSeriousWounds, 65 | SenseInvisible, 66 | ProtectFromEvil, 67 | Earthquake, 68 | SenseSurroundings, 69 | CureCriticalWounds, 70 | TurnUndead, 71 | Prayer, 72 | DispelUndead, 73 | Heal, 74 | DispelEvil, 75 | GlyphOfWarding, 76 | HolyWord, 77 | }; 78 | 79 | // Recite a prayers. 80 | static void playerRecitePrayer(int prayer_type) { 81 | int dir; 82 | 83 | switch ((PriestSpellTypes) (prayer_type + 1)) { 84 | case PriestSpellTypes::DetectEvil: 85 | (void) spellDetectEvil(); 86 | break; 87 | case PriestSpellTypes::CureLightWounds: 88 | (void) spellChangePlayerHitPoints(diceRoll(Dice_t{3, 3})); 89 | break; 90 | case PriestSpellTypes::Bless: 91 | playerBless(randomNumber(12) + 12); 92 | break; 93 | case PriestSpellTypes::RemoveFear: 94 | (void) playerRemoveFear(); 95 | break; 96 | case PriestSpellTypes::CallLight: 97 | (void) spellLightArea(py.pos); 98 | break; 99 | case PriestSpellTypes::FindTraps: 100 | (void) spellDetectTrapsWithinVicinity(); 101 | break; 102 | case PriestSpellTypes::DetectDoorsStairs: 103 | (void) spellDetectSecretDoorssWithinVicinity(); 104 | break; 105 | case PriestSpellTypes::SlowPoison: 106 | (void) spellSlowPoison(); 107 | break; 108 | case PriestSpellTypes::BlindCreature: 109 | if (getDirectionWithMemory(CNIL, dir)) { 110 | (void) spellConfuseMonster(py.pos, dir); 111 | } 112 | break; 113 | case PriestSpellTypes::Portal: 114 | playerTeleport((py.misc.level * 3)); 115 | break; 116 | case PriestSpellTypes::CureMediumWounds: 117 | (void) spellChangePlayerHitPoints(diceRoll(Dice_t{4, 4})); 118 | break; 119 | case PriestSpellTypes::Chant: 120 | playerBless(randomNumber(24) + 24); 121 | break; 122 | case PriestSpellTypes::Sanctuary: 123 | (void) monsterSleep(py.pos); 124 | break; 125 | case PriestSpellTypes::CreateFood: 126 | spellCreateFood(); 127 | break; 128 | case PriestSpellTypes::RemoveCurse: 129 | for (auto &entry : py.inventory) { 130 | // only clear flag for items that are wielded or worn 131 | if (entry.category_id >= TV_MIN_WEAR && entry.category_id <= TV_MAX_WEAR) { 132 | inventoryItemRemoveCurse(entry); 133 | } 134 | } 135 | break; 136 | case PriestSpellTypes::ResistHeadCold: 137 | py.flags.heat_resistance += randomNumber(10) + 10; 138 | py.flags.cold_resistance += randomNumber(10) + 10; 139 | break; 140 | case PriestSpellTypes::NeutralizePoison: 141 | (void) playerCurePoison(); 142 | break; 143 | case PriestSpellTypes::OrbOfDraining: 144 | if (getDirectionWithMemory(CNIL, dir)) { 145 | spellFireBall(py.pos, dir, (diceRoll(Dice_t{3, 6}) + py.misc.level), MagicSpellFlags::HolyOrb, "Black Sphere"); 146 | } 147 | break; 148 | case PriestSpellTypes::CureSeriousWounds: 149 | (void) spellChangePlayerHitPoints(diceRoll(Dice_t{8, 4})); 150 | break; 151 | case PriestSpellTypes::SenseInvisible: 152 | playerDetectInvisible(randomNumber(24) + 24); 153 | break; 154 | case PriestSpellTypes::ProtectFromEvil: 155 | (void) playerProtectEvil(); 156 | break; 157 | case PriestSpellTypes::Earthquake: 158 | spellEarthquake(); 159 | break; 160 | case PriestSpellTypes::SenseSurroundings: 161 | spellMapCurrentArea(); 162 | break; 163 | case PriestSpellTypes::CureCriticalWounds: 164 | (void) spellChangePlayerHitPoints(diceRoll(Dice_t{16, 4})); 165 | break; 166 | case PriestSpellTypes::TurnUndead: 167 | (void) spellTurnUndead(); 168 | break; 169 | case PriestSpellTypes::Prayer: 170 | playerBless(randomNumber(48) + 48); 171 | break; 172 | case PriestSpellTypes::DispelUndead: 173 | (void) spellDispelCreature(config::monsters::defense::CD_UNDEAD, (3 * py.misc.level)); 174 | break; 175 | case PriestSpellTypes::Heal: 176 | (void) spellChangePlayerHitPoints(200); 177 | break; 178 | case PriestSpellTypes::DispelEvil: 179 | (void) spellDispelCreature(config::monsters::defense::CD_EVIL, (3 * py.misc.level)); 180 | break; 181 | case PriestSpellTypes::GlyphOfWarding: 182 | spellWardingGlyph(); 183 | break; 184 | case PriestSpellTypes::HolyWord: 185 | (void) playerRemoveFear(); 186 | (void) playerCurePoison(); 187 | (void) spellChangePlayerHitPoints(1000); 188 | 189 | for (int i = PlayerAttr::A_STR; i <= PlayerAttr::A_CHR; i++) { 190 | (void) playerStatRestore(i); 191 | } 192 | 193 | (void) spellDispelCreature(config::monsters::defense::CD_EVIL, (4 * py.misc.level)); 194 | (void) spellTurnUndead(); 195 | 196 | if (py.flags.invulnerability < 3) { 197 | py.flags.invulnerability = 3; 198 | } else { 199 | py.flags.invulnerability++; 200 | } 201 | break; 202 | default: 203 | // All cases are handled, so this should never be reached! 204 | break; 205 | } 206 | } 207 | 208 | // Pray like HELL. -RAK- 209 | void pray() { 210 | game.player_free_turn = true; 211 | 212 | int item_pos_begin, item_pos_end; 213 | if (!playerCanPray(item_pos_begin, item_pos_end)) { 214 | return; 215 | } 216 | 217 | int item_id; 218 | if (!inventoryGetInputForItemId(item_id, "Use which Holy Book?", item_pos_begin, item_pos_end, CNIL, CNIL)) { 219 | return; 220 | } 221 | 222 | int choice, chance; 223 | int result = castSpellGetId("Recite which prayer?", item_id, choice, chance); 224 | if (result < 0) { 225 | printMessage("You don't know any prayers in that book."); 226 | return; 227 | } 228 | if (result == 0) { 229 | return; 230 | } 231 | 232 | Spell_t const &spell = magic_spells[py.misc.class_id - 1][choice]; 233 | 234 | // NOTE: at least one function called by `playerRecitePrayer()` sets `player_free_turn = true`, 235 | // e.g. `spellCreateFood()`, so this check is required. -MRC- 236 | game.player_free_turn = false; 237 | 238 | if (randomNumber(100) < chance) { 239 | printMessage("You lost your concentration!"); 240 | } else { 241 | playerRecitePrayer(choice); 242 | 243 | if (!game.player_free_turn) { 244 | if ((py.flags.spells_worked & (1L << choice)) == 0) { 245 | py.misc.exp += spell.exp_gain_for_learning << 2; 246 | displayCharacterExperience(); 247 | py.flags.spells_worked |= (1L << choice); 248 | } 249 | } 250 | } 251 | 252 | if (game.player_free_turn) { 253 | return; 254 | } 255 | 256 | if (spell.mana_required > py.misc.current_mana) { 257 | printMessage("You faint from fatigue!"); 258 | 259 | py.flags.paralysis = (int16_t) randomNumber((5 * (spell.mana_required - py.misc.current_mana))); 260 | py.misc.current_mana = 0; 261 | py.misc.current_mana_fraction = 0; 262 | 263 | if (randomNumber(3) == 1) { 264 | printMessage("You have damaged your health!"); 265 | (void) playerStatRandomDecrease(PlayerAttr::A_CON); 266 | } 267 | } else { 268 | py.misc.current_mana -= spell.mana_required; 269 | } 270 | 271 | printCharacterCurrentMana(); 272 | } 273 | -------------------------------------------------------------------------------- /src/player_traps.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Player functions related to traps 7 | 8 | #include "headers.h" 9 | 10 | static int playerTrapDisarmAbility() { 11 | int ability = py.misc.disarm; 12 | ability += 2; 13 | ability *= playerDisarmAdjustment(); 14 | ability += playerStatAdjustmentWisdomIntelligence(PlayerAttr::A_INT); 15 | ability += class_level_adj[py.misc.class_id][PlayerClassLevelAdj::DISARM] * py.misc.level / 3; 16 | 17 | if (py.flags.blind > 0 || playerNoLight()) { 18 | ability = ability / 10; 19 | } 20 | 21 | if (py.flags.confused > 0) { 22 | ability = ability / 10; 23 | } 24 | 25 | if (py.flags.image > 0) { 26 | ability = ability / 10; 27 | } 28 | 29 | return ability; 30 | } 31 | 32 | static void playerDisarmFloorTrap(Coord_t coord, int total, int level, int dir, int16_t misc_use) { 33 | int confused = py.flags.confused; 34 | 35 | if (total + 100 - level > randomNumber(100)) { 36 | printMessage("You have disarmed the trap."); 37 | py.misc.exp += misc_use; 38 | (void) dungeonDeleteObject(coord); 39 | 40 | // make sure we move onto the trap even if confused 41 | py.flags.confused = 0; 42 | playerMove(dir, false); 43 | py.flags.confused = (int16_t) confused; 44 | 45 | displayCharacterExperience(); 46 | return; 47 | } 48 | 49 | // avoid randomNumber(0) call 50 | if (total > 5 && randomNumber(total) > 5) { 51 | printMessageNoCommandInterrupt("You failed to disarm the trap."); 52 | return; 53 | } 54 | 55 | printMessage("You set the trap off!"); 56 | 57 | // make sure we move onto the trap even if confused 58 | py.flags.confused = 0; 59 | playerMove(dir, false); 60 | py.flags.confused += confused; 61 | } 62 | 63 | static void playerDisarmChestTrap(Coord_t coord, int total, Inventory_t &item) { 64 | if (!spellItemIdentified(item)) { 65 | game.player_free_turn = true; 66 | printMessage("I don't see a trap."); 67 | 68 | return; 69 | } 70 | 71 | if ((item.flags & config::treasure::chests::CH_TRAPPED) != 0u) { 72 | int level = item.depth_first_found; 73 | 74 | if ((total - level) > randomNumber(100)) { 75 | item.flags &= ~config::treasure::chests::CH_TRAPPED; 76 | 77 | if ((item.flags & config::treasure::chests::CH_LOCKED) != 0u) { 78 | item.special_name_id = SpecialNameIds::SN_LOCKED; 79 | } else { 80 | item.special_name_id = SpecialNameIds::SN_DISARMED; 81 | } 82 | 83 | printMessage("You have disarmed the chest."); 84 | 85 | spellItemIdentifyAndRemoveRandomInscription(item); 86 | py.misc.exp += level; 87 | 88 | displayCharacterExperience(); 89 | } else if ((total > 5) && (randomNumber(total) > 5)) { 90 | printMessageNoCommandInterrupt("You failed to disarm the chest."); 91 | } else { 92 | printMessage("You set a trap off!"); 93 | spellItemIdentifyAndRemoveRandomInscription(item); 94 | chestTrap(coord); 95 | } 96 | return; 97 | } 98 | 99 | printMessage("The chest was not trapped."); 100 | game.player_free_turn = true; 101 | } 102 | 103 | // Disarms a trap -RAK- 104 | void playerDisarmTrap() { 105 | int dir; 106 | if (!getDirectionWithMemory(CNIL, dir)) { 107 | return; 108 | } 109 | 110 | Coord_t coord = py.pos; 111 | (void) playerMovePosition(dir, coord); 112 | 113 | Tile_t const &tile = dg.floor[coord.y][coord.x]; 114 | 115 | bool no_disarm = false; 116 | 117 | if (tile.creature_id > 1 && tile.treasure_id != 0 && 118 | (game.treasure.list[tile.treasure_id].category_id == TV_VIS_TRAP || game.treasure.list[tile.treasure_id].category_id == TV_CHEST)) { 119 | objectBlockedByMonster(tile.creature_id); 120 | } else if (tile.treasure_id != 0) { 121 | int disarm_ability = playerTrapDisarmAbility(); 122 | 123 | Inventory_t &item = game.treasure.list[tile.treasure_id]; 124 | 125 | if (item.category_id == TV_VIS_TRAP) { 126 | playerDisarmFloorTrap(coord, disarm_ability, item.depth_first_found, dir, item.misc_use); 127 | } else if (item.category_id == TV_CHEST) { 128 | playerDisarmChestTrap(coord, disarm_ability, item); 129 | } else { 130 | no_disarm = true; 131 | } 132 | } else { 133 | no_disarm = true; 134 | } 135 | 136 | if (no_disarm) { 137 | printMessage("I do not see anything to disarm there."); 138 | game.player_free_turn = true; 139 | } 140 | } 141 | 142 | static void chestLooseStrength() { 143 | printMessage("A small needle has pricked you!"); 144 | 145 | if (py.flags.sustain_str) { 146 | printMessage("You are unaffected."); 147 | return; 148 | } 149 | 150 | (void) playerStatRandomDecrease(PlayerAttr::A_STR); 151 | 152 | playerTakesHit(diceRoll(Dice_t{1, 4}), "a poison needle"); 153 | 154 | printMessage("You feel weakened!"); 155 | } 156 | 157 | static void chestPoison() { 158 | printMessage("A small needle has pricked you!"); 159 | 160 | playerTakesHit(diceRoll(Dice_t{1, 6}), "a poison needle"); 161 | 162 | py.flags.poisoned += 10 + randomNumber(20); 163 | } 164 | 165 | static void chestParalysed() { 166 | printMessage("A puff of yellow gas surrounds you!"); 167 | 168 | if (py.flags.free_action) { 169 | printMessage("You are unaffected."); 170 | return; 171 | } 172 | 173 | printMessage("You choke and pass out."); 174 | py.flags.paralysis = (int16_t) (10 + randomNumber(20)); 175 | } 176 | 177 | static void chestSummonMonster(Coord_t coord) { 178 | Coord_t position = Coord_t{0, 0}; 179 | 180 | for (int i = 0; i < 3; i++) { 181 | position.y = coord.y; 182 | position.x = coord.x; 183 | (void) monsterSummon(position, false); 184 | } 185 | } 186 | 187 | static void chestExplode(Coord_t coord) { 188 | printMessage("There is a sudden explosion!"); 189 | 190 | (void) dungeonDeleteObject(coord); 191 | 192 | playerTakesHit(diceRoll(Dice_t{5, 8}), "an exploding chest"); 193 | } 194 | 195 | // Chests have traps too. -RAK- 196 | // Note: Chest traps are based on the FLAGS value 197 | void chestTrap(Coord_t coord) { 198 | uint32_t flags = game.treasure.list[dg.floor[coord.y][coord.x].treasure_id].flags; 199 | 200 | if ((flags & config::treasure::chests::CH_LOSE_STR) != 0u) { 201 | chestLooseStrength(); 202 | } 203 | 204 | if ((flags & config::treasure::chests::CH_POISON) != 0u) { 205 | chestPoison(); 206 | } 207 | 208 | if ((flags & config::treasure::chests::CH_PARALYSED) != 0u) { 209 | chestParalysed(); 210 | } 211 | 212 | if ((flags & config::treasure::chests::CH_SUMMON) != 0u) { 213 | chestSummonMonster(coord); 214 | } 215 | 216 | if ((flags & config::treasure::chests::CH_EXPLODE) != 0u) { 217 | chestExplode(coord); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/player_tunnel.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Player throw functions 7 | 8 | #include "headers.h" 9 | 10 | // Don't let the player tunnel somewhere illegal, this is necessary to 11 | // prevent the player from getting a free attack by trying to tunnel 12 | // somewhere where it has no effect. 13 | static bool playerCanTunnel(int treasure_id, int tile_id) { 14 | if (tile_id < MIN_CAVE_WALL && 15 | (treasure_id == 0 || (game.treasure.list[treasure_id].category_id != TV_RUBBLE && game.treasure.list[treasure_id].category_id != TV_SECRET_DOOR))) { 16 | game.player_free_turn = true; 17 | 18 | if (treasure_id == 0) { 19 | printMessage("Tunnel through what? Empty air?!?"); 20 | } else { 21 | printMessage("You can't tunnel through that."); 22 | } 23 | 24 | return false; 25 | } 26 | 27 | return true; 28 | } 29 | 30 | // Compute the digging ability of player; based on strength, and type of tool used 31 | static int playerDiggingAbility(Inventory_t const &weapon) { 32 | int digging_ability = py.stats.used[PlayerAttr::A_STR]; 33 | 34 | if ((weapon.flags & config::treasure::flags::TR_TUNNEL) != 0u) { 35 | digging_ability += 25 + weapon.misc_use * 50; 36 | } else { 37 | digging_ability += maxDiceRoll(weapon.damage) + weapon.to_hit + weapon.to_damage; 38 | 39 | // divide by two so that digging without shovel isn't too easy 40 | digging_ability >>= 1; 41 | } 42 | 43 | // If this weapon is too heavy for the player to wield properly, 44 | // then also make it harder to dig with it. 45 | if (py.weapon_is_heavy) { 46 | digging_ability += (py.stats.used[PlayerAttr::A_STR] * 15) - weapon.weight; 47 | 48 | if (digging_ability < 0) { 49 | digging_ability = 0; 50 | } 51 | } 52 | 53 | return digging_ability; 54 | } 55 | 56 | static void dungeonDigGraniteWall(Coord_t coord, int digging_ability) { 57 | int i = randomNumber(1200) + 80; 58 | 59 | if (playerTunnelWall(coord, digging_ability, i)) { 60 | printMessage("You have finished the tunnel."); 61 | } else { 62 | printMessageNoCommandInterrupt("You tunnel into the granite wall."); 63 | } 64 | } 65 | 66 | static void dungeonDigMagmaWall(Coord_t coord, int digging_ability) { 67 | int i = randomNumber(600) + 10; 68 | 69 | if (playerTunnelWall(coord, digging_ability, i)) { 70 | printMessage("You have finished the tunnel."); 71 | } else { 72 | printMessageNoCommandInterrupt("You tunnel into the magma intrusion."); 73 | } 74 | } 75 | 76 | static void dungeonDigQuartzWall(Coord_t coord, int digging_ability) { 77 | int i = randomNumber(400) + 10; 78 | 79 | if (playerTunnelWall(coord, digging_ability, i)) { 80 | printMessage("You have finished the tunnel."); 81 | } else { 82 | printMessageNoCommandInterrupt("You tunnel into the quartz vein."); 83 | } 84 | } 85 | 86 | static void dungeonDigRubble(Coord_t coord, int digging_ability) { 87 | if (digging_ability > randomNumber(180)) { 88 | (void) dungeonDeleteObject(coord); 89 | printMessage("You have removed the rubble."); 90 | 91 | if (randomNumber(10) == 1) { 92 | dungeonPlaceRandomObjectAt(coord, false); 93 | 94 | if (caveTileVisible(coord)) { 95 | printMessage("You have found something!"); 96 | } 97 | } 98 | 99 | dungeonLiteSpot(coord); 100 | } else { 101 | printMessageNoCommandInterrupt("You dig in the rubble."); 102 | } 103 | } 104 | 105 | // Dig regular walls; Granite, magma intrusion, quartz vein 106 | // Don't forget the boundary walls, made of titanium (255) 107 | // Return `true` if a wall was dug at 108 | static bool dungeonDigAtLocation(Coord_t coord, uint8_t wall_type, int digging_ability) { 109 | switch (wall_type) { 110 | case TILE_GRANITE_WALL: 111 | dungeonDigGraniteWall(coord, digging_ability); 112 | break; 113 | case TILE_MAGMA_WALL: 114 | dungeonDigMagmaWall(coord, digging_ability); 115 | break; 116 | case TILE_QUARTZ_WALL: 117 | dungeonDigQuartzWall(coord, digging_ability); 118 | break; 119 | case TILE_BOUNDARY_WALL: 120 | printMessage("This seems to be permanent rock."); 121 | break; 122 | default: 123 | return false; 124 | } 125 | return true; 126 | } 127 | 128 | // Tunnels through rubble and walls -RAK- 129 | // Must take into account: secret doors, special tools 130 | void playerTunnel(int direction) { 131 | // Confused? 75% random movement 132 | if (py.flags.confused > 0 && randomNumber(4) > 1) { 133 | direction = randomNumber(9); 134 | } 135 | 136 | Coord_t coord = py.pos; 137 | (void) playerMovePosition(direction, coord); 138 | 139 | Tile_t const &tile = dg.floor[coord.y][coord.x]; 140 | Inventory_t &item = py.inventory[PlayerEquipment::Wield]; 141 | 142 | if (!playerCanTunnel(tile.treasure_id, tile.feature_id)) { 143 | return; 144 | } 145 | 146 | if (tile.creature_id > 1) { 147 | objectBlockedByMonster(tile.creature_id); 148 | playerAttackPosition(coord); 149 | return; 150 | } 151 | 152 | if (item.category_id != TV_NOTHING) { 153 | int digging_ability = playerDiggingAbility(item); 154 | 155 | if (!dungeonDigAtLocation(coord, tile.feature_id, digging_ability)) { 156 | // Is there an object in the way? (Rubble and secret doors) 157 | if (tile.treasure_id != 0) { 158 | if (game.treasure.list[tile.treasure_id].category_id == TV_RUBBLE) { 159 | dungeonDigRubble(coord, digging_ability); 160 | } else if (game.treasure.list[tile.treasure_id].category_id == TV_SECRET_DOOR) { 161 | // Found secret door! 162 | printMessageNoCommandInterrupt("You tunnel into the granite wall."); 163 | playerSearch(py.pos, py.misc.chance_in_search); 164 | } else { 165 | abort(); 166 | } 167 | } else { 168 | abort(); 169 | } 170 | } 171 | 172 | return; 173 | } 174 | 175 | printMessage("You dig with your hands, making no progress."); 176 | } 177 | -------------------------------------------------------------------------------- /src/recall.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Recall_t holds the player's known knowledge for any given monster, aka memories 9 | typedef struct { 10 | uint32_t movement; 11 | uint32_t spells; 12 | uint16_t kills; 13 | uint16_t deaths; 14 | uint16_t defenses; 15 | uint8_t wake; 16 | uint8_t ignore; 17 | uint8_t attacks[MON_MAX_ATTACKS]; 18 | } Recall_t; 19 | 20 | extern Recall_t creature_recall[MON_MAX_CREATURES]; // Monster memories. -CJS- 21 | extern const char *recall_description_attack_type[25]; 22 | extern const char *recall_description_attack_method[20]; 23 | extern const char *recall_description_how_much[8]; 24 | extern const char *recall_description_move[6]; 25 | extern const char *recall_description_spell[15]; 26 | extern const char *recall_description_breath[5]; 27 | extern const char *recall_description_weakness[6]; 28 | 29 | int memoryRecall(int monster_id); 30 | void recallMonsterAttributes(char command); 31 | -------------------------------------------------------------------------------- /src/rng.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Random number generator 7 | 8 | #include "headers.h" 9 | 10 | // Define this to compile as a standalone test 11 | // #define TEST_RNG 12 | 13 | // This alg uses a prime modulus multiplicative congruential generator 14 | // 15 | // (PMMLCG), also known as a Lehmer Grammer, which satisfies the following 16 | // properties 17 | // 18 | // (i) modulus: m - a large prime integer 19 | // (ii) multiplier: a - an integer in the range 2, 3, ..., m - 1 20 | // (iii) z[n+1] = f(z[n]), for n = 1, 2, ... 21 | // (iv) f(z) = az mod m 22 | // (v) u[n] = z[n] / m, for n = 1, 2, ... 23 | // 24 | // The sequence of z's must be initialized by choosing an initial seed z[1] 25 | // from the range 1, 2, ..., m - 1. The sequence of z's is a pseudo-random 26 | // sequence drawn without replacement from the set 1, 2, ..., m - 1. 27 | // The u's form a pseudo-random sequence of real numbers between (but not 28 | // including) 0 and 1. 29 | // 30 | // Schrage's method is used to compute the sequence of z's. 31 | // Let m = aq + r, where q = m div a, and r = m mod a. 32 | // Then f(z) = az mod m = az - m * (az div m) = 33 | // = gamma(z) + m * delta(z) 34 | // Where gamma(z) = a(z mod q) - r(z div q) 35 | // and delta(z) = (z div q) - (az div m) 36 | // 37 | // If r < q, then for all z in 1, 2, ..., m - 1: 38 | // (1) delta(z) is either 0 or 1 39 | // (2) both a(z mod q) and r(z div q) are in 0, 1, ..., m - 1 40 | // (3) absolute value of gamma(z) <= m - 1 41 | // (4) delta(z) = 1 iff gamma(z) < 0 42 | // 43 | // Hence each value of z can be computed exactly without overflow as long 44 | // as m can be represented as an integer. 45 | 46 | // a good random number generator, correct on any machine with 32 bit 47 | // integers, this algorithm is from: 48 | // 49 | // Stephen K. Park and Keith W. Miller, "Random Number Generators: 50 | // Good ones are hard to find", Communications of the ACM, October 1988, 51 | // vol 31, number 10, pp. 1192-1201. 52 | // 53 | // If this algorithm is implemented correctly, 54 | // then if z[1] = 1, 55 | // then z[10001] will equal 1043618065 56 | // 57 | // Has a full period of 2^31 - 1. 58 | // Returns integers in the range 1 to 2^31-1. 59 | 60 | constexpr int32_t RNG_M = INT_MAX; // m = 2^31 - 1 61 | constexpr int32_t RNG_A = 16807L; 62 | constexpr int32_t RNG_Q = RNG_M / RNG_A; // m div a 127773L 63 | constexpr int32_t RNG_R = RNG_M % RNG_A; // m mod a 2836L 64 | 65 | // 32 bit seed 66 | static uint32_t rnd_seed; 67 | 68 | uint32_t getRandomSeed() { 69 | return rnd_seed; 70 | } 71 | 72 | void setRandomSeed(uint32_t seed) { 73 | // set seed to value between 1 and m-1 74 | rnd_seed = (uint32_t) ((seed % (RNG_M - 1)) + 1); 75 | } 76 | 77 | // returns a pseudo-random number from set 1, 2, ..., RNG_M - 1 78 | int32_t rnd() { 79 | auto high = (int32_t) (rnd_seed / RNG_Q); 80 | auto low = (int32_t) (rnd_seed % RNG_Q); 81 | auto test = (int32_t) (RNG_A * low - RNG_R * high); 82 | 83 | if (test > 0) { 84 | rnd_seed = (uint32_t) test; 85 | } else { 86 | rnd_seed = (uint32_t) (test + RNG_M); 87 | } 88 | return rnd_seed; 89 | } 90 | 91 | #ifdef TEST_RNG 92 | 93 | main() { 94 | setRandomSeed(0L); 95 | 96 | for (int32_t i = 1; i < 10000; i++) { 97 | (void) rnd(); 98 | } 99 | 100 | int32_t random = rnd(); 101 | 102 | printf("z[10001] = %ld, should be 1043618065\n", random); 103 | 104 | if (random == 1043618065L) { 105 | printf("success!!!\n"); 106 | } 107 | } 108 | 109 | #endif 110 | -------------------------------------------------------------------------------- /src/rng.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // rng.cpp 9 | uint32_t getRandomSeed(); 10 | void setRandomSeed(uint32_t seed); 11 | int32_t rnd(); 12 | -------------------------------------------------------------------------------- /src/scores.cpp: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Handle reading, writing, and displaying of high scores. 7 | 8 | #include "headers.h" 9 | #include "version.h" 10 | 11 | // High score file pointer 12 | FILE *highscore_fp; 13 | 14 | static uint8_t highScoreGenderLabel() { 15 | if (playerIsMale()) { 16 | return 'M'; 17 | } 18 | return 'F'; 19 | } 20 | 21 | // Enters a players name on the top twenty list -JWT- 22 | void recordNewHighScore() { 23 | clearScreen(); 24 | 25 | if (game.noscore != 0) { 26 | return; 27 | } 28 | 29 | if (panic_save) { 30 | printMessage("Sorry, scores for games restored from panic save files are not saved."); 31 | return; 32 | } 33 | 34 | HighScore_t new_entry{}; 35 | new_entry.points = playerCalculateTotalPoints(); 36 | new_entry.birth_date = py.misc.date_of_birth; 37 | new_entry.uid = 0; // NOTE: do we not want to use `getuid()`? -MRC- 38 | new_entry.mhp = py.misc.max_hp; 39 | new_entry.chp = py.misc.current_hp; 40 | new_entry.dungeon_depth = (uint8_t) dg.current_level; 41 | new_entry.level = (uint8_t) py.misc.level; 42 | new_entry.deepest_dungeon_depth = (uint8_t) py.misc.max_dungeon_depth; 43 | new_entry.gender = highScoreGenderLabel(); 44 | new_entry.race = py.misc.race_id; 45 | new_entry.character_class = py.misc.class_id; 46 | (void) strcpy(new_entry.name, py.misc.name); 47 | 48 | char *tmp = game.character_died_from; 49 | if ('a' == *tmp) { 50 | if ('n' == *(++tmp)) { 51 | tmp++; 52 | } 53 | while (isspace(*tmp) != 0) { 54 | tmp++; 55 | } 56 | } 57 | (void) strcpy(new_entry.died_from, tmp); 58 | 59 | if ((highscore_fp = fopen(config::files::scores.c_str(), "rb+")) == nullptr) { 60 | printMessage(("Error opening score file '" + config::files::scores + "'.").c_str()); 61 | printMessage(CNIL); 62 | return; 63 | } 64 | 65 | // Search file to find where to insert this character, if uid != 0 and 66 | // find same uid/gender/race/class combo then exit without saving this score. 67 | // Seek to the beginning of the file just to be safe. 68 | (void) fseek(highscore_fp, (long) 0, SEEK_SET); 69 | 70 | // Read version numbers from the score file, and check for validity. 71 | auto version_maj = (uint8_t) getc(highscore_fp); 72 | auto version_min = (uint8_t) getc(highscore_fp); 73 | auto patch_level = (uint8_t) getc(highscore_fp); 74 | 75 | // If this is a new score file, it should be empty. 76 | // Write the current version numbers to the score file. 77 | if (feof(highscore_fp) != 0) { 78 | // Seek to the beginning of the file just to be safe. 79 | (void) fseek(highscore_fp, (long) 0, SEEK_SET); 80 | 81 | (void) putc(CURRENT_VERSION_MAJOR, highscore_fp); 82 | (void) putc(CURRENT_VERSION_MINOR, highscore_fp); 83 | (void) putc(CURRENT_VERSION_PATCH, highscore_fp); 84 | 85 | // must fseek() before can change read/write mode 86 | (void) fseek(highscore_fp, (long) 0, SEEK_CUR); 87 | } else if (!validGameVersion(version_maj, version_min, patch_level)) { 88 | // No need to print a message, a subsequent call to 89 | // showScoresScreen() will print a message. 90 | (void) fclose(highscore_fp); 91 | return; 92 | } 93 | 94 | // set the static fileptr in save.c to the high score file pointer 95 | setFileptr(highscore_fp); 96 | 97 | HighScore_t old_entry{}; 98 | HighScore_t entry{}; 99 | 100 | int i = 0; 101 | off_t curpos = ftell(highscore_fp); 102 | readHighScore(old_entry); 103 | 104 | while (feof(highscore_fp) == 0) { 105 | if (new_entry.points >= old_entry.points) { 106 | break; 107 | } 108 | 109 | // under unix, only allow one gender/race/class combo per person, 110 | // on single user system, allow any number of entries, but try to 111 | // prevent multiple entries per character by checking for case when 112 | // birthdate/gender/race/class are the same, and game.character_died_from 113 | // of score file entry is "(saved)" 114 | if (((new_entry.uid != 0 && new_entry.uid == old_entry.uid) || 115 | (new_entry.uid == 0 && (strcmp(old_entry.died_from, "(saved)") == 0) && new_entry.birth_date == old_entry.birth_date)) && 116 | new_entry.gender == old_entry.gender && new_entry.race == old_entry.race && new_entry.character_class == old_entry.character_class) { 117 | (void) fclose(highscore_fp); 118 | return; 119 | } 120 | 121 | // only allow one thousand scores in the score file 122 | i++; 123 | if (i >= MAX_HIGH_SCORE_ENTRIES) { 124 | (void) fclose(highscore_fp); 125 | return; 126 | } 127 | 128 | curpos = ftell(highscore_fp); 129 | readHighScore(old_entry); 130 | } 131 | 132 | if (feof(highscore_fp) != 0) { 133 | // write out new_entry at end of file 134 | (void) fseek(highscore_fp, curpos, SEEK_SET); 135 | 136 | saveHighScore(new_entry); 137 | } else { 138 | entry = new_entry; 139 | 140 | while (feof(highscore_fp) == 0) { 141 | (void) fseek(highscore_fp, -(long) sizeof(HighScore_t) - (long) sizeof(char), SEEK_CUR); 142 | 143 | saveHighScore(entry); 144 | 145 | // under unix, only allow one gender/race/class combo per person, 146 | // on single user system, allow any number of entries, but try to 147 | // prevent multiple entries per character by checking for case when 148 | // birth_date/gender/race/class are the same, and game.character_died_from 149 | // of score file entry is "(saved)" 150 | if (((new_entry.uid != 0 && new_entry.uid == old_entry.uid) || 151 | (new_entry.uid == 0 && (strcmp(old_entry.died_from, "(saved)") == 0) && new_entry.birth_date == old_entry.birth_date)) && 152 | new_entry.gender == old_entry.gender && new_entry.race == old_entry.race && new_entry.character_class == old_entry.character_class) { 153 | break; 154 | } 155 | entry = old_entry; 156 | 157 | // must fseek() before can change read/write mode 158 | (void) fseek(highscore_fp, (long) 0, SEEK_CUR); 159 | 160 | curpos = ftell(highscore_fp); 161 | readHighScore(old_entry); 162 | } 163 | if (feof(highscore_fp) != 0) { 164 | (void) fseek(highscore_fp, curpos, SEEK_SET); 165 | 166 | saveHighScore(entry); 167 | } 168 | } 169 | 170 | (void) fclose(highscore_fp); 171 | } 172 | 173 | void showScoresScreen() { 174 | if ((highscore_fp = fopen(config::files::scores.c_str(), "rb")) == nullptr) { 175 | printMessage(("Error opening score file '" + config::files::scores + "'.").c_str()); 176 | printMessage(CNIL); 177 | return; 178 | } 179 | 180 | (void) fseek(highscore_fp, (off_t) 0, SEEK_SET); 181 | 182 | // Read version numbers from the score file, and check for validity. 183 | auto version_maj = (uint8_t) getc(highscore_fp); 184 | auto version_min = (uint8_t) getc(highscore_fp); 185 | auto patch_level = (uint8_t) getc(highscore_fp); 186 | 187 | // If score data present, check if a valid game version 188 | if (feof(highscore_fp) == 0 && !validGameVersion(version_maj, version_min, patch_level)) { 189 | printMessage("Sorry. This score file is from a different version of umoria."); 190 | printMessage(CNIL); 191 | (void) fclose(highscore_fp); 192 | return; 193 | } 194 | 195 | // set the static fileptr in save.c to the high score file pointer 196 | setFileptr(highscore_fp); 197 | 198 | HighScore_t score{}; 199 | readHighScore(score); 200 | 201 | char msg[100]; 202 | 203 | int i = 0; 204 | int rank = 1; 205 | 206 | while (feof(highscore_fp) == 0) { 207 | i = 1; 208 | clearScreen(); 209 | // Put twenty scores on each page, on lines 2 through 21. 210 | while ((feof(highscore_fp) == 0) && i < 21) { 211 | (void) snprintf(msg, 100, // 212 | "%-4d%8d %-19.19s %c %-10.10s %-7.7s%3d %-22.22s", // 213 | rank, // 214 | score.points, // 215 | score.name, // 216 | score.gender, // 217 | character_races[score.race].name, // 218 | classes[score.character_class].title, // 219 | score.level, // 220 | score.died_from // 221 | ); 222 | i++; 223 | putStringClearToEOL(msg, Coord_t{i, 0}); 224 | rank++; 225 | readHighScore(score); 226 | } 227 | putStringClearToEOL("Rank Points Name Sex Race Class Lvl Killed By", Coord_t{0, 0}); 228 | eraseLine(Coord_t{1, 0}); 229 | putStringClearToEOL("[ press any key to continue ]", Coord_t{23, 23}); 230 | if (getKeyInput() == ESCAPE) { 231 | break; 232 | } 233 | } 234 | 235 | (void) fclose(highscore_fp); 236 | } 237 | 238 | // Calculates the total number of points earned -JWT- 239 | int32_t playerCalculateTotalPoints() { 240 | int32_t total = py.misc.max_exp + (100 * py.misc.max_dungeon_depth); 241 | total += py.misc.au / 100; 242 | 243 | for (auto &item : py.inventory) { 244 | total += storeItemValue(item); 245 | } 246 | 247 | total += dg.current_level * 50; 248 | 249 | // Don't ever let the score decrease from one save to the next. 250 | if (py.max_score > total) { 251 | return py.max_score; 252 | } 253 | 254 | return total; 255 | } 256 | -------------------------------------------------------------------------------- /src/scores.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | #include 9 | 10 | // HighScore_t is a score object used for saving to the high score file 11 | // This structure is 64 bytes in size 12 | typedef struct { 13 | int32_t points; 14 | int32_t birth_date; 15 | int16_t uid; 16 | int16_t mhp; 17 | int16_t chp; 18 | uint8_t dungeon_depth; 19 | uint8_t level; 20 | uint8_t deepest_dungeon_depth; 21 | uint8_t gender; 22 | uint8_t race; 23 | uint8_t character_class; 24 | char name[PLAYER_NAME_SIZE]; 25 | char died_from[25]; 26 | } HighScore_t; 27 | 28 | // Number of entries allowed in the score file. 29 | constexpr uint16_t MAX_HIGH_SCORE_ENTRIES = 1000; 30 | 31 | extern FILE *highscore_fp; 32 | 33 | // TODO: these are implemented in `game_save.cpp` so need moving. 34 | void saveHighScore(HighScore_t const &score); 35 | void readHighScore(HighScore_t &score); 36 | 37 | void recordNewHighScore(); 38 | void showScoresScreen(); 39 | int32_t playerCalculateTotalPoints(); 40 | -------------------------------------------------------------------------------- /src/scrolls.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | void scrollRead(); 9 | -------------------------------------------------------------------------------- /src/spells.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // spell types used by get_flags(), breathe(), fire_bolt() and fire_ball() 9 | enum MagicSpellFlags { 10 | MagicMissile, 11 | Lightning, 12 | PoisonGas, 13 | Acid, 14 | Frost, 15 | Fire, 16 | HolyOrb, 17 | }; 18 | 19 | // Spell_t is a base data object. 20 | // Holds the base game data for a spell 21 | // Note: the names for the spells are stored in spell_names[] array at index i, +31 if priest 22 | typedef struct { 23 | uint8_t level_required; 24 | uint8_t mana_required; 25 | uint8_t failure_chance; 26 | uint8_t exp_gain_for_learning; // 1/4 of exp gained for learning spell 27 | } Spell_t; 28 | 29 | extern Spell_t magic_spells[PLAYER_MAX_CLASSES - 1][31]; 30 | extern const char *spell_names[62]; 31 | 32 | int castSpellGetId(const char *prompt, int item_id, int &spell_id, int &spell_chance); 33 | 34 | bool spellDetectTreasureWithinVicinity(); 35 | bool spellDetectObjectsWithinVicinity(); 36 | bool spellDetectTrapsWithinVicinity(); 37 | bool spellDetectSecretDoorssWithinVicinity(); 38 | bool spellDetectInvisibleCreaturesWithinVicinity(); 39 | bool spellLightArea(Coord_t coord); 40 | bool spellDarkenArea(Coord_t coord); 41 | void spellMapCurrentArea(); 42 | bool spellIdentifyItem(); 43 | bool spellAggravateMonsters(int affect_distance); 44 | bool spellSurroundPlayerWithTraps(); 45 | bool spellSurroundPlayerWithDoors(); 46 | bool spellDestroyAdjacentDoorsTraps(); 47 | bool spellDetectMonsters(); 48 | void spellLightLine(Coord_t coord, int direction); 49 | void spellStarlite(Coord_t coord); 50 | bool spellDisarmAllInDirection(Coord_t coord, int direction); 51 | void spellFireBolt(Coord_t coord, int direction, int damage_hp, int spell_type, const std::string &spell_name); 52 | void spellFireBall(Coord_t coord, int direction, int damage_hp, int spell_type, const std::string &spell_name); 53 | void spellBreath(Coord_t coord, int monster_id, int damage_hp, int spell_type, const std::string &spell_name); 54 | bool spellRechargeItem(int number_of_charges); 55 | bool spellChangeMonsterHitPoints(Coord_t coord, int direction, int damage_hp); 56 | bool spellDrainLifeFromMonster(Coord_t coord, int direction); 57 | bool spellSpeedMonster(Coord_t coord, int direction, int speed); 58 | bool spellConfuseMonster(Coord_t coord, int direction); 59 | bool spellSleepMonster(Coord_t coord, int direction); 60 | bool spellWallToMud(Coord_t coord, int direction); 61 | bool spellDestroyDoorsTrapsInDirection(Coord_t coord, int direction); 62 | bool spellPolymorphMonster(Coord_t coord, int direction); 63 | bool spellBuildWall(Coord_t coord, int direction); 64 | bool spellCloneMonster(Coord_t coord, int direction); 65 | void spellTeleportAwayMonster(int monster_id, int distance_from_player); 66 | void spellTeleportPlayerTo(Coord_t coord); 67 | bool spellTeleportAwayMonsterInDirection(Coord_t coord, int direction); 68 | bool spellMassGenocide(); 69 | bool spellGenocide(); 70 | bool spellSpeedAllMonsters(int speed); 71 | bool spellSleepAllMonsters(); 72 | bool spellMassPolymorph(); 73 | bool spellDetectEvil(); 74 | bool spellChangePlayerHitPoints(int adjustment); 75 | void spellEarthquake(); 76 | void spellCreateFood(); 77 | bool spellDispelCreature(int creature_defense, int damage); 78 | bool spellTurnUndead(); 79 | void spellWardingGlyph(); 80 | void spellLoseSTR(); 81 | void spellLoseINT(); 82 | void spellLoseWIS(); 83 | void spellLoseDEX(); 84 | void spellLoseCON(); 85 | void spellLoseCHR(); 86 | void spellLoseEXP(int32_t adjustment); 87 | bool spellSlowPoison(); 88 | void spellDestroyArea(Coord_t coord); 89 | bool spellEnchantItem(int16_t &plusses, int16_t max_bonus_limit); 90 | bool spellRemoveCurseFromAllWornItems(); 91 | bool spellRestorePlayerLevels(); 92 | -------------------------------------------------------------------------------- /src/staves.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | void staffUse(); 9 | void wandAim(); 10 | -------------------------------------------------------------------------------- /src/store.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | constexpr uint8_t MAX_OWNERS = 18; // Number of owners to choose from 9 | constexpr uint8_t MAX_STORES = 6; // Number of different stores 10 | constexpr uint8_t STORE_MAX_DISCRETE_ITEMS = 24; // Max number of discrete objects in inventory 11 | constexpr uint8_t STORE_MAX_ITEM_TYPES = 26; // Number of items to choose stock from 12 | constexpr uint8_t COST_ADJUSTMENT = 100; // Adjust prices for buying and selling 13 | 14 | // InventoryRecord_t data for a store inventory item 15 | typedef struct { 16 | int32_t cost; 17 | Inventory_t item; 18 | } InventoryRecord_t; 19 | 20 | // Store_t holds all the data for any given store in the game 21 | typedef struct { 22 | int32_t turns_left_before_closing; 23 | int16_t insults_counter; 24 | uint8_t owner_id; 25 | uint8_t unique_items_counter; 26 | uint16_t good_purchases; 27 | uint16_t bad_purchases; 28 | InventoryRecord_t inventory[STORE_MAX_DISCRETE_ITEMS]; 29 | } Store_t; 30 | 31 | // Owner_t holds data about a given store owner 32 | typedef struct { 33 | const char *name; 34 | int16_t max_cost; 35 | uint8_t max_inflate; 36 | uint8_t min_inflate; 37 | uint8_t haggles_per; 38 | uint8_t race; 39 | uint8_t max_insults; 40 | } Owner_t; 41 | 42 | extern uint8_t race_gold_adjustments[PLAYER_MAX_RACES][PLAYER_MAX_RACES]; 43 | 44 | extern Owner_t store_owners[MAX_OWNERS]; 45 | extern Store_t stores[MAX_STORES]; 46 | extern uint16_t store_choices[MAX_STORES][STORE_MAX_ITEM_TYPES]; 47 | extern bool (*store_buy[MAX_STORES])(uint8_t); 48 | extern const char *speech_sale_accepted[14]; 49 | extern const char *speech_selling_haggle_final[3]; 50 | extern const char *speech_selling_haggle[16]; 51 | extern const char *speech_buying_haggle_final[3]; 52 | extern const char *speech_buying_haggle[15]; 53 | extern const char *speech_insulted_haggling_done[5]; 54 | extern const char *speech_get_out_of_my_store[5]; 55 | extern const char *speech_haggling_try_again[10]; 56 | extern const char *speech_sorry[5]; 57 | 58 | // store 59 | void storeInitializeOwners(); 60 | void storeEnter(int store_id); 61 | 62 | // store_inventory 63 | void storeMaintenance(); 64 | int32_t storeItemValue(Inventory_t const &item); 65 | int32_t storeItemSellPrice(Store_t const &store, int32_t &min_price, int32_t &max_price, Inventory_t const &item); 66 | bool storeCheckPlayerItemsCount(Store_t const &store, Inventory_t const &item); 67 | void storeCarryItem(int store_id, int &index_id, Inventory_t &item); 68 | void storeDestroyItem(int store_id, int item_id, bool only_one_of); 69 | -------------------------------------------------------------------------------- /src/treasure.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // defines for treasure type values (tval) 9 | constexpr int8_t TV_NEVER = -1; // used by find_range() for non-search 10 | constexpr uint8_t TV_NOTHING = 0; 11 | constexpr uint8_t TV_MISC = 1; 12 | constexpr uint8_t TV_CHEST = 2; 13 | 14 | // min tval for wearable items, all items between TV_MIN_WEAR and 15 | // TV_MAX_WEAR use the same flag bits, see the TR_* defines. 16 | constexpr uint8_t TV_MIN_WEAR = 10; 17 | // items tested for enchantments, i.e. the MAGIK inscription, see the enchanted() procedure. 18 | constexpr uint8_t TV_MIN_ENCHANT = 10; 19 | constexpr uint8_t TV_SLING_AMMO = 10; 20 | constexpr uint8_t TV_BOLT = 11; 21 | constexpr uint8_t TV_ARROW = 12; 22 | constexpr uint8_t TV_SPIKE = 13; 23 | constexpr uint8_t TV_LIGHT = 15; 24 | constexpr uint8_t TV_BOW = 20; 25 | constexpr uint8_t TV_HAFTED = 21; 26 | constexpr uint8_t TV_POLEARM = 22; 27 | constexpr uint8_t TV_SWORD = 23; 28 | constexpr uint8_t TV_DIGGING = 25; 29 | constexpr uint8_t TV_BOOTS = 30; 30 | constexpr uint8_t TV_GLOVES = 31; 31 | constexpr uint8_t TV_CLOAK = 32; 32 | constexpr uint8_t TV_HELM = 33; 33 | constexpr uint8_t TV_SHIELD = 34; 34 | constexpr uint8_t TV_HARD_ARMOR = 35; 35 | constexpr uint8_t TV_SOFT_ARMOR = 36; 36 | // max tval that uses the TR_* flags 37 | constexpr uint8_t TV_MAX_ENCHANT = 39; 38 | constexpr uint8_t TV_AMULET = 40; 39 | constexpr uint8_t TV_RING = 45; 40 | constexpr uint8_t TV_MAX_WEAR = 50; // max tval for wearable items 41 | 42 | constexpr uint8_t TV_STAFF = 55; 43 | constexpr uint8_t TV_WAND = 65; 44 | constexpr uint8_t TV_SCROLL1 = 70; 45 | constexpr uint8_t TV_SCROLL2 = 71; 46 | constexpr uint8_t TV_POTION1 = 75; 47 | constexpr uint8_t TV_POTION2 = 76; 48 | constexpr uint8_t TV_FLASK = 77; 49 | constexpr uint8_t TV_FOOD = 80; 50 | constexpr uint8_t TV_MAGIC_BOOK = 90; 51 | constexpr uint8_t TV_PRAYER_BOOK = 91; 52 | constexpr uint8_t TV_MAX_OBJECT = 99; // objects with tval above this are never picked up by monsters 53 | constexpr uint8_t TV_GOLD = 100; 54 | constexpr uint8_t TV_MAX_PICK_UP = 100; // objects with higher tvals can not be picked up 55 | constexpr uint8_t TV_INVIS_TRAP = 101; 56 | 57 | // objects between TV_MIN_VISIBLE and TV_MAX_VISIBLE are always visible, 58 | // i.e. the cave fm flag is set when they are present 59 | constexpr uint8_t TV_MIN_VISIBLE = 102; 60 | constexpr uint8_t TV_VIS_TRAP = 102; 61 | constexpr uint8_t TV_RUBBLE = 103; 62 | // following objects are never deleted when trying to create another one during level generation 63 | constexpr uint8_t TV_MIN_DOORS = 104; 64 | constexpr uint8_t TV_OPEN_DOOR = 104; 65 | constexpr uint8_t TV_CLOSED_DOOR = 105; 66 | constexpr uint8_t TV_UP_STAIR = 107; 67 | constexpr uint8_t TV_DOWN_STAIR = 108; 68 | constexpr uint8_t TV_SECRET_DOOR = 109; 69 | constexpr uint8_t TV_STORE_DOOR = 110; 70 | constexpr uint8_t TV_MAX_VISIBLE = 110; 71 | 72 | extern int16_t missiles_counter; 73 | 74 | void magicTreasureMagicalAbility(int item_id, int level); 75 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // This used to be NULL, but that was technically incorrect. 7 | // CNIL is used instead of null to help avoid lint errors. 8 | constexpr char *CNIL = nullptr; 9 | 10 | // Many of the character fields used to be fixed length, which 11 | // greatly increased the size of the executable. Many fixed 12 | // length fields have been replaced with variable length ones. 13 | constexpr uint8_t MORIA_MESSAGE_SIZE = 80; 14 | typedef char vtype_t[MORIA_MESSAGE_SIZE]; 15 | 16 | // Note: since its output can easily exceed 80 characters, 17 | // an object description must always be called with an 18 | // obj_desc_t type as the first parameter. 19 | constexpr uint8_t MORIA_OBJ_DESC_SIZE = 160; 20 | typedef char obj_desc_t[MORIA_OBJ_DESC_SIZE]; 21 | 22 | typedef struct { 23 | int y; 24 | int x; 25 | } Coord_t; 26 | -------------------------------------------------------------------------------- /src/ui.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | // Panel_t holds data about a screen panel (the dungeon display) 9 | // Screen panels calculated from the dungeon/screen dimensions 10 | typedef struct { 11 | int row; 12 | int col; 13 | 14 | int top; 15 | int bottom; 16 | int left; 17 | int right; 18 | 19 | int col_prt; 20 | int row_prt; 21 | 22 | int16_t max_rows; 23 | int16_t max_cols; 24 | } Panel_t; 25 | 26 | // message line location 27 | constexpr uint8_t MSG_LINE = 0; 28 | 29 | // How many messages to save in the buffer -CJS- 30 | constexpr uint8_t MESSAGE_HISTORY_SIZE = 22; 31 | 32 | // Column for stats 33 | constexpr uint8_t STAT_COLUMN = 0; 34 | 35 | constexpr char CTRL_KEY(char x) { 36 | return static_cast((x) & 0x1F); 37 | } 38 | 39 | #undef DELETE 40 | constexpr char DELETE = 0x7f; 41 | 42 | #undef ESCAPE 43 | constexpr char ESCAPE = '\033'; // ESCAPE character -CJS- 44 | 45 | extern bool screen_has_changed; 46 | extern bool message_ready_to_print; 47 | extern vtype_t messages[MESSAGE_HISTORY_SIZE]; 48 | extern int16_t last_message_id; 49 | 50 | extern int eof_flag; 51 | extern bool panic_save; 52 | 53 | // UI - IO 54 | bool terminalInitialize(); 55 | void terminalRestore(); 56 | void terminalSaveScreen(); 57 | void terminalRestoreScreen(); 58 | ssize_t terminalBellSound(); 59 | void putQIO(); 60 | void flushInputBuffer(); 61 | void clearScreen(); 62 | void clearToBottom(int row); 63 | void moveCursor(Coord_t coord); 64 | void addChar(char ch, Coord_t coord); 65 | void putString(const char *out_str, Coord_t coord); 66 | void putStringClearToEOL(const std::string &str, Coord_t coord); 67 | void eraseLine(Coord_t coord); 68 | void panelMoveCursor(Coord_t coord); 69 | void panelPutTile(char ch, Coord_t coord); 70 | void messageLinePrintMessage(std::string message); 71 | void messageLineClear(); 72 | void printMessage(const char *msg); 73 | void printMessageNoCommandInterrupt(const std::string &msg); 74 | char getKeyInput(); 75 | bool getCommand(const std::string &prompt, char &command); 76 | bool getMenuItemId(const std::string &prompt, char &command); 77 | bool getTileCharacter(const std::string &prompt, char &command); 78 | bool getStringInput(char *in_str, Coord_t coord, int slen); 79 | bool getInputConfirmation(const std::string &prompt); 80 | int getInputConfirmationWithAbort(int column, const std::string &prompt); 81 | void waitForContinueKey(int line_number); 82 | bool checkForNonBlockingKeyPress(int microseconds); 83 | void getDefaultPlayerName(char *buffer); 84 | bool checkFilePermissions(); 85 | 86 | #ifndef _WIN32 87 | // call functions which expand tilde before calling open/fopen 88 | #define open topen 89 | #define fopen tfopen 90 | 91 | FILE *tfopen(const char *file, const char *mode); 92 | int topen(const char *file, int flags, int mode); 93 | bool tilde(const char *file, char *expanded); 94 | #endif 95 | 96 | // UI 97 | bool coordOutsidePanel(Coord_t coord, bool force); 98 | bool coordInsidePanel(Coord_t coord); 99 | void drawDungeonPanel(); 100 | void drawCavePanel(); 101 | void dungeonResetView(); 102 | 103 | void statsAsString(uint8_t stat, char *stat_string); 104 | void displayCharacterStats(int stat); 105 | void printCharacterTitle(); 106 | void printCharacterLevel(); 107 | void printCharacterCurrentMana(); 108 | void printCharacterMaxHitPoints(); 109 | void printCharacterCurrentHitPoints(); 110 | void printCharacterCurrentArmorClass(); 111 | void printCharacterGoldValue(); 112 | void printCharacterCurrentDepth(); 113 | void printCharacterHungerStatus(); 114 | void printCharacterBlindStatus(); 115 | void printCharacterConfusedState(); 116 | void printCharacterFearState(); 117 | void printCharacterPoisonedState(); 118 | void printCharacterMovementState(); 119 | void printCharacterSpeed(); 120 | void printCharacterStudyInstruction(); 121 | void printCharacterWinner(); 122 | void printCharacterStatsBlock(); 123 | void printCharacterInformation(); 124 | void printCharacterStats(); 125 | const char *statRating(Coord_t coord); 126 | void printCharacterVitalStatistics(); 127 | void printCharacterLevelExperience(); 128 | void printCharacterAbilities(); 129 | void printCharacter(); 130 | void getCharacterName(); 131 | void changeCharacterName(); 132 | void displaySpellsList(const int *spell_ids, int number_of_choices, bool comment, int non_consecutive); 133 | void displayCharacterExperience(); 134 | 135 | // UI Inventory/Equipment 136 | int displayInventoryItems(int itemIdStart, int itemIdEnd, bool weighted, int column, const char *mask); 137 | const char *playerItemWearingDescription(int body_location); 138 | int displayEquipment(bool showWeights, int column); 139 | void inventoryExecuteCommand(char command); 140 | bool inventoryGetInputForItemId(int &commandKeyId, const char *prompt, int itemIdStart, int itemIdEnd, char *mask, const char *message); 141 | -------------------------------------------------------------------------------- /src/version.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | // Current version number of Umoria 7 | // CMake will extract this information, if you rename these variables 8 | // then you must also update the CMakeLists.txt. 9 | constexpr uint8_t CURRENT_VERSION_MAJOR = 5; 10 | constexpr uint8_t CURRENT_VERSION_MINOR = 7; 11 | constexpr uint8_t CURRENT_VERSION_PATCH = 15; 12 | -------------------------------------------------------------------------------- /src/wizard.h: -------------------------------------------------------------------------------- 1 | // Copyright (c) 1981-86 Robert A. Koeneke 2 | // Copyright (c) 1987-94 James E. Wilson 3 | // 4 | // SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | #pragma once 7 | 8 | bool enterWizardMode(); 9 | void wizardCureAll(); 10 | void wizardDropRandomItems(); 11 | void wizardJumpLevel(); 12 | void wizardGainExperience(); 13 | void wizardSummonMonster(); 14 | void wizardLightUpDungeon(); 15 | void wizardCharacterAdjustment(); 16 | void wizardGenerateObject(); 17 | void wizardCreateObjects(); 18 | -------------------------------------------------------------------------------- /umoria.spec: -------------------------------------------------------------------------------- 1 | %global debug_package %{nil} 2 | 3 | %if 0%{?suse_version} || 0%{?rhel} == 8 4 | %define __builddir %{_vpath_builddir} 5 | %endif 6 | 7 | Name: umoria 8 | Version: 5.7.15 9 | Release: 3%{?dist} 10 | Summary: Umoria %{version} 11 | 12 | License: GPL-3.0 13 | URL: https://umoria.org 14 | Source0: umoria-%{version}.tar.gz 15 | 16 | BuildRequires: ncurses-devel gcc-c++ cmake 17 | Requires: ncurses-libs 18 | 19 | 20 | %description 21 | The Dungeons of Moria is a single player dungeon simulation originally written by Robert Alan Koeneke, with its first public release in 1983. 22 | The game was originally developed using VMS Pascal before being ported to the C language by James E. Wilson in 1988, and released a Umoria. 23 | 24 | 25 | %prep 26 | %autosetup 27 | 28 | 29 | %build 30 | 31 | %if 0%{?rhel} == 8 32 | mkdir %{_vpath_builddir} && (cd %{_vpath_builddir}; %cmake ..; %cmake_build ) 33 | %else 34 | %cmake 35 | %cmake_build 36 | %endif 37 | 38 | 39 | %install 40 | mkdir -p $RPM_BUILD_ROOT/%{_bindir} 41 | cp %{_vpath_builddir}/umoria/umoria $RPM_BUILD_ROOT/%{_bindir}/umoria.bin 42 | mkdir -p $RPM_BUILD_ROOT/%{_datadir}/games/umoria 43 | cp -R %{_vpath_builddir}/umoria/data $RPM_BUILD_ROOT/%{_datadir}/games/umoria 44 | mkdir -p $RPM_BUILD_ROOT/%{_docdir}/umoria/historical 45 | cp historical/* $RPM_BUILD_ROOT/%{_docdir}/umoria/historical 46 | 47 | cat << EOF > $RPM_BUILD_ROOT/%{_bindir}/umoria 48 | #!/bin/sh 49 | CONFDIR=~/.config/umoria 50 | DATADIR=%{_datadir}/games/umoria/data 51 | BIN=%{_bindir}/umoria.bin 52 | [ ! -d \$CONFDIR ] && mkdir -p \$CONFDIR && ln -s \$DATADIR \$CONFDIR/data 53 | [ ! -f \$CONFDIR/scores.dat ] && touch \$CONFDIR/scores.dat 54 | (cd \$CONFDIR; \$BIN \$@) 55 | EOF 56 | 57 | chmod +x $RPM_BUILD_ROOT/%{_bindir}/umoria 58 | 59 | %postun 60 | echo "Please remove each user's ~/.config/umoria manually, if you need." 61 | 62 | %files 63 | %{_bindir}/umoria* 64 | %{_datadir}/games/umoria/data/* 65 | %{_docdir}/umoria/historical/* 66 | %license LICENSE 67 | %doc *.md AUTHORS 68 | 69 | 70 | 71 | %changelog 72 | * Sat Dec 30 2023 Shiro Hara - 5.7.15-3 73 | - Enable roguelike keys via the CLI 74 | 75 | * Sat Jul 1 2023 Shiro Hara - 5.7.15-2 76 | - Add files in "historical" to doc (Thanks, Justin Koh) 77 | 78 | * Sat Feb 18 2023 Shiro Hara -5.7.15-1 79 | - Add .spec file 80 | --------------------------------------------------------------------------------