├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── fonts ├── Aesomatica_16x16.png ├── Anikki_square_10x10.png ├── Buddy--graphical.png ├── DarkondDigsDeeper_16x16.png ├── Ddw.png ├── Hermano.png ├── Redjack17.png ├── Vidumec_15x15.png └── arial10x10.png ├── img └── menu_background.png └── src ├── const └── mod.rs ├── func ├── combat.rs ├── items.rs ├── levels.rs ├── mod.rs └── ui.rs ├── lib.rs ├── loot └── mod.rs ├── main.rs └── types ├── deathcallback.rs ├── item.rs ├── mod.rs ├── object.rs ├── rect.rs ├── slot.rs └── tile.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | savegame 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ajmw.subs@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "bitflags" 5 | version = "0.1.1" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "bitflags" 10 | version = "1.0.4" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "fuchsia-zircon" 15 | version = "0.3.3" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | dependencies = [ 18 | "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 20 | ] 21 | 22 | [[package]] 23 | name = "fuchsia-zircon-sys" 24 | version = "0.3.3" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | 27 | [[package]] 28 | name = "gcc" 29 | version = "0.3.55" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | 32 | [[package]] 33 | name = "itoa" 34 | version = "0.4.3" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | 37 | [[package]] 38 | name = "lazy_static" 39 | version = "0.1.16" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | 42 | [[package]] 43 | name = "libc" 44 | version = "0.2.46" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [[package]] 48 | name = "pkg-config" 49 | version = "0.3.14" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | 52 | [[package]] 53 | name = "proc-macro2" 54 | version = "0.4.24" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | dependencies = [ 57 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 58 | ] 59 | 60 | [[package]] 61 | name = "quote" 62 | version = "0.6.10" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | dependencies = [ 65 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 66 | ] 67 | 68 | [[package]] 69 | name = "rand" 70 | version = "0.3.22" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | dependencies = [ 73 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 74 | "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 75 | "rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 76 | ] 77 | 78 | [[package]] 79 | name = "rand" 80 | version = "0.4.3" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | dependencies = [ 83 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 85 | "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 86 | ] 87 | 88 | [[package]] 89 | name = "rouge" 90 | version = "0.1.2" 91 | dependencies = [ 92 | "rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)", 93 | "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", 94 | "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", 95 | "serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)", 96 | "tcod 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 97 | ] 98 | 99 | [[package]] 100 | name = "ryu" 101 | version = "0.2.7" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | 104 | [[package]] 105 | name = "serde" 106 | version = "1.0.84" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | 109 | [[package]] 110 | name = "serde_derive" 111 | version = "1.0.84" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | dependencies = [ 114 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 115 | "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", 116 | "syn 0.15.23 (registry+https://github.com/rust-lang/crates.io-index)", 117 | ] 118 | 119 | [[package]] 120 | name = "serde_json" 121 | version = "1.0.34" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | dependencies = [ 124 | "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 126 | "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", 127 | ] 128 | 129 | [[package]] 130 | name = "syn" 131 | version = "0.15.23" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | dependencies = [ 134 | "proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)", 136 | "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 137 | ] 138 | 139 | [[package]] 140 | name = "tcod" 141 | version = "0.12.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | dependencies = [ 144 | "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 145 | "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 146 | "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 147 | "serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", 148 | "serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)", 149 | "tcod-sys 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 150 | ] 151 | 152 | [[package]] 153 | name = "tcod-sys" 154 | version = "4.1.0" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | dependencies = [ 157 | "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)", 158 | "libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)", 159 | "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", 160 | ] 161 | 162 | [[package]] 163 | name = "unicode-xid" 164 | version = "0.1.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | 167 | [[package]] 168 | name = "winapi" 169 | version = "0.3.6" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | dependencies = [ 172 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 173 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 174 | ] 175 | 176 | [[package]] 177 | name = "winapi-i686-pc-windows-gnu" 178 | version = "0.4.0" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | 181 | [[package]] 182 | name = "winapi-x86_64-pc-windows-gnu" 183 | version = "0.4.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | 186 | [metadata] 187 | "checksum bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a6577517ecd0ee0934f48a7295a89aaef3e6dfafeac404f94c0b3448518ddfe" 188 | "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" 189 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 190 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 191 | "checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" 192 | "checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" 193 | "checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417" 194 | "checksum libc 0.2.46 (registry+https://github.com/rust-lang/crates.io-index)" = "023a4cd09b2ff695f9734c1934145a315594b7986398496841c7031a5a1bbdbd" 195 | "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" 196 | "checksum proc-macro2 0.4.24 (registry+https://github.com/rust-lang/crates.io-index)" = "77619697826f31a02ae974457af0b29b723e5619e113e9397b8b82c6bd253f09" 197 | "checksum quote 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "53fa22a1994bd0f9372d7a816207d8a2677ad0325b073f5c5332760f0fb62b5c" 198 | "checksum rand 0.3.22 (registry+https://github.com/rust-lang/crates.io-index)" = "15a732abf9d20f0ad8eeb6f909bf6868722d9a06e1e50802b6a70351f40b4eb1" 199 | "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd" 200 | "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" 201 | "checksum serde 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)" = "0e732ed5a5592c17d961555e3b552985baf98d50ce418b7b655f31f6ba7eb1b7" 202 | "checksum serde_derive 1.0.84 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d6115a3ca25c224e409185325afc16a0d5aaaabc15c42b09587d6f1ba39a5b" 203 | "checksum serde_json 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)" = "bdf540260cfee6da923831f4776ddc495ada940c30117977c70f1313a6130545" 204 | "checksum syn 0.15.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9545a6a093a3f0bd59adb472700acc08cad3776f860f16a897dfce8c88721cbc" 205 | "checksum tcod 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "29e40ebb6f3d1eb43d7f268c7d68675a5c7be80a5eeba6b609dc44e5addb1e9a" 206 | "checksum tcod-sys 4.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "95c9e6c63782149dc204cfc2a76720b650a79162482e605349b4850408db2456" 207 | "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 208 | "checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" 209 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 210 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 211 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rouge" 3 | version = "0.1.2" 4 | authors = ["Avery Wagar "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tcod = { version = "0.12", features = ["serialization"] } 9 | rand = "0.3" 10 | serde = "1.0" 11 | serde_derive = "1.0" 12 | serde_json = "1.0" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Avery Wagar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rouge (red in french) 2 | 3 | A small procedural roguelike written in Rust. 4 | 5 | ## Screenshots 6 | 7 | __TODO:__ add screenshots... I swear it doesn't look bad. 8 | 9 | ## Installation 10 | 11 | 1. `git clone https://github.com/ajmwagar/rouge` 12 | 2. `cd rouge` 13 | 3. Either 14 | - `cargo run --release` 15 | __OR__ 16 | - `cargo install --path ./` 17 | - `rouge` 18 | 19 | ## Rules of the rogue 20 | 21 | - Permadeath (one life) 22 | - You gain some health each level you progress. 23 | - You can only hold 26 items at a time. 24 | - Infinite **procedural** levels. Dificulty progresses over time 25 | 26 | ## Controls 27 | 28 | Rouge's controls are pretty simple. 29 | 30 | The defaults are as follows: 31 | 32 | - `<`: Decend staircase 33 | - `i`: open inventory 34 | - `c`: open character menu 35 | - `Arrow Keys`: Movement 36 | - `Numpad`: Movement + Diagonal Attack + Pass turn 37 | - `esc`: return to main menu (and save) 38 | 39 | __Note:__ you can mouse over a square to see the items in it. 40 | 41 | -------------------------------------------------------------------------------- /fonts/Aesomatica_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Aesomatica_16x16.png -------------------------------------------------------------------------------- /fonts/Anikki_square_10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Anikki_square_10x10.png -------------------------------------------------------------------------------- /fonts/Buddy--graphical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Buddy--graphical.png -------------------------------------------------------------------------------- /fonts/DarkondDigsDeeper_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/DarkondDigsDeeper_16x16.png -------------------------------------------------------------------------------- /fonts/Ddw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Ddw.png -------------------------------------------------------------------------------- /fonts/Hermano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Hermano.png -------------------------------------------------------------------------------- /fonts/Redjack17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Redjack17.png -------------------------------------------------------------------------------- /fonts/Vidumec_15x15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/Vidumec_15x15.png -------------------------------------------------------------------------------- /fonts/arial10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/fonts/arial10x10.png -------------------------------------------------------------------------------- /img/menu_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/img/menu_background.png -------------------------------------------------------------------------------- /src/const/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | use tcod::map::FovAlgorithm; 4 | use tcod::Color; 5 | 6 | pub const CORPSE: char = 1u8 as char; 7 | pub const TROLL: char = 161u8 as char; 8 | pub const ORC: char = 160u8 as char; 9 | pub const WALL: char = 164u8 as char; 10 | pub const FLOOR: char = 178u8 as char; 11 | 12 | pub const INVENTORY_WIDTH: i32 = 50; 13 | pub const LEVEL_SCREEN_WIDTH: i32 = 40; 14 | pub const CHARACTER_SCREEN_WIDTH: i32 = 40; 15 | 16 | // player will always be the first object 17 | pub const PLAYER: usize = 0; 18 | 19 | // experience and level-ups 20 | pub const LEVEL_UP_BASE: i32 = 200; 21 | pub const LEVEL_UP_FACTOR: i32 = 150; 22 | 23 | 24 | // Message Console 25 | pub const MSG_X: i32 = BAR_WIDTH + 2; 26 | pub const MSG_WIDTH: i32 = SCREEN_WIDTH - BAR_WIDTH - 2; 27 | pub const MSG_HEIGHT: usize = PANEL_HEIGHT as usize - 1; 28 | 29 | // sizes and coordinates relevant for the GUI 30 | pub const BAR_WIDTH: i32 = 20; 31 | pub const PANEL_HEIGHT: i32 = 7; 32 | pub const PANEL_Y: i32 = SCREEN_HEIGHT - PANEL_HEIGHT; 33 | 34 | // Screen Size 35 | pub const SCREEN_WIDTH: i32 = 80; 36 | pub const SCREEN_HEIGHT: i32 = 40; 37 | pub const LIMIT_FPS: i32 = 60; 38 | 39 | // Spell constants 40 | pub const HEAL_AMOUNT: i32 = 40; 41 | pub const LIGHTNING_DAMAGE: i32 = 40; 42 | pub const LIGHTNING_RANGE: i32 = 20; 43 | pub const FIREBALL_DAMAGE: i32 = 25; 44 | pub const FIREBALL_RADIUS: i32 = 3; 45 | pub const CONFUSE_NUM_TURNS: i32 = 5; 46 | pub const CONFUSE_RANGE: i32 = 20; 47 | 48 | 49 | // Room Generation 50 | pub const ROOM_MAX_SIZE: i32 = 10; 51 | pub const ROOM_MIN_SIZE: i32 = 6; 52 | pub const MAX_ROOMS: i32 = 30; 53 | 54 | // Map 55 | pub const MAP_WIDTH: i32 = 80; 56 | pub const MAP_HEIGHT: i32 = 33; 57 | 58 | pub const COLOR_DARK_WALL: Color = Color { r: 64, g: 64, b: 64 }; 59 | pub const COLOR_LIGHT_WALL: Color = Color { r: 130, g: 110, b: 50 }; 60 | pub const COLOR_DARK_GROUND: Color = Color { r: 96, g: 96, b: 96 }; 61 | pub const COLOR_LIGHT_GROUND: Color = Color { r: 200, g: 180, b: 50 }; 62 | 63 | pub const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic; 64 | pub const FOV_LIGHT_WALLS: bool = true; 65 | pub const TORCH_RADIUS: i32 = 15; 66 | -------------------------------------------------------------------------------- /src/func/combat.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use rand::Rng; 3 | use crate::r#const::*; 4 | use crate::types::*; 5 | use crate::types::Tcod; 6 | use crate::types::Messages; 7 | use crate::func::*; 8 | use crate::types::object::Object; 9 | use tcod::input::{self, Event, Mouse}; 10 | use tcod::colors::{self, Color}; 11 | 12 | /// returns a clicked monster inside FOV up to a range, or None if right-clicked 13 | pub fn target_monster(tcod: &mut Tcod, 14 | objects: &[Object], 15 | game: &mut Game, 16 | max_range: Option) 17 | -> Option { 18 | loop { 19 | match target_tile(tcod, objects, game, max_range) { 20 | Some((x, y)) => { 21 | // return the first clicked monster, otherwise continue looping 22 | for (id, obj) in objects.iter().enumerate() { 23 | if obj.pos() == (x, y) && obj.fighter.is_some() && id != PLAYER { 24 | return Some(id) 25 | } 26 | } 27 | } 28 | None => return None, 29 | } 30 | } 31 | } 32 | 33 | /// return the position of a tile left-clicked in player's FOV (optionally in a 34 | /// range), or (None,None) if right-clicked. 35 | pub fn target_tile(tcod: &mut Tcod, 36 | objects: &[Object], game: &mut Game, 37 | max_range: Option) 38 | -> Option<(i32, i32)> { 39 | use tcod::input::KeyCode::Escape; 40 | loop { 41 | // render the screen. this erases the inventory and shows the names of 42 | // objects under the mouse. 43 | tcod.root.flush(); 44 | 45 | 46 | let event = input::check_for_event(input::KEY_PRESS | input::MOUSE).map(|e| e.1); 47 | let mut key = None; 48 | match event { 49 | Some(Event::Mouse(m)) => tcod.mouse = m, 50 | Some(Event::Key(k)) => key = Some(k), 51 | None => {} 52 | } 53 | render_all(tcod, objects, game, false); 54 | 55 | let (x, y) = (tcod.mouse.cx as i32, tcod.mouse.cy as i32); 56 | 57 | // accept the target if the player clicked in FOV, and in case a range 58 | // is specified, if it's in that range 59 | let in_fov = (x < MAP_WIDTH) && (y < MAP_HEIGHT) && tcod.fov.is_in_fov(x, y); 60 | let in_range = max_range.map_or( 61 | true, |range| objects[PLAYER].distance(x, y) <= range); 62 | if tcod.mouse.lbutton_pressed && in_fov && in_range { 63 | return Some((x, y)) 64 | } 65 | 66 | let escape = key.map_or(false, |k| k.code == Escape); 67 | if tcod.mouse.rbutton_pressed || escape { 68 | return None // cancel if the player right-clicked or pressed Escape 69 | } 70 | 71 | } 72 | } 73 | 74 | /// find closest enemy, up to a maximum range, and in the player's FOV 75 | pub fn closest_monster(max_range: i32, objects: &mut [Object], tcod: &Tcod) -> Option { 76 | let mut closest_enemy = None; 77 | let mut closest_dist = (max_range + 1) as f32; // start with (slightly more than) maximum range 78 | 79 | for (id, object) in objects.iter().enumerate() { 80 | if (id != PLAYER) && object.fighter.is_some() && object.ai.is_some() && 81 | tcod.fov.is_in_fov(object.x, object.y) 82 | { 83 | // calculate distance between this object and the player 84 | let dist = objects[PLAYER].distance_to(object); 85 | if dist < closest_dist { // it's closer, so remember it 86 | closest_enemy = Some(id); 87 | closest_dist = dist; 88 | } 89 | } 90 | } 91 | closest_enemy 92 | } 93 | 94 | 95 | pub fn cast_fireball(_inventory_id: usize, objects: &mut [Object],game: &mut Game, tcod: &mut Tcod) 96 | -> UseResult 97 | { 98 | // ask the player for a target tile to throw a fireball at 99 | game.log.add("Left-click a target tile for the fireball, or right-click to cancel.", 100 | colors::LIGHT_CYAN); 101 | let (x, y) = match target_tile(tcod, objects, game, None) { 102 | Some(tile_pos) => tile_pos, 103 | None => return UseResult::Cancelled, 104 | }; 105 | game.log.add(format!("The fireball explodes, burning everything within {} tiles!", FIREBALL_RADIUS), 106 | colors::ORANGE); 107 | let mut xp_to_gain = 0; 108 | for (id, obj) in objects.iter_mut().enumerate() { 109 | if obj.distance(x, y) <= FIREBALL_RADIUS as f32 && obj.fighter.is_some() { 110 | game.log.add(format!("The {} gets burned for {} hit points.", obj.name, FIREBALL_DAMAGE), 111 | colors::ORANGE); 112 | if let Some(xp) = obj.take_damage(FIREBALL_DAMAGE, game) { 113 | // Don't reward the player for burning themself! 114 | if id != PLAYER { 115 | xp_to_gain += xp; 116 | } 117 | } 118 | } 119 | } 120 | objects[PLAYER].fighter.as_mut().unwrap().xp += xp_to_gain; 121 | 122 | UseResult::UsedUp 123 | } 124 | 125 | pub fn cast_heal(_inventory_id: usize, objects: &mut [Object], game: &mut Game, _tcod: &mut Tcod) 126 | -> UseResult 127 | { 128 | // heal the player 129 | let player = &mut objects[PLAYER]; 130 | if let Some(fighter) = player.fighter { 131 | if fighter.hp == player.max_hp(game) { 132 | game.log.add("You are already at full health.", colors::RED); 133 | return UseResult::Cancelled; 134 | } 135 | game.log.add("Your wounds start to feel better!", colors::LIGHT_VIOLET); 136 | player.heal(HEAL_AMOUNT, game); 137 | return UseResult::UsedUp; 138 | } 139 | UseResult::Cancelled 140 | } 141 | pub fn cast_lightning(_inventory_id: usize, objects: &mut [Object], game: &mut Game, tcod: &mut Tcod) -> UseResult 142 | { 143 | // find closest enemy (inside a maximum range) and damage it 144 | let monster_id = closest_monster(LIGHTNING_RANGE, objects, tcod); 145 | if let Some(monster_id) = monster_id { 146 | // zap it! 147 | game.log.add(format!("A lightning bolt strikes the {} with a loud thunder! \ 148 | The damage is {} hit points.", 149 | objects[monster_id].name, LIGHTNING_DAMAGE), 150 | colors::LIGHT_BLUE); 151 | objects[monster_id].take_damage(LIGHTNING_DAMAGE, game); 152 | 153 | UseResult::UsedUp 154 | } else { // no enemy found within maximum range 155 | game.log.add("No enemy is close enough to strike.", colors::RED); 156 | UseResult::Cancelled 157 | } 158 | } 159 | 160 | pub fn cast_confuse(_inventory_id: usize, objects: &mut [Object], game: &mut Game,tcod: &mut Tcod) 161 | -> UseResult 162 | { 163 | // ask the player for a target to confuse 164 | game.log.add("Left-click an enemy to confuse it, or right-click to cancel.", colors::LIGHT_CYAN); 165 | let monster_id = target_monster(tcod, objects, game, Some(CONFUSE_RANGE as f32)); 166 | if let Some(monster_id) = monster_id { 167 | let old_ai = objects[monster_id].ai.take().unwrap_or(Ai::Basic); 168 | // replace the monster's AI with a "confused" one; after 169 | // some turns it will restore the old AI 170 | objects[monster_id].ai = Some(Ai::Confused { 171 | previous_ai: Box::new(old_ai), 172 | num_turns: CONFUSE_NUM_TURNS, 173 | }); 174 | game.log.add(format!("The eyes of {} look vacant, as he starts to stumble around!", 175 | objects[monster_id].name), 176 | colors::LIGHT_GREEN); 177 | UseResult::UsedUp 178 | } else { // no enemy fonud within maximum range 179 | game.log.add("No enemy is close enough to strike.", colors::RED); 180 | UseResult::Cancelled 181 | } 182 | } 183 | 184 | pub fn player_death(player: &mut Object, messages: &mut Messages) { 185 | // the game ended! 186 | // TODO Replace with game.log.add() 187 | message(messages, "You died!", colors::DARK_RED); 188 | 189 | // for added effect, transform the player into a corpse! 190 | player.char = CORPSE; 191 | player.color = colors::DARK_RED; 192 | } 193 | 194 | pub fn monster_death(monster: &mut Object, messages: &mut Messages) { 195 | // transform it into a nasty corpse! it doesn't block, can't be 196 | // attacked and doesn't move 197 | // TODO Replace with game.log.add() 198 | // message(messages, format!("{} is dead!", monster.name), colors::ORANGE); 199 | message(messages, format!("{} is dead! You gain {} experience points.", 200 | monster.name, monster.fighter.unwrap().xp), colors::ORANGE); 201 | monster.char = CORPSE; 202 | monster.color = colors::DARK_RED; 203 | monster.blocks = false; 204 | monster.fighter = None; 205 | monster.ai = None; 206 | monster.name = format!("remains of {}", monster.name); 207 | } 208 | 209 | 210 | pub fn player_move_or_attack(dx: i32, dy: i32, objects: &mut [Object], game: &mut Game) { 211 | // the coordinates the player is moving to/attacking 212 | let x = objects[PLAYER].x + dx; 213 | let y = objects[PLAYER].y + dy; 214 | 215 | // try to find an attackable object there 216 | let target_id = objects.iter().position(|object| { 217 | object.fighter.is_some() && object.pos() == (x, y) 218 | }); 219 | 220 | // attack if target found, move otherwise 221 | match target_id { 222 | Some(target_id) => { 223 | let (player, target) = mut_two(PLAYER, target_id, objects); 224 | player.attack(target, game); 225 | 226 | } 227 | None => { 228 | move_by(PLAYER, dx, dy, &mut game.map, objects); 229 | } 230 | } 231 | } 232 | 233 | pub fn ai_take_turn(monster_id: usize, objects: &mut [Object], game: &mut Game, fov_map: &FovMap) { 234 | use Ai::*; 235 | if let Some(ai) = objects[monster_id].ai.take() { 236 | let new_ai = match ai { 237 | Basic => ai_basic(monster_id, game, objects, fov_map), 238 | Confused{previous_ai, num_turns} => ai_confused( 239 | monster_id, &mut game.map, objects, &mut game.log, previous_ai, num_turns) 240 | }; 241 | objects[monster_id].ai = Some(new_ai); 242 | } 243 | } 244 | 245 | pub fn ai_basic(monster_id: usize, game: &mut Game, objects: &mut [Object], fov_map: &FovMap) -> Ai { 246 | // a basic monster takes its turn. If you can see it, it can see you 247 | let (monster_x, monster_y) = objects[monster_id].pos(); 248 | if fov_map.is_in_fov(monster_x, monster_y) { 249 | if objects[monster_id].distance_to(&objects[PLAYER]) >= 2.0 { 250 | // move towards player if far away 251 | let (player_x, player_y) = objects[PLAYER].pos(); 252 | move_towards(monster_id, player_x, player_y, &mut game.map, objects); 253 | } else if objects[PLAYER].fighter.map_or(false, |f| f.hp > 0) { 254 | // close enough, attack! (if the player is still alive.) 255 | let (monster, player) = mut_two(monster_id, PLAYER, objects); 256 | monster.attack(player, game); 257 | } 258 | } 259 | Ai::Basic 260 | } 261 | 262 | pub fn ai_confused(monster_id: usize, map: &Map, objects: &mut [Object], messages: &mut Messages, 263 | previous_ai: Box, num_turns: i32) -> Ai { 264 | if num_turns >= 0 { // still confused ... 265 | // move in a random idrection, and decrease the number of turns confused 266 | move_by(monster_id, 267 | rand::thread_rng().gen_range(-1, 2), 268 | rand::thread_rng().gen_range(-1, 2), 269 | map, 270 | objects); 271 | Ai::Confused{previous_ai: previous_ai, num_turns: num_turns - 1} 272 | } else { // restore the previous AI (this one will be deleted) 273 | // TODO Replace with game.log.add() 274 | message(messages, format!("The {} is no longer confused!", 275 | objects[monster_id].name), 276 | colors::RED); 277 | *previous_ai 278 | } 279 | } 280 | pub fn move_towards(id: usize, target_x: i32, target_y: i32, map: &Map, objects: &mut [Object]) { 281 | // vector from this object to the target, and distance 282 | let dx = target_x - objects[id].x; 283 | let dy = target_y - objects[id].y; 284 | let distance = ((dx.pow(2) + dy.pow(2)) as f32).sqrt(); 285 | 286 | // normalize it to length 1 (preserving direction), then round it and 287 | // convert to integer so the movement is restricted to the map grid 288 | let dx = (dx as f32 / distance).round() as i32; 289 | let dy = (dy as f32 / distance).round() as i32; 290 | move_by(id, dx, dy, map, objects); 291 | } 292 | 293 | 294 | 295 | -------------------------------------------------------------------------------- /src/func/items.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::func::combat::*; 3 | use crate::types::*; 4 | use crate::types::object::Object; 5 | use crate::types::Tcod; 6 | use crate::types::Game; 7 | use crate::types::Messages; 8 | use crate::types::MessageLog; 9 | use crate::types::UseResult; 10 | use crate::func::ui::message; 11 | use tcod::colors::{self, Color}; 12 | use crate::r#const::*; 13 | 14 | pub fn get_equipped_in_slot(slot: Slot, inventory: &[Object]) -> Option { 15 | for (inventory_id, item) in inventory.iter().enumerate() { 16 | if item.equipment.as_ref().map_or(false, |e| e.equipped && e.slot == slot) { 17 | return Some(inventory_id) 18 | } 19 | } 20 | None 21 | } 22 | 23 | pub fn toggle_equipment(inventory_id: usize, _objects: &mut [Object], game: &mut Game, _tcod: &mut Tcod) 24 | -> UseResult 25 | { 26 | let equipment = match game.inventory[inventory_id].equipment { 27 | Some(equipment) => equipment, 28 | None => return UseResult::Cancelled, 29 | }; 30 | if equipment.equipped { 31 | game.inventory[inventory_id].unequip(&mut game.log); 32 | } else { 33 | // if the slot is already being used, dequip whatever is there first 34 | if let Some(old_equipment) = get_equipped_in_slot(equipment.slot, &game.inventory) { 35 | game.inventory[old_equipment].unequip(&mut game.log); 36 | } 37 | game.inventory[inventory_id].equip(&mut game.log); 38 | } 39 | UseResult::UsedAndKept 40 | } 41 | 42 | pub fn drop_item(inventory_id: usize, 43 | inventory: &mut Vec, 44 | objects: &mut Vec, 45 | messages: &mut Messages) { 46 | let mut item = inventory.remove(inventory_id); 47 | if item.equipment.is_some() { 48 | item.unequip(messages); 49 | } 50 | item.set_pos(objects[PLAYER].x, objects[PLAYER].y); 51 | // TODO Replace with game.log.add() 52 | message(messages, format!("You dropped a {}.", item.name), colors::YELLOW); 53 | objects.push(item); 54 | } 55 | 56 | 57 | pub fn use_item(inventory_id: usize, objects: &mut [Object], 58 | game: &mut Game, tcod: &mut Tcod) { 59 | use crate::types::item::Item::*; 60 | // just call the "use_function" if it is defined 61 | if let Some(item) = game.inventory[inventory_id].item { 62 | let on_use: fn(usize, &mut [Object], &mut Game , &mut Tcod) -> UseResult = match item { 63 | Heal => cast_heal, 64 | Lightning => cast_lightning, 65 | Confuse => cast_confuse, 66 | Fireball => cast_fireball, 67 | IronSword => toggle_equipment, 68 | WoodShield => toggle_equipment, 69 | IronShield => toggle_equipment, 70 | GreatAxe => toggle_equipment, 71 | WarHammer => toggle_equipment, 72 | ClothPants => toggle_equipment, 73 | ClothShirt => toggle_equipment, 74 | LeatherHat => toggle_equipment, 75 | LeatherWristGaurds => toggle_equipment, 76 | LeatherKneeGaurds => toggle_equipment, 77 | LeatherChest => toggle_equipment, 78 | BronzeSword => toggle_equipment, 79 | Dagger => toggle_equipment, 80 | }; 81 | match on_use(inventory_id, objects, game, tcod) { 82 | UseResult::UsedUp => { 83 | // destroy after use, unless it was cancelled for some reason 84 | game.inventory.remove(inventory_id); 85 | } 86 | UseResult::UsedAndKept => {}, // do nothing 87 | UseResult::Cancelled => { 88 | game.log.add("Cancelled", colors::WHITE); 89 | } 90 | } 91 | } else { 92 | game.log.add(format!("The {} cannot be used.", game.inventory[inventory_id].name), 93 | colors::WHITE); 94 | } 95 | } 96 | 97 | /// add to the player's inventory and remove from the map 98 | pub fn pick_item_up(object_id: usize, objects: &mut Vec, inventory: &mut Vec, 99 | messages: &mut Messages) { 100 | if inventory.len() >= 26 { 101 | // TODO Replace with game.log.add() 102 | message(messages, 103 | format!("Your inventory is full, cannot pick up {}.", objects[object_id].name), 104 | colors::RED); 105 | } else { 106 | let item = objects.swap_remove(object_id); 107 | // TODO Replace with game.log.add() 108 | message(messages, format!("You picked up a {}!", item.name), colors::GREEN); 109 | let index = inventory.len(); 110 | let slot = item.equipment.map(|e| e.slot); 111 | inventory.push(item); 112 | 113 | // automatically equip, if the corresponding equipment slot is unused 114 | if let Some(slot) = slot { 115 | if get_equipped_in_slot(slot, inventory).is_none() { 116 | inventory[index].equip(messages); 117 | } 118 | } 119 | } 120 | } 121 | 122 | pub fn place_objects(room: Rect, objects: &mut Vec, map: &Map, level: u32) { 123 | // choose random number of monsters 124 | let max_monsters = from_dungeon_level(&[ 125 | Transition {level: 1, value: 2}, 126 | Transition {level: 4, value: 3}, 127 | Transition {level: 6, value: 5}, 128 | ], level); 129 | 130 | // choose random number of monsters 131 | let num_monsters = rand::thread_rng().gen_range(0, max_monsters + 1); 132 | 133 | for _ in 0..num_monsters { 134 | // choose random spot for this monster 135 | let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); 136 | let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); 137 | 138 | 139 | // only place it if the tile is not blocked 140 | if !is_blocked(x, y, map, objects) { 141 | 142 | 143 | // monster random table 144 | let troll_chance = from_dungeon_level(&[ 145 | Transition {level: 3, value: 15}, 146 | Transition {level: 5, value: 30}, 147 | Transition {level: 7, value: 60}, 148 | ], level); 149 | 150 | let monster_chances = &mut [ 151 | Weighted {weight: 80, item: "orc"}, 152 | Weighted {weight: troll_chance, item: "troll"}, 153 | ]; 154 | 155 | 156 | let monster_choice = WeightedChoice::new(monster_chances); 157 | 158 | let mut monster = match monster_choice.ind_sample(&mut rand::thread_rng()) { 159 | "orc" => { 160 | // create an orc 161 | let mut orc = Object::new(x, y, ORC, "orc", colors::DESATURATED_GREEN, true); 162 | orc.fighter = Some(Fighter { 163 | base_max_hp: from_dungeon_level(&[ 164 | Transition { level: 1, value: 20 }, 165 | Transition { level:2 , value: 25}, 166 | Transition { level:5 , value: 45}, 167 | Transition { level:10 , value: 50} , 168 | Transition { level:20 , value: 100} , 169 | ], level) as i32, 170 | hp: from_dungeon_level(&[ 171 | Transition { level: 1, value: 20 }, 172 | Transition { level:2 , value: 25}, 173 | Transition { level:5 , value: 45}, 174 | Transition { level:10 , value: 50} , 175 | Transition { level:20 , value: 100} , 176 | ], level) as i32, 177 | base_defense: (level / 2) as i32, 178 | base_power: 4 + (level) as i32, 179 | on_death: DeathCallback::Monster, 180 | xp: 5 * level as i32 181 | }); 182 | orc.ai = Some(Ai::Basic); 183 | orc 184 | } 185 | "troll" => { 186 | // create a troll 187 | let mut troll = Object::new(x, y, TROLL, "troll", colors::DARKER_GREEN, true); 188 | troll.fighter = Some(Fighter{ 189 | base_max_hp: from_dungeon_level(&[ 190 | Transition { level: 1, value: 40 }, 191 | Transition { level:2 , value: 55}, 192 | Transition { level:5 , value: 65}, 193 | Transition { level:10 , value: 75} , 194 | Transition { level:20 , value: 150} , 195 | ], level) as i32, 196 | hp: from_dungeon_level(&[ 197 | Transition { level: 1, value: 40 }, 198 | Transition { level:2 , value: 55}, 199 | Transition { level:5 , value: 65}, 200 | Transition { level:10 , value: 75} , 201 | Transition { level:20 , value: 150} , 202 | ], level) as i32, 203 | base_defense: level as i32 + 2, 204 | base_power: level as i32 + 8, 205 | on_death: DeathCallback::Monster, 206 | xp: 35 * level as i32}); 207 | troll.ai = Some(Ai::Basic); 208 | troll 209 | } 210 | _ => unreachable!(), 211 | }; 212 | 213 | 214 | monster.alive = true; 215 | objects.push(monster); 216 | } 217 | } 218 | 219 | 220 | 221 | // maximum number of items per room 222 | let max_items = from_dungeon_level(&[ 223 | Transition {level: 1, value: 1}, 224 | Transition {level: 4, value: 2}, 225 | ], level); 226 | 227 | // choose random number of items 228 | let num_items = rand::thread_rng().gen_range(0, max_items + 1); 229 | 230 | for _ in 0..num_items { 231 | // choose random spot for this item 232 | let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2); 233 | let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2); 234 | 235 | // only place it if the tile is not blocked 236 | if !is_blocked(x, y, map, objects) { 237 | 238 | // item random table 239 | let item_chances = &mut [ 240 | // healing potion always shows up, even if all other items have 0 chance 241 | Weighted {weight: 35, item: Item::Heal}, 242 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 25}], level), 243 | item: Item::Lightning}, 244 | Weighted {weight: from_dungeon_level(&[Transition{level: 6, value: 25}], level), 245 | item: Item::Fireball}, 246 | Weighted {weight: from_dungeon_level(&[Transition{level: 2, value: 10}], level), 247 | item: Item::Confuse}, 248 | Weighted {weight: from_dungeon_level(&[Transition{level: 1, value: 5}], level), 249 | item: Item::Dagger}, 250 | Weighted {weight: from_dungeon_level(&[Transition{level: 3, value: 10}], level), 251 | item: Item::ClothPants}, 252 | Weighted {weight: from_dungeon_level(&[Transition{level: 3, value: 10}], level), 253 | item: Item::ClothShirt}, 254 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 10}], level), 255 | item: Item::LeatherHat}, 256 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 10}], level), 257 | item: Item::LeatherChest}, 258 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 10}], level), 259 | item: Item::LeatherKneeGaurds}, 260 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 10}], level), 261 | item: Item::LeatherWristGaurds}, 262 | Weighted {weight: from_dungeon_level(&[Transition{level: 4, value: 5}], level), 263 | item: Item::BronzeSword}, 264 | Weighted {weight: from_dungeon_level(&[Transition{level: 6, value: 5}], level), 265 | item: Item::IronSword}, 266 | Weighted {weight: from_dungeon_level(&[Transition{level: 8, value: 15}], level), 267 | item: Item::WoodShield}, 268 | Weighted {weight: from_dungeon_level(&[Transition{level: 10, value: 15}], level), 269 | item: Item::IronShield}, 270 | ]; 271 | 272 | let item_choice = WeightedChoice::new(item_chances); 273 | 274 | 275 | let item = match item_choice.ind_sample(&mut rand::thread_rng()) { 276 | Item::Heal => { 277 | // create a healing potion 278 | let mut object = Object::new(x, y, 20u8 as char, "healing potion", colors::VIOLET, false); 279 | object.item = Some(Item::Heal); 280 | object 281 | } 282 | Item::Lightning => { 283 | // create a lightning bolt scroll 284 | let mut object = Object::new(x, y, '-', "scroll of lightning bolt", 285 | colors::LIGHT_YELLOW, false); 286 | object.item = Some(Item::Lightning); 287 | object 288 | } 289 | Item::Fireball => { 290 | // create a fireball scroll 291 | let mut object = Object::new(x, y, '-', "scroll of fireball", colors::LIGHT_RED, false); 292 | object.item = Some(Item::Fireball); 293 | object 294 | } 295 | Item::Confuse => { 296 | // create a confuse scroll 297 | let mut object = Object::new(x, y, '-', "scroll of confusion", 298 | colors::AMBER, false); 299 | object.item = Some(Item::Confuse); 300 | object 301 | }, 302 | Item::BronzeSword => { 303 | // create a sword 304 | let mut object = Object::new(x, y, '/', "bronze sword", colors::SKY, false); 305 | object.item = Some(Item::BronzeSword); 306 | object.equipment = Some(Equipment{equipped: false, slot: Slot::RightHand, power_bonus: 2, defense_bonus: 0, max_hp_bonus: 0}); 307 | object 308 | }, 309 | Item::IronSword => { 310 | // create a sword 311 | let mut object = Object::new(x, y, '/', "iron sword", colors::SKY, false); 312 | object.item = Some(Item::IronSword); 313 | object.equipment = Some(Equipment{equipped: false, slot: Slot::RightHand, power_bonus: 4, defense_bonus: 0, max_hp_bonus: 0}); 314 | object 315 | }, 316 | Item::WoodShield => { 317 | // create a shield 318 | let mut object = Object::new(x, y, '[', "wooden shield", colors::DARKER_ORANGE, false); 319 | object.item = Some(Item::WoodShield); 320 | object.equipment = Some(Equipment{equipped: false, slot: Slot::LeftHand, max_hp_bonus: 0, defense_bonus: 1, power_bonus: 0}); 321 | object 322 | } 323 | Item::IronShield => { 324 | // create a shield 325 | let mut object = Object::new(x, y, '[', "iron shield", colors::DARKER_ORANGE, false); 326 | object.item = Some(Item::IronShield); 327 | object.equipment = Some(Equipment{equipped: false, slot: Slot::LeftHand, max_hp_bonus: 0, defense_bonus: 5, power_bonus: 0}); 328 | object 329 | }, 330 | Item::Dagger => { 331 | // create a sword 332 | let mut object = Object::new(x, y, '/', "iron sword", colors::SKY, false); 333 | object.item = Some(Item::IronSword); 334 | object.equipment = Some(Equipment{equipped: false, slot: Slot::RightHand, power_bonus: 4, defense_bonus: 0, max_hp_bonus: 0}); 335 | object 336 | 337 | }, 338 | Item::GreatAxe => { 339 | // create a sword 340 | let mut object = Object::new(x, y, 'Y', "great axe", colors::VIOLET, false); 341 | object.item = Some(Item::GreatAxe); 342 | object.equipment = Some(Equipment{equipped: false, slot: Slot::RightHand, power_bonus: 20, defense_bonus: 0, max_hp_bonus: 0}); 343 | object 344 | 345 | }, 346 | Item::WarHammer => { 347 | let mut object = Object::new(x, y, 'T', "war hammer", colors::VIOLET, false); 348 | object.item = Some(Item::GreatAxe); 349 | object.equipment = Some(Equipment{equipped: false, slot: Slot::RightHand, power_bonus: 25, defense_bonus: -1, max_hp_bonus: 0}); 350 | object 351 | 352 | 353 | }, 354 | Item::ClothPants => { 355 | let mut object = Object::new(x, y, 'P', "cloth pants", colors::SKY, false); 356 | object.item = Some(Item::ClothPants); 357 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Legs, power_bonus: 0, defense_bonus: 2, max_hp_bonus: 3}); 358 | object 359 | 360 | }, 361 | Item::ClothShirt => { 362 | let mut object = Object::new(x, y, 'S', "cloth shirt", colors::SKY, false); 363 | object.item = Some(Item::ClothShirt); 364 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Curiass, power_bonus: 0, defense_bonus: 2, max_hp_bonus: 3}); 365 | object 366 | 367 | }, 368 | Item::LeatherHat => { 369 | let mut object = Object::new(x, y, 'H', "leather hat", colors::SKY, false); 370 | object.item = Some(Item::LeatherHat); 371 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Head, power_bonus: 0, defense_bonus: 3, max_hp_bonus: 3}); 372 | object 373 | 374 | }, 375 | Item::LeatherChest => { 376 | let mut object = Object::new(x, y, 'S', "leather chestpiece", colors::SKY, false); 377 | object.item = Some(Item::LeatherChest); 378 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Curiass, power_bonus: 0, defense_bonus: 4, max_hp_bonus: 3}); 379 | object 380 | 381 | }, 382 | Item::LeatherWristGaurds => { 383 | let mut object = Object::new(x, y, 'S', "leather gauntlets", colors::SKY, false); 384 | object.item = Some(Item::LeatherWristGaurds); 385 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Gauntlets, power_bonus: 0, defense_bonus: 2, max_hp_bonus: 3}); 386 | object 387 | 388 | }, 389 | Item::LeatherKneeGaurds => { 390 | let mut object = Object::new(x, y, 'S', "leather pants", colors::SKY, false); 391 | object.item = Some(Item::LeatherKneeGaurds); 392 | object.equipment = Some(Equipment{equipped: false, slot: Slot::Legs, power_bonus: 0, defense_bonus: 2, max_hp_bonus: 3}); 393 | object 394 | 395 | }, 396 | }; 397 | 398 | objects.push(item); 399 | } 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/func/levels.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use tcod::colors::{self, Color}; 3 | use crate::r#const::*; 4 | use crate::types::*; 5 | 6 | /// Returns a value that depends on level. the table specifies what 7 | /// value occurs after each level, default is 0. 8 | pub fn from_dungeon_level(table: &[Transition], level: u32) -> u32 { 9 | table.iter() 10 | .rev() 11 | .find(|transition| level >= transition.level) 12 | .map_or(0, |transition| transition.value) 13 | } 14 | 15 | pub fn level_up(objects: &mut [Object], game: &mut Game, tcod: &mut Tcod) { 16 | let player = &mut objects[PLAYER]; 17 | let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR; 18 | // see if the player's experience is enough to level-up 19 | if player.fighter.as_ref().map_or(0, |f| f.xp) >= level_up_xp { 20 | // it is! level up 21 | player.level += 1; 22 | game.log.add(format!("Your battle skills grow stronger! You reached level {}!", 23 | player.level), 24 | colors::YELLOW); 25 | let fighter = player.fighter.as_mut().unwrap(); 26 | let mut choice = None; 27 | while choice.is_none() { // keep asking until a choice is made 28 | choice = menu( 29 | "Level up! Choose a stat to raise:\n", 30 | &[format!("Constitution (+20 HP, from {})", fighter.base_max_hp), 31 | format!("Strength (+1 attack, from {})", fighter.base_power), 32 | format!("Agility (+1 defense, from {})", fighter.base_defense)], 33 | LEVEL_SCREEN_WIDTH, &mut tcod.root); 34 | }; 35 | fighter.xp -= level_up_xp; 36 | match choice.unwrap() { 37 | 0 => { 38 | fighter.base_max_hp += 20; 39 | fighter.hp = fighter.base_max_hp; 40 | } 41 | 1 => { 42 | fighter.base_power += 1; 43 | } 44 | 2 => { 45 | fighter.base_defense += 1; 46 | } 47 | _ => unreachable!(), 48 | } 49 | } 50 | } 51 | 52 | /// Advance to the next level 53 | pub fn next_level(tcod: &mut Tcod, objects: &mut Vec, game: &mut Game) { 54 | game.log.add("You take a moment to rest, and recover your strength.", colors::VIOLET); 55 | let heal_hp = objects[PLAYER].max_hp(game) / 2; 56 | objects[PLAYER].heal(heal_hp, game); 57 | 58 | game.log.add("After a rare moment of peace, you descend deeper into \ 59 | the heart of the dungeon...", colors::RED); 60 | game.dungeon_level += 1; 61 | 62 | objects[PLAYER].fighter.as_mut().unwrap().xp += (game.dungeon_level * 10) as i32; 63 | 64 | game.map = make_map(objects, game.dungeon_level); 65 | initialise_fov(&game.map, tcod); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/func/mod.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::fs::File; 3 | use std::error::Error; 4 | 5 | use rand::Rng; 6 | use rand::distributions::{Weighted, WeightedChoice, IndependentSample}; 7 | 8 | use std::cmp; 9 | 10 | use tcod::console::*; 11 | use tcod::colors::{self, Color}; 12 | use tcod::map::{Map as FovMap, FovAlgorithm}; 13 | 14 | use tcod::input::Key; 15 | use tcod::input::{self, Event, Mouse}; 16 | use tcod::input::KeyCode::*; 17 | 18 | use crate::types::*; 19 | use crate::r#const::*; 20 | 21 | pub mod levels; 22 | pub mod combat; 23 | pub mod items; 24 | pub mod ui; 25 | 26 | pub use combat::*; 27 | pub use ui::*; 28 | pub use items::*; 29 | pub use levels::*; 30 | 31 | // Handle keydown events here 32 | pub fn handle_keys(key: Key, tcod: &mut Tcod, objects: &mut Vec, 33 | game: &mut Game) -> PlayerAction { 34 | use PlayerAction::*; 35 | // todo: handle keys 36 | 37 | let player_alive = objects[PLAYER].alive; 38 | match (key, player_alive) { 39 | (Key { code: Enter, ctrl: true, .. }, _)=> { 40 | // Alt+Enter: toggle fullscreen 41 | let fullscreen = tcod.root.is_fullscreen(); 42 | tcod.root.set_fullscreen(!fullscreen); 43 | DidntTakeTurn 44 | } 45 | (Key { code: Escape, .. }, _) => Exit, // exit game 46 | // movement keys 47 | (Key { code: Up, .. }, true) | (Key { code: NumPad8, ..}, true) => { 48 | player_move_or_attack(0, -1, objects, game); 49 | TookTurn 50 | } 51 | (Key { code: Down, .. }, true) | (Key { code: NumPad2, ..}, true) => { 52 | player_move_or_attack(0, 1, objects, game); 53 | TookTurn 54 | } 55 | (Key { code: Left, .. }, true) | (Key { code: NumPad4, ..}, true) => { 56 | player_move_or_attack(-1, 0, objects, game); 57 | TookTurn 58 | } 59 | (Key { code: Right, .. }, true) | (Key { code: NumPad6, ..}, true) => { 60 | player_move_or_attack(1, 0, objects, game); 61 | TookTurn 62 | } 63 | (Key { code: Home, .. }, true) | (Key { code: NumPad7, ..}, true) => { 64 | player_move_or_attack(-1, -1, objects, game); 65 | TookTurn 66 | } 67 | (Key { code: PageUp, .. }, true) | (Key { code: NumPad9, ..}, true) => { 68 | player_move_or_attack(1, -1, objects, game); 69 | TookTurn 70 | } 71 | (Key { code: End, .. }, true) | (Key { code: NumPad1, ..}, true) => { 72 | player_move_or_attack(-1, 1, objects, game); 73 | TookTurn 74 | } 75 | (Key { code: PageDown, .. }, true) | (Key { code: NumPad3, ..}, true) => { 76 | player_move_or_attack(1, 1, objects, game); 77 | TookTurn 78 | } 79 | (Key { code: NumPad5, .. }, true) => { 80 | TookTurn // do nothing, i.e. wait for the monster to come to you 81 | } 82 | (Key { printable: 'g', .. }, true) => { 83 | // pick up an item 84 | let item_id = objects.iter().position(|object| { 85 | object.pos() == objects[PLAYER].pos() && object.item.is_some() 86 | }); 87 | if let Some(item_id) = item_id { 88 | pick_item_up(item_id, objects, &mut game.inventory, &mut game.log); 89 | } 90 | DidntTakeTurn 91 | }, 92 | (Key { printable: 'i', .. }, true) => { 93 | // show the inventory: if an item is selected, use it 94 | let inventory_index = inventory_menu( 95 | &mut game.inventory, 96 | "Press the key next to an item to use it, or any other to cancel.\n", 97 | &mut tcod.root); 98 | if let Some(inventory_index) = inventory_index { 99 | use_item(inventory_index, objects, game, tcod); 100 | } 101 | DidntTakeTurn 102 | }, 103 | (Key { printable: 'd', .. }, true) => { 104 | // show the inventory; if an item is selected, drop it 105 | let inventory_index = inventory_menu( 106 | &mut game.inventory, 107 | "Press the key next to an item to drop it, or any other to cancel.\n'", 108 | &mut tcod.root); 109 | if let Some(inventory_index) = inventory_index { 110 | drop_item(inventory_index, &mut game.inventory, objects, &mut game.log); 111 | } 112 | DidntTakeTurn 113 | }, 114 | (Key { printable: '<', .. }, true) => { 115 | // go down stairs, if the player is on them 116 | let player_on_stairs = objects.iter().any(|object| { 117 | object.pos() == objects[PLAYER].pos() && object.name == "stairs" 118 | }); 119 | if player_on_stairs { 120 | next_level(tcod, objects, game); 121 | } 122 | DidntTakeTurn 123 | }, 124 | (Key { printable: 'c', .. }, true) => { 125 | // show character information 126 | let player = &objects[PLAYER]; 127 | let level = player.level; 128 | let level_up_xp = LEVEL_UP_BASE + player.level * LEVEL_UP_FACTOR; 129 | if let Some(fighter) = player.fighter.as_ref() { 130 | let msg = format!("Character information 131 | 132 | Level: {} 133 | Experience: {} 134 | Experience to level up: {} 135 | 136 | Maximum HP: {} 137 | Attack: {} 138 | Defense: {}", level, fighter.xp, level_up_xp, player.max_hp(game), player.power(game), player.defense(game)); 139 | msgbox(&msg, CHARACTER_SCREEN_WIDTH, &mut tcod.root); 140 | } 141 | 142 | DidntTakeTurn 143 | } 144 | _ => DidntTakeTurn, 145 | } 146 | 147 | } 148 | 149 | pub fn make_map(objects: &mut Vec, level: u32) -> Map { 150 | // fill map with "blocked" tiles 151 | let mut map = vec![vec![Tile::wall(); MAP_HEIGHT as usize]; MAP_WIDTH as usize]; 152 | 153 | // Player is the first element, remove everything else. 154 | // NOTE: works only when the player is the first object! 155 | assert_eq!(&objects[PLAYER] as *const _, &objects[0] as *const _); 156 | objects.truncate(1); 157 | 158 | let mut rooms = vec![]; 159 | 160 | for _ in 0..MAX_ROOMS { 161 | // random width and height 162 | let w = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 163 | let h = rand::thread_rng().gen_range(ROOM_MIN_SIZE, ROOM_MAX_SIZE + 1); 164 | // random position without going out of the boundaries of the map 165 | let x = rand::thread_rng().gen_range(0, MAP_WIDTH - w); 166 | let y = rand::thread_rng().gen_range(0, MAP_HEIGHT - h); 167 | 168 | let new_room = Rect::new(x, y, w, h); 169 | 170 | // run through the other rooms and see if they intersect with this one 171 | let failed = rooms.iter().any(|other_room| new_room.intersects_with(other_room)); 172 | 173 | if !failed { 174 | // this means there are no intersections, so this room is valid 175 | 176 | // "paint" it to the map's tiles 177 | create_room(new_room, &mut map); 178 | 179 | // add some content to this room, such as monsters 180 | place_objects(new_room, objects, &mut map, level as u32); 181 | 182 | // center coordinates of the new room, will be useful later 183 | let (new_x, new_y) = new_room.center(); 184 | 185 | if rooms.is_empty() { 186 | // this is the first room, where the player starts at 187 | objects[PLAYER].set_pos(new_x, new_y); 188 | } else { 189 | // all rooms after the first: 190 | // connect it to the previous room with a tunnel 191 | 192 | // center coordinates of the previous room 193 | let (prev_x, prev_y) = rooms[rooms.len() - 1].center(); 194 | 195 | // toss a coin (random bool value -- either true or false) 196 | if rand::random() { 197 | // first move horizontally, then vertically 198 | create_h_tunnel(prev_x, new_x, prev_y, &mut map); 199 | create_v_tunnel(prev_y, new_y, new_x, &mut map); 200 | } else { 201 | // first move vertically, then horizontally 202 | create_v_tunnel(prev_y, new_y, prev_x, &mut map); 203 | create_h_tunnel(prev_x, new_x, new_y, &mut map); 204 | } 205 | } 206 | 207 | // finally, append the new room to the list 208 | rooms.push(new_room); 209 | } 210 | } 211 | // create stairs at the center of the last room 212 | let (last_room_x, last_room_y) = rooms[rooms.len() - 1].center(); 213 | let mut stairs = Object::new(last_room_x, last_room_y, '<', "stairs", colors::WHITE, false); 214 | stairs.always_visible = true; 215 | objects.push(stairs); 216 | 217 | map 218 | } 219 | 220 | /// Returns two muted barrows 221 | pub fn mut_two(first_index: usize, second_index: usize, items: &mut [T]) -> (&mut T, &mut T) { 222 | assert!(first_index != second_index); 223 | let split_at_index = cmp::max(first_index, second_index); 224 | let (first_slice, second_slice) = items.split_at_mut(split_at_index); 225 | if first_index < second_index { 226 | (&mut first_slice[first_index], &mut second_slice[0]) 227 | } else { 228 | (&mut second_slice[0], &mut first_slice[second_index]) 229 | } 230 | } 231 | 232 | pub fn new_game(tcod: &mut Tcod) -> (Vec, Game) { 233 | // create object representing the player 234 | let mut player = Object::new(0, 0, '@', "player", colors::WHITE, true); 235 | player.alive = true; 236 | player.fighter = Some(Fighter{base_max_hp: 100, hp: 100, base_defense: 2, base_power: 5, 237 | on_death: DeathCallback::Player, xp: 0}); 238 | 239 | // the list of objects with just the player 240 | let mut objects = vec![player]; 241 | 242 | let mut game = Game { 243 | // generate map (at this point it's not drawn to the screen) 244 | map: make_map(&mut objects, 1), 245 | // create the list of game messages and their colors, starts empty 246 | log: vec![], 247 | inventory: vec![], 248 | dungeon_level: 1, 249 | }; 250 | 251 | initialise_fov(&game.map, tcod); 252 | 253 | // initial equipment: a dagger 254 | let mut dagger = Object::new(0, 0, '-', "dagger", colors::SKY, false); 255 | dagger.item = Some(Item::Dagger); 256 | dagger.equipment = Some(Equipment { 257 | equipped: true, 258 | slot: Slot::LeftHand, 259 | max_hp_bonus: 0, 260 | defense_bonus: 0, 261 | power_bonus: 2 262 | }); 263 | game.inventory.push(dagger); 264 | 265 | // a warm welcoming message! 266 | game.log.add("Welcome stranger! Prepare to perish in the Rouge Cachot.", 267 | colors::RED); 268 | 269 | (objects, game) 270 | } 271 | 272 | pub fn initialise_fov(map: &Map, tcod: &mut Tcod) { 273 | // create the FOV map, according to the generated map 274 | for y in 0..MAP_HEIGHT { 275 | for x in 0..MAP_WIDTH { 276 | tcod.fov.set(x, y, 277 | !map[x as usize][y as usize].block_sight, 278 | !map[x as usize][y as usize].blocked); 279 | } 280 | } 281 | tcod.con.clear(); // unexplored areas start black (which is the default background color) 282 | } 283 | 284 | 285 | pub fn play_game(objects: &mut Vec, game: &mut Game, tcod: &mut Tcod) { 286 | // force FOV "recompute" first time through the game loop 287 | let mut previous_player_position = (-1, -1); 288 | 289 | let mut key = Default::default(); 290 | 291 | while !tcod.root.window_closed() { 292 | match input::check_for_event(input::MOUSE | input::KEY_PRESS) { 293 | Some((_, Event::Mouse(m))) => tcod.mouse = m, 294 | Some((_, Event::Key(k))) => key = k, 295 | _ => key = Default::default(), 296 | } 297 | 298 | // render the screen 299 | let fov_recompute = previous_player_position != (objects[PLAYER].pos()); 300 | render_all(tcod, &objects, game, fov_recompute); 301 | 302 | tcod.root.flush(); 303 | 304 | // level up if needed 305 | level_up(objects, game, tcod); 306 | 307 | // erase all objects at their old locations, before they move 308 | for object in objects.iter_mut() { 309 | object.clear(&mut tcod.con, &mut game.map) 310 | } 311 | 312 | // handle keys and exit game if needed 313 | previous_player_position = objects[PLAYER].pos(); 314 | let player_action = handle_keys(key, tcod, objects, game); 315 | if player_action == PlayerAction::Exit { 316 | save_game(objects, game); 317 | break 318 | } 319 | 320 | // let monstars take their turn 321 | if objects[PLAYER].alive && player_action != PlayerAction::DidntTakeTurn { 322 | for id in 0..objects.len() { 323 | if objects[id].ai.is_some() { 324 | ai_take_turn(id, objects, game, &tcod.fov); 325 | } 326 | } 327 | } 328 | } 329 | } 330 | 331 | pub fn main_menu(tcod: &mut Tcod) { 332 | let img = tcod::image::Image::from_file("./img/menu_background.png") 333 | .ok().expect("Background image not found"); 334 | 335 | while !tcod.root.window_closed() { 336 | // show the background image, at twice the regular console resolution 337 | tcod::image::blit_2x(&img, (0, 0), (-1, -1), &mut tcod.root, (0, 0)); 338 | 339 | tcod.root.set_default_foreground(colors::LIGHT_YELLOW); 340 | tcod.root.print_ex(SCREEN_WIDTH/2, SCREEN_HEIGHT/2 - 4, 341 | BackgroundFlag::None, TextAlignment::Center, 342 | "ROUGE"); 343 | tcod.root.print_ex(SCREEN_WIDTH/2, SCREEN_HEIGHT - 2, 344 | BackgroundFlag::None, TextAlignment::Center, 345 | "By Avery Wagar"); 346 | 347 | // show options and wait for the player's choice 348 | let choices = &["Play a new game", "Continue last game", "Quit"]; 349 | let choice = menu("", choices, 24, &mut tcod.root); 350 | 351 | 352 | 353 | match choice { 354 | Some(0) => { // new game 355 | let (mut objects, mut game) = new_game(tcod); 356 | play_game(&mut objects, &mut game, tcod); 357 | } 358 | Some(1) => { // load game 359 | match load_game() { 360 | Ok((mut objects, mut game)) => { 361 | initialise_fov(&game.map, tcod); 362 | play_game(&mut objects, &mut game, tcod); 363 | } 364 | Err(_e) => { 365 | msgbox("\nNo saved game to load.\n", 24, &mut tcod.root); 366 | continue; 367 | } 368 | } 369 | } 370 | Some(2) => { // quit 371 | break; 372 | } 373 | _ => {} 374 | } 375 | } 376 | } 377 | 378 | pub fn save_game(objects: &[Object], game: &Game) -> Result<(), Box> { 379 | let save_data = serde_json::to_string(&(objects, game))?; 380 | let mut file = File::create("savegame")?; 381 | file.write_all(save_data.as_bytes())?; 382 | Ok(()) 383 | } 384 | 385 | pub fn load_game() -> Result<(Vec, Game), Box> { 386 | let mut json_save_state = String::new(); 387 | let mut file = File::open("savegame")?; 388 | file.read_to_string(&mut json_save_state)?; 389 | let result = serde_json::from_str::<(Vec, Game)>(&json_save_state)?; 390 | Ok(result) 391 | } 392 | pub fn move_by(id: usize, dx: i32, dy: i32, map: &Map, objects: &mut [Object]) { 393 | let (x, y) = objects[id].pos(); 394 | if !is_blocked(x + dx, y + dy, map, objects) { 395 | objects[id].set_pos(x + dx, y + dy); 396 | } 397 | } 398 | 399 | 400 | pub fn is_blocked(x: i32, y: i32, map: &Map, objects: &[Object]) -> bool { 401 | // first test the map tile 402 | if map[x as usize][y as usize].blocked { 403 | return true; 404 | } 405 | // now check for any blocking objects 406 | objects.iter().any(|object| { 407 | object.blocks && object.pos() == (x, y) 408 | }) 409 | } 410 | 411 | pub fn render_all(tcod: &mut Tcod, objects: &[Object], game: &mut Game, fov_recompute: bool) { 412 | if fov_recompute { 413 | // recompute FOV if needed (the player moved or something) 414 | let player = &objects[PLAYER]; 415 | tcod.fov.compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO); 416 | 417 | // go through all tiles, and set their background color 418 | for y in 0..MAP_HEIGHT { 419 | for x in 0..MAP_WIDTH { 420 | let visible = tcod.fov.is_in_fov(x, y); 421 | let wall = game.map[x as usize][y as usize].block_sight; 422 | let color = match (visible, wall) { 423 | // outside of field of view: 424 | (false, true) => COLOR_DARK_WALL, 425 | (false, false) => COLOR_DARK_GROUND, 426 | // inside fov: 427 | (true, true) => COLOR_LIGHT_WALL, 428 | (true, false) => COLOR_LIGHT_GROUND, 429 | }; 430 | 431 | let explored = &mut game.map[x as usize][y as usize].explored; 432 | if visible { 433 | // since it's visible, explore it 434 | *explored = true; 435 | } 436 | if *explored { 437 | // show explored tiles only (any visible tile is explored already) 438 | // con.set_char_background(x, y, color, BackgroundFlag::Set); 439 | if wall { 440 | tcod.con.set_default_foreground(color); 441 | tcod.con.put_char(x, y, WALL, BackgroundFlag::Set); 442 | } 443 | else { 444 | tcod.con.set_default_foreground(color); 445 | tcod.con.put_char(x, y, FLOOR, BackgroundFlag::Set); 446 | } 447 | } 448 | } 449 | } 450 | } 451 | 452 | let mut to_draw: Vec<_> = objects.iter() 453 | .filter(|o| { 454 | tcod.fov.is_in_fov(o.x, o.y) || 455 | (o.always_visible && game.map[o.x as usize][o.y as usize].explored) 456 | }).collect(); 457 | // sort so that non-blocknig objects come first 458 | to_draw.sort_by(|o1, o2| { o1.blocks.cmp(&o2.blocks) }); 459 | // draw the objects in the list 460 | for object in &to_draw { 461 | object.draw(&mut tcod.con); 462 | } 463 | 464 | // blit the contents of "con" to the root console 465 | blit(&tcod.con, (0, 0), (MAP_WIDTH, MAP_HEIGHT), &mut tcod.root, (0, 0), 1.0, 1.0); 466 | 467 | // prepare to render the GUI panel 468 | tcod.panel.set_default_background(colors::BLACK); 469 | tcod.panel.clear(); 470 | 471 | // print the game messages, one line at a time 472 | let mut y = MSG_HEIGHT as i32; 473 | for &(ref msg, color) in game.log.iter().rev() { 474 | let msg_height = tcod.panel.get_height_rect(MSG_X, y, MSG_WIDTH, 0, msg); 475 | y -= msg_height; 476 | if y < 0 { 477 | break; 478 | } 479 | tcod.panel.set_default_foreground(color); 480 | tcod.panel.print_rect(MSG_X, y, MSG_WIDTH, 0, msg); 481 | } 482 | 483 | 484 | // show the player's stats 485 | let hp = objects[PLAYER].fighter.map_or(0, |f| f.hp); 486 | let max_hp = objects[PLAYER].max_hp(game); 487 | render_bar(&mut tcod.panel, 1, 1, BAR_WIDTH, "HP", hp, max_hp, colors::LIGHT_RED, colors::DARKER_RED); 488 | 489 | tcod.panel.print_ex(1, 3, BackgroundFlag::None, TextAlignment::Left, 490 | format!("Dungeon level: {}", game.dungeon_level)); 491 | 492 | // display names of objects under the mouse 493 | tcod.panel.set_default_foreground(colors::LIGHT_GREY); 494 | tcod.panel.print_ex(1, 0, BackgroundFlag::None, TextAlignment::Left, 495 | get_names_under_mouse(tcod.mouse, objects, &tcod.fov)); 496 | 497 | // blit the contents of `panel` to the root console 498 | blit(&mut tcod.panel, (0, 0), (SCREEN_WIDTH, PANEL_HEIGHT), &mut tcod.root, (0, PANEL_Y), 1.0, 1.0); 499 | } 500 | 501 | -------------------------------------------------------------------------------- /src/func/ui.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use tcod::colors::{self, Color}; 3 | use crate::r#const::*; 4 | use crate::types::object::Object; 5 | use crate::types::slot::Slot; 6 | use crate::types::Map; 7 | use crate::types::rect::Rect; 8 | use crate::types::tile::Tile; 9 | 10 | use rand::Rng; 11 | use rand::distributions::{Weighted, WeightedChoice, IndependentSample}; 12 | 13 | use std::cmp; 14 | 15 | use tcod::console::*; 16 | use tcod::map::{Map as FovMap, FovAlgorithm}; 17 | 18 | use tcod::input::Key; 19 | use tcod::input::{self, Event, Mouse}; 20 | use tcod::input::KeyCode::*; 21 | 22 | pub fn menu>(header: &str, options: &[T], width: i32, 23 | root: &mut Root) -> Option { 24 | assert!(options.len() <= 26, "Cannot have a menu with more than 26 options."); 25 | 26 | // calculate total height for the header (after auto-wrap) and one line per option 27 | let header_height = root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header); 28 | let height = options.len() as i32 + header_height; 29 | 30 | // create an off-screen console that represents the menu's window 31 | let mut window = Offscreen::new(width, height); 32 | 33 | // print the header, with auto-wrap 34 | window.set_default_foreground(colors::WHITE); 35 | window.print_rect_ex(0, 0, width, height, BackgroundFlag::None, TextAlignment::Left, header); 36 | 37 | // calculate total height for the header (after auto-wrap) and one line per option 38 | let header_height = if header.is_empty() { 39 | 0 40 | } else { 41 | root.get_height_rect(0, 0, width, SCREEN_HEIGHT, header) 42 | }; 43 | 44 | // print all the options 45 | for (index, option_text) in options.iter().enumerate() { 46 | let menu_letter = (b'a' + index as u8) as char; 47 | let text = format!("({}) {}", menu_letter, option_text.as_ref()); 48 | window.print_ex(0, header_height + index as i32, 49 | BackgroundFlag::None, TextAlignment::Left, text); 50 | } 51 | 52 | // blit the contents of "window" to the root console 53 | let x = SCREEN_WIDTH / 2 - width / 2; 54 | let y = SCREEN_HEIGHT / 2 - height / 2; 55 | tcod::console::blit(&mut window, (0, 0), (width, height), root, (x, y), 1.0, 0.7); 56 | 57 | // present the root console to the player and wait for a key-press 58 | root.flush(); 59 | let key = root.wait_for_keypress(true); 60 | 61 | // convert the ASCII code to an index; if it corresponds to an option, return it 62 | if key.printable.is_alphabetic() { 63 | let index = key.printable.to_ascii_lowercase() as usize - 'a' as usize; 64 | if index < options.len() { 65 | Some(index) 66 | } else { 67 | None 68 | } 69 | } else { 70 | None 71 | } 72 | 73 | } 74 | 75 | pub fn inventory_menu(inventory: &[Object], header: &str, root: &mut Root) -> Option { 76 | // how a menu with each item of the inventory as an option 77 | let options = if inventory.len() == 0 { 78 | vec!["Inventory is empty.".into()] 79 | } else { 80 | inventory.iter().map(|item| { 81 | // show additional information, in case it's equipped 82 | match item.equipment { 83 | Some(equipment) if equipment.equipped => { 84 | format!("{} (on {})", item.name, equipment.slot) 85 | } 86 | _ => item.name.clone() 87 | } 88 | }).collect() 89 | }; 90 | 91 | let inventory_index = menu(header, &options, INVENTORY_WIDTH, root); 92 | 93 | // if an item was chosen, return it 94 | if inventory.len() > 0 { 95 | inventory_index 96 | } else { 97 | None 98 | } 99 | } 100 | 101 | 102 | 103 | /// return a string with the names of all objects under the mouse 104 | pub fn get_names_under_mouse(mouse: Mouse, objects: &[Object], fov_map: &FovMap) -> String { 105 | let (x, y) = (mouse.cx as i32, mouse.cy as i32); 106 | 107 | // create a list with the names of all objects at the mouse's coordinates and in FOV 108 | let names = objects 109 | .iter() 110 | .filter(|obj| {obj.pos() == (x, y) && fov_map.is_in_fov(obj.x, obj.y)}) 111 | .map(|obj| obj.name.clone()) 112 | .collect::>(); 113 | 114 | names.join(", ") // join the names, separated by commas 115 | } 116 | 117 | pub fn message>(messages: &mut Messages, message: T, color: Color) { 118 | // if the buffer is full, remove the first message to make room for the new one 119 | if messages.len() == MSG_HEIGHT { 120 | messages.remove(0); 121 | } 122 | // add the new line as a tuple, with the text and the color 123 | messages.push((message.into(), color)); 124 | } 125 | 126 | 127 | pub fn render_bar(panel: &mut Offscreen, 128 | x: i32, 129 | y: i32, 130 | total_width: i32, 131 | name: &str, 132 | value: i32, 133 | maximum: i32, 134 | bar_color: Color, 135 | back_color: Color) 136 | { 137 | // render a bar (HP, experience, etc). First calculate the width of the bar 138 | let bar_width = (value as f32 / maximum as f32 * total_width as f32) as i32; 139 | 140 | // render the background first 141 | panel.set_default_background(back_color); 142 | panel.rect(x, y, total_width, 1, false, BackgroundFlag::Screen); 143 | 144 | // now render the bar on top 145 | panel.set_default_background(bar_color); 146 | if bar_width > 0 { 147 | panel.rect(x, y, bar_width, 1, false, BackgroundFlag::Screen); 148 | } 149 | 150 | // finally, some centered text with the values 151 | panel.set_default_foreground(colors::WHITE); 152 | panel.print_ex(x + total_width / 2, y, BackgroundFlag::None, TextAlignment::Center, 153 | &format!("{}: {}/{}", name, value, maximum)); 154 | } 155 | 156 | pub fn msgbox(text: &str, width: i32, root: &mut Root) { 157 | let options: &[&str] = &[]; 158 | menu(text, options, width, root); 159 | } 160 | 161 | pub fn create_room(room: Rect, map: &mut Map) { 162 | for x in (room.x1 + 1)..room.x2 { 163 | for y in (room.y1 + 1)..room.y2 { 164 | map[x as usize][y as usize] = Tile::empty(); 165 | } 166 | } 167 | } 168 | 169 | pub fn create_h_tunnel(x1: i32, x2: i32, y: i32, map: &mut Map) { 170 | for x in cmp::min(x1, x2)..(cmp::max(x1, x2) + 1) { 171 | map[x as usize][y as usize] = Tile::empty(); 172 | } 173 | } 174 | 175 | pub fn create_v_tunnel(y1: i32, y2: i32, x: i32, map: &mut Map) { 176 | for y in cmp::min(y1, y2)..(cmp::max(y1, y2) + 1) { 177 | map[x as usize][y as usize] = Tile::empty(); 178 | } 179 | } 180 | 181 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(uniform_paths)] 2 | extern crate tcod; 3 | extern crate rand; 4 | extern crate serde; 5 | #[macro_use] extern crate serde_derive; 6 | extern crate serde_json; 7 | 8 | pub mod loot; 9 | pub mod types; 10 | pub mod r#const; 11 | pub mod func; 12 | -------------------------------------------------------------------------------- /src/loot/mod.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/furious-monkey/rusttest/965311902372bdb29a2d7fb0913176d47b5c271d/src/loot/mod.rs -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(uniform_paths)] 2 | 3 | /* 4 | * CRATES/USE calls 5 | */ 6 | 7 | extern crate tcod; 8 | extern crate rand; 9 | extern crate serde; 10 | #[macro_use] 11 | extern crate serde_derive; 12 | extern crate serde_json; 13 | // the existing imports 14 | use std::io::{Read, Write}; 15 | use std::fs::File; 16 | use std::error::Error; 17 | 18 | use rand::Rng; 19 | use rand::distributions::{Weighted, WeightedChoice, IndependentSample}; 20 | 21 | use std::cmp; 22 | 23 | use tcod::console::*; 24 | use tcod::colors::{self, Color}; 25 | use tcod::map::{Map as FovMap, FovAlgorithm}; 26 | 27 | use tcod::input::KeyCode::*; 28 | 29 | use rouge::types::*; 30 | use rouge::types::object::Object; 31 | use rouge::types::deathcallback::DeathCallback; 32 | use rouge::types::item::Item; 33 | use rouge::types::slot::Slot; 34 | use rouge::func::combat::ai_take_turn; 35 | use rouge::r#const::*; 36 | use rouge::func::*; 37 | 38 | /* 39 | * Finally we're at main. This includes our map/player generation and gameloop. 40 | */ 41 | 42 | fn main(){ 43 | 44 | // Init the root window here. All other settings fallback to default 45 | let root = Root::initializer() 46 | .font("./fonts/DarkondDigsDeeper_16x16.png", FontLayout::AsciiInRow) 47 | .font_type(FontType::Default) 48 | .font_dimensions(16,16) 49 | .size(SCREEN_WIDTH, SCREEN_HEIGHT) 50 | .title("Rouge") 51 | .init(); 52 | 53 | // Limit FPS here 54 | tcod::system::set_fps(LIMIT_FPS); 55 | 56 | let mut tcod = Tcod { 57 | root: root, 58 | con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT), 59 | panel: Offscreen::new(SCREEN_WIDTH, PANEL_HEIGHT), 60 | fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT), 61 | mouse: Default::default(), 62 | }; 63 | 64 | main_menu(&mut tcod); 65 | // let (mut objects, mut game) = new_game(&mut tcod); 66 | // play_game(&mut objects, &mut game, &mut tcod); 67 | } 68 | -------------------------------------------------------------------------------- /src/types/deathcallback.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::func::combat::{player_death, monster_death}; 3 | 4 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 5 | pub enum DeathCallback { 6 | Player, 7 | Monster, 8 | } 9 | 10 | impl DeathCallback { 11 | pub fn callback(self, object: &mut Object, game: &mut Game) { 12 | let callback: fn(&mut Object, &mut Messages) = match self { 13 | DeathCallback::Player => player_death, 14 | DeathCallback::Monster => monster_death, 15 | }; 16 | callback(object, &mut game.log); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/types/item.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 2 | pub enum Item { 3 | // Spells/Potions 4 | Heal, 5 | Lightning, 6 | Confuse, 7 | Fireball, 8 | // Weapons 9 | BronzeSword, 10 | IronSword, 11 | GreatAxe, 12 | WarHammer, 13 | WoodShield, 14 | IronShield, 15 | Dagger, 16 | // Amour 17 | ClothShirt, 18 | ClothPants, 19 | LeatherWristGaurds, 20 | LeatherHat, 21 | LeatherChest, 22 | LeatherKneeGaurds 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/types/mod.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::io::{Read, Write}; 3 | use std::fs::File; 4 | use std::error::Error; 5 | 6 | use rand::Rng; 7 | use rand::distributions::{Weighted, WeightedChoice, IndependentSample}; 8 | 9 | use std::cmp; 10 | 11 | use tcod::console::*; 12 | use tcod::colors::{self, Color}; 13 | use tcod::map::{Map as FovMap, FovAlgorithm}; 14 | 15 | use tcod::input::Key; 16 | use tcod::input::{self, Event, Mouse}; 17 | use tcod::input::KeyCode::*; 18 | 19 | // Import types 20 | pub mod object; 21 | pub mod item; 22 | pub mod slot; 23 | pub mod deathcallback; 24 | pub mod rect; 25 | pub mod tile; 26 | 27 | 28 | // Export Types 29 | pub use self::object::Object; 30 | pub use self::item::Item; 31 | pub use self::slot::Slot; 32 | pub use self::deathcallback::DeathCallback; 33 | pub use self::rect::Rect; 34 | pub use self::tile::Tile; 35 | 36 | 37 | // Smaller types 38 | #[derive(Serialize, Deserialize, Debug)] 39 | pub enum Ai { 40 | Basic, 41 | Confused{previous_ai: Box, num_turns: i32}, 42 | } 43 | 44 | #[derive(Clone, Copy, Debug, PartialEq)] 45 | pub enum PlayerAction { 46 | TookTurn, 47 | DidntTakeTurn, 48 | Exit, 49 | } 50 | 51 | 52 | pub enum UseResult { 53 | UsedUp, 54 | UsedAndKept, 55 | Cancelled, 56 | } 57 | 58 | pub type Map = Vec>; 59 | 60 | pub type Messages = Vec<(String, Color)>; 61 | 62 | 63 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 64 | /// An object that can be equipped, yielding bonuses. 65 | pub struct Equipment { 66 | pub slot: Slot, 67 | pub equipped: bool, 68 | pub max_hp_bonus: i32, 69 | pub power_bonus: i32, 70 | pub defense_bonus: i32, 71 | } 72 | 73 | pub struct Transition { 74 | pub level: u32, 75 | pub value: u32, 76 | } 77 | 78 | pub trait MessageLog { 79 | fn add>(&mut self, message: T, color: Color); 80 | } 81 | 82 | impl MessageLog for Messages { 83 | fn add>(&mut self, message: T, color: Color) { 84 | self.push((message.into(), color)); 85 | } 86 | } 87 | 88 | #[derive(Serialize, Deserialize)] 89 | pub struct Game { 90 | pub map: Map, 91 | pub log: Messages, 92 | pub inventory: Vec, 93 | pub dungeon_level: u32, 94 | } 95 | 96 | // Cleaner params 97 | pub struct Tcod { 98 | pub root: Root, 99 | pub con: Offscreen, 100 | pub panel: Offscreen, 101 | pub fov: FovMap, 102 | pub mouse: Mouse, 103 | } 104 | 105 | // combat-related properties and methods (monster, player, NPC). 106 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 107 | pub struct Fighter { 108 | pub hp: i32, 109 | pub base_max_hp: i32, 110 | pub base_defense: i32, 111 | pub base_power: i32, 112 | pub on_death: DeathCallback, 113 | pub xp: i32, 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/types/object.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::r#const::FLOOR; 3 | use crate::r#const::COLOR_DARK_GROUND; 4 | 5 | 6 | // Object in the game 7 | #[derive(Serialize, Deserialize, Debug)] 8 | pub struct Object { 9 | pub x: i32, 10 | pub y: i32, 11 | pub char: char, 12 | pub color: Color, 13 | pub name: String, 14 | pub blocks: bool, 15 | pub alive: bool, 16 | pub fighter: Option, 17 | pub ai: Option, 18 | pub item: Option, 19 | pub always_visible: bool, 20 | pub level: i32, 21 | pub equipment: Option, 22 | 23 | } 24 | 25 | impl Object { 26 | 27 | /// returns a list of equipped items 28 | pub fn get_all_equipped(&self, game: &Game) -> Vec { 29 | if self.name == "player" { 30 | game.inventory 31 | .iter() 32 | .filter(|item| { 33 | item.equipment.map_or(false, |e| e.equipped) 34 | }) 35 | .map(|item| item.equipment.unwrap()) 36 | .collect() 37 | } else { 38 | vec![] 39 | } 40 | } 41 | 42 | pub fn max_hp(&self, game: &Game) -> i32 { 43 | let base_max_hp = self.fighter.map_or(0, |f| f.base_max_hp); 44 | let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.max_hp_bonus); 45 | base_max_hp + bonus 46 | } 47 | 48 | pub fn defense(&self, game: &Game) -> i32 { 49 | let base_defense = self.fighter.map_or(0, |f| f.base_defense); 50 | let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.defense_bonus); 51 | base_defense + bonus 52 | } 53 | 54 | pub fn power(&self, game: &Game) -> i32 { 55 | let base_power = self.fighter.map_or(0, |f| f.base_power); 56 | let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.power_bonus); 57 | base_power + bonus 58 | } 59 | 60 | 61 | /// unequip object and show a message about it 62 | pub fn unequip(&mut self, log: &mut Vec<(String, Color)>) { 63 | if self.item.is_none() { 64 | log.add(format!("Can't dequip {:?} because it's not an Item.", self), 65 | colors::RED); 66 | return 67 | }; 68 | if let Some(ref mut equipment) = self.equipment { 69 | if equipment.equipped { 70 | equipment.equipped = false; 71 | log.add(format!("Dequipped {} from {}.", self.name, equipment.slot), 72 | colors::LIGHT_YELLOW); 73 | } 74 | } else { 75 | log.add(format!("Can't dequip {:?} because it's not an Equipment.", self), 76 | colors::RED); 77 | } 78 | } 79 | /// Equip object and show a message about it 80 | pub fn equip(&mut self, log: &mut Vec<(String, Color)>) { 81 | if self.item.is_none() { 82 | log.add(format!("Can't equip {:?} because it's not an Item.", self), 83 | colors::RED); 84 | return 85 | }; 86 | if let Some(ref mut equipment) = self.equipment { 87 | if !equipment.equipped { 88 | equipment.equipped = true; 89 | log.add(format!("Equipped {} on {}.", self.name, equipment.slot), 90 | colors::LIGHT_GREEN); 91 | } 92 | } else { 93 | log.add(format!("Can't equip {:?} because it's not an Equipment.", self), 94 | colors::RED); 95 | } 96 | } 97 | /// return the distance to some coordinates 98 | pub fn distance(&self, x: i32, y: i32) -> f32 { 99 | (((x - self.x).pow(2) + (y - self.y).pow(2)) as f32).sqrt() 100 | } 101 | 102 | /// heal by the given amount, without going over the maximum 103 | pub fn heal(&mut self, amount: i32, game: &Game) { 104 | let max_hp = self.max_hp(game); 105 | if let Some(ref mut fighter) = self.fighter { 106 | fighter.hp += amount; 107 | if fighter.hp > max_hp { 108 | fighter.hp = max_hp; 109 | } 110 | } 111 | } 112 | pub fn attack(&mut self, target: &mut Object, game: &mut Game) { 113 | // a simple formula for attack damage 114 | let damage = self.power(game) - target.defense(game); 115 | if damage > 0 { 116 | // make the target take some damage 117 | game.log.add(format!("{} attacks {} for {} hit points.", self.name, target.name, damage), colors::WHITE); 118 | if let Some(xp) = target.take_damage(damage, game) { 119 | // yield experience to the player 120 | self.fighter.as_mut().unwrap().xp += xp; 121 | } 122 | } else { 123 | game.log.add(format!("{} attacks {} but it has no effect!", self.name, target.name), colors::WHITE); 124 | } 125 | } 126 | 127 | pub fn take_damage(&mut self, damage: i32, game: &mut Game) -> Option { 128 | // apply damage if possible 129 | if let Some(fighter) = self.fighter.as_mut() { 130 | if damage > 0 { 131 | fighter.hp -= damage; 132 | } 133 | } 134 | // check for death, call the death function 135 | if let Some(fighter) = self.fighter { 136 | if fighter.hp <= 0 { 137 | self.alive = false; 138 | fighter.on_death.callback(self, game); 139 | return Some(fighter.xp); 140 | } 141 | } 142 | None 143 | } 144 | /// return the distance to another object 145 | pub fn distance_to(&self, other: &Object) -> f32 { 146 | let dx = other.x - self.x; 147 | let dy = other.y - self.y; 148 | ((dx.pow(2) + dy.pow(2)) as f32).sqrt() 149 | } 150 | pub fn pos(&self) -> (i32, i32) { 151 | (self.x, self.y) 152 | } 153 | pub fn set_pos(&mut self, x: i32, y: i32) { 154 | self.x = x; 155 | self.y = y; 156 | } 157 | 158 | pub fn new(x: i32, y: i32, char: char, name: &str, color: Color, blocks: bool) -> Self { 159 | Object { 160 | x: x, 161 | y: y, 162 | char: char, 163 | color: color, 164 | name: name.into(), 165 | blocks: blocks, 166 | alive: false, 167 | fighter: None, 168 | ai: None, 169 | item: None, 170 | always_visible: false, 171 | equipment: None, 172 | level: 1, 173 | } 174 | } 175 | 176 | /// set the color and then draw the character that represents this object at its position 177 | pub fn draw(&self, con: &mut Console) { 178 | con.set_default_foreground(self.color); 179 | con.put_char(self.x, self.y, self.char, BackgroundFlag::None); 180 | } 181 | 182 | /// Erase the character that represents this object 183 | pub fn clear(&self, con: &mut Console, map: &mut Map) { 184 | // Black until explored then Floor tile when no longer visible 185 | let explored = &mut map[self.x as usize][self.y as usize].explored; 186 | if *explored { 187 | con.set_default_foreground(COLOR_DARK_GROUND); 188 | con.put_char(self.x, self.y, FLOOR, BackgroundFlag::None); 189 | } 190 | else{ 191 | con.set_default_foreground(colors::BLACK); 192 | con.put_char(self.x, self.y, ' ', BackgroundFlag::None); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/types/rect.rs: -------------------------------------------------------------------------------- 1 | // Rectangular room 2 | #[derive(Clone, Copy, Debug)] 3 | pub struct Rect { 4 | pub x1: i32, 5 | pub y1: i32, 6 | pub x2: i32, 7 | pub y2: i32, 8 | } 9 | 10 | impl Rect { 11 | pub fn center(&self) -> (i32, i32) { 12 | let center_x = (self.x1 + self.x2) / 2; 13 | let center_y = (self.y1 + self.y2) / 2; 14 | (center_x, center_y) 15 | } 16 | 17 | pub fn intersects_with(&self, other: &Rect) -> bool { 18 | // returns true if this rectangle intersects with another one 19 | (self.x1 <= other.x2) && (self.x2 >= other.x1) && 20 | (self.y1 <= other.y2) && (self.y2 >= other.y1) 21 | } 22 | 23 | pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { 24 | Rect { x1: x, y1: y, x2: x + w, y2: y + h } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/types/slot.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] 4 | pub enum Slot { 5 | LeftHand, 6 | RightHand, 7 | Gauntlets, 8 | Curiass, 9 | Legs, 10 | Head, 11 | } 12 | 13 | impl std::fmt::Display for Slot { 14 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 15 | match *self { 16 | Slot::LeftHand => write!(f, "left hand"), 17 | Slot::RightHand => write!(f, "right hand"), 18 | Slot::Head => write!(f, "head"), 19 | Slot::Gauntlets => write!(f, " gauntlets"), 20 | Slot::Legs => write!(f, "legs"), 21 | Slot::Curiass => write!(f, "curiass"), 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/types/tile.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, Debug, Serialize, Deserialize)] 2 | pub struct Tile { 3 | pub blocked: bool, 4 | pub block_sight: bool, 5 | pub explored: bool, 6 | } 7 | 8 | impl Tile { 9 | pub fn empty() -> Self { 10 | Tile{blocked: false, explored: false, block_sight: false} 11 | } 12 | 13 | pub fn wall() -> Self { 14 | Tile{blocked: true, explored: false, block_sight: true} 15 | } 16 | } 17 | --------------------------------------------------------------------------------